这记录了我,一个汇编盲+做业系统盲+硬件盲一次解决问题的过程。html
这是要写一个bootsect, 就是传说中的引导扇区, 软盘的头512个字节, 0xaa55结尾, BIOS在启动后自动把它加载到内存的0x7c00而后开始执行, 这是我仅有关于它的知识了. 我但愿在它启动以后能在屏幕上印上一个"Hello, world!\r\n"就是了.linux
查了查Wikpedia,发如今刚加电启动还在实模式的时候有两种方法现实:字体
看上去BIOS要简单点,那就搞这个吧. 先去Ralf的表里查查中断的用法, 我决定设置这些寄存器(一开始它们都是0):spa
# 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
首先我遇到了连接时的错误: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出现错误
经过实验咱们能够发现这些现象:
结合上面16位和32位的现象, 我这样解释这个错误的缘由:
mov $msg, es
这一条语句时, 须要对msg重定位R_X86_64_16
重定位.data
是内建的特殊段, 专门存放数据, 所以默认具备'AW'FlagsSHF_ALLOC
, 定义含A Flag的段将动态地分配内存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