linux4.9 aarch32
linux fixup table说明:
此exception table不是用于处理硬件异常的(那是IDT表的工作),但它确实和硬件异常有一点关系,具体来说是和Page Fault有关系。Exception Table的具体机制在内核文档”Exception”中有详细介绍,你可以在/path_to_your_kernel_src/ Documentation/exception.txt中找到它。这里为了说明问题做一点简要介绍。
我们尊敬的Linus大神为了避免内核在访问用户态地址时进行有效性检查带来的开销(我们总是需要这样的检查,虽然大部分情况下结果是成功的),利用了page fault的处理函数来完成这项任务,这样只有在真正访问了一个坏的用户态地址时检查才会发生。或许你会问:此时检查有什么用?一个例子就很容易说明问题,假设我们有一个函数叫is_user_addr_ok(),用于检查传入的用户态地址是否合法。那么,当地址非法时它能干什么?什么都不能干,仅仅是告诉内核:“这是个非法地址,你不要访问”。这样便带来了个问题,让它在90%的时间里告诉内核:“这是个合法地址,去吧!”是件很无聊的事情。既然该函数对非法地址无能为力,我们干脆就什么都不要干,直到内核真访问到一个非法地址时再告诉调用者:“噢,抱歉,您访问到一个非法地址。”不管用哪种方法,调用者遇到非法地址最终结果都是获得一个错误码,但后者明显省下了对合法地址进行检查的开销。让我们来看看如何用自定义section完成这个任务。
如果你顺着copy_from_user()向下找几层,会看到__get_user_asm宏,该宏展开后可读性太差,我们用下面的伪代码来描述它:
1: movb (%from),(%to) /* 这里访问用户态地址,当地址非法时会产生一个page fault*/
2:
/*注意,后面的代码在最终的目标文件中不是跟在标号2后的*/
.section .fixup,"ax"
3: movl $ERROR_CODE,%eax
xorb %dl,%dl
jmp 2b
.section __ex_table,"a"
.align 4
.long 1b,3b
上面的伪代码描述了__get_user_asm宏的用途,它将用户态地址from中的内容拷贝到内核地址to。当from是个非法地址时,会产生page fault从而执行内核的do_page_fault(),在进行一系列检查处理后fixup_exception()被调用,该函数会调用search_exception_tables()查找exception table,将EIP设置成对应handler的地址并返回。至此该非法地址造成的错误就交由exception table中的handler处理了。
所有问题的归结到了exception table的建立和错误处理handler的设置。其实上面的伪代码已经告诉我们答案了。首先,标号”1”代表了可能产生page fault的EIP,当page fault产生时这个地址会被记录在struct pt_regs的ip字段中(不知道的看看do_page_fault()的参数);其次,标号”3”是错误处理handler的地址,很明显,它只是返回了一个错误码(EAX是x86的返回值寄存器)。jmp 2b跳到了产生page fault的指令的下一条指令继续执行。这里
.section .fixup,"ax"
创建了名为.fixup的自定义section,并将整个handler放入其中。标号”1”后的代码是位于.text section的,故你看到它们在源代码里写在了一起,但在目标文件中去是分开的,它们在不同的section。
好了,我们已经有了会产生错误的代码地址,也有了错误处理handler的地址,
.section __ex_table,"a"
将它们放到了自定义的__ex_table section中(.long 1b,3b),以如下格式存放:
出错地址,处理函数地址
内核用结构体struct exception_table_entry表示该格式,定义如下:
struct exception_table_entry {
unsigned long insn, fixup;
};
很明显,exception table的格式简单,表项的前4个字节是出错地址,后4个字节是处理函数的地址。下图展示了通过exception table解决一次访问用户态非法地址产生的错误。
图3. 通过execption table处理非法用户态地址访问的过程
怎样,所有的事实都清楚了。当在内核在不同位置调用copy_from_user()时,展开的__get_user_asm宏都会将可能出错的地址和处理函数的地址存入该源文件对应.o文件的__ex_table section中。链接脚本的如下代码:
__ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) { __start___ex_table = .; *(__ex_table) __stop___ex_table = .; } :text = 0x9090
|
将分散的__ex_table收集起来产生一张完整的exception表,并将表的起始地址和结束地址记录在__start___ex_table和__stop___ex_table两个全局变量中,从而search_exception_tables()函数可以顺利的索引该表。
===============================copy_from_user========================================
copy_from_user与memcpy都是共用的copy_template.s,只是有些函数的名字不同而已,对比下更容易清楚,copy_from_user 如何利用fixup机制,ldrusr macro 定义在arch/arm/include/asm/assembler.h
.macro ldrusr, reg, ptr, inc, cond=al, rept=1, abort=9001f
usracc ldr, \reg, \ptr, \inc, \cond, \rept, \abort
.endm
.macro usracc, instr, reg, ptr, inc, cond, rept, abort, t=TUSER()
.rept \rept
9999:
.if \inc == 1
\instr\cond\()b\()\t \reg, [\ptr], #\inc
.elseif \inc == 4
\instr\cond\()\t \reg, [\ptr], #\inc
.else
.error "Unsupported inc macro argument"
.endif
.pushsection __ex_table,"a"
.align 3
.long 9999b, \abort //把这段地址对放到section __ex_table中,含义是如果地址9999b 发生了异常,它的fixup code放在abort中,do_page_fault 会根据发生异常时候的PC查询 exttable,并执行fixup code,code:do_page_fault-->do_kernel_fault-->fixup_exception
.popsection
.endr
.endm
这样在copy 的时候调用的每个ldr*w的时候都会在exception table中插入一条fixup code,如下图所示,当访问当前地址出现的pagefault就会去查询exception table
===========================缺页中断硬件============================================
缺页中断叫中断是因为这个是mmu硬件产生的,通过查询cp15的寄存fsr可以得到发生缺页中断的时候的地址和缺页异常类型
缺页异常的类型有page translation 和 permission fault 两种
对于软件来说,要做的事情是从中断向量表开始,__vectors_start是中断异常处理的起点,具体到缺页异常路径是:
__vectors_start-->vector_dabt-->__dabt_usr/__dabt_svc-->dabt_helper-->v7_early_abort-->do_DataAbort-->fsr_info-->do_translation_fault/do_page_fault/do_sect_fault。
重点是do_page_fault。
=====================================正文====================================
do_page_fault 里才有针对user 和 kernel 发生的不同page fault的不同处理flow,为了查看page fault与exception table如何协作,我们用qemu做下实验
理论上kernel space是可以直接访问user space的地址的,只不过如果该地址只是还没有建立对物理地址的映射关系的时候会发生缺页异常,kernel space发生user space的地址的缺页异常的时候,如果是非法(权限)的地址,kernel space 要保证系统还能继续的正常运行下去(fixup table),所以kernel space在访问userspace的时候需要调用copy_from_user
如果给copy_from_user的from指定一个奇怪的地址比如0x7000就会发生pagefault,copy_from_user返回值是剩余的字节数,得到的LOG的结果如上,可见0x7000的地址是过得了access的检查的,但是发生data abort然后fsr会记录发生abort的地址和错误类型,然后跳到do_page_fault ,直接使用0x7000的地址因为该地址不属于本task分配过的任何虚拟地址空间,所以还没走到建立页表映射的位置。在__do_kernel_fault函数中调用了fixup code然后返回。
换种方式造一个已经分配虚拟地址还未分配物理地址的情形,user space申请1M的空间,然后用addr+1M的地址传递给kernel,这种情况的异常的LOG如下
从LOG看这次缺页异常被系统正常的处理了copy成功了,fault返回值为0,而且也没有走fixup code的flow