利用LLDB动态调试定位Crash原因
前言
LLDB是LLVM项目里面一个使用非常广泛的调试器,配合debugserver,对本地调试或者远程调试都有很好的支持。
网上也有很多关于这两个程序的介绍,这里就不多说了,本文主要记录了我在实际工作中遇到的两个例子,利用LLDB的调试功能,在没有代码的情况下,定位Crash的具体符号。通过这两个例子,可以对这一类问题提供通用的思路:找到某个功能调用的具体符号,方便排查问题,或者开发对相关符号的钩子(Hook)功能。
案例一
某些特殊情况下,App运行后主动退出,在没有代码的情况下,我们不清楚App主动退出的原因,这个时候可以借助LLDB,通过attach命令,调试程序,并打上一些猜测的断点,进一步观察情况。 下面以进程名字DACS_ARM_Tool
为例
通过attach进程名的方式,等待进程启动。
attach -n DACS_ARM_Tool --waitfor |
运行程序,可以看到lldb如下输入:
(lldb) attach -n DACS_ARM_Tool --waitfor |
上面显示进程中断运行,目前执行到1号线程,中断原因是signal SIGSTOP。
在这里我们可以思考一下,App如果是主动退出,一般会调用哪些符号,常见的有-[NSApplication terminate:]
,exit
等,所以先对这两个符号打上断点。
lldb打断点的命令是breakpoint set,缩写是br s,后面跟上符号的类型以及符号名字,-n表示符号名,-r表示正则匹配,具体用法参考文末官方文档。
(lldb) br s -n exit |
接着输入命令c,让程序恢复运行,此时可以输入br list命令,可以看到当前识别到的断点位置:
(lldb) br list |
可以看到一共断点到4个位置,继续运行程序,出发程序退出逻辑,这样就可以看到程序在退出之前被中断了。接着使用bt命令查看调用栈,一般就可以看到一些原因了。
Process 35564 stopped |
上面lldb的输出可以看到,程序进入testCrash方法,调用了terminate:导致的退出。
上面就是一个完整的分析例子,例子比较简单,所以很快就可以看到原因。实际开发中一般会遇到更加复杂的情况,比如除了上面打断的两个退出符号之外,还有其他符号,又或者程序退出中途又会调用多个名为exit的函数,这样使用bt是看不到正确的调用栈的,需要慢慢分析。
案例二
某次对login进程注入了一个动态库之后,发现login无法启动,控制台出现了一句话之后Crash。
2021-10-09 16:07:52.355 login[63473:8543240] The application with bundle ID (null) is running setugid(), which is not allowed. Exiting. |
从字面看,意思就是程序没有bundle id导致无法调用setugid,这个可能是我注入的动态库中运行了某些hook类的代码导致的,但是因为代码很多,一行一行注释排查需要花费很长时间,如果能借助LLDB,通过在某些符号上打断点,能确定出问题的地方,那效率肯定能提高一些。
开始排查。还是和上面一样进入LLDB,通过attach进程名字:
attach -n login --waitfor |
在终端里单独运行login程序,发现lldb能attach成功并且中断了程序:
(lldb) attach -n login --waitfor |
因为这次我不清楚具体触发Crash的符号名,所以这里使用正则匹配的形式,模糊匹配符号:
(lldb) br s -r "bundle" |
还没显示断点信息,恢复程序,就可以看到程序运行一会马上中断了,这一次可以看到断点信息了,一共有300多个断点:
(lldb) c |
可以看到程序断点在符号+[OS_xpc_bundle load]
里面。通过br list命令,可以看到所有的断点信息(数量太多了,摘取其中一部分放上来)
(lldb) br list |
可以看到正则匹配的符号实在是太多了,断点了几百个符号,很容易导致程序运行时随便就匹配到无关断点,很难排查问题。此时输入c,会看到程序又卡在另一个断点上了,所以必须先删除所有断点,重新设置一个更加详细的正则匹配的符号名。
通过下面命令删除所有断点:
br del |
这里好像没有什么特别的技巧了,只能看看注入的动态库中有什么符号,或者什么符号和bundle ID有关系的,经过一系列测试,最后可以发现断点到bundleIdentifier
这个符号上,能找到Crash的最后调用信息。
(lldb) br s -r bundleIdentifier |
输入c继续运行,发现第二次c之后,程序crash了,所以可以判断crash的地方就是第二次触发断点的地方。
(lldb) c |
所以在第二次触发断点之后,查看调用栈:
(lldb) bt |
真想大白了,是因为用到了NSWindow这个类,内部会调用方法_NSCheckForIllegalSetugidApp
检查bundle id,因为login这个程序并不是一个普通的app结构,因此检查不通过,导致程序crash了。
知道了crash的原因,只需要把所有NSWindow相关的代码都移除掉,就可以正常运行被注入之后的login程序了。
思考
上面两个案例,都是通过LLDB来发现出现问题的具体位置,除了crash问题,其他问题,比如网络的,都可以通过类似的手法来找到程序实际调用了哪些符号,对症下药,找到最终解决问题的方案。其中LLDB还有很多功能,比如查看断点处的汇编指令d:
(lldb) d |
我对LLDB的其他功能也不是很熟悉,这里就不多说了。