[关闭]
@qidiandasheng 2024-06-05T19:58:19.000000Z 字数 7716 阅读 1515

iOS符号(😁)

iOS理论


符号类型

.debug:调试符号,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项调用编译驱动程序时才会得到这张表。
.symtab:符号表,它存放在程序中定义和引用的函数和全局变量信息。

格式:

可执行文件中Symbol Table存储着全局符号和局部符号; DWARF存储着符号的行号信息。

编译步骤:

Xcode编译实际的操作步骤是:生成带有 DWARF 调试信息的可执行文件 -> 提取可执行文件中的调试信息打包成 dSYM -> 去除符号化信息。去除符号是单独的步骤,使用的是 strip 命令。

调试符号

Generate Debug Symbols

GCC_GENERATE_DEBUGGING_SYMBOLS,生成调试符号,当Generate Debug Symbols选项设置为YES时,每个源文件在编译成.o文件时,编译参数多了-g-gmodules两项。

截屏2022-01-08 下午9.09.05.png-510kB

对应的目标文件,会多一些调试信息section:

截屏2022-01-08 下午9.10.36.png-428.6kB

Generate Debug Symbols设置为NO的时候,在Xcode中设置的断点不会中断。但是在程序中打印[NSThread callStackSymbols],依然可以看到类名和方法名。

Debug Information Level

CLANG_DEBUG_INFORMATION_LEVEL,这个配置项表示调试信息的等级,默认为Compiler default

另一个选项是Line tables only,这种类型的调试信息允许获得带有函数名、文件名和行号的函数调用栈,但是不包含其他数据(比如局部变量和函数参数)。

Strip符号

Deployment Postprocessing

Deployment Postprocessing如果为 YES,在编译生成目标文件之后要进行后续的Strip处理;如果为 NO,则不会有后续处理;使用 Xcode Archive 进行编译,Deloyment Postprocessing 的值恒为YES;

Strip Linked Product

Strip Linked Product如果为 YES,则进行Strip符号;如果为 NO,则不进行Strip符号。如果Deployment Postprocessing设置为NO,则此选项不生效。

Strip Linked Product为YES时我们可以看到Xcode在编译完成可执行文件后,有对可执行文件进行Strip的操作:

截屏2022-01-08 下午9.57.22.png-105.7kB

Strip Style

Deployment PostprocessingStrip Linked Product都为YES才生效;去除的是可执行文件中的符号。

截屏2022-01-08 下午10.08.58.png-168kB

截屏2022-01-08 下午10.18.01.png-572.9kB

截屏2022-01-08 下午10.12.07.png-160.6kB

截屏2022-01-08 下午10.22.26.png-674.2kB

截屏2022-01-08 下午10.06.11.png-204.1kB

截屏2022-01-08 下午10.25.34.png-627kB

llvm-strip

对可执行文件进行符号的strip,使用的是llvm的llvm-strip命令,比如之前的几种Strip Style的差别只是调用llvm-strip的参数不一样而已。

  1. /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/strip -S -T /Users/dasheng/Library/Developer/Xcode/DerivedData/HelloDemo-adntqoddwxvflvcgqagptmjkooku/Build/Intermediates.noindex/ArchiveIntermediates/HelloDemo-Example/InstallationBuildProductsLocation/Applications/HelloDemo_Example.app/HelloDemo_Example
  1. /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/strip -x -T /Users/dasheng/Library/Developer/Xcode/DerivedData/HelloDemo-adntqoddwxvflvcgqagptmjkooku/Build/Intermediates.noindex/ArchiveIntermediates/HelloDemo-Example/InstallationBuildProductsLocation/Applications/HelloDemo_Example.app/HelloDemo_Example
  1. /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/strip /Users/dasheng/Library/Developer/Xcode/DerivedData/HelloDemo-adntqoddwxvflvcgqagptmjkooku/Build/Intermediates.noindex/ArchiveIntermediates/HelloDemo-Example/InstallationBuildProductsLocation/Applications/HelloDemo_Example.app/HelloDemo_Example

具体的strip参数可参考llvm-strip文档

--strip-debug, -d, -g, -S

Remove all debug sections from the output.

--discard-all, -x

Remove most local symbols from the output. Different file formats may limit this to a subset of the local symbols. For example, file and section symbols in ELF objects will not be discarded. Additionally, remove all debug sections.

-T

Remove Swift symbols.


注意

动态库和静态库不能去除全部符号(Strip All Symbols),要保留全局符号(选择Non-Global Symbols),他们是库和其他库链接时沟通的桥梁;失去了全局符号,动态库和静态库就成为了黑盒。

去除符号的操作对于 dSYM 文件中的符号信息没有影响;对于动态库和可执行二进制文件,可以将符号尽可能去除掉减少二进制体积的大小。需要符号进行符号化崩溃日志时,再从 dSYM 文件中找对应符号。

解析符号表

为什么崩溃日志需要解析

如果我们线上app发生了崩溃,会有崩溃日志,但是因为我们的可执行文件里的符号依旧被strip掉了,所以你看到的调用堆栈都是二进制地址,而不是可读的函数名称。

dSYM符号文件

上面我们说过没有对应的符号是因为没有符号被strip掉了,那我们只要保存好这份被strip掉的符号不就行了。在xcode的build setting里可以生成这份被strip掉的符号文件,也就是dSYM符号文件,如下图设置即可:

截屏2022-01-10 下午10.00.33.png-117.5kB

截屏2022-01-10 下午10.02.48.png-204.3kB

截屏2022-01-10 下午10.05.42.png-42.5kB

解析crash文件

这里我真机跑了一个crash代码,然后连接电脑导出.crash文件。

同时在/Users/dasheng/Library/Developer/Xcode/DerivedData/HelloDemo-adntqoddwxvflvcgqagptmjkooku/Build/Products/Release-iphoneos目录下找到对应的.dSYM文件。

然后在/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash找到解析工具symbolicatecrash

把以上三个文件放到同一级目录:

截屏2022-01-10 下午10.35.58.png-24kB

解析原理

dSYM目录结构(右键打开包内容),显示如下图:

截屏2022-01-10 下午10.39.18.png-55.9kB

其中真正保存保存数据的是 DWARF 文件(DWARF结构), DWARF(Debuging With Arbitrary Format)是ELF 和 Mach-O 等文件格式中用来存储和处理调试信息的标准格式。 DWARF 中的数据是高度 压 缩 的 , 可以通过dwarfdump命令提取可读信息,比如提取关键的调试信息.debug_info.debug_line

  1. dwarfdump --debug-info HelloDemo_Example.app.dSYM > debuginfo.txt

如下图我在类DSViewController24行声明了NSMutableArray变量:

截屏2022-01-10 下午10.45.07.png-264.2kB

然后在debuginfo.txt,能看到响应的信息:

截屏2022-01-10 下午10.48.28.png-380kB

解析流程

我们打开.carsh文件,如下图所示红框内为崩溃的堆栈:

截屏2022-01-10 下午11.11.28.png-635.6kB

然后再往下就是对应线程的调用堆栈,我们往下拉找到HelloDemo_Example的堆栈地址,红框内地址部分第一列为当前指令地址,第二列为模块基地址,第三列为运行时的偏移地址

截屏2022-01-10 下午11.12.22.png-638.7kB

HelloDemo_Example的所有行其实地址都相同,如图起始地址为0x100930000
因为崩溃信息里会包括基址信息,我们图中的21行基本就是main函数的起始地址,也就是基址。如果不是崩溃堆栈的话,可以手动获取基址信息。

  1. // 获取HelloDemo_Example模块的基址
  2. uintptr_t baseAddress = getBaseAddress("HelloDemo_Example");
  3. // 获取指定模块的基地址
  4. uintptr_t getBaseAddress(const char *moduleName) {
  5. const struct mach_header *header = _dyld_get_image_header(0);
  6. if (!header) {
  7. return 0;
  8. }
  9. for (uint32_t i = 0; i < _dyld_image_count(); i++) {
  10. if (strstr(_dyld_get_image_name(i), moduleName) != NULL) {
  11. return (uintptr_t)_dyld_get_image_header(i);
  12. }
  13. }
  14. return 0;
  15. }

我们可以看到第一张图有两个地址是相近的,这两个地址都属于HelloDemo_Example,其中0x100937928,我们下面能看到偏移量为31016 = 0x7928。因为起始地址相同,另一个地址0x10093781c的偏移量为0x10093781c-0x100930000 = 30748 = 0x781C

以上地址均为 app 发生崩溃时的运行地址,根据虚拟内存偏移地址不变的原理,只要知道符号表 TEXT 段的起始地址,加上偏移量就能得到崩溃地址对应符号表中的地址。
使用如下命令输出符号表中TEXT段起始位置为0x0000000100000000

  1. otool -l HelloDemo_Example.app.dSYM/Contents/Resources/DWARF/HelloDemo_Example | grep __TEXT -C 5
  2. --
  3. nsects 0
  4. flags 0x0
  5. Load command 3
  6. cmd LC_SEGMENT_64
  7. cmdsize 1032
  8. segname __TEXT
  9. vmaddr 0x0000000100000000
  10. vmsize 0x0000000000030000
  11. fileoff 98304
  12. filesize 116
  13. maxprot 0x00000005
  14. initprot 0x00000005
  15. nsects 12
  16. flags 0x0
  17. Section
  18. sectname __text
  19. segname __TEXT
  20. addr 0x0000000100007748
  21. size 0x000000000001d240
  22. offset 0
  23. align 2^2 (4)
  24. reloff 0
  25. --

使用符号表基址+之前获取的崩溃堆栈偏移量,0x0000000100000000+0x7928 = 0x1000079280x0000000100000000+0x781C = 0x10000781C

使用以下命令解析对应地址在符号表中的符号,如图为解析内容,我们能看到对应的文件以及函数。

  1. dwarfdump HelloDemo_Example.app.dSYM --lookup 0x10000781C
  2. atos -o HelloDemo_Example.app.dSYM/Contents/Resources/DWARF/HelloDemo_Example -arch arm64 -l 0x0000000100000000 0x10000781C

注:这里的基地址为0x0000000100000000,本地打的release包也是0x0000000100000000,需要实际打的线上release基地址才不同。

截屏2022-01-10 下午11.35.35.png-1304.1kB

获取该地址对应的准确行数,这需要借助debug_line文件,使用以下代码得到debugline.txt,然后我们就能找到地址0x10000781C准确的行数了,如下图所示:

  1. dwarfdump --debug-line HelloDemo_Example.app.dSYM > debugline.txt

截屏2022-01-10 下午11.44.26.png-175kB

解析当前线程的堆栈符号

  1. uintptr_t baseAddress = getBaseAddress("GlobalPax");
  2. NSLog(@"GlobalPax Base Address: 0x%lu", (unsigned long)baseAddress);
  3. NSLog(@"----callStack----:%@",[NSThread callStackSymbols]);
  1. GlobalPax Base Address: 0x4333338624
  2. ----callStack----:(
  3. 0 GlobalPax 0x000000010478d870 GlobalPax + 36657264
  4. 1 GlobalPax 0x00000001036804e4 GlobalPax + 18777316
  5. 2 libdispatch.dylib 0x00000001097fcb98 _dispatch_call_block_and_release + 32
  6. 3 libdispatch.dylib 0x00000001097fe7bc _dispatch_client_callout + 20
  7. 4 libdispatch.dylib 0x000000010980130c _dispatch_queue_override_invoke + 1056
  8. 5 libdispatch.dylib 0x0000000109812ae4 _dispatch_root_queue_drain + 404
  9. 6 libdispatch.dylib 0x00000001098134d8 _dispatch_worker_thread2 + 188
  10. 7 libsystem_pthread.dylib 0x00000001ed7878f8 _pthread_wqthread + 228
  11. 8 libsystem_pthread.dylib 0x00000001ed7840cc start_wqthread + 8
  12. )
  1. // 偏移量:36657264=0x22f5870
  2. // 符号表基址:0x0000000100000000
  3. // 0x0000000100000000+0x22f5870=0x1022F5870
  4. dwarfdump GlobalPax.app.dSYM --lookup 0x1022F5870
  5. // GlobalPax模块基址:0x4333338624
  6. // 0x4333338624+0x22f5870=0x433562DE94
  7. atos -o GlobalPax.app.dSYM/Contents/Resources/DWARF/GlobalPax -arch arm64 -l 0x4333338624 0x433562DE94

问题

为何去除符号之后还可恢复线程堆栈符号

因为iOS是一门动态语言,我们可以通过类名、方法名等字符串动态的执行方法之类。因此可执行文件中一定会存有类名和方法名(不是在符号表中),所以恢复方案就是:

根据以上原理我们知道因为OC是动态语言所以可以恢复Objective-C的函数符号,但如果C++函数符号被Strip后,我们是没有办法恢复其符号的。

参考

llvm-strip
iOS 符号二三事
DWARF数据结构

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注