这记录了我,一个汇编盲+作业系统盲+硬件盲一次解决问题的过程。
头一次写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):
- ah = 0x13: 视讯功能中断
- al = 0x01: 写入字符后更新光标位置
- bl = 0x0F: 高四位是背景色(0为黑), 低四位是字体色(F为白)
- cx = msg_len: 字串长度
- 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