ch13_2 源码分析
boot/head.s
页表初始化:
- 目标:初始化分页机制,将线性地址空间映射到物理内存(前 16MB),为保护模式下的内存管理做准备。
- 核心流程
- 分配页目录表和页表的物理内存空间(通过
.org
指令指定地址)。 - 初始化1个页目录 + 4个页表
- 设置页目录项,指向4个页表(属性:Present+User/RW):
- 反向填充页表项,把四个页表的页表项:一共4k个页表项填满,对应的是16M的物理内存,4k个物理页面。
- 通过
CR3
指向页目录表的基地址(物理地址0)和CR0
寄存器的PG
位启用分页机制。
- 分配页目录表和页表的物理内存空间(通过
c
.org 0x1000 ; 告诉汇编器,将接下来的代码或数据从内存地址 0x1000 开始放置。
pg0:
.org 0x2000
pg1:
.org 0x3000
pg2:
.org 0x4000
pg3:
.org 0x5000
.align 2
setup_paging:
; 初始化1个页目录 + 4个页表
movl $1024*5,%ecx ; 初始化5页(1页目录+4页表)
xorl %eax,%eax ; 异或自身,置零eax(填充值)
xorl %edi,%edi ; 置零edi(页目录起始地址)
cld ; 清除方向标志(DF=0),确保地址递增,edi += 4
;rep stosl ; 循环将 EAX 的值写入 EDI 指向的内存。
; ECX = 循环填充次数 每次写入edi += 4(因 DF=0)。
; 设置页目录项,指向4个页表(属性:Present+User/RW)
movl $pg0+7,pg_dir ; 页表项(0x1007)包括高二十位的页框地址(0x1去掉后12位)+ 12位属性(7)
movl $pg1+7,pg_dir+4 ; 0x7表示Present(存在位)、R/W(可写)、U/S(用户可访问)。
movl $pg2+7,pg_dir+8
movl $pg3+7,pg_dir+12
; 反向填充页表项,映射16MB物理内存
; 仅需设置 pg3+4092 作为初始地址,结合循环即可覆盖 pg3 → pg2 → pg1 → pg0 的全部 4k 个页表项。
; eax 值变化:0xFFF007 → 0xFFE007 → ... → 0x000007 4k次操作
; 初始 pg3+4092 pg3 的第1023项 0xFFF000~0xFFFFFF (4KB)
; 终止 pg0+0 pg0 的第0项 0x000000~0x000FFF
movl $pg3+4092,%edi ; edi = 0x4FFF(pg3 的最后一个4字节项)
movl $0xfff007,%eax ; eax = 0xFFF007(物理地址的高20位 + 属性0x7)
std ;(DF=1),edi -= 4
1: stosl ; 写入4k个页表项:将eax值写入[edi],同时 edi-= 4
subl $0x1000,%eax
jge 1b
cld ; 清除方向标志(DF=0)
; 设置PG位启用分页
xorl %eax,%eax
movl %eax,%cr3 ; cr3指向页目录(物理地址0)
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 ; 设置CR0的PG位(分页使能)
ret ; 返回并刷新预取队列
page.s
核心功能总结
- 保存用户态上下文:保护寄存器,确保处理程序不破坏用户进程的运行状态。
- 切换到内核特权级:通过设置段寄存器访问内核数据结构。
- 提取关键信息
- CR2 寄存器:获取触发页错误的线性地址。
- 错误码:分析错误类型(缺页或写保护)。
- 分支处理
- 缺页(P=0) :调用
do_no_page
分配或加载页面。 - 写保护(P=1) :调用
do_wp_page
处理写时复制(COW)。
- 缺页(P=0) :调用
- 恢复现场并返回 :清理栈空间,恢复寄存器,通过
iret
返回到用户程序。
c
/*
* linux/mm/page.s
*
* (C) 1991 Linus Torvalds
*/
/*
* page.s contains the low-level page-exception code.
* the real work is done in mm.c
*/
.globl page_fault
; 处理器触发页错误(如访问未映射或受保护的地址)时跳转到 page_fault。
page_fault:
; 在页错误发生时:
; 1. 栈顶是错误码(由 CPU 自动压入)
; 2. 交换 EAX 和栈顶的值后,EAX = 错误码,栈顶存储原 EAX 的值
xchgl %eax,(%esp)
;寄存器保护:
; 保存EAX、ECX、EDX、DS、ES、FS 以确保处理程序不会破坏用户进程的上下文。
pushl %ecx
pushl %edx
push %ds
push %es
push %fs
;内核模式设置:
; 将 DS/ES/FS 设置为 0x10(内核数据段),确保后续内存操作在内核特权级进行。
; 该指令将立即数 0x10(二进制 00000000 00010000)加载到 %edx 寄存器:
; Index: 00000000 0010(高 13 位,即 0x10 >> 3 = 2,对应 GDT 的第 2 项)。
; TI: 0(使用 GDT)。
; RPL: 00(内核特权级)
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
mov %dx,%fs
;CR2 寄存器存储触发页错误的线性地址,压栈后作为 do_no_page 或 do_wp_page 的参数。
movl %cr2,%edx
pushl %edx
;错误码最低位(P 位)决定异常类型:
;P=0 → 缺页异常,调用 do_no_page() 分配或加载页面。
;P=1 → 写保护异常,调用 do_wp_page() 处理写时复制(COW)。
pushl %eax
testl $1,%eax ;检查 %eax 的最低位(等价于 eax & 1)
jne 1f ;如果 %eax 的最低位=1(ZF=0 零标志位为非),跳转到标签 1:;否则继续执行
call do_no_page
jmp 2f
1: call do_wp_page
;恢复现场:
; 按逆序恢复之前保存的寄存器和段寄存器。
; 压栈的反顺序:
2: addl $8,%esp ;跳过栈顶的 8 字节数据(相当于清理 2 个 pushl 操作压入的未弹出数据)。
pop %fs ;跳过错误码和 CR2 参数(已传递给 C 函数),使栈指针指向 FS 保存的位置。
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax
;中断返回:iret 恢复 CS、EIP、EFLAGS,返回到触发页错误的指令继续执行。
iret
为什么设置 DS/ES/FS=0x10
(内核数据段)?
- 确保后续内存操作在内核特权级进行 。
- 虽然 CPU 在异常处理中自动切换到内核态(CPL=0) ,但段寄存器(如
DS
)的段选择子可能仍指向用户态描述符 (例如用户数据段是0x17
,TI=0、Index=3、RPL=3)。- 将
DS/ES/FS
设置为0x10
(内核数据段),该指令将立即数0x10(
二进制00000000 00010000
)加载到%edx
寄存器:
Index: 00000000 0010
(高 13 位,即0x10 >> 3 = 2
,对应 GDT 的第 2 项)。TI: 0
(使用 GDT)。RPL: 00
(内核特权级)
下面给出流程示意图:
c
+-----------------------+
| CPU 触发页错误 |
| 硬件自动执行以下操作: |
| 1. 压入错误码到栈 |
| 2. 跳转到 page_fault |
+-----------+------------+
v
+------------+-------------+
| page_fault 处理程序开始 |
+------------+-------------+
|
+----------+------------+
| 交换 eax 和栈顶值 |
| (xchgl %eax, (%esp)) |
+----------+------------+
|
+----------+------------+ 保存用户进程的寄存器上下文
| 压入 ecx, edx, ds, es, fs |
+----------+------------+
|
+----------+------------+
| 设置内核数据段 (DS/ES/FS=0x10) |
+----------+------------+ 确保内核内存访问安全
|
+----------+------------+
| 读取 CR2 → edx,压入栈 |
+----------+------------+
|
+----------+------------+ 压入错误码 (eax) 到栈
| 测试错误码最低位(P位) |
+----------+------------+
|
+-------------------------+-------------------------+
| P=0(缺页异常) | P=1(写保护异常) |
v v
+------------------+ +------------------+
| 调用 do_no_page() | | 调用 do_wp_page() |
+------------------+ +------------------+
| |
+-------------------------+
|
+----------v------------+
| 清理栈空间(addl $8, %esp)|
+----------+------------+ 跳过错误码和 CR2
|
+----------+------------+
| 逆序恢复寄存器(fs, es, ds, edx...)|
+----------+------------+
|
+----------v------------+
| iret 返回到用户态 | 恢复用户程序执行
+-----------------------+
memory.c
这个是主要文件:分成一段一段的去看
invalidate()
宏:刷新TLB
c
// 刷新快表TLB
#define invalidate() \
__asm__("movl %%eax,%%cr3"::"a" (0))
首先要看懂GNU 内联汇编(GNU Inline Assembly) 的语法,在C语言中嵌入汇编指令。
GNU 内联汇编的基本格式
c__asm__ [volatile] ( "汇编指令模板" // 必选:汇编指令字符串 : 输出操作数约束 // 可选:指定输出操作数及其约束 : 输入操作数约束 // 可选:指定输入操作数及其约束 : 破坏的寄存器列表 // 可选:声明被指令修改的寄存器 );
__asm__
是关键字,也可写作asm
,表示开始内联汇编。
volatile
是可选关键字,用于禁止编译器优化该汇编指令(内核中常用)。四个部分用
:
分隔,即使某部分无内容,对应的:
也不能省略。基本操作数约束(单个字符)
约束符 含义 适用操作数类型 a
使用 CPU 的 EAX/AX/AL 寄存器传递操作数 整数(int、long 等) b
使用 EBX/BX/BL 寄存器传递操作数 整数 c
使用 ECX/CX/CL 寄存器传递操作数 整数 d
使用 EDX/DX/DL 寄存器传递操作数 整数 S
使用 ESI 寄存器传递操作数 整数 D
使用 EDI 寄存器传递操作数 整数 r
使用 任意通用寄存器(EAX/EBX/ECX/EDX/ESI/EDI 等)传递操作数 整数 q
r
的别名,等价于r
整数 g
使用 任意寄存器、内存或立即数传递操作数(编译器自动选择) 整数、内存变量、立即数 m
使用 内存地址传递操作数(操作数在内存中) 内存变量(如数组、结构体成员) o
使用 内存地址 传递操作数,且地址是 可优化的(编译器可能选择更优寻址方式) 内存变量 V
使用 内存地址 传递操作数,且地址是 不可优化的(强制使用给定寻址方式) 内存变量 i
操作数是 立即数,且可作为指令的操作码部分(如移位指令的移位次数) 立即数(常量表达式) F
操作数是 浮点常数(如浮点数立即数) 浮点数常量 f
使用 浮点寄存器传递操作数 浮点数变量 t
使用 第一个寄存器(通常是 EAX)传递操作数 整数 u
使用 第二个寄存器(通常是 EDX)传递操作数 整数 w
允许使用 字长寄存器(如 AX、BX 等 16 位寄存器) 16 位整数 x
通用约束符,等价于 g
整数、内存、立即数
那么这个代码可以看成,把CR3
寄存器设置成0
。
c
mov eax, 0 ; 将 0 存入 eax
mov cr3, eax ; 将 eax 的值写入 cr3
为什么设置 CR3 为 0?
为了刷新
TLB
:只要是写入CR3
,不i管值变不变都会刷新。
CR3
寄存器在head.s
里面就被设置成0
了,始终指向页目录基址0
,再置零不改变他的值。- 此处调用
invalidate()
的目的并非修改CR3
的值,而是通过 写入 CR3 寄存器(即使值不变)触发 CPU 的 TLB 刷新机制。x86 架构规定:当向 CR3 写入数据时,无论值是否变化,CPU 都会 清空 TLB 缓存,迫使后续虚拟地址转换时重新查询页目录和页表,确保使用最新的地址映射关系。