45

从猫蛇之战看内核戏 CPU(三)

 5 years ago
source link: https://mp.weixin.qq.com/s/X_hzuFyJEifvEiZCYD4Oeg?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

庐山归来,终于有些空闲,见缝插针,今天赶紧把没有写完的“猫蛇之战”补齐。

NnA32uQ.jpg!web

如果没有读过前两篇或者想复习一下的,请点击:

从猫蛇之战看内核戏CPU

从猫蛇之战再看内核戏CPU

先说明一下,“连续剧”的成本有点高,无论如何,这一篇会把这个问题写完。

回顾一下,最初的问题是“为什么在调试器里读写空指针不会崩溃?”第一篇通过读源代码的方法揭示了调试器会使用特殊的probe函数:

probe_kernel_read

probe_kernel_write

上一篇通过试验证实,使用probe函数时CPU也会发怒报异常。本篇继续介绍CPU报了异常之后,内核是如何处理这个事件,将其“摆平”的。

在著名的《幽梦影》一书中有很多妙语,其中有不少是关于写作技巧的,比如:

“作文之法: 意之曲折者,宜写之以显浅之词; 理之显浅者,宜运之以曲折之笔; ”

因为这个系列讨论的问题有点复杂和曲折,所以我们是遵循“意之曲折者,宜写之以显浅之词”的原则来写的。

继续贯彻这个原则,直接回答刚才的问题,“摆平”CPU靠的是LINUX内核里一种基于表的异常处理机制,这个机制一般被称为“异常表(Exception Table)”,简称extable。

下面继续结合我们故意访问地址880的例子来理解extable机制。

在CPU查找页表发现线性地址0x880无效而发怒后,它通过IDT表中登记地址跳转到LINUX中处理异常的入口函数,这个入口函数是以汇编语言编写的,名为page_fault,在arch/x86/entry/entry_64.S中。

汇编函数不适合做太多逻辑,只是保存寄存器等信息后便调用C语言编写的do_page_fault。

do_page_fault内部获取CR2的值后便调用__do_page_fault。

__do_page_fault内部的逻辑错综复杂,一个条件判断接着另一个,我们只挑与我们有关的说。

与try{}catch等异常捕捉机制类似,extable机制也是需要编译期就做好准备的。

仔细观察probe函数所调用的拷贝函数,可以看到在它的末尾是有些特别机关的。

YRFjEz3.png!web

注意上图中的两个_ASM_EXTABLE宏,它们就是给危险代码增加保险(异常处理)的“安全带”。

这个宏定义在asm.h,如下图所示。

NNzeYvI.png!web

阅读上面的宏,其作用是在专门描述异常处理器的异常表(__extable)里增加一行,这一行包含三个信息:

from

to

handler

简单来说,前两个都是代码地址,一个是触发异常的,一个是处理异常的,最后一个是函数指针。最后一个是4.6版本内核新增的,为了支持更复杂的处理策略。在_ASM_EXTABLE宏中,使用的是ex_handler_default,选择这个的处理器的效果是:如果from处发生异常,那么就跳转到to处执行,不要panic,也不要发信号,封锁信息,低调处理,像什么都发生一样。

异常表表项的结构体定义在extable.h中,即:

struct exception_table_entry {

int insn, fixup, handler;

};

在extable.c文件中,有ex_handler_default函数的代码,摘录如下:

__visible bool ex_handler_default(

const struct exception_table_entry *fixup,

struct pt_regs *regs, int trapnr)

{

regs->ip = ex_fixup_addr(fixup);

return true;

}

EXPORT_SYMBOL(ex_handler_default);

各位看官请睁大眼睛,到关键地方了。请特别注意加粗的那一行代码,左边写的是regs结构体中的程序指针(ip),右边是处理异常代码的位置(即to参数)。

进一步说,这个regs结构体是在栈上形成的,报告异常时,CPU在准备起飞前先压入当时的执行位置,也就是段寄存器和程序指针,跳到page_fault后,内核中的代码继续把其它寄存器也压入栈,于是就在栈上形成了一个数据结构。对于熟悉NT内核的朋友来说,这相当于那个著名的陷阱帧(TRAP_FRAME)。

这种直接修改程序指针的方法是内核处理危机的杀手锏。经过这样飞针后,__do_page_fault就直接返回了,do_page_fault也返回,到了汇编写的page_fault函数后,就开始恢复寄存器了,也就是把保存在栈上的regs结构体中的寄存器弹出栈,加载到CPU中的物理寄存器。

软件保存的寄存器都恢复好后,执行iret指令。

eaUNvyE.jpg!web

执行iret指令时,CPU从栈上弹出已经被修改了的ip寄存器,跳过去执行。于是便开始执行to指定的异常处理代码了。这个代码在Linux内核中,被称为fixup,意思是“修修补补”。下图记录了这个特别飞跃的过程。

V3mey2i.jpg!web

上面是CPU执行iret前的栈内容,最上面便是IP和CS。单步一下后,CPU执行iret,从栈上弹出CS:IP,跳转到修补代码。

好一个飞跃,这一跃,从随时可能跌入深渊的do_page_fault中跳出,告别了敏感的异常处理上下文,化险为夷了。

这一跳跃,很像是猫蛇之战时小猫的紧急后退。小猫伸爪挑逗毒蛇是为了消耗蛇的体力,被激怒的毒蛇举头袭击,很是危险,小猫巧妙躲闪,灵活后退,华丽转身。

I3aqEbY.gif

在源代码中,修补函数是有特别标注的,放在特殊的.fixup段中,比如:

.section .fixup,"ax"

.L_fixup_4x8b_copy:

shll $6,%ecx

addl %ecx,%edx

jmp .L_fixup_handle_tail

执行好修补代码片段后,因为保存在栈上的copy函数的返回地址并没有变化,所以当修补函数返回时,线程会返回到probe函数中继续执行。并且,从probe函数看来,copy函数的返回值不为0,代表剩下的字节数,正常copy时,copy函数返回前会将ax寄存器置零,代表完成所有复制任务。因此,probe函数便可以根据copy函数的返回值不为0而返回-EFAULT了,也就是我们在第一篇文章中曾经解释过的这个代码。

UNbQNf7.jpg!web

讲到这里,第三个问题(Q3)的答案也有了。那么第二个问题呢?如果充分理解了上面描述的过程,那么也可以回答了,留着给大家思考吧。

最后分享一张老雷在庐山所拍的照片吧。

N7jMB3F.jpg!web

上了很多次庐山,第一次遇上山上的白玉兰盛开,高大的树木上挂满花朵,远远就可以望见。走到近处,花香袭人,坐在石阶上,透过鲜花和树干,还可以欣赏不远处的瀑布,来自庐山主峰汉阳峰的一股清泉,奔流直下,拍击岩石,溅出水珠无数......

***********************************************************

正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生。

欢迎关注格友公众号

UbmEZnB.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK