看看MachO到底干了什么
通过命令clang test.m -o test
把.m
文件直接编译为可执行文件
再通过命令objdump --macho -d test
来查看MachO
的代码段到底在做什么
可以看到MachO
根据系统分配的虚拟内存地址,一行一行的来执行机器指令,里面显示的汇编代码则是帮助我们来理解指令到底在做什么
同样可以看看目标文件到底在做什么,通过指令clang -c test.m -o test.o
先来生成目标文件
还是通过指令objdump --macho -d test.o
来查看代码段是什么
通过对比目标文件里并未变为虚拟内存地址而是只是有一些偏移,但是目标文件在链接的时候需要系统来告诉它真实的偏移,通过命令objdump --macho -reloc test.o
查看重定位符号表来得知哪里是需要重定位的
同样在重定位符号表里看到我们定义的变量global
在30、28、20
的地址处也需要重定位
那么在跳转到_test的时候是如何得到_test地址的呢?
大端模式
: 最高有效位存放在低位地址
小端模式
: 最高有效位存放在高位地址
先看到指令那行,小端模式下先得到偏移值0xFFFFF3
,得到偏移值补码为0xD
再乘以一条指令的字节数4得到0x34
,跳转指令的地址-偏移值即为0x100003f98 - 0x34 = 0x100003f64
所以MachO
就是根据这些数据和偏移来做事情
dSYM
在程序发生崩溃时可以看到一些信息来告诉我们哪里发生了问题,但是这些信息是从哪里来的呢?这些信息就是来自于dSYM
文件,这里存放的就是调试符号的信息,dSYM
文件就是保存按照DWARF
格式保存调试信息的文件。DWARF
是一种被众多编译器和调试器使用的用于支持源代码级别调试的调试文件格式。
clang -g -c test.m -o test.o
这里的-g
是要生成调试信息
接着来查看一下头部信息objdump --macho --private-headers test.o
可以看到出现了一些名为__DWARF
的段
那么在链接过程生成可执行文件后这些段便不再存于这里了,将移动至符号表内
同样为了验证这个说法,先使用命令objdump --macho --private-headers test
来查看
这里再没有发现有__DWARF
的段存在,那么现在查看一下符号表nm -pa test
所有的调试符号都在符号表里可以看到,符号表内有所有的符号不止是调试符号
刚才在生成可执行文件的时候已经生成了dSYM
文件,现在来看看这个文件里到底存储了什么
dwarfdump test.dSYM
对比之前查看的符号表,它是把符号表里的信息以一种特定的格式重新生成了这个dSYM
文件,每个符号的名字以及所在文件还有行数所有信息都有,这也就是发生崩溃的时候为什么能查看到相关的信息
上述过程可以总结为以下:
- 读取
debug map
- 从.o文件中加载
__DWARF
段的信息 - 重定位所有地址
- 将全部的
DWARF
打包为dSYM bundle
利用工程来验证dSYM是如何恢复符号的
创建一个工程里面只写一个可以造成崩溃的代码
这个应该是很熟悉的画面了,这里是已经还原过的场景,因为我们工程保存了相关的符号信息,这时应该设置为脱符号,但是笔者在这里发生了一点问题发现并没有真正脱去符号,如果有读者知晓还望告知
这里本应该在设置Deployment Postprocessing
为Yes
以及strip linked product
为Yes
,strip style
为all symbols
后就会脱去符号,但是我发现并没有,所以这里设置为release
本应该显示-[ViewController viewDidLoad]
的地方此时已经无法看到相关信息,那么我们在上传正式版本后类似于bugly
这样的软件是如何恢复调用栈的呢?它是利用了dSYM
文件
-
首先看到这里的地址
0x102b6a134
,这个地址是虚拟地址值
再加上ASLR
-
ASLR
是dyld
给加上的一个随机值,这里可以通过下图知道是0x102b68000
-0x100000000
- 那么在
dSYM
文件中的地址应该是0x102b6a134 - 0x2b68000 = 0x100002134
接下来在dSYM
文件中来查看这个地址中到底是什么即可
dwarfdump --lookup 0x100002134 TestInject.app.dSYM
可以看到就是我们想要的,这也就是如何通过dSYM
文件来定位确认出现崩溃的代码在哪里的过程
第三方库的断点信息
我们在平常使用第三方库的时候大多是直接以源码的形式来引入的,这样对于调试是很方便的,断点调试信息都可以直接在三方库源码中来看到,但是现在如果只给出framework
的话是否能进行调试呢?
在主工程中调用framework
中的方法,并对这个方法打上断点
另外framework
在引入主工程时只给出framework
的方式来做
这里可以看到断点已经成功,继续进入下一步,按照断点的确进入了源码
其实它也是根据符号表中的信息中源码文件的位置来定位到源码,接下来先来看一下framework
的符号表信息
nm -pa TestFramework.framework/TestFramework
可以看到这个路径就存放的是源码文件
那么上述问题可以换个角度来验证它,这里可以把framework
先移动到别的路径后再次进行编译,然后再重新运行再打断点来观察
首先可以看到framework
的符号表信息中路径已经发生了改变
这时重复打断点的工作后发现此时无法再像之前定位到源码中
那么如果这个猜想是正确的话只需要在这个路径放入源码文件即可,这里尝试后发现的确可以实现,那么根据上述内容可以设想在构建组件二进制化的时候可以考虑将源码文件固定在某个路径中来实现不再来回切换二进制文件和源码来实现。
总结:
lldb
的调试信息是根据符号表里给出源码的路径来找到源码文件进行调试的