用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的结果
在这里插入图片描述

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