文章系列:
用qemu模拟Intel x86平台实验环境 —— 概述
用qemu模拟Intel x86平台实验环境 —— 启动系统
用qemu模拟Intel x86平台实验环境 —— 加载并运行app
本章目标
- 我们的大目标是制作一个光盘,该光盘实现两个功能:
- 存放引导代码,用来加载应用程序到内存,并跳转到应用程序处执行程序
- 存放应用程序,这个引用程序是作为一个文件放到光盘中
- 为了实现这个目标,本章作两个预备工作:
- 制作一个光盘并格式化成文件系统,可以存放应用程序,我们选取FAT12格式的文件系统
- 在格式化成文件系统后的光盘的第一个扇区中,写入引导代码,这段引导代码将用作加载应用程序到内存,本章暂时不作,只实现点亮屏幕的功能。
实现原理
文件存放
- 要存放文件,通常做法是把存储介质格式化成操作系统可以识别的文件系统,比如windows的
FAT12/16/32,ExFAT,VFAT,NTFS
或者linux的ext2/3/4
等。我们采用同样的做法,首先制作一个空的磁盘介质,然后将其格式化成FAT12文件系统,如下:
dd if=/dev/zero of=floopy bs=512 count=2880
losetup /dev/loop0 floopy
mkdosfs -F 12 /dev/loop0
losetup -d /dev/loop0
- 查看其内容,前512字节是一个引导扇区(Boot Sector),前62字节是BPB的结构,BPB数据结构的字段是windows文件系统和操作系统约定好的,用于描述磁盘的物理布局,可以适用于FAT系列文件系统和NTFS文件系统。这里我们看到最后的文件系统类型为FAT12
- linux下的也有自己引导扇区,叫做MBR,引导扇区里是自己设计的数据结构,比如分区表。
引导原理
- 光盘格式化之后,挂载光盘,就可以拷贝应用程序到文件系统了,但这个光盘只有存放文件的功能,还不能引导代码,因此我们需要把引导代码也拷贝到引导扇区中。这里有两种方法,一种是保留格式化时工具留下的BPB数据结构信息,只将引导代码dd到62自己之后,另一种就是将让引导代码中包含BPB数据结构,dd引导代码的时候从偏移0开始,直接覆盖BPB数据结构。我们选用者。
- 上图描述了FAT12文件系统引导扇区的数据结构。
具体实现
- BPB(BIOS Parameter Block)
# BS_jmpBoot 短跳转指令
jmp label_init
nop
# BS_OEMName 厂商名字
.ascii "ForrestY"
# BS_BytsPerSec 每扇区字节数
.word 512
# BPB_SecPerClus 每簇扇区数
.byte 1
# BPB_RsvdSecCnt 引导记录(MBR)占用的扇区数
.word 1
# PBP_NumFATs FAT表数目
.byte 2
# PBP_RootEntCnt 根目录文件最大数
.word 0xe0
# PBP_TotSec16 扇区总数
.word 2880
# PBP_Media 介质描述符
.byte 0xf0
# PBP_FATSz16 每FAT扇区数
.word 9
# PBP_SecPerTrk 每磁道扇区数
.word 18
# PBP_NumHeads 磁头数
.word 2
# PBP_HiddSec 隐藏扇区数
.long 0
# PBP_TotSec32 如果PBP_TotSec16为0,该域记录扇区数
.long 0
# BS_DrvNum 中断13的驱动器号
.byte 0
# BS_Reserved1 未使用
.byte 0
# BS_BootSig 扩展引导标记
.byte 0x29
# 卷序列号
.long 0
# 卷标
.ascii "VirtualBoot"
# 文件系统类型
.ascii "FAT12 "
- 引导代码,点亮屏幕
label_init:
movw $0x7c0, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
movw $0x180, %sp
call DispStr
loop:
jmp loop
DispStr:
movw $BootMsg, %ax
movw %ax, %bp
movw $16, %cx
movw $0x1301, %ax
movw $0xc, %bx
movb $0, %dl
int $0x10
ret
BootMsg:
.ascii "Hello, OS World!"
.org 510
.word 0xAA55
实验结果
引导固件
引导固件包含了BPB数据结构和引导代码两部分,名为boot.bin
- 写一个run.sh的脚本,方便测试:
[root@hy b]# cat run.sh
#!/bin/bash
DEBUG="false"
PWD="$(cd `dirname $0`;pwd)"
[ $# -eq 1 ] && [ "X$1" == "X-h" ] && echo "$(basename $0) debug --for debug" && exit 0
[ $# -eq 1 ] && [ "X$1" == "Xdebug" ] && DEBUG="true"
if [ "X$DEBUG" == "Xtrue" ]; then
echo "waiting for connect gdb server..."
qemu-system-x86_64 -machine pc-i440fx-4.0 -m 2G -smp 2,sockets=2,cores=1,threads=1 \
-boot strict=on -drive file=$PWD/a.img,format=raw,if=none,id=drive-fdc0-0-0 \
-global isa-fdc.driveA=drive-fdc0-0-0 -global isa-fdc.bootindexA=1 -S -s
else
qemu-system-x86_64 -machine pc-i440fx-4.0 -m 2G -smp 2,sockets=2,cores=1,threads=1 \
-boot strict=on -drive file=$PWD/a.img,format=raw,if=none,id=drive-fdc0-0-0 \
-global isa-fdc.driveA=drive-fdc0-0-0 -global isa-fdc.bootindexA=1
fi
- make后,运行
run.sh
- 运行
run.sh debug
,qemu启动后暂停,等待gdb连接
- 连接gdbserver,可以在0x7c00处断住,查看寄存器信息等
工具格式化和固件格式化对比
- 拷贝之前,使用mkdofs工具已经将a.img格式化成了FAT12文件系统。可以看到前62字节的BPB数据结构。
- dd命令写入固件后,格式化引导扇区
dd if=boot.bin ibs=512 skip=4096 of=a.img obs=512 seek=0 count=1 conv=notrunc
后,可以看到BPB结构的字段有了变化,说明BPB信息被覆盖了,并且BPB之后多了数据,这是引导代码。
- 拷贝测试文件test.txt,可以看到,重新被覆盖的BPB数据结构,仍然可以被操作系统识别,作为文件系统。拥有正常的文件保存功能。
下一步工作
-
根据根目录表项和FAT表找到特定文件名的文件
分析test.txt文件的位置: 引导扇区中定义了根目录条目最大数0xe0=224,每个根目录条目大小32 byte,根目录所占扇区数
RootDirSectors = ((PBP_RootEntCnt * 32) + (BS_BytsPerSec - 1)) / BS_BytsPerSec = (224 * 32) + 511 / 512 = 14。
FAT12文件系统布局如上图,可以看到引导扇区+ FAT1 + FAT2一共占用了1+9+9 = 19个扇区,根目录占了14个扇区,因此数据区从第19+14=33个扇区开始,开始于第33个扇区,偏移33 * 512 = 0x4200。注意数据区的第一个簇的号是2,不是0或者1。
从根目录表项中看到,test.txt的文件内容从簇号3开始,对应数据区的第二个簇号,偏移0x4400。可以对上。
再看FAT1表的位图F0 FF FF 00 F0 FF
,它表示文件的下一个簇号。
前三个字节是簇号0和簇号1的位图,忽略entry_2 = 0x000 // 簇号2中没有文件
entry_3 = 0xFFF // 簇号3中文件的下一个簇号是0xFFF,这是特殊簇号,表示这个簇是文件的最后一个簇。
现在,用xxd查看镜像文件,根据FAT12文件系统规范,可以通过根目录的表项和FAT表找到文件在镜像中的偏移和占用簇的数目,下一章,介绍怎么在boot.bin的代码中找到这个test.txt的文件。
完整代码见 my github