ch13_2 源码分析
boot/head.s
页表初始化:
- 目标:初始化分页机制,将线性地址空间映射到物理内存(前 16MB),为保护模式下的内存管理做准备。
- 核心流程
- 分配页目录表和页表的物理内存空间(通过
.org
指令指定地址)。 - 初始化1个页目录 + 4个页表
- 设置页目录项,指向4个页表(属性:Present+User/RW):
- 反向填充页表项,把四个页表的页表项:一共4k个页表项填满,对应的是16M的物理内存,4k个物理页面。
- 通过
CR3
指向页目录表的基地址(物理地址0)和CR0
寄存器的PG
位启用分页机制。
- 分配页目录表和页表的物理内存空间(通过
.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~0x000FFFmovl $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,%eaxjge 1bcld ; 清除方向标志(DF=0); 设置PG位启用分页xorl %eax,%eax movl %eax,%cr3 ; cr3指向页目录(物理地址0)movl %cr0,%eaxorl $0x80000000,%eaxmovl %eax,%cr0 ; 设置CR0的PG位(分页使能)ret ; 返回并刷新预取队列
page.s
核心功能总结
- 保存用户态上下文:保护寄存器,确保处理程序不破坏用户进程的运行状态。
- 切换到内核特权级:通过设置段寄存器访问内核数据结构。
- 提取关键信息
- CR2 寄存器:获取触发页错误的线性地址。
- 错误码:分析错误类型(缺页或写保护)。
- 分支处理
- 缺页(P=0):调用
do_no_page
分配或加载页面。 - 写保护(P=1):调用
do_wp_page
处理写时复制(COW)。
- 缺页(P=0):调用
- 恢复现场并返回:清理栈空间,恢复寄存器,通过
iret
返回到用户程序。
/** 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 %ecxpushl %edxpush %dspush %espush %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,%edxmov %dx,%dsmov %dx,%esmov %dx,%fs;CR2 寄存器存储触发页错误的线性地址,压栈后作为 do_no_page 或 do_wp_page 的参数。movl %cr2,%edxpushl %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_pagejmp 2f
1: call do_wp_page;恢复现场:
; 按逆序恢复之前保存的寄存器和段寄存器。
; 压栈的反顺序:
2: addl $8,%esp ;跳过栈顶的 8 字节数据(相当于清理 2 个 pushl 操作压入的未弹出数据)。pop %fs ;跳过错误码和 CR2 参数(已传递给 C 函数),使栈指针指向 FS 保存的位置。pop %espop %dspopl %edxpopl %ecxpopl %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
(内核特权级)
下面给出流程示意图:
+-----------------------+| 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
// 刷新快表TLB
#define invalidate() \
__asm__("movl %%eax,%%cr3"::"a" (0))
首先要看懂GNU 内联汇编(GNU Inline Assembly) 的语法,在C语言中嵌入汇编指令。
GNU 内联汇编的基本格式
__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
。
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 缓存,迫使后续虚拟地址转换时重新查询页目录和页表,确保使用最新的地址映射关系。