《操作系统真象还原》学习笔记:第五章 保护模式进阶,向内核迈进

一.获取物理内存容量

1.利用BIOS中断0x15子功能0xe820获取内存

BIOS中断0x15的子功能0xE820能够获取系统的内存布局,每次BIOS只返回一种类型的内存信息,直到将所有内存类型返回完毕。内存信息的内容是用地址范围描述符来描述的,用于存储这种描述符的结构称之为地址范围描述符(ARDS)
在这里插入图片描述

  • 此结构中的字段都是4字节大小,共5个字段,所以此结构大小为20字节。每次int 0x15之后,BIOS就返回这样一个结构的数据。
  • 注意:ARDS结构中用64位来描述这段内存的基地址及其长度。但由于我们在32位环境下工作,所以在ARDS结构属性中,我们只用到低32位,即BaseAddrLow + LengthLow是一片内存区域的上限。type字段用来描述这段内存的类型(内存的用途)
    在这里插入图片描述


    为什么BIOS会按类型来返回内存信息呢?原因是这段内存可能是:
    • 系统的ROM
    • ROM用到了这部分内存
    • 设备内存映射到了这部分内存。
    • 由于某种原因,这段内存不适合标准设备使用

在这里插入图片描述
1.填写好“调用前输入”中列出的寄存器
2.执行中断调用int 0x15
3. 在CF位为0表示未出错,“返回后输出”中对应的寄存器便会有对应结果

2. 利用BIOS中断0x15子功能0xe801获取内存

  • 此方法最大只能识别4GB内存,检测到的内存分别放到两组寄存器中。低于15MB的内存以1KB为单位大小来记录,单位数量在AX和CX中记录,AX和CX值是一样的,16MB~4GB是以64KB为单位来记录的,单位数量在寄存器BX和DX中记录,BX和DX的值是一样的
    在这里插入图片描述
  • 此方法检测到的内存会小于实际内存1MB,所以在检测结果的基础上一定要加1MB

3. 利用BIOS中断0x15子功能0x88获取内存

  • 该方法最大只能识别64MB的内存,又因为中断只会显示1MB之上的内存,所以只会显示63M内存
    在这里插入图片描述

4.实战内存容量检测

;loader.s

%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	       ;limit=(0xbffff-0xb8000)/4k=0x7
	       			  dd    DESC_VIDEO_HIGH4   ; 此时dpl已改为0

   GDT_SIZE          equ   $ - GDT_BASE        ;GDT的大小
   GDT_LIMIT         equ   GDT_SIZE -	1      ;GDT的界限
   
   times 60 dq 0			                   ; 此处预留60个描述符的空位
   SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0      ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
   SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	  ; 同上
   SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	  ; 同上 

;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+ard_nr2,256字节
			ards_buf times 244 db 0
			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
   jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能
   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
   jmp .mem_get_ok

;------  int 15h ax = E801h 获取内存大小,最大支持4G  ------
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
   mov ax,0xe801
   int 0x15
   jc .e801_failed_so_try88   ;若当前e801方法失败,就尝试0x88方法

;1 先算出低15M的内存,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寄存器备份

;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
   xor eax,eax
   mov ax,bx		
   mov ecx, 0x10000	;0x10000十进制为64KB
   mul ecx		;32位乘法,默认的被乘数是eax,积为64,32位存入edx,32位存入eax.
   add esi,eax		;由于此方法只能测出4G以内的内存,32位eax足够了,edx肯定为0,只加eax便可
   mov edx,esi		;edx为总内存大小
   jmp .mem_get_ok

;-----------------  int 15h ah = 0x88 获取内存大小,只能获取64M之内  ----------
.e801_failed_so_try88: 
   ;int 15后,ax存入的是以kb为单位的内存容量
   mov  ah, 0x88
   int  0x15
   jc .error_hlt
   and eax,0x0000FFFF
      
   ;16位乘法,被乘数是ax,积为32.积的高16位在dx中,积的低16位在ax中
   mov cx, 0x400     ;0x400等于1024,将ax中的内存容量换为以byte为单位
   mul cx
   shl edx, 16	     ;把dx移到高16位
   or edx, eax	     ;把积的低16位组合到edx,32位的积
   add edx,0x100000  ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB

.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
   
; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转
; 这将导致之前做的预测失效,从而起到了刷新的作用。   
	jmp  dword SELECTOR_CODE:p_mode_start	    

.error_hlt:		      ;出错则挂起
   hlt
[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
   mov gs, ax

   mov byte [gs:160], 'P'

   jmp $

在这里插入图片描述

在这里插入图片描述
在命令窗口Ctrl+C中断bochs运行,再输入xp 0xb00回车。查询到total_mem_bytes处为0x2000000,转换后正好是32MB

二. 启用分页机制

1. 一级页表

  • 尽管在保护模式中段寄存中的内容已经是选择子,但选择子最终也就是为了要找到段基址,其内存访问的核心机制依然是“段基址:段内偏移地址”,这两个地址相加就是线性地址,线性地址是物理地址。段基址和段内偏移地址相加求和的工作是由CPU段部件自动完成的。
  • 如果打开了分页机制,段部件输出的线性地址就不再等同于物理地址了,我们称之为虚拟地址,它是逻辑上的,是假的,不应该被送上地址总线(因为地址只是个数字,任何数字都可以当做地址,这里说的“不应该”是指应该人为保证送上地址总线上的数字是正确的地址)。CPU必须拿到物理地址才行,此虚拟地址对应的物理地址需要在页表中查找,这项工作是由页部件自动完成的
    在这里插入图片描述
  • 经过段部件的处理后,保护模式的寻址空间是4GB,这个寻址空间是指线性地址空间,它在逻辑上是连续的。分页机制的思想是:通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续。

在这里插入图片描述

  • 一页大小为4KB,4GB地址空间被划分成4GB/4KB=1M个页,所以页表中有1048576个页表项。用20位就可以索引一个页,12位就可以表达4kb之内的任意地址。
  • 因为一个页表项大小是4字节,所以索引 * 4才是页表项相对于页表物理地址的字节偏移量
    在这里插入图片描述

2. 二级页表

  • 既然有了一级页表,为什么还要二级页表呢?
    1. 一级页表中最多可容纳1M(1048576)个页表项,每个页表项是4字节,如果页表项全满的话,便是4MB大小。
    2. 一级页表中所有页表项必须要提交前建好,原因是操作系统要占用4GB虚拟地址空间的高1GB,用户进程要占用低3GB
    3. 每个进程都有自己的页表,进程一多,光是页表占用的空间就很可观了。
  • 归根结底,我们要解决的是:不要一次性地将全部页表建好,需要时动态创建页表项。
    在这里插入图片描述
  • 无论几级页表,页的尺寸都是4KB,所以4GB线性地址空间最多有1M个页。一级页表是将这1M个页放置到一张页表中,二级页表是将这1M个页放置到1K个页表中。每个页表中包含有1K个页表项。页表项是4KB大小,页表包含1K个页表项,故页表大小为4KB,这恰恰是一个标准页的大小。
  • 专门有个页目录来存储这些也表,每个页表的物理地址在页目录表中都以页目录项的形式存储,页目录表也是4KB大小
  • 定位某一个物理页,必然要先找到其所属的页表。
    1. 由于页目录中1024个页表,只需要10位就够了,所以,虚拟地址的高10位(第31~22位)用来在页目录中定位一个页目录项(页表)。
    2. 由于页表中可以容纳1024个物理页,故只需要10位就够了。所以虚拟地址的中间10位(第21~12位)用来在页表中定位具体的物理页(页表项)。
    3. 由于12位便可以表达4KB内的任意地址,故线性地址中余下的12位(第11位~第0位)用于页内偏移量。

在这里插入图片描述

(1)用虚拟地址的高 10 位乘以 4,作为页目录表内的偏移地址,加上页目录表的物理地址,所得的和,便是页目录项的物理地址。读取该页目录项,从中获取到页表的物理地址。
(2)用虚拟地址的中间 10 位乘以 4,作为页表内的偏移地址,加上在第 1 步中得到的页表物理地址,所得的和,便是页表项的物理地址。读取该页表项,从中获取到分配的物理页地址。
(3)虚拟地址的高 10 位和中间 10 位分别是 PDE 和 PTE 的索引值,所以它们需要乘以 4。但低 12 位
就不是索引值啦,其表示的范围是 0~0xfff,作为页内偏移最合适,所以虚拟地址的低 12 位加上第 2 步
中得到的物理页地址,所得的和便是最终转换的物理地址。

  • 这种自动化较强的工作,还是由页部件自动完成的。

在这里插入图片描述

目录项和页表项中的都是物理页地址,标准页大小是 4KB,故地址都是 4K 的倍数,也就是地址的低 12位是 0,所以只需要记录物理地址高 20 位就可以啦。这样省出来的 12 位(第 0~11 位)可以用来添加其他属性,下面对这些属性从低到高逐位介绍。

  • P:存在位。若为 1 表示该页存在于物理内存中,若为 0 表示该表不在物理内存中。操作系统的页式虚拟内存管理便是通过 P 位和相应的 pagefault 异常来实现的。
  • RW:读写位。若为 1 表示可读可写,若为 0 表示可读不可写。
  • US:普通用户/超级用户位。若为 1 时,表示处于 User 级,任意级别(0、1、2、 3)特权的程序都可以访问该页。若为 0,表示处于Supervisor 级,特权级别为 3 的程序不允许访问该页,该页只允许特权级别为 0、1、2 的程序可以访问。
  • PWT:页级通写位,也称页级写透位。若为 1 表示此项采用通写方式,表示该页不仅是普通内存,还是高速缓存。此项和高速缓存有关,“通写”是高速缓存的一种工作方式,本位用来间接决定是否用此方式改善该页的访问效率。这里咱们直接置为 0 就可以啦。
  • PCD:页级高速缓存禁止位。若为 1 表示该页启用高速缓存,为 0 表示禁止将该页缓存。这里咱们将其置为 0。
  • A,访问位。若为 1 表示该页被 CPU 访问过啦,所以该位是由 CPU 设置的。还记得段描述符中的 A 位和 P 位吗?这两位在一起可以实现段式虚拟内存管理。和它们一样,这里页目录项和页表项中的 A 位也可以用来记录某一内存页的使用频率(操作系统定期将该位清 0,统计一段时间内变成 1 的次数),从而当内存不足时,可以将使用频率较低的页面换出到外存(如硬盘),同时将页目录项或页表项的 P位置 0,下次访问该页引起 pagefault 异常时,中断处理程序将硬盘上的页再次换入,同时将 P 位置 1。
  • D,Dirty,意为脏页位。当 CPU 对一个页面执行写操作时,就会设置对应页表项的 D 位为 1。此项仅针对页表项有效,并不会修改页目录项中的 D 位。
  • PAT,Page Attribute Table,意为页属性表位,能够在页面一级的粒度上设置内存属性。比较复杂,将
    此位置 0 即可。
  • G,Global,意为全局位。由于内存地址转换也是颇费周折,先得拆分虚拟地址,然后又要查页目录,又要查页表的,所以为了提高获取物理地址的速度,将虚拟地址与物理地址转换结果存储在 TLB(Translation Lookaside Buffer)中,TLB 以后咱们会细说。在此先知道 TLB 是用来缓存地址转换结果的高速缓存就 ok 啦。此 G 位用来指定该页是否为全局页,为 1 表示是全局页,为 0 表示不是全局页。若为全局页,该页将在高速缓存 TLB 中一直保存,给出虚拟地址直接就出物理地址啦,无需那三步骤转换。由于 TLB 容量比较小(一般速度较快的存储设备容量都比较小),所以这里面就存放使用频率较高的页面。顺便说一句,清空 TLB 有两种方式,一是用 invlpg 指令针对单独虚拟地址条目清理,或者是重新加载 cr3 寄存器,这将直接清空 TLB。
  • AVL:可用软件,操作系统可用该位,CPU 不理会该位的值,那咱们也不理会吧。

启用分页机制,我们要按顺序做好三件事。
(1)准备好页目录表及页表。
(2)将页表地址写入控制寄存器 cr3。 (3)寄存器 cr0 的 PG 位置 1

3.规划页表之操作系统与用户进程关系

  • 页表的设计是内存分布来决定的,在用户进程4GB虚拟地址空间的高3GB以上的部分划分给操作系统,0~3GB是用户进程自己的虚拟空间。
  • 为了实现操共享操作系统,让所有用户进程3GB~4GB的虚拟地址空间都指向同一个操作系统,也就是所有进程的虚拟地址3GB~4GB本质上都是指向同一片物理地址。

页目录表的位置,我们就放在物理地址0x100000处,为了让页表和页目录表紧凑一些(这不是必须
的),咱们让页表紧挨着页目录表。页目录本身占 4KB,所以第一个页表的物理地址是 0x101000。在这里插入图片描述

0~1G: 0x00000000~0x3FFFFFFFF
1~2G: 0x40000000~0x7FFFFFFFF
2~3G: 0x80000000~0xBFFFFFFFF
3~4G: 0xC0000000~0xFFFFFFFFF


;loader.s

%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	       ;limit=(0xbffff-0xb8000)/4k=0x7
	       			  dd    DESC_VIDEO_HIGH4   ; 此时dpl已改为0

   GDT_SIZE          equ   $ - GDT_BASE        ;GDT的大小
   GDT_LIMIT         equ   GDT_SIZE -	1      ;GDT的界限
   
   times 60 dq 0			                   ; 此处预留60个描述符的空位
   SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0      ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
   SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	  ; 同上
   SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	  ; 同上 

;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+ard_nr2,256字节
			ards_buf times 244 db 0
			ards_nr  dw  0          ;用于记录ARDS结构体数量

;0xc00
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
   jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能
   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
   jmp .mem_get_ok

;------  int 15h ax = E801h 获取内存大小,最大支持4G  ------
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
   mov ax,0xe801
   int 0x15
   jc .e801_failed_so_try88   ;若当前e801方法失败,就尝试0x88方法

;1 先算出低15M的内存,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寄存器备份

;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
   xor eax,eax
   mov ax,bx		
   mov ecx, 0x10000	;0x10000十进制为64KB
   mul ecx		;32位乘法,默认的被乘数是eax,积为64,32位存入edx,32位存入eax.
   add esi,eax		;由于此方法只能测出4G以内的内存,32位eax足够了,edx肯定为0,只加eax便可
   mov edx,esi		;edx为总内存大小
   jmp .mem_get_ok

;-----------------  int 15h ah = 0x88 获取内存大小,只能获取64M之内  ----------
.e801_failed_so_try88: 
   ;int 15后,ax存入的是以kb为单位的内存容量
   mov  ah, 0x88
   int  0x15
   jc .error_hlt
   and eax,0x0000FFFF
      
   ;16位乘法,被乘数是ax,积为32.积的高16位在dx中,积的低16位在ax中
   mov cx, 0x400     ;0x400等于1024,将ax中的内存容量换为以byte为单位
   mul cx
   shl edx, 16	     ;把dx移到高16or edx, eax	     ;把积的低16位组合到edx,32位的积
   add edx,0x100000  ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB

.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
   
; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转
; 这将导致之前做的预测失效,从而起到了刷新的作用。   
	jmp  dword SELECTOR_CODE:p_mode_start	    

.error_hlt:		      ;出错则挂起
   hlt
[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
   mov gs, ax


; 创建页目录及页表并初始化页内存位图
   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], 'V'     ;视频段段基址已经被更新,用字符v表示virtual addr

   jmp $
   
   
   
   
   

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


 ;开始创建页目录项(PDE)
 .create_pde:
	mov eax,0x101000	; 此时 eax 为第一个页表的位置及属性
	mov ebx,eax
	or eax, PG_US_U | PG_RW_W | PG_P
	
	mov [0x100000 + 0], eax			;在页目录表中的第 1 个目录项写入第一个页表的位置(0x101000)及属性(7)
	mov [0x100000 + 0xc00], eax		; 0xc00 表示第 768 个页表占用的目录项,0xc00 以上的目录项用于内核空间
	sub eax,0x1000
	mov [0x100000 + 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:
	mov [0x101000+esi*4],edx
	add edx,4096
	inc esi
	loop .create_pte
	
;创建内核其他页表的PDE
	mov eax,0x102000					; 此时 eax 为第二个页表的位置
	or eax, PG_US_U | PG_RW_W | PG_P 	; 页目录项的属性 US、 RW 和 P 位都为 1
	mov ecx,254
	mov esi,769
.create_kernel_pde:
	mov [0x100000+esi*4],eax
	inc esi
	add eax,0x1000
	loop .create_kernel_pde
	ret
	

在这里插入图片描述

  • 第一行:将物理地址低端1M内存映射到虚拟地址低端1M内存
  • 第二行:将物理地址低端1M内存映射到虚拟地址0xc0000000~0xc00fffff
  • 第三行:将虚拟地址0xffc00000拆分后高10位是1023定位到最后一个页目录表项,由于最后一个页目录表项当初填写的是页目录表的起始地址0x100000,所以中间10位0 定位到第0个页目录表项指向的页表,最后12位可以修改第0个页表的任意页表项。所以物理地址0x000000101000-0x000000101fff正好是第0个页表到第一个页表之间的地址。
  • 第四行:将虚拟地址0xfff00000拆分后,高10位依然是1023定位到最后一个页目录项,中间10位为768 ~ 1022定位到第768 ~ 1022页目录表指向的页表,最后12位则可以修改这些页表的每一个页表项。
  • 第五行:将虚拟地址0xfffff000拆分后,高10位依然是1023定位到最后一个页目录项,中间10位为1023定位到第1023个页目录表项,由于第1023个页目录表项里的内容是页目录表的起始地址0x100000,所以最后12位可以修改任意1024个页目录表项。

总结:

  1. 访问页目录表物理地址:让虚拟地址的高20位为0xfffff,低12位,即0xfffff000,这也是页目录表中第0个页目录项自身的物理地址
  2. 获取页表物理地址:使虚拟地址为0xfffffxxx,其中xxx是页目录项的索引乘以4的积
  3. 访问页表中的页表项:使虚拟地址高10位为0x3ff,目的是获取页目录表物理地址。中间10位为页表的索引值,低12位为页表内的偏移地址,用来定位页表项,它必须是已经乘以4后的值
    公式为 0x3ff<<22 +中间 10 位<<12 +低 12 位

4.快表 TLB(Translation Lookaside Buffer)简介

三. 加载内核

1.用C语言写内核

  • 用gcc编译程序:gcc 源文件 -m32 -c -o 输出文件
    -c的作用是编译、汇编到目标代码,不进行链接,也就是直接生成目标文件。
    -o的作用是将输出的文件以指定文件名来存储,有同名文件存在时直接覆盖。

  • 链接:ld -m elf_i386 源文件 -Ttext 起始虚拟地址 -e main -o 输出文件
    -Ttext:指定起始虚拟地址
    -o:将输出的文件以指定文件名来存储,有同名文件存在时直接覆盖。
    -e:指定程序的起始地址,起始地址可是是数字形式的地址, 也可以是符号名

2.elf格式的二进制文件

在这里插入图片描述

  • 程序最重要的的部分就是段(segment)和节(section),它们是真正的程序体,是真真切切的程序资源。段是由节组成的,多个节经过链接之后就被合成一个段。段和节的信息也是用header来描述的,程序头是program header,节头是section header。
  • 程序头表(program header table)和节头表(section header table)里存储的是多个程序头(segment)和多个节头(section)的信息。
  • elf header 这个数据结构用来描述 程序头表和节头表的大小及位置信息

在这里插入图片描述

1. elf header

在这里插入图片描述

在这里插入图片描述

  • e_ident[16]

在这里插入图片描述

  • e_type占用2个字节,是用来指定elf目标文件的类型,取值见下表:
    在这里插入图片描述
  • e_machine占用2个字节,用来描述elf目标文件的体系结构类型,也就是说该文件要在哪种平台(哪种机器)上才能运行。
    在这里插入图片描述

 

  • e_version:占用4个字节,用来表示版本信息
  • e_entry:占用4个字节,用来指明操作系统运行该程序时,将控制权转交到的虚拟地址。
  • e_phoff:占用4个字节,用来指明程序头表(program header table)在文件内的字节偏移量。如果没有程序头表,该值为0
  • e_shoff:占用4个字节,用来指明节头表(section header table)在文件内的字节偏移量。若没有节头表,该值为0
  • e_flags:占用4个字节,用来指明与处理器相关的标志。
  • e_ehsize:占用 2 个字节,用来指明 elf header 的字节大小。
  • e_phentsize:占用2个字节,用来指明程序头表(program header table)中每个条目(entry)的字节大小,即每个用来描述段信息的数据结构的字节大小
  • e_phnum :占用 2 个字节,用来指明程序头表中条目的数量。实际上就是段的个数。
  • e_phentsize:占用2个字节,用来指明节头表(section header table)中每个条目(entry)的字节大小,即每个用来描述节信息的数据结构的字节大小。
  • e_shnum:占用2个字节,用来指明节头表中条目的数量。实际上就是节的个数。
  • e_shstrndx:占用2个字节,用来指明string name table在节头表中的索引index。




下面是程序头表中的条目的数据结构elf32_phdr,这是用来描述各个段的信息用的:
在这里插入图片描述

  • p_type:占用4个字节,用来指明程序中该段的类型,见下表:
    在这里插入图片描述
     
  • p_offset:占用4个字节,用来指明本段在文件内的起始偏移字节
  • p_vaddr:占用4个字节,用来指明本段在内存中的起始虚拟地址
  • p_paddr:占用4个字节,仅用于与物理地址相关的系统中,因为System V忽略用户程序中所有的物理地址,所以此项暂且保留,未设定。
  • p_filesz:占用4个字节,用来指明本段在文件中的大小
  • p_memsz:占用4个字节,用来指明本段在内存中的大小
  • p_flags:占用4个字节,用来指明与本段相关的标志,此标志取值见下表:

在这里插入图片描述

  • p_align:占用4个字节,用来指明本段在文件和内存中的对齐方式。如果值为0或1,则表示不对齐。否则则p_align应该是2的幂次数。

链接后,程序运行的代码、数据等资源都是在段中





3. 将内核载入内存

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

#编译
gcc -m32 -c -o main.o main.c 
#链接
ld -m elf_i386 main.o -Ttext 0xc0001500 -e main -o kernel.bin 
#将内核文件kernel.bin,把内核文件写入硬盘第9个扇区
dd if=kernel.bin of=/ your_path/hd60M.img bs=512 count=200 seek=9 conv=notrunc

loader.s需要修改两个地方

  • 加载内核:需要把内核文件从硬盘上拷贝到内存中。
  • 初始化内核:需要在分页后,将加载进来的elf内核文件安置到相应的虚拟内存地址,然后跳过去执行,从此loader的工作结束
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值