用qemu模擬Intel x86平臺實驗環境 —— 加載並運行app

文章系列:
用qemu模擬Intel x86平臺實驗環境 —— 概述
用qemu模擬Intel x86平臺實驗環境 —— 啓動系統
用qemu模擬Intel x86平臺實驗環境 —— 加載並運行app

本章目標

  • 我們終極目標是製作一個光盤,既能存放應用程序,又能加載應用程序並執行它,上一章已經驗證了基本的引導代碼可用,這一章主要實現加載應用程序到內存並執行的功能
  • 如下圖所示,我們將引導代碼製作成固件dd到引導扇區,引導代碼的功能是加載數據區的應用程序並執行
  • 本章具體工作有:
  1. 在FAT12文件系統的根目錄區找到指定名字的應用程序APP.bin的條目
  2. 根據條目找到APP.bin在數據區的位置和長度
  3. 加載數據區的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

實驗結果

如果找到了app.bin,就在屏幕上打印"Ready."
運行./run.sh debug的結果
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章