crash工具解析_crash工具和x86-64汇编基础

在前面的文章中,已经出现了作为Linux重要调试手段之一的crash工具的身影。在后续的文章里,我们还会继续用到它。因此在这里,准备对Linux中的crash工具的原理和使用方法,做一个相对全面的介绍。

crash工具可用于从正在运行的内核获取信息,而更多的时候,则是用来分析产生的coredump文件,以定位内核陷入异常的原因。coredump文件本质上记录的是内核crash时的内存数据,因此通常被译作「内核转储」。

产生 - 三个前提

如果内核自己都已经崩溃了,那系统里面还有谁能完成导出内存的工作呢?你们可能不知道,内核是有一个backup的(称为"capture kernel"),而为它创建这个backup的,是一个名为"Kdump"的service。

capture kernel的使命是导出完整的内存,可是其运行本身也要触碰到物理内存,这势必会破坏掉原有内核(production kernel)之前的内存信息。所以呢,这个capture kernel所需使用的内存必须事先预留出来。如果kdump发现系统没有为capture kernel预留内存,就压根儿不肯起来工作。

capture kernel要做的事情相对比较简单,用不了太多内存,至于预留多少比较合适,可以结合系统总内存的大小进行指定,或者相信内核,直接设置为"auto",让内核自己计算。

那内核“自动”算出来的到底是多少呢?从"/proc/iomem"中查看预留内存的物理分布,然后算一下,嗯,是161MB。

其实也不用手动算啦,在dmesg的输出里面,内核已经告诉我们啦:

万一dmesg在启动阶段的信息被冲掉了,那也没关系,可以换用"kdumpctl showmem"命令来查看。

通常,产生的coredump文件位于"/var/crash"目录下,如果有特殊需要,可以修改""配置文件:

检查过内存是预留了的,kdump是启动了的并且配置也没有问题,可是怎么系统真正crash后,"/var/crash"里面还是空空如也?那可能是你还遗漏了对"ulimit"的调整。用"ulimit -a"命令看一下,如果"core file size"这一项的内容为0,那么将其设置为"unlimited"。

一般来说,coredump是内存经过压缩后产生的,体积不会太大,但在一些嵌入式设备里,flash的容量有限,在这种情况下,可通过网络传输的方式,将coredump导出到其他位置。找到对应的配置文件来修改传输方式当然是可以的,嫌麻烦的话,就借助之前介绍的cockpit管理工具:

搭配 - 调试信息

提供给crash工具的,除了coredump文件,还需要产生这个coredump的内核的镜像文件。不过,假设你直接用"extract-vmlinux"工具来解压"/boot"目录下的vmlinuz镜像文件,把得到的vmlinux喂给crash,将会得到crash的无情报错:

提示很明显,crash要的是包含了调试信息的内核镜像。如果是自己编译内核,那么应该在config文件里设置"CONFIG_DEBUG_INFO=y"(相当于是给CFLAG加了"-g"的编译选项)。对于发出去的release版本,不能包含debug信息,但可以事先把debug信息单独提取出来,以便遇到问题时使用:

objcopy --only-keep-debug vmlinux vmlinux.debug

如果使用的是标准Linux发行版,那么通常从网上就能获取到对应内核的debuginfo。以CentOS为例,可以从这个镜像源下载,安装后的vmlinux的默认路径为"/usr/lib/debug/lib/modules/<kernel-version>"。

这里分享一个笔者使用crash进行在线分析时踩过的小坑。在线分析是借助"/proc/kcore"文件来充当dumpfile的,但运行crash工具后,提示说vmlinux的版本与当前运行的内核版本不一致:

对比了一下,明明是一样的啊,不都是""吗,一个字母都不差啊:

最后发现,两者的编译时间不一样。同一个内核为啥编译时间会不一样呢?咳咳,一个是CentOS的,一个是RHEL的。看来啊,crash的检查还是很严格的,差一点都不行。

分析 - 懂点汇编

因为要解析众多的symbol信息,crash工具的启动时间相对还是比较长的,大概15秒左右吧。要说掌握crash那些内置命令的用法,其实并不难,借助"help <command>"或者在线的文档,就以获得每条命令的详细用法和示例。而且,也不用像systemtap, bpftrace一样,得记住一些新语言的语法和规则才能入门。

其真正的门槛隐藏在对Linux内核的重要数据结构的掌握,进程、内存和文件系统的关系的理解等方面,还有就是一定的汇编基础。目前服务器领域还是以x86-64体系的CPU为主,不过希望大家不要因为以前教科书上介绍的x86繁杂的寻址方式,而对x86的汇编心生恐惧和抵触情绪。

事实上,在目前普遍使用的64位系统上,segmentation机制已基本成为一具空壳,其寻址方式已经没有那么复杂。虽然相比于ARM,x86的汇编还是多了一些tricky的技法(比如"eax"作为隐含寄存器),但具体到使用crash工具来分析,也并不需要对其拿捏的那么深入,了解一些基本的知识,足以应对大多数的场景。

比如最基础的"mov"指令,将"r12"寄存器的内容复制到"rcx"寄存器,是这么写的:

mov %r12,%rcx (对应C语言里"a=b")

稍微扩展一下,将"r12"寄存器存储的地址加上8,得到一个新的地址,取这个新的内存地址的内容,拷贝到"rcx":

mov 0x8(%r12),%rcx (对应C语言里"a=*(b+8)")

这么基础的也要讲?嗯,Intel的SDM手册里使用的是Intel汇编语法自不用说,很多介绍x86汇编的书籍也是如此,但crash里disassembly看到的AT&T汇编,分不清的话,怕是看个"mov"都会犯迷糊。

【函数调用和传参】

除了反汇编,crash里还涉及到很重要的一点就是stack信息的阅读,而stack又跟函数的调用和返回密切相关。上层函数(calling function/caller)通过"callq"指令调用子函数 (called function/callee),在这个环节,主要有两件事要做:

  • 将callq的下一条指令的地址(即"return address")压栈(这一步相当于执行"push %rip"),当子函数执行完毕,将返回到return address的位置继续执行(注1)。
  • 跳转到子函数的第一条指令的地址,开始执行子函数的代码。

子函数会首先将当前"rbp"寄存器的值压栈(也就是上层函数的bp),这就形成了子函数stack的第一个8字节的内容。将"rbp"压栈保存,是为了在子函数的stack完全弹出后,能够找到上层函数的stack的起始地址。

push %rbp

按照这个法则,子函数也需要通过将当前"rsp"寄存器的内容拷贝到"rbp",记录并保存下自己的bp值,这样它的下一级函数才可以知道需要返回的stack的地址。

mov %rsp %rbp

接下来"rsp"就可以自由活动了,向低地址处移动若干字节,以腾出要压栈的空间,然后子函数通过push指令,将自己需要改写的寄存器压栈保存(以便跳回到上层函数的时候,可以恢复这些寄存器在上层函数中的值)。

一个函数再单薄,比如scheduler(),它的stack frame也必须包含两个部分:return address和rbp(即返回后继续执行的代码地址,和继续执行的函数的栈底指针),因此其大小也至少占据16个字节。

在32位的x86中,寄存器数目较少,因此函数的参数都通过stack来传递,当进入64位的x86-64时代后,寄存器的数目有所增加,为了提高效率,函数的前面几个参数通常使用寄存器传递,多出的参数再fallback到stack传递的方式。

不同的OS在这一点上有不同的实现,包括Linux在内的类Unix系统会使用6个寄存器来传递参数(包括"rdi", "rsi", "rdx, "rcx, "r8和"r9"),而且这里是有顺序要求的,即第一个参数用rdi传递,第二个参数用rsi传递,以此类推。Windows 64采用的则是"rcx", rdx", r8"和r9"这4个寄存器(参考这篇文章)。

正由于需要用几个指定的寄存器来传参,所以在调用子函数之前,如果要传递的参数不在指定寄存器中,往往涉及到寄存器内容的腾挪转移。

在调试的时候,寄存器传参的方式会给直接从stack来获取参数的值带来困难,在后面的一篇文章中,将演示如何通过back trace,来间接地推导出函数参数的信息。此外,还可以尝试使用pykdump工具的fregs命令来辅助分析。

小结

得到coredump转储文件,获取debuginfo调试信息,再装备一些汇编知识,接下来,就勇敢地上路吧,在更多的实际案例中,抽丝剥茧,层层深入,探索crash工具在我们的“破案”过程中,那不可替代的美妙作用。

注1:

某些恶意软件会故意让stack溢出到return address所在的栈位置,以更改return address的值,返回的时候就会跳转到恶意代码所在的位置,这种攻击手段被称为"Buffer Overflow Attack"。

参考:

原创文章,转载请注明出处。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值