博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从猫蛇之战三看内核戏CPU
阅读量:3589 次
发布时间:2019-05-20

本文共 2820 字,大约阅读时间需要 9 分钟。

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

640?wx_fmt=jpeg

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

   

    

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

    回顾一下,最初的问题是“为什么在调试器里读写空指针不会崩溃?” 通过读源代码的方法揭示了调试器会使用特殊的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函数所调用的拷贝函数,可以看到在它的末尾是有些特别机关的。

640?wx_fmt=png

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

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

640?wx_fmt=png

    阅读上面的宏,其作用是在专门描述异常处理器的异常表(__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指令。

640?wx_fmt=png

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

640?wx_fmt=png

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

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

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

640?wx_fmt=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了,也就是我们在第一篇文章中曾经解释过的这个代码。

640?wx_fmt=png

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

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

640?wx_fmt=jpeg

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

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

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

欢迎关注格友公众号

640?wx_fmt=jpeg

转载地址:http://qrpwn.baihongyu.com/

你可能感兴趣的文章
springboot参数检验,Assert使用
查看>>
htonl函数原理
查看>>
MACOS的Python虚拟环境使用笔记
查看>>
MAC系统使用Matplotlib显示中文问题亲测有效
查看>>
JavaScript的类型转换笔记
查看>>
JavaScript闭包实现计数器
查看>>
JavaScript中this关键字
查看>>
JavaScript两种定时器的使用
查看>>
阿里云服务器配置Nginx访问不到问题
查看>>
MAC电脑使用jupyter notebook
查看>>
Windows上设置jupternotebook远程访问
查看>>
查找数组中指定值下标
查看>>
不用strcat进行连接
查看>>
排列组合Cnm,有参数有返回值
查看>>
嵌套数组中查找元素
查看>>
gets函数
查看>>
查找句子中单词数,串
查看>>
局部变量存储类别
查看>>
Ubuntu18安装vim
查看>>
第39级台阶 c++
查看>>