linux那些事之page fault(AMD64架构)(1)

linux那些事之page fault(AMD64架构)(1),第1张

linux那些事之page fault(AMD64架构)(1)

应用程序或者内核都是运行在虚拟内存空间之中,kernel 启动完成之后如果一个虚拟地址要访问物理内存需要通过CPU MMU硬件进行地址转换,整个虚拟地址访问物理内存逻辑过程如下:

kernel 启动完成之后,应用程序或内核访问内存触发CPU MMU 硬件转换,将VA(虚拟地址)->转换成PA(物理内存)转换成功之后,并且锁访问的物理内存存在,则使用PA访问物理内存转换失败或者物理内存不存在,触发CPU中断异常机制,中断号为#PF,14号中断CPU根据kernel启动过程中设置的#PF 中断函数asm_exc_page_fault(),跳转到该中断函数,并由硬件自动将#PF具体错误码 传递到该内存中#PF中断函数入口asm_exc_page_fault(), 根据传入的错误码和从CR2寄存器读取到产生page fault的地址,进入到exc_page_fault函数继续进一步处理,如果是内核空间地址发生#PF, 则进入到do_kern_addr_fault函数,如果是用户空间则进入do_user_addr_fault()函数进一步处理。asm_exc_page_fault(()函数为page fault中断函数处理入口,主要由汇编语言组成,处理完成之后进入到exc_page_fault()处理部分为C处理入口。 MMU硬件地址转换

kernel启动完毕之后,一个应用程序或内核虚拟地址访问内存时,都需要通过MMU开启对虚拟地址转换成物理地址转换,其主要转换逻辑过程如下:

 虚拟地址转换成物理地址,MMU首先会通过TLB 缓存中查找是否有对应地址映射,如果有则进入3过程说明TLB HIT,如果没有映射则进入2阶段说明TLB MISS当进入3阶段之后,TLB命中之后,硬件会对权限进行检查,如果权限检查成功,则 地址转换成功获取到物理地址当TLB MISS处于第2阶段时,则开启Page Table从内存中分级遍历 page table(注意这里如果相应级别开启缓存机制会首先从缓存中查找)(请参考《linux那些事之 page translation(硬件篇)》),如果成功则进入权限检查如果walk page table失败,则触发#PG 中断同样即使地址转换成功,但是权限检查失败即第7阶段也会触发#PG.  与PAGE FAULT相关的硬件部分

从《linux那些事之中断与异常(AMD64架构)_1》了解到page fault中断号为14号,简称为#PG,根据AMD64官方说明,能够产生#PG主要由以下几点:

经过MMU 地址转换之后,TLB HIT命中之后获取的物理地址不存在MMU MISS之后,通过walk page table 过程中entry 不存在。尝试加载指令时,指令对应的物理地址没有执行权限物理页内存权限检查pageing-protection checks失败当CR4.PSE=1或者CR4.PAE=1时,page table entry中的保留位被置成1, 地址转换过程中会发生#PG 在用户模式下获取数据时,因为protecion kery 检查未过,也会发生#PG

A page-translation-table entry or physical page involved in translating the memory access is not present in physical memory. This is indicated by a cleared present bit (P=0) in the translation-table entry. An attempt is made by the processor to load the instruction TLB with a translation for a nonexecutable page.The memory access fails the paging-protection checks (user/supervisor, read/write, or both).A reserved bit in one of the page-translation-table entries is set to 1. A #PF occurs for this reason only when CR4.PSE=1 or CR4.PAE=1.A data access to a user-mode address caused a protection key violation

CR2 

当发生#PG时,硬件会自动把发生#PG的虚拟地址保存到CR2寄存器中,当32位CPU时,CR2保存的是32位地址。当64位CPU是,保存的是64位虚拟地址:

Page_fault Error Code Returned

 page fault error code用于表示具体的发生#PG错误码,没有专门的寄存器表示。当发生#PG时被硬件自动压入中断函数的栈中,中断函数可以从栈中获取到相关详细错误码,具体错误码分布如下:

具体错误原因如下:

P(present):BIt 0.当P位为0时,表示导致page fault原因时对应的物理页 不存在。当P位被置1时,表示  由page-protection物理页保护隔离导致的R/W(Read/Write):BIt 1。当被置0时。导致page fault原因时读内存,如果被置1. 对内存进行写导致的。U/S(User/Supervisor):BIt 2. 当被置0时,表明一个超级管理模式(CPL=0,or 2)对内存 *** 作导致的。当被置1, 表明时用户模式 *** 作内存导致的(CPL=3).RSV(Reserved):Bit 3。当被置1,表明当进行地址转换过程中,page table entry中的reserved位被置1 导致的。当被置0,表明entry reserved 没有被置1.I/D(Instruction/Data):BIt 4,当被置1,表明 page fault是在指令获取时导致的。当被置0,表明时数据访问时导致的。PK(protection key):Bit 5.当被置1,表明时由于用户地址由protection key导致的(《linux内核那些事之Memory protection keys(硬件原理)》有介绍MPK特性)。SS(Shadow Stack):Bit 6。当被置1,表明时有SS 访问导致的,注意只有当CR2.CET=1时才有效RMP:Bit 31。如果被置1,说明#PG时有RMP 导致的。  asm_exc_page_fault #PF 中断函数初始化

《linux那些事之中断与异常(AMD64架构)_2》中说明了整个中断函数在kernel初始化过程,#PF的中断函数最终是:

static const __initconst struct idt_data early_pf_idts[] = {
	INTG(X86_TRAP_PF,		asm_exc_page_fault),
};

对应asm_exc_page_fault中断函数,该函数是#PF的中断入口。

asm_exc_page_fault定义

asm_exc_page_fault()定义稍微复杂,主要是由于汇编和C两个混合实现的,该函数使用宏DECLARE_IDTENTRY_RAW_ERRORCODE(archx86includeasmidtentry.h):

DECLARE_IDTENTRY_RAW_ERRORCODE(X86_TRAP_PF,	exc_page_fault);

X86_TRAP_PF 为#PF 中断向量号定义,DECLARE_IDTENTRY_RAW_ERRORCODE宏由于在汇编和C语言中都有切dtentry.h文件(archx86includeasmidtentry.h)汇编语言和C语言都会加载使用到,因此该文件实现有两个部分,分别为汇编加载该头文件使用部分以及C加载该头文件使用部分:

#ifndef __ASSEMBLY__  //C语言实现部分,被C文件引用
... ...

#define DECLARE_IDTENTRY_RAW_ERRORCODE(vector, func)			
	DECLARE_IDTENTRY_ERRORCODE(vector, func)
... ...

#else 
... ...    //汇编实现部分,被汇编文件引用

#define DECLARE_IDTENTRY_RAW_ERRORCODE(vector, func)			
	DECLARE_IDTENTRY_ERRORCODE(vector, func)

... ...

#endif

vector为中断向量号,fuc为中断函数名,最后都是调用DECLARE_IDTENTRY_ERRORCODE:

#ifndef __ASSEMBLY__ //C语言部分, 被c文件引用

... ...


#define DECLARE_IDTENTRY_ERRORCODE(vector, func)			
	asmlinkage void asm_##func(void);				
	asmlinkage void xen_asm_##func(void);				
	__visible void func(struct pt_regs *regs, unsigned long error_code)

... ...

#else  //汇编部分,被汇编文件引用头文件部分

... ... 

#define DECLARE_IDTENTRY_ERRORCODE(vector, func)			
	idtentry vector asm_##func func has_error_code=1

... ...

#endif

DECLARE_IDTENTRY_ERRORCODE 汇编部分主要是定义func函数,当传入vector中断向量号为X86_TRAP_PF, func为exc_page_fault,汇编部分展开结构部分为:

idtentry vector asm_exc_page_fault exc_page_fault has_error_code=1

主要是实现 asm_exc_page_fault 函数功能, has_error_code为具体硬件返回的错误码,调用使用汇编定义的idtentry 宏。而C语言部分展开:

asmlinkage void asm_exc_page_fault (void);                
    asmlinkage void xen_asm_exc_page_fault (void);                
    __visible void exc_page_fault(struct pt_regs *regs, unsigned long error_code)

主要是对  asm_exc_page_fault申明,最后调用exc_page_fault函数,asm_exc_page_fault--->exc_page_fault, asmlinkage 表明asm_exc_page_fault函数是通过栈传参。

idtentry宏

idtentry宏定义是使用.macro 宏进行定义位于archx86entryentry_64.s文件中,.macro宏用法如下:

.macro macname macpara
...
...
.endm

macname为宏名称,macpara为宏参数,可以接多个参数,idtentry宏定义如下:

.macro idtentry vector asmsym cfunc has_error_code:req
SYM_CODE_START(asmsym)
	UNWIND_HINT_IRET_REGS offset=has_error_code*8
	ASM_CLAC

	.if has_error_code == 0
		pushq	$-1			
	.endif

	.if vector == X86_TRAP_BP
		
		testb	, CS-ORIG_RAX(%rsp)
		jnz	.Lfrom_usermode_no_gap_@
		.rept	6
		pushq	5*8(%rsp)
		.endr
		UNWIND_HINT_IRET_REGS offset=8
.Lfrom_usermode_no_gap_@:
	.endif

	idtentry_body cfunc has_error_code

_ASM_NOKPROBE(asmsym)
SYM_CODE_END(asmsym)
.endm

vector 为中断向量号,asmsym为相对于中断向量号对应的汇编中断函数,cfunc对应的该中断函数C语言部分,has_error_code:req调用传入的error值,此时值为1,在#PF中断中asmsym为asm_exc_page_fault函数,cfunc为exc_page_fault函数,

SYM_CODE_START(asmsym)

...

SYM_CODE_END(asmsym)

为asm_exc_page_fault实现,asm_exc_page_fault函数最后处理部分为((page fault部分传入的has_error_code为1,中断为#PG,处理跳过直接进入idtentry_body):

idtentry_body cfunc has_error_code

调用idtentry_body宏。

idtentry_body 宏

idtentry_body宏定义如下:

.macro idtentry_body cfunc has_error_code:req

	call	error_entry
	UNWIND_HINT_REGS

	movq	%rsp, %rdi			

	.if has_error_code == 1
		movq	ORIG_RAX(%rsp), %rsi	
		movq	$-1, ORIG_RAX(%rsp)	
	.endif

	call	cfunc

	jmp	error_return
.endm

has_error_code为1,表明是通过栈传递参数,将发生的具体page fault error code值,通过movq指令压入栈中,最后通过call 功能调用到 cfunc即C语言部分, page fault为exc_page_fault函数,从而跳入到C语言部分。

exc_page_fault

exc_page_fault函数定义位于(archx86mmfault.c)文件中:

DEFINE_IDTENTRY_RAW_ERRORCODE(exc_page_fault)
{
	unsigned long address = read_cr2();
	bool rcu_exit;

	prefetchw(¤t->mm->mmap_lock);

	
	if (kvm_handle_async_pf(regs, (u32)address))
		return;

	
	rcu_exit = idtentry_enter_cond_rcu(regs);

	instrumentation_begin();
	handle_page_fault(regs, error_code, address);
	instrumentation_end();

	idtentry_exit_cond_rcu(regs, rcu_exit);
}

address = read_cr2() 从CR2寄存器中获取到发生异常的虚拟地址。 对当前进程mm加锁current->mm->mmap_lock。kvm_handle_async_pf: KVM相关 *** 作。idtentry_enter_cond_rcu: RCU处理。instrumentation_begin: 主要是配合noinstr变量修饰的函数,用于防止在当前中断增在处理过程中,再次同样的中断发生,以 覆盖当前一些状态寄存器(https://lwn.net/Articles/877229/),begin 为开始锁定区域。handle_page_fault 进一步处理page fault, 传入error code 及 虚拟地址。instrumentation_end: end为结束锁定区域。idtentry_exit_cond_rcu:退出处理。 DEFINE_IDTENTRY_RAW_ERRORCODE

DEFINE_IDTENTRY_RAW_ERRORCODE宏定义如下:


#define DEFINE_IDTENTRY_RAW_ERRORCODE(func)				
__visible noinstr void func(struct pt_regs *regs, unsigned long error_code)

 exc_page_fault函数定义扩展为:

__visible noinstr void exc_page_fault(struct pt_regs *regs, unsigned long error_code)

 noinstr变量修饰中断函数,主要是用于防止当前中断正在处理过程中,硬件再次发生同样的中断以覆盖某些状态寄存器:

+Non-instrumentable code - noinstr
+---------------------------------
+
+Low level transition code cannot be instrumented before RCU is watching and
+after RCU went into a non watching state (NOHZ, NOHZ_FULL) as most
+instrumentation facilities depend on RCU.
+
+Aside of that many architectures have to save register state, e.g. debug or
+cause registers before another exception of the same type can happen. A
+breakpoint in the breakpoint entry code would overwrite the debug registers
+of the inital breakpoint.
+
+Such code has to be marked with the 'noinstr' attribute. That places the
+code into a special section which is taboo for instrumentation and debug
+facilities.
+In a function which is marked 'noinstr' it's only allowed to call into
+non-instrumentable code except when the invocation of instrumentable code
+is annotated with a instrumentation_begin()/instrumentation_end() pair
 handle_page_fault

handle_page_fault主要处理如下: 

static __always_inline void
handle_page_fault(struct pt_regs *regs, unsigned long error_code,
			      unsigned long address)
{
	trace_page_fault_entries(regs, error_code, address);

	if (unlikely(kmmio_fault(regs, address)))
		return;

	
	if (unlikely(fault_in_kernel_space(address))) {
		do_kern_addr_fault(regs, error_code, address);
	} else {
		do_user_addr_fault(regs, error_code, address);
		
		local_irq_disable();
	}
}

发生page fault地址位于kernel 空间,则调用fault_in_kernel_space进行处理发生在用户据空间,则调用do_user_addr_fault 参考资料

.macro.

https://lwn.net/Articles/877229/

《AMD64 Architecture Programmer’s Manual》

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5713704.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存