赞
踩
本篇对应书籍第五章5.1–5.2的内容
本篇介绍并实践获取物理内存容量的方法,以及开启分页机制
上一篇章中,我们从CPU的实模式进入到了保护模式,首先要处理的第一个问题就是内存地址的访问,保护模式下,内存地址使用虚拟地址到物理地址映射访问,在那之前,要先获取物理内存的大小
可以通过调用 BIOS中断 0x15 实现,BIOS中断 0x15 有3个子功能:
0xe820能够获取系统的内存布局,每次BIOS只返回一种类型的内存信息,直到全部返回完成;
内存信息被存在一个结构中–地址范围描述符 ARDS:

一共5个字段,每个字段4个字节
Type字段含义:

为什么 BIOS 要按类型来返回呢?
因为内存可能处于多种状态,按类型好区分
BIOS 中断 0x15 子功能 0xe820 说明:


此中断的调用步骤:
这种方法最大识别内存为 4GB
BIOS 中断 0x15 子功能 0xe801 说明:

此中断的调用步骤:
只能识别到 64MB内存

此中断的调用步骤:
本节主要用于演示 BIOS 0x15 中断的用法,可跳过
将:
jmp LOADER_BASE_ADDR
修改成:
jmp LOADER_BASE_ADDR + 0x300 ; 跳过数据段, 跳跃到代码区
%include "boot.inc" section loader vstart=LOADER_BASE_ADDR LOADER_STACK_TOP equ LOADER_BASE_ADDR ; loader在保护模式下的栈指针地址,esp ; 构建GDT及其内部描述符, 每个描述符8个字节, 拆分为高低各4字节(32位) GDT_BASE: dd 0x00000000 ; 第0个描述符,不可用 dd 0x00000000 CODE_DESC: dd 0x0000ffff ; 低32位31~16位为段基址15~0位, 15~0位为段界限15~0位 dd DESC_CODE_HIGH4 DATA_STACK_DESC: dd 0x0000ffff ; 数据段(栈段)描述符 dd DESC_DATA_HIGH4 VIDEO_DESC: dd 0x80000007 ; 0xB8000 到0xBFFFF为文字模式显示内存 段界限:limit=(0xbffff-0xb8000) / 4k=0x7 dd DESC_VIDEO_HIGH4 ; 0xB GDT_SIZE: equ $ - GDT_BASE ; 当前位置减去GDT_BASE的地址 等于GDT的大小 GDT_LIMIT: equ GDT_SIZE - 1 ; SIZE - 1即为最大偏移量 times 60 dq 0 ; 预留60个 四字型 描述符空位, 用于后续扩展 SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 段选择子: 低3位为TI RPL状态, 其余为描述符索引值 SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。 ; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900, ; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址 total_mem_bytes dd 0 ; gdt指针, 前2字节为gdt界限, 后4字节为gdt起始地址(共48位) gdt_ptr dw GDT_LIMIT dd GDT_BASE ; 人工对齐:total_mem_bytes 4字节 + gdt_ptr 6字节 + ards_buf 244字节 + ards_nr 2字节 , 共256字节 ards_buf times 244 db 0 ; 记录内存大小的缓冲区 ards_nr dw 0 ; 记录 ARDS 结构体数量 loader_start: ; ------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局 ------- xor ebx, ebx ; 第一次调用将eb置0 mov edx, 0x534d4150 ; edx只赋值一次,循环体中不会改变 mov di, ards_buf ; di指向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数量加1 cmp ebx, 0 ; 若 ebx 为 0 且 cf 不为 1, 这说明 ards 全部返回,当前已是最后一个 jnz .e820_mem_get_loop ; 不为0则循环获取 ; 在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量 mov cx, [ards_nr] ; 遍历每一个ARDS结构体,循环次数是ARDS的数量 mov ebx, ards_buf xor edx, edx ; 用edx记录最大值, 在这里先清零 .find_max_mem_area: ; 无须判断type是否为1,最大的内存块一定是可被使用 mov eax, [ebx] ; base_add_low add eax, [ebx+8] ; base_add_low + length_low = 这块ADRS容量 add ebx, 20 ; 指向下一块ARDS cmp edx, eax ; 找出最大,edx寄存器始终是最大的内存容量 jge .next_ards ; 如果edx>=eax, 继续遍历下一块 mov edx, eax ; 如果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 15 jc .e801_failed_so_try88 ; 若当前e801方法失败,就尝试0x88方法 ; 1 先算出低15M的内存, ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位 ; 乘数在eax, 即内存数量, 积高16位在edx, 低16位在eax mov cx, 0x400 ; 0x400 = 1024 mul cx shl edx, 16 ; 左移16位, 将低16位放到edx高16位 and eax, 0x0000FFFF ; 0x0000FFFF = 1111 1111 1111 1111, 高16位置0 or edx, eax ; 获得完整的32位积 add edx, 0x100000 ; edx比实际少1M, 故要加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 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 gate ; 2 加载gdt ; 3 将cr0 的 pe位(第0位)置1 ; ----------------- 打开A20 ---------------- in al, 0x92 ; 端口号0x92 中的第1位变成 1 即可 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 ; 刷新流水线 .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 $

nasm -I include/ -o mbr.bin mbr.asm
nasm -I include/ -o loader.bin loader.asm
sudo dd if=/home/steven/source/os/bochs/code/mbr.bin of=/home/steven/source/os/bochs/hd60M.img bs=512 count=1 conv=notrunc
sudo dd if=/home/steven/source/os/bochs/code/loader.bin of=/home/steven/source/os/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc
Bochsrc.disk 配置文件的内存设置:
megs: 512
Bochs运行起来后,使用xp 0xb00查看获取结果:

0x20000000 换成十进制正是 512 MB
0x20000000 = 536,870,912
536,870,912 / 1024 / 1024 = 512
段基址 + 段内偏移地址是线性地址,是连续的,是唯一的,只能属于某一个进程
有时候内存中剩余的连续的地址空间无法装下一个新的进程,内存空间利用率低下,为了能让可用地址空间不是连续的也能用,于是有了分页机制
分页机制建立在分段机制之上:

分页机制的思想是:通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续。
分页机制的作用:

经过段部件输出的线性地址也叫虚拟地址,地址先进行逻辑上的分段,然后将线性地址的段拆分成以页为单位的大小相同的小内存块,也就是图片中间那部分
CPU 采用的页大小是 4KB ,4GB 地址空间被划分出 1M 个页,只要是 4KB 的地址空间都可以称为一页,线性地址的一页都对应物理地址的一页,这就是一级页表
页表是一个线性表,就像数组一样,可以用下标进行索引,页表的表项只有一个字段,那就是这个页所对应的真实的物理地址
分页机制打开前要将页表地址加载到控制寄存器CR3中,这个地址是实际的物理地址,不会被分页进行转换
一级页表转换原理:
知道页表的物理地址后,用虚拟地址的高20位用来索引页表的表项,因为页表表项大小为4字节,所以将高20位乘4即是虚拟地址所对应的表项的地址,内容就是对应的物理地址
然后再加上低12位的偏移,即是真正的物理地址,如下图所示:

二级页表是另一种页表形式,为了减少页表的空间占用,所以需要二级页表,二级页表占用内存示意图如下:

每个页表的物理地址在页目录表中都以页目录项(Page Directory Entry,PDE)的形式存储。
原来一级页表是1M个表项,二级页表则是将1M个表项分成1K个页表,每个页表有1K个表项的形式,页表的地址装载页目录表中页目录表也有1024个项
二级页表转换原理:
10位二进制位刚好能表示0~1023这1024个表项或者页表
把4G内存分成了1024份,每份的大小是4M,每份又分成了1024小份,每小份大小是4K
地址转换则是先按4M来分,找到属于哪一个4M,然后再看属于哪一个4K,然后再以此为基址加上偏移地址找到真实地址
示意图如下图:

每个进程都有自己的页表

启用分页机制要做三件事:
存储段地址有专门的寄存器–段描述符缓冲寄存器,存储页表地址也有专门的寄存器:cr3,也叫页目录基址寄存器:

只需要把页目录无敌离职的高20位写入CR3寄存器即可
设计页表也就是设计内存布局
用户代码加上需要用的操作系统中的部分代码才是完整的程序,用户进程需要与操作系统共同配合
所以用户进程都需要共享操作系统,只要让虚拟内存空间的0~3GB分给用户进程,3~4GB分给操作系统即可实现共享,让所有进程的虚拟空间的3~4GB部分都指向同一个物理地址
新增如下内容:
PAGE_DIR_TABLE_POS equ 0x100000
;--------------页表 属性---------------------
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 10b
PG_US_S equ 000b
PG_US_U equ 100b
这里首先将gdt里面的显存 0xb8000 修改到了内核内存部分中 0xc00b8000
然后将 gdt 的地址修改到了 内核部分中,这里主要是为了以后不重复加载
由于修改了页表,现在访问虚拟内存0xc0000000开始1M的地址,都会map到0-1M内存的位置
最后通过操作显存,
p_mode_start内容有变动:
其中setup_page函数在后文单独列出
[bits 32] p_mode_start: ; 初始化32位的段寄存器 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需要放在内核里 ; 将描述符表地址&偏移量写入内存gdt_ptr,一会用新的地址加载 sgdt [gdt_ptr] ; 取出GDT地址和偏移信息,存放在gdt_ptr这个内存位置上 ; 视频段需要放在内核里与用户进程进行共享 ; 将gdt描述符中视频段的段基址 + 0xc0000000 mov ebx, [gdt_ptr + 2] ; 这里gdt_ptr前2字节是偏移量,后4字节是GDT基址,先选中GDT or dword [ebx + 0x18 + 4], 0xc0000000 ; 一个描述符8字节,0x18处是第3个段描述符也就是视频段, 修改段基址最高位为C, +4进入高4字节, 用or修改即可 ; 将gdt的基址加上 0xc0000000 成为内核所在的地址 add dword [gdt_ptr + 2], 0xc0000000 add esp, 0xc0000000 ; 将栈指针同样map到内核地址 ; 页目录地址赋值给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] ; 重新初始化gs寄存器 mov ax, SELECTOR_VIDEO mov gs, ax mov byte [gs:160], 'V' jmp $
坑点:最后打印V的时候,这里书上存在问题,寄存器没刷新直接写入,这样写入基址的还是0xb8000,这里需要重新初始化一下gs寄存器,才能向0xc00b8000基址写入内容
因为系统需要被共享,让所有用户进程在自己的虚拟空间都能访问到系统,所以系统所在的虚拟地址对应的页目录,应该指向系统真实存在的页目录,指向同一个页表
页目录表0号项与768号项均指向第一个页表——0号项指向第一个页表(loader这个程序会运行在0-4M空间内,且跨越了段机制与页机制,顺序映射(第一个页表映射0开始4MB,第二个页表映射紧挨着下一个4MB空间)可以保证之前段机制下的线性地址和分页后的虚拟地址对应的物理地址一致,在这4M空间内,分段下的线性地址=分页后的虚拟地址=物理地址),768号项指向第一个页表是为了让分页机制下3GB开始的4MB虚拟地址空间(虚拟地址0-3G空间用户用,3-4G空间操作系统用)对应到了0-4M实际物理空间,因为这里放着我们的操作系统。这是为了所有进程共享操作系统做准备。页目录表最后一项1023号项指向自己——为的是将来动态操作页表做准备
初始化第一个页表(非页目录表)——因为这个页表管理着0—4M的物理地址空间,我们的操作系统就在这个空间内。
初始化页目录表769号-1022号项,769号项指向第二个页表(此页表紧挨着上面的第一个页表),770号指向第三个,以此类推,——为将来用户进程做准备,使所有用户进程共享内核空间(从768号项—1022号项的页目录表项会被拷贝到所有用户进程的页目录表项中)
; ------------- 创建页目录及页表 --------------- setup_page: ; 先把页目录所占空间清 0 mov ecx, 4096 ; 1024 * 4 = 4096 mov esi, 0 .clear_page_dir: mov byte [PAGE_DIR_TABLE_POS + esi], 0 inc esi loop .clear_page_dir ; 开始创建页目录项(Page Directory Entry) .create_pde: mov eax, PAGE_DIR_TABLE_POS add eax, 0x1000 ; 第一个页表的位置(仅次于页目录表,页目录表大小4KB) mov ebx, eax ; 0x00101 000, 储存到ebx为创建PTE时使用 ; 下面将页目录项0和OxcOO都存为第一个页表的地址 ,每个页表表示4MB内存 ; 这样Oxc03fffff(3G-3G04M)以下的地址和Ox003fffff(0-4M)以下的地址都 指向相同的页表 ; 这是为将地址映射为内核地址做准备 or eax, PG_US_U | PG_RW_W | PG_P ; 用户特权级,可读可写,存在内存 mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第一个目录项,0x00101 007 mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 第0xc00高10位0x300=768个页表占用的目录项,0xc00以上属于kernel空间 ; 这里是把第768个目录页和第1个目录页指向同一个页表的物理地址:0x101000 ; 系统实际位于0~0x100000内存地址中,将系统虚拟地址0xc00000000映射到这低1M的空间内,只需要让0xc0000000的地址指向和低1M相同的页表即可 sub eax, 0x1000 mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一个目录项指向页目录表自己的位置, 4092 = 1023 * 4 ; 创建页表项(Page Table Entry) mov ecx, 256 ; 1M低端内存/每页大小4K = 256 mov esi, 0 mov edx, PG_US_U | PG_RW_W | PG_P ; edx中地址为0x0,属性为7,即111b .create_pte: mov [ebx + esi * 4], edx ; ebx = 0x00101 000, 即第一个PTE起始地址, 每个PTE = 4 byte add edx, 4096 ; edx + 4KB inc esi loop .create_pte ; 低端1M内存中,物理地址=虚拟地址,这里创建了1M空间的页表项 ; 创建内核其他页表的PDE mov eax, PAGE_DIR_TABLE_POS add eax, 0x2000 ; eax指向第二个页表(每个页表对应一个PDE, 含有1024个页表项) or eax, PG_US_U | PG_RW_W | PG_P mov ebx, PAGE_DIR_TABLE_POS mov ecx, 254 ; 769~1022的所有目录项数量, 1022 - 769 + 1 = 254 mov esi, 769 .create_kernel_pde: mov [ebx + esi * 4], eax inc esi add eax, 0x1000 ; eax指向下一个页表 loop .create_kernel_pde ret
完整代码:
%include "boot.inc" section loader vstart=LOADER_BASE_ADDR LOADER_STACK_TOP equ LOADER_BASE_ADDR ; loader在保护模式下的栈指针地址,esp ; 构建GDT及其内部描述符, 每个描述符8个字节, 拆分为高低各4字节(32位) GDT_BASE: dd 0x00000000 ; 第0个描述符,不可用 dd 0x00000000 CODE_DESC: dd 0x0000ffff ; 低32位31~16位为段基址15~0位, 15~0位为段界限15~0位 dd DESC_CODE_HIGH4 DATA_STACK_DESC: dd 0x0000ffff ; 数据段(栈段)描述符 dd DESC_DATA_HIGH4 VIDEO_DESC: dd 0x80000007 ; 0xB8000 到0xBFFFF为文字模式显示内存 段界限:limit=(0xbffff-0xb8000) / 4k=0x7 dd DESC_VIDEO_HIGH4 ; 0xB GDT_SIZE: equ $ - GDT_BASE ; 当前位置减去GDT_BASE的地址 等于GDT的大小 GDT_LIMIT: equ GDT_SIZE - 1 ; SIZE - 1即为最大偏移量 times 60 dq 0 ; 预留60个 四字型 描述符空位, 用于后续扩展 SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 段选择子: 低3位为TI RPL状态, 其余为描述符索引值 SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。 ; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900, ; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址 total_mem_bytes dd 0 ; gdt指针, 前2字节为gdt界限, 后4字节为gdt起始地址(共48位) gdt_ptr dw GDT_LIMIT dd GDT_BASE ; 人工对齐:total_mem_bytes 4字节 + gdt_ptr 6字节 + ards_buf 244字节 + ards_nr 2字节 , 共256字节 ards_buf times 244 db 0 ; 记录内存大小的缓冲区 ards_nr dw 0 ; 记录 ARDS 结构体数量 loader_start: ; ------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局 ------- xor ebx, ebx ; 第一次调用将eb置0 mov edx, 0x534d4150 ; edx只赋值一次,循环体中不会改变 mov di, ards_buf ; di指向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数量加1 cmp ebx, 0 ; 若 ebx 为 0 且 cf 不为 1, 这说明 ards 全部返回,当前已是最后一个 jnz .e820_mem_get_loop ; 不为0则循环获取 ; 在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量 mov cx, [ards_nr] ; 遍历每一个ARDS结构体,循环次数是ARDS的数量 mov ebx, ards_buf xor edx, edx ; 用edx记录最大值, 在这里先清零 .find_max_mem_area: ; 无须判断type是否为1,最大的内存块一定是可被使用 mov eax, [ebx] ; base_add_low add eax, [ebx+8] ; base_add_low + length_low = 这块ADRS容量 add ebx, 20 ; 指向下一块ARDS cmp edx, eax ; 找出最大,edx寄存器始终是最大的内存容量 jge .next_ards ; 如果edx>=eax, 继续遍历下一块 mov edx, eax ; 如果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 15 jc .e801_failed_so_try88 ; 若当前e801方法失败,就尝试0x88方法 ; 1 先算出低15M的内存, ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位 ; 乘数在eax, 即内存数量, 积高16位在edx, 低16位在eax mov cx, 0x400 ; 0x400 = 1024 mul cx shl edx, 16 ; 左移16位, 将低16位放到edx高16位 and eax, 0x0000FFFF ; 0x0000FFFF = 1111 1111 1111 1111, 高16位置0 or edx, eax ; 获得完整的32位积 add edx, 0x100000 ; edx比实际少1M, 故要加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 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 gate ; 2 加载gdt ; 3 将cr0 的 pe位(第0位)置1 ; ----------------- 打开A20 ---------------- in al, 0x92 ; 端口号0x92 中的第1位变成 1 即可 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 ; 刷新流水线 .error_hlt: ; 出错则挂起 hlt ; 处理器暂停, 直到出现中断或复位信号才继续 [bits 32] p_mode_start: ; 初始化32位的段寄存器 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需要放在内核里 ; 将描述符表地址&偏移量写入内存gdt_ptr,一会用新的地址加载 sgdt [gdt_ptr] ; 取出GDT地址和偏移信息,存放在gdt_ptr这个内存位置上 ; 视频段需要放在内核里与用户进程进行共享 ; 将gdt描述符中视频段的段基址 + 0xc0000000 mov ebx, [gdt_ptr + 2] ; 这里gdt_ptr前2字节是偏移量,后4字节是GDT基址,先选中GDT or dword [ebx + 0x18 + 4], 0xc0000000 ; 一个描述符8字节,0x18处是第3个段描述符也就是视频段, 修改段基址最高位为C, +4进入高4字节, 用or修改即可 ; 将gdt的基址加上 0xc0000000 成为内核所在的地址 add dword [gdt_ptr + 2], 0xc0000000 add esp, 0xc0000000 ; 将栈指针同样map到内核地址 ; 页目录地址赋值给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] ; 重新初始化gs寄存器 mov ax, SELECTOR_VIDEO mov gs, ax mov byte [gs:160], 'V' jmp $ ; ------------- 创建页目录及页表 --------------- setup_page: ; 先把页目录所占空间清 0 mov ecx, 4096 ; 1024 * 4 = 4096 mov esi, 0 .clear_page_dir: mov byte [PAGE_DIR_TABLE_POS + esi], 0 inc esi loop .clear_page_dir ; 开始创建页目录项(Page Directory Entry) .create_pde: mov eax, PAGE_DIR_TABLE_POS add eax, 0x1000 ; 第一个页表的位置(仅次于页目录表,页目录表大小4KB) mov ebx, eax ; 0x00101 000, 储存到ebx为创建PTE时使用 ; 下面将页目录项0和OxcOO都存为第一个页表的地址 ,每个页表表示4MB内存 ; 这样Oxc03fffff(3G-3G04M)以下的地址和Ox003fffff(0-4M)以下的地址都 指向相同的页表 ; 这是为将地址映射为内核地址做准备 or eax, PG_US_U | PG_RW_W | PG_P ; 用户特权级,可读可写,存在内存 mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第一个目录项,0x00101 007 mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 第0xc00高10位0x300=768个页表占用的目录项,0xc00以上属于kernel空间 ; 这里是把第768个目录页和第1个目录页指向同一个页表的物理地址:0x101000 ; 系统实际位于0~0x100000内存地址中,将系统虚拟地址0xc00000000映射到这低1M的空间内,只需要让0xc0000000的地址指向和低1M相同的页表即可 sub eax, 0x1000 mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一个目录项指向页目录表自己的位置, 4092 = 1023 * 4 ; 创建页表项(Page Table Entry) mov ecx, 256 ; 1M低端内存/每页大小4K = 256 mov esi, 0 mov edx, PG_US_U | PG_RW_W | PG_P ; edx中地址为0x0,属性为7,即111b .create_pte: mov [ebx + esi * 4], edx ; ebx = 0x00101 000, 即第一个PTE起始地址, 每个PTE = 4 byte add edx, 4096 ; edx + 4KB inc esi loop .create_pte ; 低端1M内存中,物理地址=虚拟地址,这里创建了1M空间的页表项 ; 创建内核其他页表的PDE mov eax, PAGE_DIR_TABLE_POS add eax, 0x2000 ; eax指向第二个页表(每个页表对应一个PDE, 含有1024个页表项) or eax, PG_US_U | PG_RW_W | PG_P mov ebx, PAGE_DIR_TABLE_POS mov ecx, 254 ; 769~1022的所有目录项数量, 1022 - 769 + 1 = 254 mov esi, 769 .create_kernel_pde: mov [ebx + esi * 4], eax inc esi add eax, 0x1000 ; eax指向下一个页表 loop .create_kernel_pde ret

内存布局:

编译,写入硬盘。
运行,显示出了V:

gs寄存器的基地址,还有GDT的基地址,和视频段的基址都是虚拟地址:

为了减少CPU频繁访问内存导致效率下降,于是发明了存储页信息的调整缓存,就是TLB,快表。
快表 TLB (Translation Lookaside Buffer) 中的条目是虚拟地址的高 20 位到物理地址高 20 位的映射结果。除此之外还有一些属性位,比如页表项的 RW 属性。

修改了页表之后,需要对快表进行更新
TLB对开发人员不可见,但又两种方法可以更新其中的条目:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。