第一次编写bootsect时的问题解决过程

这记录了我,一个汇编盲+做业系统盲+硬件盲一次解决问题的过程。html

头一次写bootsect

这是要写一个bootsect, 就是传说中的引导扇区, 软盘的头512个字节, 0xaa55结尾, BIOS在启动后自动把它加载到内存的0x7c00而后开始执行, 这是我仅有关于它的知识了. 我但愿在它启动以后能在屏幕上印上一个"Hello, world!\r\n"就是了.linux

查了查Wikpedia,发如今刚加电启动还在实模式的时候有两种方法现实:字体

  • 趁BIOS还活在0xFFFF0用int 10/AH=0x13 BIOS中断
  • 0xb800:0000 - 0xb800:07ff是彩色文字模式下VGA显卡的显存在内存中的映射, 直接往里头写数据, 这些数据就会以latin1显示在屏幕上

看上去BIOS要简单点,那就搞这个吧. 先去Ralf的表里查查中断的用法, 我决定设置这些寄存器(一开始它们都是0):spa

  1. ah = 0x13: 视讯功能中断
  2. al = 0x01: 写入字符后更新光标位置
  3. bl = 0x0F: 高四位是背景色(0为黑), 低四位是字体色(F为白)
  4. cx = msg_len: 字串长度
  5. es:bp = msg: 字串首地址

# bootsect.s
# figure 0

.global _start

.section .text
_start:
  mov $0x1301, %ax
  mov $0x000f, %bx
  mov $msg_len, %cx
  mov $msg, %bp

  int $0x10
_pause:
  jmp _pause

.section .data
msg:
  .ascii "Hello, world\r\n"
  .equ msg_len, . - msg
  .org 0x200 - 2
  .word 0xaa55

这个程序就像是把Linux的经典0x80中断直接改为了0x10中断而已. 为了
方便编译我顺手写了个Makefile:debug

# Makefile
all: bootsect.img

bootsect.img: bootsect.o
    ld bootsect.o -o bootsect.img

bootsect.o: bootsect.s
    as bootsect.s -o bootsect.o

run: bootsect.img
    qemu-system-x86_64 -fda bootsect.img

make run以后我就遇到了错误. 而后我就开始了漫长的修BUG之旅.3d

大量错误QAQ

首先我遇到了连接时的错误:
relocation truncated to fit: R_X86_64_16 against '.data'
relocation truncated to fit: R_X86_64_16 against '.text'code

错误的大意是说R_X86_64_16不支持.data段和.text段. R_X86_64_16是64位as的一个重定位类型(relocation type, 在elf.h中定义).所谓重定位, 就是从新把代码中的一些符号定位. 好比msg, 在编译的时候,msg的地址是0x0(由于在段的开头), 然而连接的时候, 就要考虑到程序装载的位置, 若.data被装载到了0x4000, 那msg就要重定位到0x4000. 关于重定位的更多信息能够从Wiki中获取.orm

从错误信息里几乎没有得到什么修改方案, 但也有两点引发了个人注意:htm

  • 16: 为什么连接器会自动采用16位重定位?
    顺着这个思路, 我决定把全部寄存器改为32位, 也就是加e试试看.ip

    # figure 1, based on figure 0
    mov $0x1301, %eax
    mov $0x0001, %ebx
    mov $msg_len, %ecx
    mov $msg, %ebp

    编译成功了, 看来的确跟16位有关系.

  • '.data': 为何是'.data'而不是'data'?

    # figure 2, based on figure 0
    .section text
    # ...
    .section data

    编译经过. 难不成是section名字的问题? 名字不该当有这样的做用.

既然在段上有这样的问题, 那就用readelf来观察一下:

$ readelf -S bootsect.img

  [Nr] Name          Type         Address         Offset
     Size          EntSize        Flags  Link  Info  Align
  ...
  figure 1
  [ 1] .text         PROGBITS       00000000004000b0  000000b0
     0000000000000018  0000000000000000  AX     0     0     4
  [ 2] .data         PROGBITS       00000000006000c8  000000c8
     0000000000000200  0000000000000000  WA     0     0     4

  ...
  figure 2
  [ 1] text          PROGBITS       0000000000000000  00000040
     0000000000000014  0000000000000000         0     0     1
  [ 2] data          PROGBITS       0000000000000000  00000054

对比两个figure的输出, 能够发现Flags和Address不一样, 可是这些不一样是否真的带来了错误呢? 经过翻查文档, Flags能够在.section伪指令后加参数:.section name [, flags] 来指定; 而Address能够经过在ld后加参数:ld -section-start=name=org来指定 (-section-start=.text=org <=> -Ttext=org; -section-start=.data=org <=> -Tdata=org 如此类推).那么照这两个属性咱们来作些对比实验.

进行实验

# figure 3, based on figure 0
# bootsect.s
.section text, "x"
# ...
.section data, "w"

F3连接经过

# figure 4, based on figure 0
# bootsect.s
.section text, "ax"
# ...
.section data, "wa"

F4出现错误

# figure 5, based on figure 0
# Makefile
    ld bootsect.o -Ttext=0x0 -Tdata=0x14 -o bootsect.img

F5连接经过

# figure 6, based on figure 0
# bootsect.s
.section text, "ax"
# ...
.section data, "wa"

# Makefile
    ld bootsect.o -section-start=text=0x0 -section-start=data=0x14 -o bootsect.img

F6连接经过

# figure 7, based on figure 0
# Makefile
    ld bootsect.o -Ttext=0x60000 -Tdata=0x400000 -o bootsect.img

F7出现错误

经过实验咱们能够发现这些现象:

  • 在Flags中加上"a"就会出现错误
  • 指定的地址小于0xFFFFF就会成功, 而过大就又会出错
  • (没有列出代码) 加入了"a"Flag的段, 地址会超过0xFFFF

结论与各类解决

结合上面16位和32位的现象, 我这样解释这个错误的缘由:

  • 连接器在处理mov $msg, es这一条语句时, 须要对msg重定位
  • 考虑到es是16位寄存器, 所以采用R_X86_64_16重定位
  • .data是内建的特殊段, 专门存放数据, 所以默认具备'AW'Flags
  • elf.h中咱们看到SHF_ALLOC, 定义含A Flag的段将动态地分配内存
  • 在80x86 CPU的惯例, 它将被置于大于0xFFFF的某一个地方
  • 如本代码中, .data被分配到0x600000, 即重定位时msg将被定位到0x600000
  • R_X86_64_16没法将0x600000表示为一个WORD, 因而报错

这样对于Error1的解决方案就出来了. 方案有不少, 基本上就是围绕把两个段的内存地址降到0xFFFF如下就是了. 参考Linus的方案, 把.data取消掉方便寻址;在Makefile中加入-Ttext=0x7c00. 这样, msg就会被重定位到以0x7c00附近, 无需长跳(来更变cs)或者更改es.

# bootsect.s
.section .text
...
msg:
    .ascii "Hello, world\r\n"
...

# Makefile
    ld bootsect.o -Ttext=0x7c00 --oformat binary -o bootsect.img

这时我发现连接出来的bootsect.img远远超过512kB, 这时我回忆起刚才的readelf显示不止.text段. 它们时什么乱七八糟的啊......Elf文件头是Linux的特性, 放到bootsect里也没用, 这些东西都要删掉. 不过在这以前, 我得先搞清楚从那儿到那儿是.text.

$ readelf -S bootsect.img
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  ...
  [ 1] .text             PROGBITS         0000000000000000  00200000
       0000000000000200  0000000000000000  AX       0     0     4
  [ 2] .shstrtab         STRTAB           0000000000000000  00200200
       0000000000000021  0000000000000000           0     0     1
  ...

那就是从0x00200000 - 0x002001ff了, 恰好512字节.

$ dd if=bootsect.img of=bootsect-new.img skip=`printf "%d" 0x200000` bs=1 count=512
  512+0 records in
  512+0 records out
  512 bytes (512 B) copied, 0.00115595 s, 443 kB/s

用dd好像比较高端洋气, 可是很容易一改代码就又要同时修改命令, 由于位置是不固定的. 查了一下很容易知道只须要用ld自带的参数--oformat binary就能够输出只有.text段且没有文件头的二进制纯指令文件. 我将把其加入Makefile中.

# Makefile
    ld bootsect.o -Ttext=0x7c00 --oformat binary -o bootsect.img

如今我有一个512字节的引导扇区了, 运行make run在qemu里运行吧.

+-----------------------------------+
|                                   |
|  Booting from Floppy...           |
|  _                                |
|                                   |
+-----------------------------------+

诶? Hello, world呢?
在我检查过代码认为没错以后, 我转过头来去查资料. 看到网上的bootsect.s示例都有一句.code16以后, 我尝试把它加了进去. 而后qemu里就有东西出来了.

那也就是说默认是编译成32位代码. 那它和个人16位有什么不一样啊...回过头把elf头加进去, 而后用objdump -D bootsect.img一看...由于objdump的bug, 16位的机器码被objdump译得一团乱麻. 好比, 某些mov指令会缩写, 16位会缩写mov %ax但32位则缩写mov %eax. 略有不一样, 总的来讲也是神似.

成功以后

+-----------------------------------+
|                                   |
|  Hello, world                     |
|                                   |
|                                   |
+-----------------------------------+

看上去个人目的已经达到了. 卧槽只不过记录一下竟然写了这么长一篇东西. 今天解决的问题不少在网上在网上都是没有现成答案的, 须要靠本身来探索. 这么一来, 的确是学会了不少东西. 下面吗时最终的代码:

# bootsect.s

.global _start
.code16

.section .text

_start:
    mov $0x1301, %ax
    mov $0x000f, %bx
    mov $msg_len, %cx
    mov $msg, %bp

    int $0x10
_pause:
    jmp _pause

msg:
    .ascii "Hello, world\r\n"
    .equ msg_len, . - msg
    .org 0x200 - 2
    .word 0xaa55

# Makefile
all: bootsect.img


bootsect.img: bootsect.o
        #ld bootsect.o -Ttext=0 -o bootsect.img
        ld bootsect.o -s -Ttext=0x7c00 --oformat binary -o bootsect.img

bootsect.o: bootsect.s Makefile
        as bootsect.s -o bootsect.o

run: bootsect.img
        qemu-system-x86_64 -fda bootsect.img

clean:
        rm *.o

# for debug
rund: bootsect.img bootsect.g.img
        qemu-system-x86_64 -s -S -fda bootsect.img &
        echo target remote :1234 > gdbinit
        echo set arch i8086 >> gdbinit
        gdb -x gdbinit
        rm gdbinit
相关文章
相关标签/搜索