辅助函数
字符串打印print(es:bp)
先前在实现一个最简单的FYOS中打印字符串是通过10号中断每次打印1个字符不断循环实现完整打印,而不是一次性全部打印出
BIOS 中的字符串打印
- 指定打印参数( AX = 0x1301, BX = 0x0007 )
- 指定字符串的内存地址( ES:BP = 串地址 )
- 指定字符串的长度( CX = 串长度 )
- 中断调用( int 0x10)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ------------------------------------------------------------------ INT 0x10功能0x13 -------------------------------------------------------------- 描述: 以电传打字机的方式显示字符串 接受参数: AH 0x13 AL 显示模式 BH 视频页 BL 属性值(如果AL=0x00或0x01) CX 字符串的长度 DH,DL 屏幕上显示起始位置的行、列值 ES:BP 字符串的段:偏移地址 返回值: 无 显示模式(AL): 0x00:字符串只包含字符码,显示之后不更新光标位置,属性值在BL中 0x01:字符串只包含字符码,显示之后更新光标位置,属性值在BL中 0x02:字符串包含字符码及属性值,显示之后不更新光标位置 0x03:字符串包含字符码及属性值,显示之后更新光标位置
|
- 汇编中可以定义函数( 函数名使用标签定义 )
1.call function
2.函数体的最后一条指令为 ret
- 如果代码中定义了函数 , 那么需要自定义桟空间
1.用于保存关键寄存器的值
2.栈顶地址通过 sp 寄存器保存
- 汇编中的 “常量定义” ( equ )
用法 : Const equ 0x7c00 ; #define Const 0x7c00
与dx (db, dw,dd) 的区別 :(db, dw,dd) 定义占用相应的内存空间
equ定义不会占用任何内存空间
boot.asm如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| org 0x7c00 ; 注意主引导区的前3字节是跳转指令,不应该存储变量值 jmp short start ;短跳转,jmp占用了两字节 nop ;剩1字节用空指令填充
define: ;汇编指令是从低地址0x7c00从低向高增长 ;但是0x7c00自定义栈是从0x7c00从高向低增长并不会冲突 BaseOfStack equ 0x7c00 ;自定义栈空间的起始地址
header: ;文件头非可执行代码 BS_OEMName db "fengyun" BPB_BytsPerSec dw 512 BPB_SecPerClus db 1 BPB_RsvdSecCnt dw 1 BPB_NumFATs db 2 BPB_RootEntCnt dw 224 BPB_TotSec16 dw 2880 BPB_Media db 0xF0 BPB_FATSz16 dw 9 BPB_SecPerTrk dw 18 BPB_NumHeads dw 2 BPB_HiddSec dd 0 BPB_TotSec32 dd 0 BS_DrvNum db 0 BS_Reserved1 db 0 BS_BootSig db 0x29 BS_VolID dd 0 BS_VolLab db "F.Y.OS-0.01" BS_FileSysType db "FAT12 "
start: ;寄存器初始化操作,初始化 mov ax, cs mov ss, ax mov ds, ax mov es, ax mov sp, BaseOfStack ;栈顶偏移地址 ;调用打印字符串函数 mov bp, MsgStr ;基地址寄存器,保存了目标值地址 mov cx, MsgLen ;长度 call Print ;调用参数指定好cs:bp 与 cx
last: hlt jmp last
; 寄存器参数传递 ; es:bp --> string address ; cs --> string length Print: mov ax, 0x1301 ;指定打印参数 mov bx, 0x0007
int 0x10 ret ;汇编函数的最后一条指令必须是ret指令
MsgStr db "Hello, FYOS!" MsgLen equ ($-MsgStr)
Buf: times 510-($-$$) db 0x00 db 0x55, 0xaa
|
Print可以实现基础的字符串打印
寄存器参数传递
- es:bp –> string address
- cs –> string length
软盘读取ReadSector(逻辑扇区号ax,扇区数cx–>物理内存ES:BX)
软盘的构造
- 一个软盘有 2 个盘面 , 每个盘面对应1 个磁头
- 每一盘面被划分为若干个圆圈 ,成为柱面(磁道)
- 每一柱面被划分为若干个扇区 每个扇区 (512 字节)
3.5寸软盘的数据特性
每个盘面一共 80 个柱面( 编号为 0 - 79 )
每个柱面有 18 个扇区( 编号为 1- 18 )
存储大小 :
2 * 80 * 18 * 512 = 1474560 Bytes = 1440 KB
软盘数据读取
- 软盘数据以扇区( 512 字节 ) 为单位进行读取
- 指定数据所在位置的磁头号 , 柱面号 , 扇区号 ,指出这三元组后才可标识一个具体的物理扇区
- 计算公式:(逻辑扇区号是线性的)
BIOS 中的软盘数据读取( int 0x13,借助13号中断即可 )
功能描述:读扇区
入口参数:AH=02H
AL=扇区数(长度),CH=柱面号,CL=起始扇区号,DH=磁头号,DL=驱动器号,00H~7FH:软盘
;80H~0FFH:硬盘
ES:BX=缓冲区的地址
出口参数:CF=0——操作成功,AH=00H,AL=传输的扇区数,否则,AH=状态代码,参见功能号01H中的说明
**功能描述:软驱复位 **
入口参数:AH=0x00H
DL=驱动器号,00H~7FH:软盘
(0表示A盘,data.img就是A盘);80H~0FFH:硬盘
出口参数:CF=0——操作成功,AH=00H,否则,AH=状态代码,参见功能号01H中的说明
注意上述的入口参数,全部设置好才可调用int中断
软盘数据读取流程:
前置知识:汇编中的 16 位除法操作( div )
• 被除数放到 AX 寄存器
• 除数放到通用寄存器或内存单元( 8位 )
• 结果 : 商位于 AL , 余数位于 AH
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| ;软驱复位函数 ResetFloopy: push ax push dx
mov ah, 0x00 mov dl, [BS_DrvNum] ; mov dl, 0 FAT第0扇区中就定义了中断13号的软驱号 int 0x13 pop dx pop ax ;注意这里借助栈保存ax与dx,函数返回前应要还原 ret
; ax --> logic sector number ; cx --> number of sector ; es:bx --> target address ; 指定数据所在位置的磁头号,柱面号,扇区号 ReadSector: push ax push bx push cx push dx
call ResetFloopy
push bx push cx
mov bl, [BPB_SecPerTrk] ;柱面扇区数18 在FAT第0扇区就定义好了 div bl ; 逻辑扇区数/柱面扇区数 mov cl, ah add cl, 1 ;扇区号=余数+1 mov ch, al shr ch, 1 ;柱面号=商>>1 mov dh, al ;磁头号=商&1 and dh, 1 mov dl, [BS_DrvNum];驱动器号
pop ax ;ax = cx al是扇区的长度,要读取多少个扇区 pop bx
mov ah, 0x02 ;读取软盘的操作功能号
read: int 0x13 jc read ; CF位是否出错,若出错重新读取 pop ax pop dx pop cx pop bx ret
|
结果测试
打开data.img并且以二进制形式查看,发现先前写入软盘的文件cp test.txt /mnt/hgfs/
位于0x4400
0x4400为于34扇区的第0字节处,尝试读取是否得到此字符串
1 2 3 4 5 6 7 8
| mov ax, 34 mov cx, Buf
call ReadSector
mov bp, Buf mov cx, MsgLen call Print
|
中途出现了一个bug,解决方案:
反汇编ndisasm -o 0x7c00 boot.bin > boot.txt
,然后bochs调试设置好函数断点,检查寄存器的值是否是我们想要的,嗯,比较麻烦
内存比较MemCmp(cx长度DS:SI<–>ES:DI==>cx=0?)
先前使用了C++语言实现了在根目录区中查找目标文件,而现在只不过是换成了汇编语言来实现
内存比较
- - 指定源起始地址( DS:SI )
- - 指定目标超治祖( ES:DI )
- - 判断在期望长度(CX)内每一个字节是否都相等
汇编中的比较与跳转
1 2
| cmp cx,0 ;比较 cx 的值是否为 0 jz equal ;如果比较的结果为真,则跳转到equal标签处
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| ; 内存比较函数 ; ds:si --> source ; es:di --> destination ; cx --> length ; ; return: ; (cx == 0) ? equal : noequal MemCmp: push si push di push ax compare: ;相当于while循环 cmp cx, 0 jz equal mov al, [si] cmp al, byte [di] jz goon ;相等 jmp noequal ;跳转到不相等标签 goon: inc si inc di dec cx jmp compare
equal: noequal: pop ax pop di pop si
ret
|
根目录区查找目录项FindEntry(根目录区地址BX,目标串地址si,目标串长度cx==>dx=0?es:bx目录项地址)
从根目录区(从第19扇区开始14个扇区大小)里要先查找到目标的目录项(32字节)
此for循环前需要加载软盘的根目录区到物理内存中,借助先前实现的函数读取数据
1 2 3 4 5
| mov ax, 19 ;从第19逻辑扇区开始 mov cx, 14 ;连续读取14个扇区 mov bx, Buf ;读取至Buf中
call ReadSector
|
汇编小贴士:访问栈空间中的栈顶数据
1.不能使用 sp 直接访问栈顶数据
2.通过其它通用寄存器间接访问栈顶数据
1 2 3 4 5
| push cx mov bp, sp ; ...... mov cx, [sp] ; mov cx,[sp] is ERROR!!! ; ......
|
es:bx表示根目录区的目录项起始地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| ; es:bx --> root entry offset ; ds:si --> target string ; cx --> target length ; ; return: (dx, bx) ; (dx != 0) ? exist : noexist ; if exist ---> bx is the target entry FindEntry: push di push bp push cx
mov dx, [BPB_RootEntCnt];最大查找次数 mov bp, sp ;bp用于反复获取压入栈的cx值
find: cmp dx, 0 ;寄存器的值为0说明找不到 jz noexist mov di, bx ;bx寄存器记录根目录区中每一项的入口地址 mov cx, [bp] ; call MemCmp ; cmp cx, 0 jz exist add bx, 0x32 ;查找下一项,bx的值增加32 dec dx jmp find exist:
noexist: pop cx pop bp pop di ret
|
最后的冲刺
- 备份目标文件的目录信息( MemCpy )
- 加载 Fat 表 , 并完成 Fat 表项的查找与读取( FatVec )
内存拷贝memcpy(DS:SI,cx长度=>ES:DI)
根目录区占用14个扇区,完全没必要把整个目录信息拷贝,如果只需要找到loader程序/文件,我们仅仅需要拷贝32个字节(一个FAT文件表项的大小),因此创建EntryItem新地址,实现一个memcpy拷贝到EntryItem即可
memcpy源地址内存和目标地址内存有重叠的话,那么必须考虑拷贝方向。如果无重叠,两个方向的拷贝并无任何不同
1 2 3 4 5 6 7 8
| cmp si, di ja btoe ; if( si > di ) ; ...... ; ......
btoe: ;...... ;......
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| ; ds:si --> source ; es:di --> destination ; cx --> length MemCpy: push si push di push cx push ax cmp si, di
ja btoe add si, cx add di, cx dec si dec di jmp etob
btoe: ;begin To end cmp cx, 0 jz done mov al, [si] mov byte [di], al inc si ;si++ inc di ;di++ dec cx jmp btoe
etob: ;end To begin cmp cx, 0 jz done mov al, [si] mov byte [di], al dec si ;si-- dec di ;di-- dec cx done: pop ax pop cx pop di pop si ret
|
结果还是我们所期望的,但是位置有点奇怪,主要是之前print参数有问题,应该print开头设置dx寄存器为0.
读取FAT表项的值FatVec(FAT表地址bx,第cx个FAT项->dx)(返回所在的簇号)
将有用的32字节目录项拷贝到EntryItem后,注意FAT表一个表项1.5字节,这对于计算机该如何读取呢???我们需要转换为2字节,我们就可以将2字节存储在dx寄存器中
Fat 表中的每个表项占用 1.5 个字节 , 即 :使用3 个字节表示 2 个表项 ,我们需要关注的FatVec具体的值,而不是内存如何存储的。
Fat 表项的 “动态组装”
FatVec[j]的 “动态组装’
- j = 0, 2, 4, 6, 8,
- i = j / 2 * 3 (i,j均为整型数)
- FatVec[j] = ( (Fat[i+1] & 0x0F) << 8 ) | Fat[i];
- FatVec[j+1] = (Fat[i+2] << 4) | ( (Fat[i+1] >> 4) & 0x0F );
汇编中的 16 位乘法操作(mul)
• 被乘数放到 AL 寄存器
• 乘数放到通用寄存器或内存单元( 8位 )
• 相乘的结果放到 AX 寄存器中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| ; cx --> index ; bx --> fat table address ; ; return: ; dx --> fat[index] FatVec: mov ax, cx mov cl, 2 ;除以2看他的奇偶 div cl ;ax保存余数和商
push ax
mov ah, 0 ;先计算内存下标起始字节数 mov cx, 3 mul cx ;al*3即内存起始地址 mov cx, ax ;cx保存了fat[index]的起始内存位置index
pop ax
cmp ah, 0 ;fat下标是否偶数 jz even jmp odd
even: ; FatVec[j] = ( (Fat[i+1] & 0x0F) << 8 ) | Fat[i]; mov dx, cx add dx, 1 add dx, bx mov bp, dx mov dl, byte [bp] and dl, 0x0F shl dx, 8 add cx, bx mov bp, cx or dl, byte [bp] jmp return odd: ; FatVec[j+1] = (Fat[i+2] << 4) | ( (Fat[i+1] >> 4) & 0x0F ); mov dx, cx add dx, 2 add dx, bx mov bp, dx mov dl, byte [bp] mov dh, 0 shl dx, 4 add cx, 1 add cx, bx mov bp, cx mov cl, byte [bp] shr cl, 4 and cl, 0x0F mov ch, 0 or dx, cx
return: ret
|
FatVec函数:拿取目标文件的FAT表项信息即FAT[i],即此文件下一个的簇的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| start: mov ax, cs mov ss, ax mov ds, ax mov es, ax mov sp, BaseOfStack mov ax, RootEntryOffset mov cx, RootEntryLength mov bx, Buf call ReadSector mov si, Target mov cx, TarLen mov dx, 0 call FindEntry cmp dx, 0 jz output mov si, bx ;源地址 mov di, EntryItem ;拷贝到EntryItem mov cx, EntryItemLength call MemCpy ;拷贝到EntryItem mov ax, FatEntryLength ;ax = 9 mov cx, [BPB_BytsPerSec];cx = 512 mul cx ;计算整个fat表占用多少内存,结果保存在ax寄存器 mov bx, BaseOfLoader ;0x9000,FAT表应该在LOADER前面 sub bx, ax ;bx变为了fat表的起始位置 mov ax, FatEntryOffset ;ax逻辑扇区号 mov cx, FatEntryLength ;cx扇区数目 call ReadSector ;ax,cx 读取到--->es:bx mov cx, [EntryItem + 0x1A];目标文件起始位置 即 DIR_FstClus文件开始的簇号 call FatVec ;拿取目标文件的FAT表项信息即FAT[i]下一个文件开始的簇的地址 bx[cx]-->dx jmp last output: mov bp, MsgStr mov cx, MsgLen call Print last: hlt jmp last
|
加载LOADER程序
内存布局
首先看一下目前运行内存的使用情况
- 内存0x7c00-0x7e00处是BIOS将data.img中的boot.bin载入到内存(boot.bin是由boot.asm编译生成,dd命令写入到data.img第0扇区中,512字节大小的主引导程序)
- 内存中0x7e00-0x9000主引导程序中将data.img软盘中的整个fat表读取进入内存
- 内存0x9000-….是loader文件,loader文件可以通过mount挂载到/mnt/hgfs下后写入data.img内
- 内存0x0000-0x7c00是栈空间,用于函数调用的压栈和出栈
想要加载loader文件内容到内存中,肯定得借助loader的目录项【EntryItem32字节大小】和FAT表
根目录 区中的条目格式:
名称 |
偏移 |
长度 |
描述 |
DIR_Name |
0 |
0xB |
文件名8字节,扩展名3字节 |
DIR_Attr |
0xB |
1 |
文件属性 |
保留位 |
0xC |
10 |
保留位 |
DIR_WrtTime |
0x16 |
2 |
最后一次写入时间 |
DIR_WrtDate |
0x18 |
2 |
最后一次写入日期 |
DIR_FstClus |
0x1A |
2 |
此条目对应的开始簇号 |
DIR_FileSize |
0x1C |
4 |
文件大小 |
比较重要的就是DIR_FstClus这一项(base+0x1A),它告诉我们文件存放在磁盘的什么位置,从而让我们可以找到它。软盘中一簇只包含一个扇区。数据区的第一个簇的簇号是2,而不是0或者1。
通过FAT表加载文件内容
bx寄存器指向FAT表,bp+0x1A使得bp指向目标文件所在的第一个簇DIR_FstClus,存储在dx,si指向loader要加载的物理内存地址
开始循环,当dx等于0xFF7代表已经读完了。
逻辑扇区号指定位dx+31(因为FAT表项值+33-2得到正确的位置),指定好参数调用ReadSector读取到物理内存es:si当中,然后通过当前扇区号callfac获取下一个扇区号;si+512,物理内存指针应要+512(即一个扇区的大小)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| start: mov ax, cs mov ss, ax mov ds, ax mov es, ax mov sp, BaseOfStack mov ax, RootEntryOffset mov cx, RootEntryLength mov bx, Buf call ReadSector mov si, Target mov cx, TarLen mov dx, 0 call FindEntry cmp dx, 0 jz output mov si, bx ;源地址 mov di, EntryItem ;拷贝到EntryItem mov cx, EntryItemLength call MemCpy ;拷贝到EntryItem mov ax, FatEntryLength ;ax = 9 mov cx, [BPB_BytsPerSec];cx = 512 mul cx ;计算整个fat表占用多少内存,结果保存在ax寄存器 mov bx, BaseOfLoader ;0x9000,FAT表应该在LOADER前面 sub bx, ax ;bx变为了fat表的起始位置0x7e00 mov ax, FatEntryOffset ;ax逻辑扇区号 mov cx, FatEntryLength ;cx扇区数目 call ReadSector ;ax,cx 读取到--->es:bx,0x7e00 mov dx, [EntryItem + 0x1A];目标文件起始位置 即 DIR_FstClus文件开始的簇号 mov si, BaseOfLoader loading: mov ax, dx add ax, 31 ; ax + 33 - 2 第33个扇区开始是文件数据区,头2个FAT表项不使用 mov cx, 1 push dx push bx mov bx, si ; 读取到BaseOfLoader 0x9000 call ReadSector pop bx pop cx call FatVec ; 读取FAT表项 FAT[cx] cmp dx, 0xFF7 jnb BaseOfLoader add si, 512 jmp loading
|
验证加载文件程序是否正确
实验步骤
- 在虚拟软盘中创建体积较大的文本文件(Loader)
- 将 Loader 的内容加载到 BaseOfLoader 地址处
- 打印 Loader 中的文本(判断加载是否完全)
然而非常不幸,代码出错了,原因是主引导程序超过了512字节,因此考虑把函数的push和pop操作删除,原先push和pop存在的原因是因为为了保存函数前寄存器的上下文状态,不对他进行改变,但是为了程序能继续下去,决定删除一些代码
修改后,还是能看到loader.bin的内容,还是比较成功的
实现LOADER程序
第一个 Loader 程序
起始地址 0x9000 ( org 0x9000 )
通过 int 0x10 在屏幕上打印字符串
零标志位ZF:
- 判断最近的那一次运算的结果是否为 0
- 当运算的结果为 0 时 , ZF 位的值为 1
1 2 3 4 5 6
| mov ax, 1 cmp ax, 1 ;实际减法操作,zf位会被置为1 jz zf_is_one ;.... zf_is_one: ;.....
|
jxx 代表了j指令族 , 功能是根据标志位进行调整
- jo 当 OF 为 1 则跳转
- jc 当 CF 为 1 则跳转
- jns 当 SF 不为 1 则跳转
- jz 当 ZF 为1则跳转
- je 比较结果为相等则跳转( 即 : jz )
建立一个简单的loader.asm
注意一定要有org 0x9000
,将loader.bin加载到内存0x9000处
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| org 0x9000
begin: mov si, msg
print: mov al, [si] add si, 1 cmp al, 0x00 je end mov ah, 0x0E mov bx, 0x0F int 0x10 jmp print
end: hlt jmp end msg: db 0x0a, 0x0a db "Hello, fengyun!" db 0x0a, 0x0a db 0x00
|
将loader程序进行挂载到data.img
1 2 3 4
| fengyun@ubuntu:~/share/os$ nasm loader.asm -o loader fengyun@ubuntu:~/share/os$ sudo mount -o loop data.img /mnt/hgfs fengyun@ubuntu:~/share/os$ sudo cp loader /mnt/hgfs/ fengyun@ubuntu:~/share/os$ sudo umount /mnt/hgfs
|
运行结果确实是我们所期望的
结论
- Boot 需要进行重构保证在 512 字节内完成功能
- 在汇编程序中尽量确保函数调用前后通用寄存器的状态不变
- Boot 成功加载 Loader 后将控制权转移
- Loader 程序没有代码体积上的限制