本章目标
- 我们终极目标是制作一个光盘,既能存放应用程序,又能加载应用程序并执行它,上一章已经验证了基本的引导代码可用,这一章主要实现加载应用程序到内存并执行的功能
- 如下图所示,我们将引导代码制作成固件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
的结果