前言

由于涉及到马上要搞实习的事情,搞得我十分的浮躁,自己也是频繁失眠,想来还是自己太过懒了,没控制住自己,自己也在这一个多月没搞好,尤其是本来想花几天时间来写一个高性能服务器,也把游双大佬的linux 高性能服务器编程大概看完了,然后自己也跟着视频写了写,但总是感觉自己没有真的懂,然后自己在github上下来的项目也总是感觉不好下手,然后我自己又想去搞csapp的实验了,结果前几个实验还好,但是在做那个shell lab的时候总感觉自己没有很好的搞懂,遂又放弃,然后自己又迷上了原神,又是荒废了十几天,这一个多月来真的自己心态一直有问题,就是自己急于求快,对于我这样的没天赋的选手来讲,是最不能急于求快的,我很清楚了自己的天赋,想靠几天时间就开发一个比较完整的高性能服务器也不现实,同样几天时间做完csapp实验也不太现实
痛定思痛,仔细观察了自己的缺点,容易半途而废,容易拖时间,容易被分心,我是不适合像操作系统一样不停的进行上下文切换的,反之结果就会导致我实验没完成,项目没完成,操作系统也没写完,所以我以后比较大的事情我直接专心一件事情更好。
同时,我也只能等秋招了,实在是没啥信心来暑期实习啊。但是也决定不能再拖了,再拖就彻底凉了,我很清楚还有大概两个月来准备,基于对操作系统的热爱,我决定一定要把操作系统写完去,之后便着手准备八股文。

实验

5.1获取物理内存容量

关于获取物理内存容量就是根据中断子功能来判断就可以了,这书中已经说的很详细了,也就没啥可说的。

代码

;----int 15h ax=e801h获取内存大小,最大支持4G
;返回后,ax cx值一样,以1kb为单位 bx,dx 值一样,以63kb为单位
;在ax和cx寄存器中为低16MB,在bx,dx寄存器中为16MB到4GB

.e820_failed_so_try_e801:
    mov ax,0xe801
    int 0x15
    jc .e801_failed_so_try88    ;若当前e801方法失败,就尝试0x88的方法

;先算出低15MB的内存
;ax和cx是以kb为单位的内存数量,将其转化成以byte为单位
    mov cx,0x400            ;cx和ax值都一样,cx用作乘数
    mul cx
    shl edx,16
    and eax,0x0000ffff
    or edx,eax
    add edx,0x100000        ;ax只是15MB,故要加上1MB
    mov esi,edx             ;想把低15MB的内存容量存入esi作为备份

;再将16MB以上的内存转化成byte为单位
;寄存器bx和dx中是64kb为单位的内存数量
    xor eax,eax
    mov ax,bx
    mov ecx,0x10000         ;10机制是64kb
    mul ecx                 ;32位乘法,默认的被乘数是eax,结果为64位
                            ;高32位存入edx,低32位存入eax
    add esi,eax
;由于此方法只能测出4GB以内的内存,故32位eax足够了
;edx肯定为0,只加eax便可
    mov edx,esi                 ;edx为总内存大小
    jmp .mem_get_ok

;---int 15h ah=0x88获取内存大小,只能获取64MB之内
.e801_failed_so_try88:
    ;int 15后,ax存入的是以kb为单位的内存容量
    mov ah,0x88 
    int 0x15 
    jc .error_hlt
    and eax,0x0000ffff

    ;16为乘法,被乘数是ax,结果32位,高位16在dx,低32位在ax
    mov cx,0x400 
    ;0x400等于1024,将ax中的内存容量换为以byte为单位
    mul cx
    shl edx,16              ;把dx移动到高16位
    or edx,eax              ;把结果低16位组合到edx,为32位的结果
    add ebx,0x100000        ;0x88子功能只会返回1MB内存,故实际内存要加上1MB

.error_hlt:
    hlt    ;暂停的意思

.mem_get_ok:
    mov [total_mem_bytes],edx   ;将内存转换为byte后存入total_mem_bytes处

5.2 启用内存分页机制,畅游虚拟空间

对于这一部分,毫无疑问是这一章最精彩的部分,先说一下为啥要分页,主要是分段是有问题的,首先是内存碎片,同时由于段是连续的,也不好换入换出,于是就提出分页。通过页表的映射,这时的段基址:偏移地址也就不再是物理地址,而称作线性地址,或者虚拟地址,那页的大小也需要考量,不能太小,不然页表的内存容量就太大了,这样反而得不偿失,现在统一的页的大小是4kb,这样的话,对于32位机器来说,也就是一共4G的空间,4G/4kb=2^20=1MB,这其实还是挺大的,要知道我们这个操作系统的内核也就准备了1MB的物理内存,所以有2级页表,当然肯定主要不是由于页表的内存容量的问题,我从哈工大操作系统中可以知道,主要是对于页表项的索引,操作系统需要查找,但是页表项过多的话,就查找很费时间,而使用二级页表就能有效的避免这个问题,毕竟2级页表后,无论是页目录项还是页表项都只有1024个,这当然很轻松的就能查找。现代操作系统也一般用2级页表,所以书中也用2级页表。一级页表也就是页目录表存储的是页目录项,页目录项中有页表的物理地址(不是虚拟地址),页表中存储的就是页表项,页表项中有虚拟地址对应的物理地址。总而言之总的流程是这样的,首先,通过全局描述符(GDT)拿到线性地址,然后由于地址是32位的,操作系统把高10位作为页目录表的索引,而中10位作为页表中页表下项的索引,低12位就作为偏移地址+通过页表映射得到的地址最终得到虚拟地址所对应的物理地址。
当然,在实际中第1023个页目录项是没用的(从0开始数),因为存储的是页目录表自己的物理地址,无论是页目录表还是页表肯定都存储在内存中,所以有自己的物理地址,而为啥这样,这主要是为了方便定位到自己想要的页目录项和页表。

代码

;-------------   创建页目录及页表   ---------------
setup_page:
;先把页目录占用的空间逐字节清0
   mov ecx, 4096
   mov esi, 0
.clear_page_dir:
   mov byte [PAGE_DIR_TABLE_POS + esi], 0
   inc esi
   loop .clear_page_dir

;开始创建页目录项(PDE)
.create_pde:				     ; 创建Page Directory Entry
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x1000 			     ; 此时eax为第一个页表的位置及属性
   mov ebx, eax				     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。

;   下面将页目录项0和0xc00都存为第一个页表的地址,
;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
;   这是为将地址映射为内核地址做准备
   or eax, PG_US_U | PG_RW_W | PG_P	     ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
   mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)
   mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
					     ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
   sub eax, 0x1000
   mov [PAGE_DIR_TABLE_POS + 4092], eax	     ; 使最后一个目录项指向页目录表自己的地址

;下面创建页表项(PTE)
   mov ecx, 256				     ; 1M低端内存 / 每页大小4k = 256
   mov esi, 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
   inc esi
   loop .create_pte

;创建内核其它页表的PDE
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x2000 		     ; 此时eax为第二个页表的位置
   or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性US,RW和P位都为1
   mov ebx, PAGE_DIR_TABLE_POS
   mov ecx, 254			     ; 范围为第769~1022的所有目录项数量
   mov esi, 769
.create_kernel_pde:
   mov [ebx+esi*4], eax
   inc esi
   add eax, 0x1000
   loop .create_kernel_pde
   ret

5.3 加载内核

关于加载内核我们所知道的是,对于汇编来说我们可以指定文件的入口地址,利用jmp指令来跳转就够了,但是对于.c文件来说,可无法指定其地址,并且其地址弄成静态也不好,而对于.c文件来说一共要进行预处理->编译->汇编->链接这4个步骤,在没链接前生成的是目标文件,也就是可重定位文件,可重定位也就是其加载到内存的地址还未确定,需要通过链接来确定,链接后才可以得到其入口地址,因为对于文件来说起始分为两部分,一部分是文件头,这个部分存储着入口地址等信息,而另一部分就是程序部分,也就是由cpu执行的那些部分。而我们要加载内核首先要做的就是确定kernel.bin的入口地址。

代码

gcc -c -o main.o main.c
ld main.o -Ttext 0xc0001500 -e main -o kernel.bin
enter_kernel:    
   call kernel_init
   mov esp, 0xc009f000
   jmp KERNEL_ENTRY_POINT                 ; 用地址0x1500访问测试,结果ok

;----------将kernel.bin中的segment拷贝到编译的地址
kernel_init:
   xor eax, eax
   xor ebx, ebx		;ebx记录程序头表地址
   xor ecx, ecx		;cx记录程序头表中的program header数量
   xor edx, edx		;dx 记录program header尺寸,即e_phentsize

   mov dx, [KERNEL_BIN_BASE_ADDR + 42]	  ; 偏移文件42字节处的属性是e_phentsize,表示program header大小
   mov ebx, [KERNEL_BIN_BASE_ADDR + 28]   ; 偏移文件开始部分28字节的地方是e_phoff,表示第1 个program header在文件中的偏移量
					  ; 其实该值是0x34,不过还是谨慎一点,这里来读取实际值
   add ebx, KERNEL_BIN_BASE_ADDR
   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
   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

;----------  逐字节拷贝 mem_cpy(dst,src,size) ------------
;输入:栈中三个参数(dst,src,size)
;输出:无
;---------------------------------------------------------
mem_cpy:		      
   cld
   push ebp
   mov ebp, esp
   push ecx		   ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份
   mov edi, [ebp + 8]	   ; dst
   mov esi, [ebp + 12]	   ; src
   mov ecx, [ebp + 16]	   ; size
   rep movsb		   ; 逐字节拷贝

   ;恢复环境
   pop ecx		
   pop ebp
   ret

代码汇总

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR

;构建gdt及其内部的描述符
GDT_BASE:   dd 0x00000000
            dd 0x00000000
CODE_DESC:  dd 0x0000ffff
            dd DESC_CODE_HIGH4
DATA_STACK_DESC:    dd 0x0000ffff
                    dd DESC_DATA_HIGH4 
VIDEO_DESC: dd 0x80000007
            dd DESC_VIDEO_HIGH4 

GDT_SIZE  equ  $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0
SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL0
SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL0
SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0

;total_mem_bytes 用于保存内存容量
;当前偏移loader.bin文件头0x200字节,也就是512字节
;loader.bin的加载地址是0x900
;故total_mem_bytes内存地址是0xb00

total_mem_bytes dd 0

;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr  dw GDT_LIMIT 
         dd GDT_BASE

;人工对齐:total_mem_bytes4+gdt_ptr6+ards_buf244+ards_nr2,共256字节

ards_buf times 244 db 0
ards_nr dw 0

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
    int 0x15
    jc .e820_failed_so_try_e801     ;若cf位有1则有错误发生,尝试0xe801子功能
    add di,cx                       ;使di增加20字节
    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

.next_ards:
    loop .find_max_mem_area
    jmp .mem_get_ok

;----int 15h ax=e801h获取内存大小,最大支持4G
;返回后,ax cx值一样,以1kb为单位 bx,dx 值一样,以63kb为单位
;在ax和cx寄存器中为低16MB,在bx,dx寄存器中为16MB到4GB

.e820_failed_so_try_e801:
    mov ax,0xe801
    int 0x15
    jc .e801_failed_so_try88    ;若当前e801方法失败,就尝试0x88的方法

;先算出低15MB的内存
;ax和cx是以kb为单位的内存数量,将其转化成以byte为单位
    mov cx,0x400            ;cx和ax值都一样,cx用作乘数
    mul cx
    shl edx,16
    and eax,0x0000ffff
    or edx,eax
    add edx,0x100000        ;ax只是15MB,故要加上1MB
    mov esi,edx             ;想把低15MB的内存容量存入esi作为备份

;再将16MB以上的内存转化成byte为单位
;寄存器bx和dx中是64kb为单位的内存数量
    xor eax,eax
    mov ax,bx
    mov ecx,0x10000         ;10机制是64kb
    mul ecx                 ;32位乘法,默认的被乘数是eax,结果为64位
                            ;高32位存入edx,低32位存入eax
    add esi,eax
;由于此方法只能测出4GB以内的内存,故32位eax足够了
;edx肯定为0,只加eax便可
    mov edx,esi                 ;edx为总内存大小
    jmp .mem_get_ok

;---int 15h ah=0x88获取内存大小,只能获取64MB之内
.e801_failed_so_try88:
    ;int 15后,ax存入的是以kb为单位的内存容量
    mov ah,0x88 
    int 0x15 
    jc .error_hlt
    and eax,0x0000ffff

    ;16为乘法,被乘数是ax,结果32位,高位16在dx,低32位在ax
    mov cx,0x400 
    ;0x400等于1024,将ax中的内存容量换为以byte为单位
    mul cx
    shl edx,16              ;把dx移动到高16位
    or edx,eax              ;把结果低16位组合到edx,为32位的结果
    add ebx,0x100000        ;0x88子功能只会返回1MB内存,故实际内存要加上1MB

.error_hlt:
    hlt

.mem_get_ok:
    mov [total_mem_bytes],edx   ;将内存转换为byte后存入total_mem_bytes处

    ;-------准备进入保护模式----------
    ;1 打开A20
    ;2 加载gdt
    ;3 将cr0的pe 为置1

    ;---------------------------打开A20----------
    in al,0x92
    or al,0000_0010b
    out 0x92,al

    ;------------------------加载GDT------------------
    lgdt [gdt_ptr];
    
    ;----------------------cr0第0位置1-------------------
    mov eax,cr0 
    or eax,0x00000001
    mov cr0,eax
    
    jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线
    
    [bits 32]
    p_mode_start:
        mov ax,SELECTOR_DATA
        mov ds,ax
        mov es,ax
        mov ss,ax
        mov esp,LOADER_STACK_TOP
        mov ax,SELECTOR_VIDEO        ;低32位放入显存中
        mov gs,ax
;--------加载kernel---------
   mov eax, KERNEL_START_SECTOR        ; kernel.bin所在的扇区号
   mov ebx, KERNEL_BIN_BASE_ADDR       ; 从磁盘读出后,写入到ebx指定的地址
   mov ecx, 200			       ; 读入的扇区数

   ; 创建页目录及页表并初始化页内存位图
   call setup_page

   ;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载
   sgdt [gdt_ptr]	      ; 存储到原来gdt所有的位置

   ;将gdt描述符中视频段描述符中的段基址+0xc0000000
   mov ebx, [gdt_ptr + 2]  
   or dword [ebx + 0x18 + 4], 0xc0000000      ;视频段是第3个段描述符,每个描述符是8字节,故0x18。
					      ;段描述符的高4字节的最高位是段基址的31~24位

   ;将gdt的基址加上0xc0000000使其成为内核所在的高地址
   add dword [gdt_ptr + 2], 0xc0000000

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

   ; 把页目录地址赋给cr3
   mov eax, PAGE_DIR_TABLE_POS
   mov cr3, eax

   ; 打开cr0的pg位(第31位)
   mov eax, cr0
   or eax, 0x80000000
   mov cr0, eax

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

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

   jmp SELECTOR_CODE:enter_kernel ;强制刷新流水线

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

;----------将kernel.bin中的segment拷贝到编译的地址
kernel_init:
   xor eax, eax
   xor ebx, ebx		;ebx记录程序头表地址
   xor ecx, ecx		;cx记录程序头表中的program header数量
   xor edx, edx		;dx 记录program header尺寸,即e_phentsize

   mov dx, [KERNEL_BIN_BASE_ADDR + 42]	  ; 偏移文件42字节处的属性是e_phentsize,表示program header大小
   mov ebx, [KERNEL_BIN_BASE_ADDR + 28]   ; 偏移文件开始部分28字节的地方是e_phoff,表示第1 个program header在文件中的偏移量
					  ; 其实该值是0x34,不过还是谨慎一点,这里来读取实际值
   add ebx, KERNEL_BIN_BASE_ADDR
   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
   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

;----------  逐字节拷贝 mem_cpy(dst,src,size) ------------
;输入:栈中三个参数(dst,src,size)
;输出:无
;---------------------------------------------------------
mem_cpy:		      
   cld
   push ebp
   mov ebp, esp
   push ecx		   ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份
   mov edi, [ebp + 8]	   ; dst
   mov esi, [ebp + 12]	   ; src
   mov ecx, [ebp + 16]	   ; size
   rep movsb		   ; 逐字节拷贝

   ;恢复环境
   pop ecx		
   pop ebp
   ret

;-------------   创建页目录及页表   ---------------
setup_page:
;先把页目录占用的空间逐字节清0
   mov ecx, 4096
   mov esi, 0
.clear_page_dir:
   mov byte [PAGE_DIR_TABLE_POS + esi], 0
   inc esi
   loop .clear_page_dir

;开始创建页目录项(PDE)
.create_pde:				     ; 创建Page Directory Entry
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x1000 			     ; 此时eax为第一个页表的位置及属性
   mov ebx, eax				     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。

;   下面将页目录项0和0xc00都存为第一个页表的地址,
;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
;   这是为将地址映射为内核地址做准备
   or eax, PG_US_U | PG_RW_W | PG_P	     ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
   mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)
   mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
					     ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
   sub eax, 0x1000
   mov [PAGE_DIR_TABLE_POS + 4092], eax	     ; 使最后一个目录项指向页目录表自己的地址

;下面创建页表项(PTE)
   mov ecx, 256				     ; 1M低端内存 / 每页大小4k = 256
   mov esi, 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
   inc esi
   loop .create_pte

;创建内核其它页表的PDE
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x2000 		     ; 此时eax为第二个页表的位置
   or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性US,RW和P位都为1
   mov ebx, PAGE_DIR_TABLE_POS
   mov ecx, 254			     ; 范围为第769~1022的所有目录项数量
   mov esi, 769
.create_kernel_pde:
   mov [ebx+esi*4], eax
   inc esi
   add eax, 0x1000
   loop .create_kernel_pde
   ret

;-------功能:读取硬盘n个扇区---------
rd_disk_m_32:	   
;-------------------------------------------------------------------------------
							 ; eax=LBA扇区号
							 ; ebx=将数据写入的内存地址
							 ; ecx=读入的扇区数
      mov esi,eax	   ; 备份eax
      mov di,cx		   ; 备份扇区数到di
;读写硬盘:
;第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

;;;;;;; 至此,硬盘控制器便从指定的lba地址(eax)处,读出连续的cx个扇区,下面检查硬盘状态,不忙就能把这cx个扇区的数据读出来

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

;第5步:从0x1f0端口读数据
      mov ax, di	   ;以下从硬盘端口读数据用insw指令更快捷,不过尽可能多的演示命令使用,
			   ;在此先用这种方法,在后面内容会用到insw和outsw等

      mov dx, 256	   ;di为要读取的扇区数,一个扇区有512字节,每次读入一个字,共需di*512/2次,所以di*256
      mul dx
      mov cx, ax	   
      mov dx, 0x1f0
  .go_on_read:
      in ax,dx		
      mov [ebx], ax
      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

实验结果&&注意事项

PE读取本机BIOS_页表


注意gcc版本为4.4版本比较好,并且链接需要时32位的

gcc -m32 -c -o main.o main.c
ld -m elf_i386 main.o -Ttext 0xc0001500 -e main -o kernel.bin

总结

至此,这前5章也正式的结束了,也终于是踏入了内核,好好的回顾这前五章的工作。

首先计算机一开机便会把cs:ip的地址自动指向0xf000:0xfff0,这样地址就是0xffff0,还剩下16字节根本就不够bios进行一些配置,于是就会跳转到另一个地址来执行,然后在对扇区进行检查的时候就会碰到mbr.bin,于是就会加载mbr,加载到地址为0x7c00地址上,之后,mbr执行,并且读硬盘,此时就会把loader.bin加载到地址为0x900的地址上,之后在loader.bin中执行读物理内存,设置GDT,开启分页,读硬盘加载kernel.bin到0x700000地址上,之后会把内核映象拷贝到地址为0x1500的地址上,之后就跳转到这个地址上去执行内核了

PE读取本机BIOS_页表_02