《操作系统真象还原》第5章
预告一下,本章很难啃,分页机制复杂,且到了最后随着内核代码的扩张很容易在调试过程中迷失,所以要格外仔细。
本章内容较多,大家看书本吧,就不把内容再抄录一遍了。
但还是要放几张图片:
步骤:
1.获取物理内存容量
2.启动分页机制
3.加载内核
1.获取物理内存容量
直接上手修改loader.S:
注意与书中不同:
①因为我们的gdt_base=0xc0000903,所以第18、19行(书中第20行),应该为:
times 59 dq 0
times 5 db 0
所以:
1 %include "boot.inc"
2 SECTION LOADER vstart=LOADER_BASE_ADDR ;同书上,设置为0x900
3 LOADER_STACK_TOP equ LOADER_BASE_ADDR ;初始化栈顶,0x900向下为栈空间
4 jmp loader_start
5
6 ;构建GDT及其内部的描述符
7 GDT_BASE: dd 0x00000000 ;没用的第0个段描述符
8 dd 0x00000000
9 CODE_DESC: dd 0x0000FFFF
10 dd DESC_CODE_HIGH4
11 DATA_STACK_DESE: dd 0x0000FFFF
12 dd DESC_DATA_HIGH4
13 VIDEO_DESC: dd 0x80000007 ;limit=(0xbffff-0xb8000)/4k=0x7
14 dd DESC_VIDEO_HIGH4 ;此时DPL为0
15 GDT_SIZE equ $-GDT_BASE ;地址差作尺寸:当前行地址-GDT_BASE地址
16 GDT_LIMIT equ GDT_SIZE-1
17
18 times 59 dq 0 ;dq 定义4字/8字节
19 times 5 db 0
20
21 total_mem_bytes dd 0
22 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
23 SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL0
24 ;相当于(CODE_DESC-GDT_BASE)/8+TI_GDT+RPL0
25 SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL0
26 SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0
27
28 ;以下是GDT的指针GDTR,6B/48bit,前2字节是GDT界限,后4字节是GDT起始地址
29 gdt_ptr dw GDT_LIMIT
30 dd GDT_BASE
31
32 ards_buf times 244 db 0
33 ards_nr dw 0
34
35 loader_start:
36
37 ;int 15h eax=0000E820h, edx=534D4150h('SMAP'的ASCII码)获取内存布局
38 xor ebx,ebx ;第一次调用时,ebx要为0
39 mov edx,0x534d4150 ;edx只赋值一次,循环体中不会改变,用于签名校验
40 mov di,ards_buf ;ards结构缓冲区
41 .e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构
42 mov eax,0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子>功能号
43 mov ecx,20 ;ARDS地址范围描述符结构大小是20字节
44 int 0x15 ;0x15为获取内存容量的中断号
45 jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能
46 add di,cx ;使di增加20字节指向缓冲区中新的ARDS结构位置
47 inc word [ards_nr] ;记录ARDS数量
48 cmp ebx,0 ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
49
50 jnz .e820_mem_get_loop
51
52 ;在所有ards结构中找出(base_add_low+length_low)的最大值,即内存的容量
53 mov cx,[ards_nr] ;遍历每个ARDS结构体,循环次数是ARDS的数量
54 mov ebx,ards_buf
55 xor edx,edx ;此后edx用于记录最大内存容量,在此先请0
56 .find_max_mem_area: ;无需判断type是否为1,最大的内存块一定是可以被使用的
57 mov eax,[ebx] ;base_add_low
58 add eax,[ebx+8] ;length_low
59 add ebx,20 ;指向缓冲区下一个ARDS结构
60 cmp edx,eax ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
61 jge .next_ards
62 mov edx,eax ;edx为总内存大小
63 .next_ards:
64 loop .find_max_mem_area
65 jmp .mem_get_ok
66
67 ;----- int 15h ax=E801h 获取内存大小,最大支持4G -----
68 ;返回后,ax cx值一样,以KB为单位,bx dx值一样,以64KB为单位
69 ;在ax和cx寄存器中为低16MB,在bx和dx寄存器中为16MB到4GB
70 .e820_failed_so_try_e801:
71 mov ax,0xe801
72 int 0x15
73 jc .e801_failed_so_try88 ;若当前e801方法也失败,则尝试0x88方法
74
75 ;1.先算出低15MB内存。ax cx是以KB为单位的内存数量,将其转换为以Byte为单位
76 mov cx,0x400 ;0x400十进制为1K。cx和ax一样,cx用作乘数
77 mul cx
78 shl edx,16
79 and eax,0x0000FFFF
80 or edx,eax
81 add edx,0x100000 ;ax只是15MB,故要追加1MB
82 mov esi,edx ;先把低15MB内存容量存入esi寄存器备份
83
84 ;2.再将16MB以上的内存转换为Byte为单位。bx dx是以64KB为单位的内存数量
85 xor eax,eax
86 mov ax,bx
87 mov ecx,0x10000 ;0x10000十进制为64KB
88 mul ecx ;32位乘法,默认的被乘数是eax,积为64位。高32位存入edx,低32位存入eax
89 add esi,eax ;4GB内存,edx为0,低32位足矣
90 mov edx,esi ;edx为总内存大小
91 jmp .mem_get_ok
92
93 ;----- int 15h ah=0x88获取内存大小,只能获取64MB之内 -----
94 .e801_failed_so_try88:
95 ;int 15h后,ax存入的是以KB为单位的内存容量
96 mov ah,0x88
97 int 0x15
98 jc .error_hlt
99 and eax,0x0000FFFF
100
101 ;16位乘法,被乘数是ax,积为32位。积的高16位在dx中,低16位在ax中
102 mov cx,0x400
103 mul cx
104 shl edx,16 ;把edx移动高16位
105 or edx,eax ;把低16位组合到edx,即为32为积
106 add edx,0x100000 ;0x88只会返回1MB以上的内存,故实际内存还要加上1MB
107
108 .error_hlt:
109 jmp $
110
111 .mem_get_ok:
112 mov [total_mem_bytes],edx ;将内存换位Byte单位后存入total_mem_bytes处
113
114 ;--------------------- 准备进入保护模式 ------------------------
115 ;1 打开A20
116 ;2 加载GDT
117 ;3 将cr0的PE位置1
118
119 ;--------------------------- 打开A20 ---------------------------
120 in al,0x92
121 or al,0000_0010B ;简单说,将端口0x92的第1位置1即可
122 out 0x92,al
123
124 ;--------------------------- 加载GDT ---------------------------
125 lgdt [gdt_ptr] ;load GDT [addr]
126
127 ;-------------------------- cr0第0位置1 ------------------------
128 mov eax,cr0
129 or eax,0x00000001
130 mov cr0,eax
131
132 jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线。因为要远转移,cs更新,所>以流水线上的其它指令都没用了,就会刷新
133
134 [bits 32] ;开启32位指令
135 p_mode_start:
136 mov ax,SELECTOR_DATA
137 mov ds,ax
138 mov es,ax
139 mov ss,ax
140 mov esp,LOADER_STACK_TOP
141 mov ax,SELECTOR_VIDEO
142 mov gs,ax
143
144 mov byte [gs:160],'P' ;第2行首字符打印P
145
146 jmp $
然后是:
nasm -I include/ -o loader.bin loader.S
考虑到上一章所犯的错误,这次我们要看一眼loader.bin的大小,再确定要更改的盘数。
输入:
ls -l
得到:
很好,loader.bin只有923B,没有超过2*512B,所以我们和上章一样,还是更改两个盘,count=2:
dd if=/home/zbb/bochs/loader.bin of=/home/zbb/bochs/hd60M.img bs=512 count=2 seek=2 conv=notrunc
运行结果为:
没问题,0xb00处的内容为0x02000000,与记忆中设置的内存大小相等。
如果你忘记了设置的内存多大,输入:
cat -n bochsrc.disk
即可查看到当前设置的内存:
32MB,没问题,与0x0200000相等。
如果你没有修改书中的错误,就会遗憾的发现:
寄!
用bochs调试发现:
变量total_mem_bytes并不在0xb00处,所以才需要我们修改一下。
好吧,其实我完成这一步耗时更久的地方在第46行,add我写成了and,所以显示的内存容量始终不对。。。有的时候真的感觉自己很蠢。
2.启动分页机制
首先要准备页目录和页表
先修改include/boot.inc吧,增加两处内容:
然后是对于loader.S,增加部分内容如下:
1 ;创建页目录及页表并初始化页内存位图
2 call setup_page
3 ;要将描述符表地址及偏移量写入内存gdt_ptr,一会儿用新地址重新加载
4 sgdt [gdt_ptr] ;存储到原来gdt所在的位置
5
6 mov ebx,[gdt_ptr+2]
7 or dword [ebx+0x18+4],0xc0000000
8
9 add dword [gdt_ptr+2],0xc0000000
10 add esp,0xc0000000
11
12 mov eax,PAGE_DIR_TABLE_POS
13 mov cr3,eax
14 mov eax,cr0
15 or eax,0x80000000
16 mov cr0,eax
17
18 lgdt [gdt_ptr]
19
20 mov byte [gs:160],'V'
21
22 jmp $
和
1 ;------------------- 建立页表 -------------------
2 setup_page:
3 mov ecx,4096
4 mov esi,0
5 .clear_page_dir:
6 mov byte [PAGE_DIR_TABLE_POS+esi],0
7 inc esi
8 loop .clear_page_dir
9
10 ;创建页目录项(PDE)
11 .create_pde:
12 mov eax,PAGE_DIR_TABLE_POS
13 add eax,0x1000 ;此时的eax为第一个页表的物理地址
14 mov ebx,eax ;ebx=eax,为后续的.create_pte做准备,ebx为基址
15
16 ;下面将偏移地址0x0(第1个)和0xc00(第768个页目录项)存为第1个页表的地址,每个页表表示4MB内存
17 or eax,PG_US_U|PG_RW_W|PG_P ;最低特权级|可读写|存在
18 mov [PAGE_DIR_TABLE_POS+0x0],eax ;第1个页目录项
19 mov [PAGE_DIR_TABLE_POS+0xc00],eax ;第768个页目录项
20 sub eax,0x1000
21 mov [PAGE_DIR_TABLE_POS+4092],eax ;最后一个页目录项指向页目录自己
22
23 ;创建页表项(PTE)
24 mov ecx,256 ;对低端内存1MB建页表:1MB/4KB=256(256个页表项,1个页表足矣)
25 mov esi,0
26 mov edx,PG_US_U|PG_RW_W|PG_P ;最低特权第|可读写|存在
27 .create_pte:
28 mov [ebx+esi*4],edx ;逐个页表项设置
29 add edx,4096 ;因为1个页表4KB,所以edx的基址+4KB
30 inc esi
31 loop .create_pte
32
33 ;创建内核其它页表的PDE
34 mov eax,PAGE_DIR_TABLE_POS
35 add eax,0x2000 ;此时的eax为第二个页表的物理地址
36 or eax,PG_US_U|PG_RW_W|PG_P ;最低特权级|可读写|存在
37 mov ebx,PAGE_DIR_TABLE_POS
38 mov ecx,254
39 mov esi,769
40 .create_kernel_pde:
41 mov [ebx+esi*4],eax ;将第2个~第256个页表的地址逐个存入页表项
42 inc esi
43 add eax,0x1000 ;下一个页表的地址
44 loop .create_kernel_pde
45
46 ret
如果搞不明白代码,可以多画图,将页目录(项)和每个页表(项)的所在物理地址和其保存的地址画出来看看。
查看loader.bin文件大小:
好吧,所以我们的接下来的命令是:
nasm -I include/ -o loader.bin loader.S
dd if=/home/zbb/bochs/loader.bin of=/home/zbb/bochs/hd60M.img bs=512 count=3 seek=2 conv=notrunc
bin/bochs -f bochsrc.disk
最后成功:
3.加载内核
首先编写内核程序main.c,不过有个前置操作——新创建一个文件夹吧,里面存放与内核有关的各种文件:
然后cd kernel,在该文件夹中vim main.c:
int main()
{
while(1);
return 0;
}
先尝试一下罢了,所以main.c简陋一些。
保存后cd回到bochs下(当然不退出也完全可以,只要路径对就ok),将main.c编译链接复制到磁盘上:
gcc -c -o kernel/main.o kernel/main.c
ld kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin
dd if=/home/zbb/bochs/kernel/kernel.bin of=/home/zbb/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
可能你会怀疑第二个命令,输出文件的文件名是不是写错了,怎么会是kernel.bin,不是main.bin吗?好吧,我又看了下书本,没问题,就是kernel.bin。
seek为9,目的是跨过前9个扇区(第0~8个)扇区,我们在第9个扇区写入。一是为了loader万一哪天要扩展,得预留出磁盘空间,二是作者乐意,我也还行。。。
count为200,目的是一次往参数of指定的文件中写入200个扇区。我们干脆一步到位,因为我们将来的内核大小不会超过100KB,所以直接把count改为200块扇区。
(哈哈,看书中作者描述他自己也因为没有及时更改count而深受其苦,幸灾乐祸ing)
用file命令检查一下main.o状态,得到:
用nm命令查看符号地址,得到:
内核是由loader加载的,所以还要修改loader.S:
有两步要做:
- 加载内核
- 初始化内核
先做第一个:
1 mov eax,KERNEL_START_SECTOR ;kernel.bin所在扇区号
2 mov ebx,KERNEL_BIN_BASE_ADDR;从磁盘读出后,写入到ebx指定的地址
3 mov ecx,200 ;读入的扇区数
4
5 call rd_disk_m_32
6
7 ;创建页目录及页表并初始化页内存位图
8 call setup_page
看最后一行你应该知道要把这段代码放在哪里了。
因为count=200,所以直接把要读入的扇区数ecx也设置成200好了。rd_disk_m_32与rd_disk_m_16相比只是版本由16位变成了32位,因为实模式变成了保护模式。
然后肯定还要修改include/boot.inc:
(偷个懒,就不一步步修改了,直接把本章所有的修改都放上去吧,顺便强迫症对齐一下格式)
添加KERNEL_START_SECTOR和KERNEL_BIN_BASE_ADDR等。
至于为什么把内核文件kernel.bin加载到0x70000处,给大家看张书中的图:
还记得之前的内存布局吗,途中打勾的空间就是现在可用的部分(是的,MBR已经不需要了,悲),然后就随便找个高地址(因为将来内核文件还会经过loader解析生成内核映像覆盖原来的内核文件hernel.bin)吧——就你了0x70000。
插播一段:根据最后给出的博客博主的提示,我们需要将gcc的版本降级至gcc 4版本,为大家直接贴出链接吧:
ubuntu 16.04 gcc高低版本切换_总会习惯的博客-CSDN博客_ubuntu 16.04 gcc-4.4
我自己所用的ubuntu版本为20.04,其默认gcc为gcc 9.3.0,不过步骤是一样的。
对于之后的编译命令:
loader.S不变,还是:
nasm -I include/ -o loader.bin loader.S
dd if=/home/zbb/bochs/loader.bin of=/home/zbb/bochs/hd60M.img bs=512 count=3 seek=2 conv=notrunc
main.c编译变成32位编译:
gcc -m32 -c -o kernel/main.o kernel/main.c
ld -m elf_i386 kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin
dd if=/home/zbb/bochs/kernel/kernel.bin of=/home/zbb/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
如果你已经按照之前的命令编译好了main.o和kernel.bin,那就麻烦删掉然后重新编译吧。
然后是初始化内核:
额,考虑到这步确实比较复杂,直接在最后放个loader.S的完整代码吧。
我们来看看结果:
乍一看jmp .-2,确实while(1)循环又回来了,但是位置是0x00000d35,没有跳到高1GB空间啊,坏了,果然没这么轻松。
接下来是开始调试之路。
one day later...
有些头昏眼花,好像是书中的代码有点错误,改正后终于成了,最后的语句位置为0xc0001503:
总结一下,最后的代码为:
①mbr.S:

1 %include "boot.inc" 2 SECTION MBR vstart=0x7c00 ;起始地址编译为0x7c00 3 mov ax,cs ; 因为是jmp 0:0x7c00跳转到MBR的,故cs此时为0。ds、es、ss、fs等sreg只能用通用寄存器赋值,本例采用ax赋值 4 mov ds,ax 5 mov es,ax 6 mov ss,ax 7 mov fs,ax 8 mov sp,0x7c00 ; 初始化栈指针 9 mov ax,0xb800 ; 0xb800为文本显示起始区 10 mov gs,ax ; gs = ax 充当段基址的作用 11 12 ;ah = 0x06,al = 0x00 想要调用int 0x06的BIOS提供的中断对应的函数,即向上移动即完成清屏功能 13 ;cx,dx 分别存储左上角与右下角的左边,详情看int 0x06函数调用 14 mov ax,0x600 15 mov bx,0x700 16 mov cx,0 17 mov dx,0x184f 18 19 ;调用BIOS中断,实现清屏 20 int 0x10 21 22 ;新增功能:直接操作显存部分 23 ;预设输出"Hell0er." 24 25 mov byte [gs:0x00],'H' ;低位字节储存ASCII字符,小端储存内存顺序相反。用关键词byte指定操作数所占空间,因为[gs:0x00]和'H'所占空间均为不定的,所以需要自己指定空>间大小 26 mov byte [gs:0x01],0xA4 ;背景储存在第二个字节,含字符与背景属性。A表示绿色背景闪烁,4表示前景色为红色 27 28 mov byte [gs:0x02],'e' 29 mov byte [gs:0x03],0xA4 30 31 mov byte [gs:0x04],'l' 32 mov byte [gs:0x05],0xA4 33 34 mov byte [gs:0x06],'l' 35 mov byte [gs:0x07],0xA4 36 37 mov byte [gs:0x08],'0' 38 mov byte [gs:0x09],0xA4 39 40 mov byte [gs:0x0A],'e' 41 mov byte [gs:0x0B],0xA4 42 43 mov byte [gs:0x0C],'r' 44 mov byte [gs:0x0D],0xA4 45 46 mov byte [gs:0x0E],'.' 47 mov byte [gs:0x0F],0xA4 48 49 mov eax,LOADER_START_SECTOR ; 起始扇区lba地址 50 mov bx,LOADER_BASE_ADDR ; 写入的地址 51 mov cx,4 ; 待读入的扇区数 52 call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区) 53 54 jmp LOADER_BASE_ADDR 55 56 ;----------------------------------- 57 ;功能:读取硬盘n个扇区 58 rd_disk_m_16: 59 ;----------------------------------- 60 ; eax=LBA 扇区号 61 ; bx=将数据写入的内存地址 62 ; cx=读入的扇区数 63 mov esi,eax ;备份eax 64 mov di,cx ;备份cx 65 ;读写硬盘: 66 ;第1步:设置要读取的扇区数 67 mov dx,0x1f2 68 mov al,cl 69 out dx,al ;读取的扇区数 70 mov eax,esi ;恢复ax 71 72 ;第2步:将LBA地址存入0x1f3~0x1f6 73 ;LBA地址7~0位写入端口0x1f3 74 mov dx,0x1f3 75 out dx,al 76 77 ;LBA地址15~8位写入端口0x1f4 78 mov cl,8 79 shr eax,cl 80 mov dx,0x1f4 81 out dx,al 82 83 ;LBA地址23~16位写入端口0x1f5 84 shr eax,cl 85 mov dx,0x1f5 86 out dx,al 87 88 shr eax,cl 89 and al,0x0f ;lba第24~27位 90 or al,0xe0 ;设置7~4位为1110,表示lba模式 91 mov dx,0x1f6 92 out dx,al 93 94 ;第3步:向0x1f7端口写入读命令,0x20 95 mov dx,0x1f7 96 mov al,0x20 97 out dx,al 98 99 ;第4步:检测硬盘状态 100 .not_ready: 101 ;同一端口,写时表示写入命令字,读时表示读入硬盘状态 102 nop 103 in al,dx 104 and al,0x88 ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙 105 cmp al,0x08 106 jnz .not_ready ;若未准备好,继续等 107 108 ;第5步:从0x1f0端口读数据 109 mov ax,di 110 mov dx,256 111 mul dx 112 mov cx,ax ;di为要读取的扇区数 113 mov dx,0x1f0 114 .go_on_read: 115 in ax,dx 116 mov [bx],ax 117 add bx,2 118 loop .go_on_read 119 ret 120 121 times 510-($-$$) db 0 ; 将512B的剩余部分填充为0 122 db 0x55,0xaa ; 魔数
②boot.inc:

1 ;----------------- loader 和 kernel ----------------- 2 LOADER_BASE_ADDR equ 0x900 3 LOADER_START_SECTOR equ 0x2 4 5 KERNEL_START_SECTOR equ 0x9 6 KERNEL_BIN_BASE_ADDR equ 0x70000 7 KERNEL_ENTER_ADDR equ 0xc0001500 8 PT_NULL equ 0x0 9 ;------------------ gdt描述符属性 ------------------- 10 DESC_G_4K equ 1_00000000000000000000000b ;第23位G 表示4K或者1MB位 段界限的单位值 此时为1则为4k 11 DESC_D_32 equ 1_0000000000000000000000b ;第22位D/B位 表示地址值用32位EIP寄存器 操作数与指令码32位 12 DESC_L equ 0_000000000000000000000b ;第21位 设置成0表示不设置成64位代码段 忽略 13 DESC_AVL equ 0_00000000000000000000b ;第20位 是软件可用的 操作系统额外提供的 可不设置 14 15 PAGE_DIR_TABLE_POS equ 0x100000 ;页目录表物理地址 16 17 DESC_LIMIT_CODE2 equ 1111_0000000000000000b ;第16-19位 段界限的最后四位 全部初始化为1 因为最大段界限*粒度必须等于0xffffffff 18 DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 ;相同的值 数据段与代码段段界限相同 19 DESC_LIMIT_VIDEO2 equ 0000_0000000000000000b ;第16-19位 显存区描述符VIDEO2 书上后面的0少打了一位 这里的全是0为高位 低位即可表示段基址 20 21 DESC_P equ 1_000000000000000b ;第15位 P Present判断段是否存在于内存 22 DESC_DPL_0 equ 00_0000000000000b ;第13-14位 DPL Descriptor Privilege Level 0-3 23 DESC_DPL_1 equ 01_0000000000000b ;0为操作系统,权力最高;3为用户段,用于保护 24 DESC_DPL_2 equ 10_0000000000000b 25 DESC_DPL_3 equ 11_0000000000000b 26 27 DESC_S_sys equ 0_000000000000b ;第12位为0 则表示系统段 为1则表示数据段 28 DESC_S_CODE equ 1_000000000000b ;第12位与type字段结合 判断是否为系统段还是数据段 29 DESC_S_DATA equ DESC_S_CODE 30 31 DESC_TYPE_CODE equ 1000_00000000b ;第9-11位表示该段状态 1000 可执行 不允许可读 已访问位0 32 ;x=1 e=0 w=0 a=0 33 DESC_TYPE_DATA equ 0010_00000000b ;第9-11位type段 0010 可写 34 ;x=0 e=0 w=1 a=0 35 36 ;代码段描述符高位4字节初始化 (0x00共8位 <<24 共32位初始化0) 37 ;4KB为单位 Data段32位操作数 初始化的部分段界限 最高权限操作系统代码段 P存在表示 状态 38 DESC_CODE_HIGH4 equ (0x00<<24) + DESC_G_4K + DESC_D_32 + \ 39 DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + \ 40 DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0X00 41 42 ;数据段描述符高位4字节初始化 43 DESC_DATA_HIGH4 equ (0x00<<24) + DESC_G_4K + DESC_D_32 + \ 44 DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + \ 45 DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0X00 46 47 ;显存段描述符高位4字节初始化 48 DESC_VIDEO_HIGH4 equ (0x00<<24) + DESC_G_4K + DESC_D_32 + \ 49 DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + \ 50 DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0X0B 51 52 ;-------------------- 页表相关属性 ----------------------------- 53 PG_P equ 1b 54 PG_RW_R equ 00b 55 PG_RW_W equ 10b 56 PG_US_S equ 000b 57 PG_US_U equ 100b 58 59 ;-------------------- 选择子属性 -------------------------------- 60 ;第0-1位 RPL 特权级比较是否允许访问;第2位 TI 0表示GDT 1表示LDT;第3-15位索引值 61 RPL0 equ 00b 62 RPL1 equ 01b 63 RPL2 equ 10b 64 RPL3 equ 11b 65 TI_GDT equ 000b 66 TI_LDT equ 100b
③loader.S:

1 %include "boot.inc" 2 SECTION LOADER vstart=LOADER_BASE_ADDR ;同书上,设置为0x900 3 LOADER_STACK_TOP equ LOADER_BASE_ADDR ;初始化栈顶,0x900向下为栈空间 4 jmp loader_start 5 6 ;构建GDT及其内部的描述符 7 GDT_BASE: dd 0x00000000 ;没用的第0个段描述符 8 dd 0x00000000 9 CODE_DESC: dd 0x0000FFFF 10 dd DESC_CODE_HIGH4 11 DATA_STACK_DESE: dd 0x0000FFFF 12 dd DESC_DATA_HIGH4 13 VIDEO_DESC: dd 0x80000007 ;limit=(0xbffff-0xb8000)/4k=0x7 14 dd DESC_VIDEO_HIGH4 ;此时DPL为0 15 GDT_SIZE equ $-GDT_BASE ;地址差作尺寸:当前行地址-GDT_BASE地址 16 GDT_LIMIT equ GDT_SIZE-1 17 18 times 59 dq 0 ;dq 定义4字/8字节 19 times 5 db 0 20 21 total_mem_bytes dd 0 22 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 23 SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL0 24 ;相当于(CODE_DESC-GDT_BASE)/8+TI_GDT+RPL0 25 SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL0 26 SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0 27 28 ;以下是GDT的指针GDTR,6B/48bit,前2字节是GDT界限,后4字节是GDT起始地址 29 gdt_ptr dw GDT_LIMIT 30 dd GDT_BASE 31 32 ards_buf times 244 db 0 33 ards_nr dw 0 34 35 loader_start: 36 ;int 15h eax=0000E820h, edx=534D4150h('SMAP'的ASCII码)获取内存布局 37 xor ebx,ebx ;第一次调用时,ebx要为0 38 mov edx,0x534d4150 ;edx只赋值一次,循环体中不会改变,用于签名校验 39 mov di,ards_buf ;ards结构缓冲区 40 .e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构 41 mov eax,0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子>功能号 42 mov ecx,20 ;ARDS地址范围描述符结构大小是20字节 43 int 0x15 ;0x15为获取内存容量的中断号 44 jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能 45 add di,cx ;使di增加20字节指向缓冲区中新的ARDS结构位置 46 inc word [ards_nr] ;记录ARDS数量 47 cmp ebx,0 ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个 48 49 jnz .e820_mem_get_loop 50 51 ;在所有ards结构中找出(base_add_low+length_low)的最大值,即内存的容量 52 mov cx,[ards_nr] ;遍历每个ARDS结构体,循环次数是ARDS的数量 53 mov ebx,ards_buf 54 xor edx,edx ;此后edx用于记录最大内存容量,在此先请0 55 .find_max_mem_area: ;无需判断type是否为1,最大的内存块一定是可以被使用的 56 mov eax,[ebx] ;base_add_low 57 add eax,[ebx+8] ;length_low 58 add ebx,20 ;指向缓冲区下一个ARDS结构 59 cmp edx,eax ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量 60 jge .next_ards 61 mov edx,eax ;edx为总内存大小 62 .next_ards: 63 loop .find_max_mem_area 64 jmp .mem_get_ok 65 66 ;---------- int 15h ax=E801h 获取内存大小,最大支持4G ---------- 67 ;返回后,ax cx值一样,以KB为单位,bx dx值一样,以64KB为单位 68 ;在ax和cx寄存器中为低16MB,在bx和dx寄存器中为16MB到4GB 69 .e820_failed_so_try_e801: 70 mov ax,0xe801 71 int 0x15 72 jc .e801_failed_so_try88 ;若当前e801方法也失败,则尝试0x88方法 73 74 ;1.先算出低15MB内存。ax cx是以KB为单位的内存数量,将其转换为以Byte为单位 75 mov cx,0x400 ;0x400十进制为1K。cx和ax一样,cx用作乘数 76 mul cx 77 shl edx,16 78 and eax,0x0000FFFF 79 or edx,eax 80 add edx,0x100000 ;ax只是15MB,故要追加1MB 81 mov esi,edx ;先把低15MB内存容量存入esi寄存器备份 82 83 ;2.再将16MB以上的内存转换为Byte为单位。bx dx是以64KB为单位的内存数量 84 xor eax,eax 85 mov ax,bx 86 mov ecx,0x10000 ;0x10000十进制为64KB 87 mul ecx ;32位乘法,默认的被乘数是eax,积为64位。高32位存入edx,低32位存入eax 88 add esi,eax ;4GB内存,edx为0,低32位足矣 89 mov edx,esi ;edx为总内存大小 90 jmp .mem_get_ok 91 92 ;------- int 15h ah=0x88获取内存大小,只能获取64MB之内 --------- 93 .e801_failed_so_try88: 94 ;int 15h后,ax存入的是以KB为单位的内存容量 95 mov ah,0x88 96 int 0x15 97 jc .error_hlt 98 and eax,0x0000FFFF 99 100 ;16位乘法,被乘数是ax,积为32位。积的高16位在dx中,低16位在ax中 101 mov cx,0x400 102 mul cx 103 shl edx,16 ;把edx移动高16位 104 or edx,eax ;把低16位组合到edx,即为32为积 105 add edx,0x100000 ;0x88只会返回1MB以上的内存,故实际内存还要加上1MB 106 107 .error_hlt: 108 jmp $ 109 110 .mem_get_ok: 111 mov [total_mem_bytes],edx ;将内存换位Byte单位后存入total_mem_bytes处 112 113 ;--------------------- 准备进入保护模式 ------------------------ 114 ;1 打开A20 115 ;2 加载GDT 116 ;3 将cr0的PE位置1 117 118 ;--------------------------- 打开A20 --------------------------- 119 in al,0x92 120 or al,0000_0010B ;简单说,将端口0x92的第1位置1即可 121 out 0x92,al 122 123 ;--------------------------- 加载GDT --------------------------- 124 lgdt [gdt_ptr] ;load GDT [addr] 125 126 ;-------------------------- cr0第0位置1 ------------------------ 127 mov eax,cr0 128 or eax,0x00000001 129 mov cr0,eax 130 131 jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线。因为要远转移,cs更新,所>以流水线上的其它指令都没用了,就会刷新 132 133 [bits 32] ;开启32位指令 134 p_mode_start: 135 mov ax,SELECTOR_DATA 136 mov ds,ax 137 mov es,ax 138 mov ss,ax 139 mov esp,LOADER_STACK_TOP 140 141 ;-------------------------- 加载kernel ------------------------ 142 mov eax,KERNEL_START_SECTOR ;kernel.bin所在扇区号 143 mov ebx,KERNEL_BIN_BASE_ADDR ;从磁盘读出后,写入到ebx指定的地址 144 mov ecx,200 ;读入的扇区数 145 146 call rd_disk_m_32 147 148 ;-------------------------- 建立页表 -------------------------- 149 ;创建页目录及页表并初始化页内存位图 150 call setup_page 151 152 sgdt [gdt_ptr] 153 154 mov ebx,[gdt_ptr+2] 155 156 or dword [ebx+0x18+4],0xc0000000 157 add dword [gdt_ptr+2],0xc0000000 158 159 add esp,0xc0000000 160 161 mov eax,PAGE_DIR_TABLE_POS 162 mov cr3,eax 163 164 mov eax,cr0 165 or eax,0x80000000 166 mov cr0,eax 167 168 lgdt [gdt_ptr] 169 170 mov eax,SELECTOR_VIDEO 171 mov gs,eax 172 173 jmp SELECTOR_CODE:enter_kernel 174 175 ;---------------------- 跳转到kernel ----------------------- 176 enter_kernel: 177 call kernel_init 178 mov esp,0xc009f000 179 jmp KERNEL_ENTER_ADDR 180 181 ;------------------------ 创建页表 ------------------------ 182 setup_page: 183 mov ecx,4096 184 mov esi,0 185 .clear_page_dir: 186 mov byte [PAGE_DIR_TABLE_POS+esi],0 187 inc esi 188 loop .clear_page_dir 189 190 ;创建页目录项(PDE) 191 .create_pde: 192 mov eax,PAGE_DIR_TABLE_POS 193 add eax,0x1000 ;此时的eax为第一个页表的物理地址 194 mov ebx,eax ;ebx=eax,为后续的.create_pte做准备,ebx为基址 195 196 ;下面将偏移地址0x0(第1个)和0xc00(第768个页目录项)存为第1个页表的地址,每个页表表示4MB内存 197 or eax,PG_US_U|PG_RW_W|PG_P ;最低特权级|可读写|存在 198 mov [PAGE_DIR_TABLE_POS+0x0],eax ;第1个页目录项 199 mov [PAGE_DIR_TABLE_POS+0xc00],eax ;第768个页目录项 200 sub eax,0x1000 201 mov [PAGE_DIR_TABLE_POS+4092],eax ;最后一个页目录项指向页目录自己 202 203 ;创建页表项(PTE) 204 mov ecx,256 ;对低端内存1MB建页表:1MB/4KB=256(256个页表项,1个页表足矣) 205 mov esi,0 206 mov edx,PG_US_U|PG_RW_W|PG_P ;最低特权第|可读写|存在 207 .create_pte: 208 mov [ebx+esi*4],edx ;逐个页表项设置 209 add edx,4096 ;因为1个页表4KB,所以edx的基址+4KB 210 inc esi 211 loop .create_pte 212 213 ;创建内核其它页表的PDE 214 mov eax,PAGE_DIR_TABLE_POS 215 add eax,0x2000 ;此时的eax为第二个页表的物理地址 216 or eax,PG_US_U|PG_RW_W|PG_P ;最低特权级|可读写|存在 217 mov ebx,PAGE_DIR_TABLE_POS 218 mov ecx,254 219 mov esi,769 220 .create_kernel_pde: 221 mov [ebx+esi*4],eax ;将第2个~第256个页表的地址逐个存入页表项 222 inc esi 223 add eax,0x1000 ;下一个页表的地址 224 loop .create_kernel_pde 225 226 ret 227 228 ;------ 初始化内核 把缓冲区的内核代码放到0x1500区域 ------ 229 kernel_init: 230 xor eax,eax 231 xor ebx,ebx ;ebx记录程序头表地址 232 xor ecx,ecx ;cx记录程序头表中的program header数量 233 xor edx,edx ;dx记录program header尺寸,即e_phentsize 234 235 mov dx,[KERNEL_BIN_BASE_ADDR+42] ;偏移文件42字节处的属性是e_phentsize,表示program header大小 236 mov ebx,[KERNEL_BIN_BASE_ADDR+28] ;偏移文件28字节处是e_phoff,表示第一个program>在文件中的偏移量 237 add ebx,KERNEL_BIN_BASE_ADDR 238 mov cx,[KERNEL_BIN_BASE_ADDR+44] ;偏移文件44字节处是e_phnum,表示有几个program header 239 .each_segment: 240 cmp byte [ebx+0],PT_NULL ;若p_type等于PT_NULL,说明此program header未使用 241 je .PTNULL 242 243 mov eax,[ebx+8] 244 mov esi,0xc0001500 245 cmp eax,esi 246 jb .PTNULL 247 248 ;为函数memcpy压入参数,参数是从右往左依次压入,函数原型类似于memcpy(dst,src,size) 249 push dword [ebx+16] ;偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数:size 250 mov eax,[ebx+4] ;偏移4字节的位置是p_offset 251 add eax,KERNEL_BIN_BASE_ADDR ;加上kernel.bin被加载到的物理地址,eax为该段的物理地址 252 push eax ;压入函数memcpy的第二个参数:src 253 push dword [ebx+8] ;压入函数memcpy的第三个参数:dst,偏移量为8字节的位置是p_vaddr 254 call mem_cpy ;调用memcpy完成段复制 255 add esp,12 ;清理栈中压入的三个参数 256 .PTNULL: 257 add ebx,edx ;edx为program header大小,即e_phentsize,ebx指向下一个program_header 258 loop .each_segment 259 ret 260 261 mem_cpy: 262 cld 263 push ebp 264 mov ebp,esp 265 push ecx ;rep指令需要ecx,但ecx还此时用于外循环中,所以先push保存一下 266 mov edi,[ebp+8] 267 mov esi,[ebp+12] 268 mov ecx,[ebp+16] 269 rep movsb ;逐字节拷贝 270 271 ;恢复 272 pop ecx 273 pop ebp 274 ret 275 276 ;------ rd_disk_m_32,类似于mbr.S中的rd_disk_m_16 ------ 277 rd_disk_m_32: 278 ;1 写入待操作磁盘数 279 ;2 写入LBA 低24位寄存器 确认扇区 280 ;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1 281 ;4 command 写指令 282 ;5 读取status状态寄存器 判断是否完成工作 283 ;6 完成工作 取出数据 284 285 ;1 写入待操作磁盘数 286 mov esi,eax ; !!! 备份eax 287 mov di,cx ; !!! 备份cx 288 289 mov dx,0x1F2 ; 0x1F2为Sector Count 端口号 送到dx寄存器中 290 mov al,cl ; !!! 忘了只能由ax al传递数据 291 out dx,al ; !!! 这里修改了 原out dx,cl 292 293 mov eax,esi ; !!!袄无! 原来备份是这个用 前面需要ax来传递数据 麻了 294 295 ;2 写入LBA 24位寄存器 确认扇区 296 mov cl,0x8 ; shr 右移8位 把24位给送到 LBA low mid high 寄存器中 297 298 mov dx,0x1F3 ; LBA low 299 out dx,al 300 301 mov dx,0x1F4 ; LBA mid 302 shr eax,cl ; eax为32位 ax为16位 eax的低位字节 右移8位即8~15 303 out dx,al 304 305 mov dx,0x1F5 306 shr eax,cl 307 out dx,al 308 309 ;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1 310 ; 24 25 26 27位 尽管我们知道ax只有2 但还是需要按规矩办事 311 ; 把除了最后四位的其他位置设置成0 312 shr eax,cl 313 314 and al,0x0f 315 or al,0xe0 ;!!! 把第四-七位设置成0111 转换为LBA模式 316 mov dx,0x1F6 ; 参照硬盘控制器端口表 Device 317 out dx,al 318 319 ;4 向Command写操作 Status和Command一个寄存器 320 mov dx,0x1F7 ; Status寄存器端口号 321 mov ax,0x20 ; 0x20是读命令 322 out dx,al 323 324 ;5 向Status查看是否准备好惹 325 ;设置不断读取重复 如果不为1则一直循环 326 .not_ready: 327 nop ; !!! 空跳转指令 在循环中达到延时目的 328 in al,dx ; 把寄存器中的信息返还出来 329 and al,0x88 ; !!! 0100 0100 0x88 330 cmp al,0x08 331 jne .not_ready; !!! jump not equal == 0 332 333 ;6 读取数据 334 mov ax,di ;把 di 储存的cx 取出来 335 mov dx,256 336 mul dx ;与di 与 ax 做乘法 计算一共需要读多少次 方便作循环 低16位放ax 高16位放dx 337 mov cx,ax ;loop 与 cx相匹配 cx-- 当cx == 0即跳出循环 338 mov dx,0x1F0 339 .go_read_loop: 340 in ax,dx ;两字节dx 一次读两字 341 mov [ebx],ax 342 add ebx,2 343 loop .go_read_loop 344 ret
本次实验耗时两天半(有小黑子?),后面还有一节有关特权级的部分尚未阅读,先完结本章吧。
最后,我希望从一个全局的视角来看待整个操作系统启动的过程,随便复习并检查一下遗漏的地方,让我们来捋一捋这几章到底干了哪些事儿:
1.对于BIOS:
在开机接电的一瞬间,CPU的cs:ip被强制初始化为0xf000:fff0(0xffff0),然后执行jmp f000:e05b指令,跳转到0xfe05b
在0xfe05b,BIOS会进行硬件自检、建立中断向量表、加载MBR并jmp 0000:7c00(跳转到MBR)
2.对于mbr.S(512B):
在0x7c00处,它将
(1)初始化寄存器
(2)BIOS中断(int 0x10)清屏
(3)直接操控显存空间(0xb800)输出“Hell0er.”
(4)call rd_disk_m_16 ——①加载loader.bin,即将磁盘上第二个扇区起的loader.bin读取到内存中的0x900处 ②填充0 ③设置魔数0x55 0xaa
(5)jmp LOADER_BASE_ADDR(0x900,跳转到LOADER)
3.对于loader.S:
在0x900处,它将
(1)jmp loader_start(0xc00)——①利用0x15中断获取内存容量,存入total_mem_bytes(0xb00)处
(2)打开gate A20
(3)lgdt [gdt_ptr]加载GDT
(4)cr0第0位(PE)置1
【(2)~(4)总的目的就是打开保护模式】
(5)jmp dword SELECTOR_CODE:p_mode_start——
①重新初始化寄存器
②call rd_disk_m_32——加载kernel.bin,即将磁盘上第9个扇区起的kernel.bin读取到内存缓冲区0x70000处
③call setup_page——创建页表
④修改esp成内核地址,重新加载gdt到内核空间中
⑤jmp SELECTOR_CODE:enter_kernel——call kernel_init(即利用mem_cpy逐个program读取);jmp KERNEL_ENTER_ADDR(进入main.c)
好了,这大概就是我理解中前5章的全部过程了,若有错误还请指出。可以关上书本,根据记忆或代码回想一下整个过程,相信能对这些内容有更深刻的印象。
然后还有一张我眼中的地址转换图:
不知道是不是完全正确,还望各位指点一二。
终于要与汇编语言说再见了。大家加油!
参考博客:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库