這記錄了我,一個彙編盲+做業系統盲+硬件盲一次解決問題的過程。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