《操作系统真象还原》第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   ; 魔数
View Code
复制代码

②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
View Code
复制代码

③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
View Code
复制代码

本次实验耗时两天半(有小黑子?),后面还有一节有关特权级的部分尚未阅读,先完结本章吧。


 最后,我希望从一个全局的视角来看待整个操作系统启动的过程,随便复习并检查一下遗漏的地方,让我们来捋一捋这几章到底干了哪些事儿:

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章的全部过程了,若有错误还请指出。可以关上书本,根据记忆或代码回想一下整个过程,相信能对这些内容有更深刻的印象。

然后还有一张我眼中的地址转换图:

不知道是不是完全正确,还望各位指点一二。

 

终于要与汇编语言说再见了。大家加油!


参考博客:

posted @   Hell0er  阅读(460)  评论(0编辑  收藏  举报
编辑推荐:
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示