Linux0.11内存管理:相关代码
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 等)传递操作数 整数 qr的别名,等价于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 缓存,迫使后续虚拟地址转换时重新查询页目录和页表,确保使用最新的地址映射关系。
