第一次編寫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
相關文章
相關標籤/搜索