1

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

头一次写bootsect

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

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

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

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

  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:

# 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之旅.

大量错误QAQ

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

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

从错误信息里几乎没有获得什么修改方案, 但也有两点引起了我的注意:

  • 16: 为何链接器会自动采用16位重定位?
    顺着这个思路, 我决定把所有寄存器改成32位, 也就是加e试试看.

    # 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

Shihira
1.1k 声望43 粉丝

桜の花が舞う あの日のように