文章系列:
用qemu模擬Intel x86平臺實驗環境 —— 概述
用qemu模擬Intel x86平臺實驗環境 —— 啓動系統
用qemu模擬Intel x86平臺實驗環境 —— 加載並運行app
本章目標
- 我們終極目標是製作一個光盤,既能存放應用程序,又能加載應用程序並執行它,上一章已經驗證了基本的引導代碼可用,這一章主要實現加載應用程序到內存並執行的功能
- 如下圖所示,我們將引導代碼製作成固件dd到引導扇區,引導代碼的功能是加載數據區的應用程序並執行
- 本章具體工作有:
- 在FAT12文件系統的根目錄區找到指定名字的應用程序APP.bin的條目
- 根據條目找到APP.bin在數據區的位置和長度
- 加載數據區的APP.bin到內存並執行
實現原理
固件佈局
- 爲了將調試信息加到固件裏面,我們必須使用as彙編器,而as會把源文件彙編成elf格式的重定向文件,重定向文件由多個section組成,對我們有用的section只有代碼段所在section,其它的我們不需要,而且加在固件裏面bios也識別不了。因此通過as編譯出來的elf,在鏈接時只取其中的代碼所在section,生成最後的固件。
- 假設我們的源文件叫boot.S,製作固件的命令行如下
as --64 -gstabs -o boot.o boot.S
ld -o boot.bin boot.o -Tboot.ld
- 鏈接腳本如下
[root@hy c]# cat boot.ld
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64", "elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SECTIONS
{
. = 0;
.boot : {*(.s16)} // 將所有輸入文件裏面的.s16 section組織到一起,放入輸出文件的.boot segment
. = ASSERT(. <= 512, "Boot too big!");
}
- 通過
readelf -l
讀取二進制文件的佈局
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000200000 0x0000000000000000 0x0000000000000000
0x0000000000000200 0x0000000000000200 R E 200000
分析segment各個字段含義:
Offset: 0x200000,表示.boot segment在二進制文件中的偏移
FileSiz: 0x200,表示.boot segment的大小
VirtAddr: 0x00,表示程序應該加載的內存地址,爲什麼叫應該?因爲鏈接腳本中會根據VirtAddr的值重新計算源代碼中所有標號的值,除非代碼位置無關,否則如果不按照VirtAddr指定的地址加載程序,程序運行會異常
- 通過分析最終生成的二進制程序的segment,我們知道這裏面對我們有用的segment就是.boot,它距離文件開始處0x200000,大小爲0x200,因此我們需要將這段內容導出來,dd到光盤作爲固件。如下:
dd if=boot.bin ibs=512 skip=4096 of=a.img obs=512 seek=0 count=1 conv=notrunc
固件代碼實現
- 在上一章的基礎上,繼續添加代碼,實現在根目錄區找到文件名爲app.bin的entry,找到後打印"Ready." 到屏幕
讀寫磁盤
- bios接口用法
讀寫磁盤用到了bios的int 0x13中斷,解釋如下:
要初始化的參數:
ah=02
al=要讀的扇區數目
ch=柱面號(或磁道號)
cl=起始扇區號
dh=磁頭號
dl=驅動器號
es:bx=數據緩衝區(讀出的數據放到哪裏)
- 接口說明
- 扇區號與柱面號,磁頭,當前柱面的扇區號
從上面可以看出,bios接口要的不是扇區號,而是磁盤具體的磁盤物理位置參數,因此需要對扇區號加以轉換。
我們用的floppy軟盤,共2面,每面80個磁道,每個磁道劃分了18個扇區,因此總容量:
28018*512=1474560bytes,約等於1.44MB,扇區號與磁盤物理位置參數的轉換關係如下
# 設扇區號爲 x
# ┌ 柱面號 = y >> 1
# x ┌ 商 y ┤
# -----------------=> ┤ └ 磁頭號 = y & 1
# 磁道扇區數 |
# └ 餘 z => 起始扇區號 = z + 1
- 關鍵代碼
ReadSector:
pushl %ebp
movl %esp, %ebp
# 闢出兩個字節的堆棧區域保存要讀的扇區數: byte [ebp-2]
subl $2, %esp
movb %cl, -2(%ebp)
# 使用bx前保存它
pushw %bx
# bl: 除數,每磁道扇區數
movb BPB_SecPerTrk, %bl
# 商y 在 al 中, 餘數z 在 ah 中
div %bl
# z++
inc %ah
# cl <- 起始扇區號
movb %ah, %cl
# dh <- y
movb %al, %dh
# y >> 1 (y/BPB_NumHeads)
shr $1, %al
# ch <- 柱面號
movb %al, %ch
# dh & 1 = 磁頭號
and $1, %dh
# 恢復 bx
popw %bx
# 至此, "柱面號, 起始扇區, 磁頭號" 全部得到
# 驅動器號 (0 表示 A 盤)
movb BS_DrvNum, %dl
bios_read_sector:
# 讀
movb $2, %ah
# 讀 al 個扇區
movb -2(%ebp), %al
# 調用bios接口
int $0x13
jc disp_error
add $2, %esp
popl %ebp
ret
disp_error:
movb $3, %dh
call DispStr
搜索根目錄條目
- 根目錄格式
app.bin文件存放在文件系統的數據區,其元數據信息放在根目錄區,包括文件名和大小,以及文件起始的cluster號,代碼的目的就是在根目錄區找到DIR_Name域名爲app.bin的條目。
FAT12文件系統存放文件時,文件名一律爲大寫,共11字節,文件名長度不足的補空格,這裏app.bin在根目錄區條目中的文件名應該是"APP BIN"
,中間有5個空格。這裏我們創建一個假的二進制程序app.binecho "abc" >> app.bin
,用來驗證程序是否會找到這個文件。
根目錄區的內容按條目存放,每個條目長度固定32字節,格式如下:
每個域解釋如下:
實際內容:
- 關鍵代碼
movw $BaseOfLoader, %ax
# es <- BaseOfLoader
movw %ax, %es
# bx <- OffsetOfLoader
movw $OffsetOfLoader, %bx
# ax <- Root Directory 中的某 Sector 號
movw wSectorNo, %ax
movb $1, %cl
call ReadSector
# 到此,根目錄區的一個扇區被讀到了內存0x90100
# ds:si -> "APP BIN"
movw $LoaderFileName, %si
# es:di -> BaseOfLoader:0100
movw $OffsetOfLoader, %di
# 準備好要比較的字符串
# 將ds:si指向指向文件名的內存地址
# es:di存放從磁盤讀取的內容
cld
movw $0x10, %dx
label_search_for_loaderbin:
# 循環次數控制
# 一個扇區最多比較16次,因爲一次是一個條目32字節
cmp $0, %dx
# 如果已經讀完了一個 Sector,就跳到下一個 Sector
jz label_goto_next_sector_in_root_dir
dec %dx
movw $11, %cx
# 一次性比較11個字節
label_cmp_filename:
cmp $0, %cx
# 如果比較了 11 個字符都相等, 表示找到
jz label_filename_found
dec %cx
# ds:si -> al
lodsb
# es:di
cmpb %al, %es:(%di)
jz label_go_on
# 只要發現不一樣的字符就表明本 DirectoryEntry
# 不是我們要找的 APP.BIN
jmp label_different
加載app.bin
- 如果搜索順利,找到根目錄時0x90100的內存地址加載的是一個扇區,這個扇區包含的根目錄中有app.bin文件。和磁盤上的內容一樣。
這個條目偏移爲0x1a
的兩個字節,存放內容是文件開始的簇號FstCluster
,這裏是3,這個簇號ID是相對數據區的,而且數據的起始簇ID是2,所以可以知道app.bin文件數據的從數據區的第2個簇開始。
andw $0xffe0, %di 回到條目開頭
addw $0x1a, %di 偏移0x1a處
movw %es:(%di), %cx 將0x1a處的2個字節內容取出放到cx中,cx中裝的就是3
- 根據app.bin的起始簇號,找到它在FAT表對應內容。獲取文件佔用的簇號直到簇號是0xFFF,說明當前簇是文件最後的簇。找到後,打印
Ready.
movb $1, %cl
# 把app.bin文件所在的扇區讀到0x90100地址處
call ReadSector
popw %ax
# 獲取app.bin文件在FAT表中的條目,檢查什麼時候結束
call GetFATEntry
# 檢查FAT表的一項爲FFF,表示當前簇是最後一個簇
cmpw $0xfff, %ax
je label_file_loaded
# 保存 Sector 在 FAT 中的序號
pushw %ax
movw $RootDirSectors, %dx
addw %dx, %ax
addw $DeltaSectorNo, %ax
addw BPB_BytsPerSec, %bx
jmp label_goon_loading_file
label_file_loaded:
# "Ready."
movb $1, %dh
# 顯示字符串
call DispStr
- 加載FAT表的內容,首先將FAT表所在的簇拷貝到
BaseOfLoader
後面的4K空間,這個地址由es:bx
指向,app.bin的起始簇號是3,每個簇在FAT表中佔用12bit,前面有0,1,2簇佔用的12bit * 3 = 36bit,剛好在第4字節 = 36bit / 8bit,
- 關鍵代碼
GetFATEntry:
pushw %es
pushw %bx
pushw %ax
movw $BaseOfLoader, %ax
# | 在 BaseOfLoader 後面留出 4K 空間用於存放 FAT
subw $0x100, %ax
movw %ax, %es
popw %ax
movb $0, bOdd
movw $3, %bx
# dx:ax = ax * 3
mulw %bx
movw $2, %bx
# dx:ax / 2 ==> ax <- 商, dx <- 餘數
divw %bx
cmpw $0, %dx
jz label_even
movb $1, bOdd
#偶數
label_even:
# 現在 ax 中是 FATEntry 在 FAT 中的偏移量,下面來
# 計算 FATEntry 在哪個扇區中(FAT佔用不止一個扇區)
xorw %dx, %dx
movw BPB_BytsPerSec, %bx
# dx:ax / BPB_BytsPerSec
# ax <- 商 (FATEntry 所在的扇區相對於 FAT 的扇區號)
# dx <- 餘數 (FATEntry 在扇區內的偏移)
divw %bx
pushw %dx
# bx <- 0 於是, es:bx = (BaseOfLoader - 100):00
movw $0, %bx
# 此句之後的 ax 就是 FATEntry 所在的扇區號
addw $SectorNoOfFAT1, %ax
movb $2, %cl
# 讀取 FATEntry 所在的扇區,第一次肯定是從第2個扇區開始讀,
# 一次讀兩個, 避免在邊界發生錯誤
# 因爲一個 FATEntry 可能跨越兩個扇區
call ReadSector
label_read_2sector_done:
popw %dx
addw %dx, %bx
movw %es:(%bx), %ax
movb bOdd, %cl
# cmpb 指令爲sub指令,ZF值記錄結果,相等ZF=1
cmpb $1, %cl
# 如果bOdd == 1,不需要右移,跳轉到label_even_2
# 如果bOdd != 1,需要右移4bit
jne label_even_2
shrw $4, %ax
label_even_2:
andw $0xfff, %ax
label_get_fat_entry_ok:
popw %bx
popw %es
ret
- 代碼實現
my github
實驗結果
如果找到了app.bin,就在屏幕上打印"Ready."
運行./run.sh debug
的結果