共计 5347 个字符,预计需要花费 14 分钟才能阅读完成。
简介: 造成零碎异样宕机(无响应、异样重启)的起因有很多种,最常见的是操作系统外部缺点和设施驱动缺点。本文作者将和大家分享内存转储剖析的底层逻辑和方法论,并通过一个线上实在案例来展现从剖析到得出结论的整个过程,心愿对同学们解决此类问题和对系统的了解上有所帮忙。
置信但凡与计算机高频密切接触的人,都遇到过零碎无响应,或忽然重启的状况。这样的状况如果产生在客户端设施,如手机,或者笔记本电脑上,且不是频繁呈现,基本上咱们的解法就是鸵鸟算法,即默默重启设施,而后持续应用,当作什么都没产生过。
然而,如果这样的问题产生在服务端,比方运行微信、微博后台程序的虚拟机或者物理机上,那往往会产生相当严重的影响。轻则导致业务中断,重则导致业务长时间无奈工作。
大家都晓得,驱动这些计算机的是运行在其上的操作系统,如 Windows 或者 Linux 等。零碎异样宕机(无响应、异样重启)的起因有很多种,但总体来看,操作系统外部缺点,或者设施驱动缺点是最常见的两类起因。
从根本上解决这类问题“惟一正确”的办法,是操作系统内存转储剖析(Memory Dump Analysis)。内存转储剖析属于高阶的软件调试能力,须要工程师有丰盛且全面的零碎级别理论知识和大量的疑案破解似的上手实践经验。
内存转储剖析的方法论
内存转储剖析是对业余能力要求极高的一个工作,也是十分不容易的一件事件。在以往案例分享后,失去比拟乏味的反馈,如“耳边想起了柯南的配音”,或者“真黑猫警长!做的是 IT 工程师,却终日搞刑侦工作”。
内存转储剖析须要用到的根底能力,包含但不限于反汇编、汇编剖析、各种语言的代码剖析,零碎层面各种构造的了解,如堆,栈,虚表等,甚至深刻到 bit 级别。
试想,一个零碎运行了很长一段时间。在这段时间里,零碎积攒了大量失常、甚至不失常的状态。这时如果零碎忽然呈现了一个问题,那这个问题十有八九跟长时间积攒下来的状态有关系。
剖析内存转储,就是剖析产生问题时,零碎产生的“快照”。实际上须要工程师以这个快照为出发点,追溯历史,找出问题产生源头。这有点像是从案发现场,推理案发通过一样。
死锁分析方法
内存转储分析方法,能够从所要解决问题的角度,简略分成两类,别离是死锁分析方法,和异样分析方法。这两种办法的区别在于,死锁分析方法以零碎全局为出发点,而异样剖析则从具体异样点开始。
死锁问题体现进去,就是零碎不响应问题。死锁分析方法着眼于全局。这里的全局,就是整个操作系统,包含所有过程在内的零碎全貌。咱们从教科书里学到的常识,一个运行中的程序,包含了代码段,数据段和堆栈段。用这个办法去看一个零碎也同样适宜。零碎的全貌,其实就包含正在被执行的代码(线程),和保留状态的数据(数据、堆栈)。
死锁的实质,是零碎中局部或者全副线程,进入了相互期待且相互依赖的状态,使得过程所承载的工作无奈被继续执行了。所以咱们剖析这类问题的中心思想,就是剖析零碎中所有的线程的状态和它们之间的依赖关系,正如如图 1 所示。
图 1
线程的状态相对来说是比拟确定的信息。咱们能够通过读取内存转储中线程的状态标记位,来获取这类的信息。而依赖关系剖析则须要很多的技巧和实践经验。最罕用分析方法有对象的持有期待关系剖析,时序剖析等。
异样分析方法
绝对死锁剖析,异样分析方法的外围是异样。咱们常常遇到的异样有除零操作,非法指令执行,谬误地址拜访,甚至包含软件层面自定义的非法操作等。这些异样反馈到操作系统层面,就是异样重启类宕机问题。
异样问题归根结底是处理器执行了具体的指令而触发的。换句话说,咱们看到的景象,必定是处理器踩到了异样点。所以剖析异样类问题,咱们须要从异样点登程,逐渐地推导出代码执行到这一点的残缺逻辑。
以教训来看,懂得做内存转储异样剖析的工程师不多,而了解以上一点的人更是少之又少。很多工程师剖析异样重启问题,基本上只停留在异样自身,基本没有推导出问题背地的整个逻辑。
相比死锁分析方法,异样剖析的办法没有那么多固定的章法,甚至很多时候,因为问题逻辑简单,咱们没有方法找出根本原因。
总体看来,异样剖析的底层逻辑,是一直地比照预期和非预期的情况,而后找出背地的起因。比方解决其执行了谬误指令而触发的异样,那咱们须要从答复失常被执行的指令应该是什么,为什么处理器拿到了这个谬误指令这两个问题开始,不断深入,追本溯源。
用死锁分析方法解决异样问题,用异样分析方法解决死锁问题
以上两种内存转储分析方法,是基于问题剖析的终点和一般性剖析伎俩来分类的。在理论问题处理过程中,咱们常常须要从零碎全局状态中,找到进一步解决异样问题的思路,也会用具体细节剖析伎俩,来给全局类问题最初一击。
黑客与宕机
问题背景
宕机问题有一种比拟少见的问题模式,就是看起来齐全不相干的机器同时呈现宕机。解决这个模式的问题,咱们须要找到在这些机器上能同时触发问题的条件。
通常,这些机器要么简直在同一时间点呈现问题,要么从某一个工夫点开始,相继呈现问题。对于前一种状况,比拟常见的情景是,物理设施故障导致运行在其上的所有虚拟机宕机,或者一个远程管理软件同时杀死了多个零碎的要害过程;对于后一种状况,可能的一个起因是,用户在所有实例上部署了同一个有问题的模块(软件、驱动)。
而实例被大范畴地攻打,则是另一个常见的起因。比方在 WannaCry 勒索病毒肆虐的时候,经常出现一些公司,或者一些部门的机器全副蓝屏的情景。
在这个案例中,用户装置了阿里云的云监控产品之后,呈现了大范畴云服务器间断宕机的状况。为了自证清白,咱们消耗了不少膂力脑力来深入分析这个问题。通过此案例分享,心愿能给读者以启发。
坏掉的内核栈
咱们解决操作系统宕机类问题的惟一正确办法是内存转储。不论是 Linux 或 Windows,在零碎宕机之后,都可能通过主动,或者人工的形式,产生内存转储。
剖析 Linux 内存转储的第一步,咱们应用 crash 工具关上内存转储,并用 sys 命令察看零碎的根本信息和宕机的间接起因。对于这个问题来说,宕机的间接起因是 ”Kernel panic – not syncing: stack-protector: Kernel stack is corrupted in: ffffxxxxxxxx87eb”,如图 2 所示。
图 2
对于这条信息,咱们必须逐字解读。”Kernel panic – not syncing:” 这部分内容在内核函数 panic 里输入,但凡调用到 panic 函数,必然会有这一部分输入,所以这一部分内容和问题没有间接关系。而 ”stack-protector: Kernel stack is corrupted in:” 这部分内容,在内核函数 __stack_chk_fail,这个函数是一个堆栈查看函数,它会查看堆栈,同时在发现问题的时候调用 panic 函数产生内存转储报告问题。
而它报告的问题是堆栈损坏。对于这个函数,后续咱们会进一步剖析。
而 ffffxxxxxxxx87eb 这个地址,是函数 __builtin_return_address(0) 的返回值。当这个函数的参数是 0 的时候,这个函数的输入值是调用它的函数的返回地址。这句话当初有点绕,然而后续剖析完调用栈,问题就会变得很分明。
函数调用栈
剖析宕机问题的外围,就是剖析 panic 的调用栈。图 3 中的调用栈,乍看起来是 system_call_fastpath 调用了 __stack_chk_fail,而后 __stack_chk_fail 调用了 panic,报告了堆栈损坏的问题。然而略微和相似的堆栈作一点比拟的话,就会发现,事实并非这么简略。
图 3
图 4 是一个相似的,以 system_call_fastpath 函数开始的调用栈。不晓得大家有没有看进去这个调用栈和上边调用栈的不同。实际上,以 system_call_fastpath 函数开始的调用栈,示意这是一次零碎调用(system call)的内核调用栈。
图 4
图 4 的调用栈,示意用户模式的过程,有一次 epoll 的零碎调用,而后这个调用进入了内核模式。而图 3 中的调用栈显然是有问题的,因为咱们就算查遍所有的文档,也不会找到一个零碎调用,会对应于内核 __stack_chk_fail 函数。
这里须要揭示的是,这边引出另外一个,在剖析内存转储的时候须要留神的问题,就是用 bt 打印进去的调用栈有的时候是谬误的。
所谓的调用栈,其实不是一种数据结构。用 bt 打印进去的调用栈,其实是从真正的数据结构,线程内核堆栈中,依据肯定的算法重构进去的。而这个重构过程,其实是函数调用过程的一个逆向工程。
置信大家都晓得堆栈的个性,即先进后出。对于函数调用,以及堆栈的应用,能够参考图 5。能够看到,每个函数调用,都会在堆栈上调配到肯定的空间。而 CPU 执行每个函数调用指令 call,都会顺便把这条 call 指令的下一条指令压栈。这些“下一条指令”,就是所谓的函数返回地址。
图 5
这个时候,咱们再回头看 Panic 的间接起因那一部分,即函数 __builtin_return_address(0) 的返回值。
这个返回值,其实就是调用 __stack_chk_fail 的 call 指令的下一条指令,这条指令属于调用者函数。这条指令地址被记录为 ffffxxxxxxxx87eb。
如图 6 所示,咱们用 sym 命令查看这个地址邻近的函数名,显然这个地址不属于函数 system_call_fastpath,也不属于内核任何函数。这也再次验证了,panic 调用栈是谬误的这个论断。
图 6
对于 raw stack,如图 7 所示,咱们能够用 bt -r 命令来查看。因为 raw stack 往往有几个页面,这里只截图和 __stack_chk_fail 相干的这一部分内容。
图 7
这部分内容,有三个重点数据须要留神,panic 调用 __crash_kexec 函数的返回值,这个值是 panic 函数的一条指令的地址;__stack_chk_fail 调用 panic 函数的返回值,同样的,它是 __stack_chk_fail 函数的一条指令的地址;ffffxxxxxxxx87eb 这个指令地址,属于另外一个未知函数,这个函数调用了 __stack_chk_fail。
Syscall number 和 Syscall table
因为带有 system_call_fastpath 函数的调用栈,对应着一次零碎调用,而 panic 的调用栈是坏的,所以这个时候咱们自然而然会疑难,到底这个调用栈对应的是什么零碎调用。
在 linux 操作系统实现中,零碎调用被实现为异样。而操作系统通过这次异样,把零碎调用相干的参数,通过寄存器传递到内核。在咱们应用 bt 命令打印出调用栈的时候,咱们同时会输入,产生在这个调用栈上的异样上下文,也就是保留下来的,异样产生的时候,寄存器的值。
对于零碎调用(异样),要害的寄存器是 RAX,如图 8 所示。它保留的是零碎调用号。咱们先找一个失常的调用栈验证一下这个论断。0xe8 是十进制的 232。
图 8
应用 crash 工具,sys -c 命令能够查看内核零碎调用表。咱们能够看到,232 对应的零碎调用号,就是 epoll,如图 9 所示。
图 9
这个时候咱们再回头看“函数调用栈”这节的图 3,咱们会发现异常上下文中 RAX 是 0。失常状况下这个零碎调用号对应 read 函数,如图 10 所示。
图 10
从图 11 中,咱们能够看出,有问题的零碎调用表显然是被批改过的。批改零碎调用表(system call table)这种事件,常见的有两种代码会做,这个相当辩证。一种是杀毒软件,而另外一种是病毒或木马程序。当然还有另外一种状况,就是某个糟糕的内核驱动,有意识地改写了零碎调用表。
另外咱们能够看到,被改写过的函数的地址,显然和最后被 __stack_chk_fail 函数报进去的地址,是十分邻近的。这也能够证实,零碎调用的确是走进了谬误的 read 函数,最终踩到了 __stack_chk_fail 函数。
图 11
Raw data
基于上边的数据,来齐全压服客户,总归还是有点经验主义。更何况,咱们甚至不能辨别,问题是由杀毒软件导致的,还是木马导致的。这个时候咱们破费了比拟多的工夫,尝试从内存转储里挖掘出 ffffxxxxxxxx87eb 这个地址更多的信息。
有一些最根本的尝试,比方尝试找出这个地址对应的内核模块等等,然而都无功而返。这个地址既不属于任何内核模块,也不被已知的内核函数所援用。这个时候,咱们做了一件事件,就是把这个地址前后间断的,所有曾经落实(到物理页面)的页面,用 rd 命令打印进去,而后看看有没有什么奇怪的字符串能够用来作为 signature 定位问题。
就这样,咱们在邻近地址发现了下边这些字符串,如图 12 所示。很显著这些字符串应该是函数名。咱们能够看到 hack_open 和 hack_read 这两个函数,对应被 hacked 的 0 和 2 号零碎调用。还有函数像 disable_write_protection 等等。这些函数名,显然阐明这是一段“不平庸”的代码。
图 12
后记
宕机问题的内存转储剖析,须要咱们足够的急躁。我集体的一条教训是:every bit matters,就是不要放过任何一个 bit 的信息。内存转储因为机制自身的起因,和生成过程中一些随机的因素,必然会有数据不统一的状况,所以很多时候,一个小的论断,须要从不同的角度去验证。