《操作系统真象还原》 第五章 保护模式进阶

发布于:2025-08-02 ⋅ 阅读:(17) ⋅ 点赞:(0)

1.获取物理内存容量

操作系统需要管理硬件,自然就需要知道自己有哪些硬件可用以及这些硬件的信息。内存容量是一个非常重要的信息,操作系统获得内存容量的方法通常在实模式工作时就调用BIOS中断来获取,然后存储在内存中,进入保护模式后再使用这些信息(因为保护模式不能使用BIOS中断)。BIOS 中断 0x15 的子功能 0xE820 能够获取系统的内存布局,由于系统内存各部分的类型属性不同,BIOS 就按照类型属性来划分这片系统内存,所以这种查询呈迭代式,每次 BIOS 只返回一种类型的内存信息,直到将所有内存类型返回完毕。

地址范围描述符:内存信息的内容是用地址范围描述符来描述的,用于存储这种描述符的结构称之为地址范围描述符(Address Range Descriptor Structure,ARDS)

此结构中的字段大小都是 4 字节,共 5 个字段,所以此结构大小为 20 字节。每次 int 0x15 之后,BIOS 就返回这样一个结构的数据。注意,ARDS 结构中用 64 位宽度的属性来描述这段内存基地址(起始地址)及其长度,所以表中的基地址和长度都分为低 32 位和高 32 位两部分。(因为我们所实现的操作系统是32位,故基地址只用取低32位)。

代码实现:在第四章的loader.s文件中添加一下代码

total_mem_bytes dd 0				                    ; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。
                                                        ; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
                                                        ; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址	

ards_buf times 244 db 0                                 ;人工对齐total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
ards_nr dw 0		                                    ;用于记录ards结构体数量

loader_start:
                                                        ;-------  int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局  -------

    xor ebx, ebx		                                ;第一次调用时,ebx值要为0
    mov edx, 0x534d4150	                                ;edx只赋值一次,循环体中不会改变
    mov di, ards_buf	                                ;ards结构缓冲区
    .e820_mem_get_loop:	                                ;循环获取每个ARDS内存范围描述结构
    mov eax, 0x0000e820	                                ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
    mov ecx, 20		                                    ;ARDS地址范围描述符结构大小是20字节
    int 0x15
    add di, cx		                                    ;使di增加20字节指向缓冲区中新的ARDS结构位置
    inc word [ards_nr]	                                ;记录ARDS数量
    cmp ebx, 0		                                    ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
    jnz .e820_mem_get_loop

                                                        ;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
    mov cx, [ards_nr]	                                ;遍历每一个ARDS结构体,循环次数是ARDS的数量
    mov ebx, ards_buf 
    xor edx, edx		                                ;edx为最大的内存容量,在此先清0
.find_max_mem_area:	                                    ;无须判断type是否为1,最大的内存块一定是可被使用
    mov eax, [ebx]	                                    ;base_add_low
    add eax, [ebx+8]	                                ;length_low
    add ebx, 20		                                    ;指向缓冲区中下一个ARDS结构
    cmp edx, eax		                                ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
    jge .next_ards
    mov edx, eax		                                ;edx为总内存大小
.next_ards:
    loop .find_max_mem_area

    mov [total_mem_bytes], edx	                        ;将内存换为byte单位后存入total_mem_bytes处。

total_mem_bytes是一个4 字节大小的变量,此变量用于存储获取到的内存容量,以字节为单位。并且total_mem_bytes加载到内存时的地址为0xb00。ards_buf为ARDS结构的缓冲区,用于存放每执行一次BIOS中断所返回的ARDS结构,便于以后进行对所有的内存段进行遍历。ards_nr用于存放ARDS结构的数量。

修改mbr.s文件

jmp LOADER_BASE_ADDR+0x300

使得mbr跳转到loader时直接跳转到loader的代码段起始地址。经过nasm命令编译,dd命令写入磁盘后,启动bochs虚拟机,通过x /30xw 0xb0a 就可以获取到6个ARDS结构体数据,如下图所示:

2.分页机制

在内存分段机制下,如果系统的应用进程过多或者内存碎片过多无法装入新的进程,就需要根据段描述符对段进行换入换出的操作。当物理内存过小就会导致进程无法运行,或进程的段比较大,就需要做更多的IO操作导致整个系统的性能下降。因此,就引入了分页机制。

 

(1)一级页表

由于虚拟内存中是连续的,而在物理内存中每个页都是不连续的,被离散的插在了不同地方。所以我们要有一个类似于目录的东西,来找到虚拟内存中的每一块对应到物理内存中的某一块。也就是我给你一个虚拟地址,你就要给我一个对应的物理地址,指向某一个数据。我们使用一个页表来存储物理内存对应的页表项PTE。这个页表类似于一个目录。 这个目录我们的每一项表示一个4K大小的页,而不是定位到某个具体物理地址,那么这个目录我们只需要1M个项即可,而每一项中的内容其实就是4K页的起始地址。不是为每个地址创建一条表单条目,而是为每个page创建一条表单条目,所以每一次地址翻译都是针对一个page,那么我们页表共有1M个条目来存储物理页的序号,每个条目占4byte,所以这个页表总共占4M。

一个32为地址,可以用高20位来定位到那个物理页,用低12为来表示4k页中的偏移量。例如地址0x1234(0000 0000 0000 0000 0001 0010 0011 0100),高20位1表示到第一个页进行索引,得到该页的起始地址为0x9000,低12位为0x234表示在该页中的偏移量,故物理地址为该页的起始地址(0x9000)+页内偏移量(0x234)=0x9234

 (2) 二级页表

为何要有二级页表

第一级页表要常驻内存。一级页表大小是4M。如果开启多个进程,每个进程占用4M就很占内存。
这还只是是32位系统,如果64位系统那就是2的64次方除以10。非常大,放到内存中是不可能的。无论是几级页表,标准页的尺寸都是 4KB,这一点是不变的。所以 4GB 线性地址空间最多有 1M 个 标准页。一级页表是将这 1M 个标准页放置到一张页表中,二级页表是将这 1M 个标准页平均放置 1K 个页表中。每个页表中包含有 1K 个页表项。页表项是 4 字节大小,页表包含 1K 个页表项,故页表大小为4KB,这恰恰是一个标准页的大小。

 

每个页表中可容纳 1024 个物理页,故每个页表可表示的内存容量是 1024*4KB=4MB。页目录中共有 1024 个页表,故所有页表可表示的内存容量是 1024*4MB=4GB,这已经达到了 32 位地址空间的最大容量。 所以说,任意一个 32 位物理地址,它必然在某个页表之内的某个物理页中。我们定位某一个物理页,必然 要先找到其所属的页表。页目录中 1024 个页表,只需要 10 位二进制就能够表示了,所以,虚拟地址的高 10 位(第 31~22 位)用来在页目录中定位一个页表,也就是这高 10 位用于定位页目录中的页目录项 PDE, PDE 中有页表物理页地址。找到页表后,到底是页表中哪一个物理页呢?由于页表中可容纳 1024 个物理页, 故只需要 10 位二进制就能够表示了。所以虚拟地址的中间 10 位(第 21~12 位)用来在页表中定位具体的 物理页,也就是在页表中定位一个页表项 PTE,PTE 中有分配的物理页地址。由于标准页都是 4KB,12 位 二进制便可以表达 4KB 之内的任意地址,故线性地址中余下的 12 位(第 11~0 位)用于页内偏移量。例如地址0x1234567(0000 0001 0010 0011 0100 0101 0110 0111),高10位为4,表示为第4个页目录项,通过页目录表物理地址+4*4(一个页目录项大小)即可得到该页目录项所指向页表的起始物理地址。页目录表物理地址存放在cr3寄存器中。再通过中10位(0x234)得到在二级页表的第几个页表项(得到该页表项的起始地址),最后通过低12位(0x567)页内偏移量就可以得到真实的物理地址。

(3)页目录项和页表项结构

 

如上图所示,页目录项和页表项大小都为4字节大小,高20位代表物理地址,低12位为属性值,因为页目录项和页表项中的都是物理页地址,标准页大小是 4KB,故地址都是 4K 的倍数,也就是地址的低12位是0,故低12位可用来代表特有的属性值。

启动分页机制的三个步骤:

   1.初始化页目录表以及页表

   2.将页目录表地址存入cr3寄存器

   3.寄存器cr0的PG位置为1

 为了系统的安全和实现共享,虚拟地址空间的 0~3GB 是用户进程,3GB~4GB 是操作系统。

在boot.inc文件中添加以下配置:

PAGE_DIR_TABLE_POS equ 0x100000                     ;页目录表在内存中的起始位置——从1M开始的位置

                                                    ;---------模块化的页目录表字段,PWT PCD A D G AVL 暂时不用设置   ----------
PG_P  equ   1b
PG_RW_R	 equ  00b 
PG_RW_W	 equ  10b 
PG_US_S	 equ  000b 
PG_US_U	 equ  100b  

loader.s文件加入以下代码:

setup_page:                                             ;------------------------------------------   创建页目录及页表  -------------------------------------
                                                        ;----------------以下6行是将1M开始的4KB置为0,将页目录表初始化
    mov ecx, 4096                                       ;创建4096个byte 0,循环4096次
    mov esi, 0                                          ;用esi来作为偏移量寻址
.clear_page_dir:
    mov byte [PAGE_DIR_TABLE_POS + esi], 0
    inc esi
    loop .clear_page_dir

                                                        ; ----------------初始化页目录表,让0号项与768号指向同一个页表,该页表管理从0开始4M的空间
.create_pde:				                            ;一个页目录表项可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,这是为将地址映射为内核地址做准备
    mov eax, PAGE_DIR_TABLE_POS                         ; eax中存着页目录表的位置
    add eax, 0x1000 			                        ; 在页目录表位置的基础上+4K(页目录表的大小),现在eax中第一个页表的起始位置
    mov ebx, eax				                        ; 此处为ebx赋值,现在ebx存着第一个页表的起始位置
    or eax, PG_US_U | PG_RW_W | PG_P	                ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
                                                        ; 现在eax中的值符合一个页目录项的要求了,高20位是一个指向第一个页表的4K整数倍地址,低12位是相关属性设置
    mov [PAGE_DIR_TABLE_POS + 0x0], eax                 ; 页目录表0号项写入第一个页表的位置(0x101000)及属性(7)
    mov [PAGE_DIR_TABLE_POS + 0xc00], eax               ; 页目录表768号项写入第一个页表的位置(0x101000)及属性(7)
					                                    
    sub eax, 0x1000                                     ;----------------- 使最后一个目录项指向页目录表自己的地址,为的是将来动态操作页表做准备
    mov [PAGE_DIR_TABLE_POS + 4092], eax	            ;属性包含PG_US_U是为了将来init进程(运行在用户空间)访问这个页目录表项
                                                        
    mov ecx, 256				                        ; -----------------初始化第一个页表,因为我们的操作系统不会超过1M,所以只用初始化256项
    mov esi, 0                                          ; esi来做寻址页表项的偏移量
    xoe edx, edx                                        ;将edx置为0,现在edx指向0地址
    mov edx, PG_US_U | PG_RW_W | PG_P	                ; 属性为7,US=1,RW=1,P=1
.create_pte:				                            ; 创建Page Table Entry
    mov [ebx+esi*4],edx			                        ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 
    add edx,4096                                        ; edx指向下一个4kb空间,且已经设定好了属性,故edx中是一个完整指向下一个4kb物理空间的页表表项
    inc esi                                             ; 寻址页表项的偏移量+1
    loop .create_pte                                    ;循环设定第一个页表的256项

                                                        ; -------------------初始化页目录表769号-1022号项,769号项指向第二个页表的地址(此页表紧挨着上面的第一个页表),770号指向第三个,以此类推
    mov eax, PAGE_DIR_TABLE_POS                         ; eax存页目录表的起始位置
    add eax, 0x2000 		                            ; 此时eax为第二个页表的位置
    or eax, PG_US_U | PG_RW_W | PG_P                    ; 设置页目录表项相关属性,US,RW和P位都为1,现在eax中的值是一个完整的指向第二个页表的页目录表项
    mov ebx, PAGE_DIR_TABLE_POS                         ; ebx现在存着页目录表的起始位置
    mov ecx, 254			                            ; 要设置254个表项
    mov esi, 769                                        ; 要设置的页目录表项的偏移起始
.create_kernel_pde:
    mov [ebx+esi*4], eax                                ; 设置页目录表项
    inc esi                                             ; 增加要设置的页目录表项的偏移
    add eax, 0x1000                                     ; eax指向下一个页表的位置,由于之前设定了属性,所以eax是一个完整的指向下一个页表的页目录表项
    loop .create_kernel_pde                             ; 循环设定254个页目录表项
    ret

该段代码定义了函数setup_page,第一部分执行一段循环,将页目录表占用的空间清零,ecx寄存器中存放循环次数,loop每执行一次ecx寄存器减1,减到0为止。 第二部分寄存器 eax中是页目录项的内容(提醒一下,PG_US_U | PG_RW_W | PG_P 逻辑或的结果是0x7),分别将其存入到页目录项的第 0 项和第 768 项,0xc00/4=768。页目录项代表一个页表,也就是说,这两处都是指向同一个页表。

在loader.s文件中调用上面所定义的函数完成页目录表和页表的初始化工作,因为操作系统应该位于虚拟内存空间的3GB以上,所以将gdt表的基址、显存段描述符中的段基址、栈指针+0xc0000000映射到内核空间中。

    call setup_page                                     ;创建页目录表的函数,我们的页目录表必须放在1M开始的位置,所以必须在开启保护模式后运行

                                                        ;以下两句是将gdt描述符中视频段描述符中的段基址+0xc0000000
    mov ebx, [gdt_ptr + 2]                              ;ebx中存着GDT_BASE
    or dword [ebx + 0x18 + 4], 0xc0000000               ;视频段是第3个段描述符,每个描述符是8字节,故0x18 = 24,然后+4,是取出了视频段段描述符的高4字节。然后or操作,段基址最高位+c
                                           
    add dword [gdt_ptr + 2], 0xc0000000                 ;将gdt的基址加上0xc0000000使其成为内核所在的高地址

    add esp, 0xc0000000                                 ; 将栈指针同样映射到内核地址

    mov eax, PAGE_DIR_TABLE_POS                         ; 把页目录地址赋给cr3
    mov cr3, eax
                                                        
    mov eax, cr0                                        ; 打开cr0的pg位(第31位)
    or eax, 0x80000000  
    mov cr0, eax
                                                      
    lgdt [gdt_ptr]                                      ;在开启分页后,用gdt新的地址重新加载

    mov byte [gs:160], 'V'                              ;视频段段基址已经被更新,用字符v表示virtual addr

编译mbr.s和loader.s文件并写入磁盘,启动bochs虚拟机,使用info gdt可以看到gdt表的基址被修改到了0xc0000900,显存段的段描述符中的段基址被修改为0xc0000b00。

 3.loader的最后使命

loader最后需要将操作系统的二进制文件加载到内存中,根据elf文件头信息为其创建内核映像。

程序中最重要的部分就是段和节。program header用于描述每个段的信息,section header用于描述每个节的信息。由于程序中段与节的大小和数量是不固定的,所以program header与section header的数量大小也是不固定的,因此需要为它们专门找个数据结构来描述它们,这个描述结构就是program header table与section header table。但是,多一个段,就多一个program header,program header table就会变大,所以program header table与section header table的大小也是不固定的,就又需要一个elf header来描述program header table与section header table。

通过elf头可以得到Program header table的地址,通过Program header table可以分别获取Program header的信息,根据这个信息去确定每个段的大小,起始地址,在内存中的目的地址(都是虚拟地址)。elf header和Program header的结构详情见书上p215页。

创建一个c语言文件kernel.c作为内核文件,该文件内容如下:

int main(){
    while(1);
    return 0;
}

使用gcc-4.4 /home/yyx/Desktop/chapter5_b/c/kernel/main.c -o /home/yyx/Desktop/kernel -c -m32
命令编译该c语言文件,使用ld /home/yyx/Desktop/kernel -Ttext 0xc0001500 -e main -o /home/rlk/Desktop/kernel.bin -m elf_i386命令完成链接,最后使用dd if=/home/yyx/Desktop/kernel.bin of=/home/yyx/Desktop/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9命令写入磁盘的9号扇区。

gcc降低版本参考:https://blog.csdn.net/qq_41124687/article/details/141950588?ops_request_misc=%257B%2522request%255Fid%2522%253A%25227d22685c093b773d5b6f8a31bfdc5177%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=7d22685c093b773d5b6f8a31bfdc5177&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-5-141950588-null-null.142^v102^pc_search_result_base7&utm_term=ubuntu22.04%20gcc%E7%89%88%E6%9C%AC%E5%88%87%E6%8D%A2&spm=1018.2226.3001.4187

在boot.inc中添加一些宏定义

LOADER_STACK_TOP equ LOADER_BASE_ADDR               ;这一条之前是在loader.S中定义,现在搬过来了

KERNEL_BIN_BASE_ADDR equ 0x70000                    ;定义内核在内存中的缓冲区,也就是将编译好的内核文件暂时存储在内存中的位置
KERNEL_START_SECTOR equ 0x9                         ;定义内核在磁盘的起始扇区
KERNEL_ENTRY_POINT equ 0xc0001500                   ;定义内核可执行代码的入口地址
                                                    ;-------------  程序段的 type 定义   --------------
PT_NULL equ 0

在loader.s定义在32位环境下读取磁盘中内容的函数

                                                        ;-------------------------------------------------------------------------------
                                                        ;功能:读取硬盘n个扇区
rd_disk_m_32:	   
                                                        ;-------------------------------------------------------------------------------
				                                        ; eax=LBA扇区号
				                                        ; ebx=将数据写入的内存地址
				                                        ; ecx=读入的扇区数
    mov esi,eax	                                        ;备份eax
    mov di,cx		                                    ;备份cx
                                                        ;读写硬盘:
                                                        ;第1步:选择特定通道的寄存器,设置要读取的扇区数
    mov dx,0x1f2
    mov al,cl
    out dx,al                                           ;读取的扇区数

    mov eax,esi	                                        ;恢复ax

                                                        ;第2步:在特定通道寄存器中放入要读取扇区的地址,将LBA地址存入0x1f3 ~ 0x1f6
                                                        ;LBA地址7~0位写入端口0x1f3
    mov dx,0x1f3                       
    out dx,al                          

                                                        ;LBA地址15~8位写入端口0x1f4
    mov cl,8
    shr eax,cl
    mov dx,0x1f4
    out dx,al

                                                        ;LBA地址23~16位写入端口0x1f5
    shr eax,cl
    mov dx,0x1f5
    out dx,al

    shr eax,cl
    and al,0x0f	                                        ;lba第24~27位
    or al,0xe0	                                        ; 设置7~4位为1110,表示lba模式
    mov dx,0x1f6
    out dx,al

                                                        ;第3步:向0x1f7端口写入读命令,0x20 
    mov dx,0x1f7
    mov al,0x20                        
    out dx,al

                                                        ;第4步:检测硬盘状态
.not_ready:
                                                        ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
    nop
    in al,dx
    and al,0x88	                                        ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
    cmp al,0x08
    jnz .not_ready	                                    ;若未准备好,继续等。

                                                        ;第5步:从0x1f0端口读数据
    mov ax, di                                          ;di当中存储的是要读取的扇区数
    mov dx, 256                                         ;每个扇区512字节,一次读取两个字节,所以一个扇区就要读取256次,与扇区数相乘,就等得到总读取次数
    mul dx                                              ;8位乘法与16位乘法知识查看书p133,注意:16位乘法会改变dx的值!!!!
    mov cx, ax	                                        ; 得到了要读取的总次数,然后将这个数字放入cx中
    mov dx, 0x1f0
.go_on_read:
    in ax,dx
    mov [ebx],ax                                        ;与rd_disk_m_16相比,就是把这两句的bx改成了ebx
    add ebx,2		        
                                                        ; 由于在实模式下偏移地址为16位,所以用bx只会访问到0~FFFFh的偏移。
                                                        ; loader的栈指针为0x900,bx为指向的数据输出缓冲区,且为16位,
                                                        ; 超过0xffff后,bx部分会从0开始,所以当要读取的扇区数过大,待写入的地址超过bx的范围时,
                                                        ; 从硬盘上读出的数据会把0x0000~0xffff的覆盖,
                                                        ; 造成栈被破坏,所以ret返回时,返回地址被破坏了,已经不是之前正确的地址,
                                                        ; 故程序出会错,不知道会跑到哪里去。
                                                        ; 所以改为ebx代替bx指向缓冲区,这样生成的机器码前面会有0x66和0x67来反转。
                                                        ; 0X66用于反转默认的操作数大小! 0X67用于反转默认的寻址方式.
                                                        ; cpu处于16位模式时,会理所当然的认为操作数和寻址都是16位,处于32位模式时,
                                                        ; 也会认为要执行的指令是32位.
                                                        ; 当我们在其中任意模式下用了另外模式的寻址方式或操作数大小(姑且认为16位模式用16位字节操作数,
                                                        ; 32位模式下用32字节的操作数)时,编译器会在指令前帮我们加上0x66或0x67,
                                                        ; 临时改变当前cpu模式到另外的模式下.
                                                        ; 假设当前运行在16位模式,遇到0X66时,操作数大小变为32位.
                                                        ; 假设当前运行在32位模式,遇到0X66时,操作数大小变为16位.
                                                        ; 假设当前运行在16位模式,遇到0X67时,寻址方式变为32位寻址
                                                        ; 假设当前运行在32位模式,遇到0X67时,寻址方式变为16位寻址.
    loop .go_on_read
    ret

然后将我们写入磁盘中的内核文件读取到指定的内存地址

                                                        ; -------------------------   加载kernel  ----------------------
    mov eax, KERNEL_START_SECTOR                        ; kernel.bin所在的扇区号
    mov ebx, KERNEL_BIN_BASE_ADDR                       ; 从磁盘读出后,写入到ebx指定的地址
    mov ecx, 200			                            ; 读入的扇区数
    call rd_disk_m_32

我们需要根据elf文件格式解析内核文件,将文件中的各个段拷贝到自己被编译的虚拟地址处,先定义拷贝函数和解析elf文件函数kernel_init。

                                                        ;----------  逐字节拷贝 mem_cpy(dst,src,size) ------------
                                                        ;输入:栈中三个参数(dst,src,size)
                                                        ;输出:无
                                                        ;---------------------------------------------------------
mem_cpy:		      
    cld                                                 ;将FLAG的方向标志位DF清零,rep在执行循环时候si,di就会加1
    push ebp                                            ;这两句指令是在进行栈框架构建
    mov ebp, esp
    push ecx		                                    ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份
    mov edi, [ebp + 8]	                                ; dst,edi与esi作为偏移,没有指定段寄存器的话,默认是ss寄存器进行配合
    mov esi, [ebp + 12]	                                ; src
    mov ecx, [ebp + 16]	                                ; size
    rep movsb		                                    ; 逐字节拷贝

                                                        ;恢复环境
    pop ecx		
    pop ebp
    ret

kernel_init:
    xor eax, eax                                        ;清空eax
    xor ebx, ebx		                                ;清空ebx, ebx记录程序头表地址
    xor ecx, ecx		                                ;清空ecx, cx记录程序头表中的program header数量
    xor edx, edx		                                ;清空edx, dx 记录program header尺寸

    mov dx, [KERNEL_BIN_BASE_ADDR + 42]	                ; 偏移文件42字节处的属性是e_phentsize,表示program header table中每个program header大小
    mov ebx, [KERNEL_BIN_BASE_ADDR + 28]                ; 偏移文件开始部分28字节的地方是e_phoff,表示program header table的偏移,ebx中是第1 个program header在文件中的偏移量
					                                    ; 其实该值是0x34,不过还是谨慎一点,这里来读取实际值
    add ebx, KERNEL_BIN_BASE_ADDR                       ; 现在ebx中存着第一个program header的内存地址
    mov cx, [KERNEL_BIN_BASE_ADDR + 44]                 ; 偏移文件开始部分44字节的地方是e_phnum,表示有几个program header
.each_segment:
    cmp byte [ebx + 0], PT_NULL		                    ; 若p_type等于 PT_NULL,说明此program header未使用。
    je .PTNULL

                                                        ;为函数memcpy压入参数,参数是从右往左依然压入.函数原型类似于 memcpy(dst,src,size)
    push dword [ebx + 16]		                        ; program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数:size
    mov eax, [ebx + 4]			                        ; 距程序头偏移量为4字节的位置是p_offset,该值是本program header 所表示的段相对于文件的偏移
    add eax, KERNEL_BIN_BASE_ADDR	                    ; 加上kernel.bin被加载到的物理地址,eax为该段的物理地址
    push eax				                            ; 压入函数memcpy的第二个参数:源地址
    push dword [ebx + 8]			                    ; 压入函数memcpy的第一个参数:目的地址,偏移程序头8字节的位置是p_vaddr,这就是目的地址
    call mem_cpy				                        ; 调用mem_cpy完成段复制
    add esp,12				                            ; 清理栈中压入的三个参数
.PTNULL:
   add ebx, edx				                            ; edx为program header大小,即e_phentsize,在此ebx指向下一个program header 
   loop .each_segment
   ret

最后,在loader.s文件中跳转到内核开始执行。启动bochs虚拟机进行测试。ctrl+c,发现程序停留在0xc0001503地址处(jmp指令占3个字节大小)

    lgdt [gdt_ptr]                                      ;在开启分页后,用gdt新的地址重新加载

enter_kernel:    
    call kernel_init
    mov esp, 0xc009f000
    jmp KERNEL_ENTRY_POINT                              ; 用地址0x1500访问测试,结果ok


网站公告

今日签到

点亮在社区的每一天
去签到