image-20220503233644366

辅助函数

字符串打印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:字符串包含字符码及属性值,显示之后更新光标位置

image-20220501165528146

  • 汇编中可以定义函数( 函数名使用标签定义 )
    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 字节)

image-20220501211106555

3.5寸软盘的数据特性

  • 每个盘面一共 80 个柱面( 编号为 0 - 79 )

  • 每个柱面有 18 个扇区( 编号为 1- 18 )

  • 存储大小 :
    2 * 80 * 18 * 512 = 1474560 Bytes = 1440 KB

软盘数据读取

  • 软盘数据以扇区( 512 字节 ) 为单位进行读取
  • 指定数据所在位置的磁头号 , 柱面号 , 扇区号 ,指出这三元组后才可标识一个具体的物理扇区
  • 计算公式:(逻辑扇区号是线性的)
    image-20220501211250084

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中断

软盘数据读取流程:

image-20220501211913935

前置知识:汇编中的 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

image-20220501215302890

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

image-20220501215241571

中途出现了一个bug,解决方案:
反汇编ndisasm -o 0x7c00 boot.bin > boot.txt,然后bochs调试设置好函数断点,检查寄存器的值是否是我们想要的,嗯,比较麻烦

内存比较MemCmp(cx长度DS:SI<–>ES:DI==>cx=0?)

先前使用了C++语言实现了在根目录区中查找目标文件,而现在只不过是换成了汇编语言来实现

内存比较

  • - 指定源起始地址( DS:SI )
  • - 指定目标超治祖( ES:DI )
  • - 判断在期望长度(CX)内每一个字节是否都相等

image-20220502141103793

汇编中的比较与跳转

1
2
cmp cx,0 ;比较 cx 的值是否为 0
jz equal ;如果比较的结果为真,则跳转到equal标签处

image-20220502141631057

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字节)

image-20220502144832150

此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即可

image-20220503220542080

memcpy源地址内存和目标地址内存有重叠的话,那么必须考虑拷贝方向。如果无重叠,两个方向的拷贝并无任何不同

image-20220503220633252

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

image-20220503225649294

结果还是我们所期望的,但是位置有点奇怪,主要是之前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具体的值,而不是内存如何存储的。

image-20220503230110532

Fat 表项的 “动态组装”
image-20220503230232258

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程序

内存布局

首先看一下目前运行内存的使用情况

image-20220504203156478

  1. 内存0x7c00-0x7e00处是BIOS将data.img中的boot.bin载入到内存(boot.bin是由boot.asm编译生成,dd命令写入到data.img第0扇区中,512字节大小的主引导程序)
  2. 内存中0x7e00-0x9000主引导程序中将data.img软盘中的整个fat表读取进入内存
  3. 内存0x9000-….是loader文件,loader文件可以通过mount挂载到/mnt/hgfs下后写入data.img内
  4. 内存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表加载文件内容

image-20230216111546565

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

验证加载文件程序是否正确

实验步骤

  1. 在虚拟软盘中创建体积较大的文本文件(Loader)
  2. 将 Loader 的内容加载到 BaseOfLoader 地址处
  3. 打印 Loader 中的文本(判断加载是否完全)

然而非常不幸,代码出错了,原因是主引导程序超过了512字节,因此考虑把函数的push和pop操作删除,原先push和pop存在的原因是因为为了保存函数前寄存器的上下文状态,不对他进行改变,但是为了程序能继续下去,决定删除一些代码

image-20220504215851513

修改后,还是能看到loader.bin的内容,还是比较成功的

image-20220504221712745

实现LOADER程序

第一个 Loader 程序
起始地址 0x9000 ( org 0x9000 )
通过 int 0x10 在屏幕上打印字符串

零标志位ZF:

  • 判断最近的那一次运算的结果是否为 0
  • 当运算的结果为 0 时 , ZF 位的值为 1

image-20220504222111906

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

运行结果确实是我们所期望的

image-20220504225547364

结论

  • Boot 需要进行重构保证在 512 字节内完成功能
  • 在汇编程序中尽量确保函数调用前后通用寄存器的状态不变
  • Boot 成功加载 Loader 后将控制权转移
  • Loader 程序没有代码体积上的限制