@qidiandasheng
2024-06-05T19:58:19.000000Z
字数 7716
阅读 1453
iOS理论
.debug
:调试符号,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项调用编译驱动程序时才会得到这张表。
.symtab
:符号表,它存放在程序中定义和引用的函数和全局变量信息。
格式:
Mach-O: 可执行文件,源文件编译链接的结果。包含映射调试信息(对象文件)具体存储位置的 Debug Map
。
DWARF:一种通用的调试文件格式,支持源码级别的调试,调试信息存在于对象文件 中,一般都比较大。Xcode 调试模式下一般都是使用 DWARF 来进行符号化的。
dSYM:独立的符号表文件,主要用来做发布产品的崩溃符号化。dSYM 是一个压缩包,里面包含了DWARF 文件。
可执行文件中Symbol Table
存储着全局符号和局部符号; DWARF
存储着符号的行号信息。
编译步骤:
Xcode编译实际的操作步骤是:生成带有 DWARF 调试信息的可执行文件 -> 提取可执行文件中的调试信息打包成 dSYM -> 去除符号化信息。去除符号是单独的步骤,使用的是 strip 命令。
GCC_GENERATE_DEBUGGING_SYMBOLS
,生成调试符号,当Generate Debug Symbols
选项设置为YES时,每个源文件在编译成.o文件时,编译参数多了-g
和-gmodules
两项。
对应的目标文件,会多一些调试信息section:
当Generate Debug Symbols
设置为NO的时候,在Xcode中设置的断点不会中断。但是在程序中打印[NSThread callStackSymbols]
,依然可以看到类名和方法名。
CLANG_DEBUG_INFORMATION_LEVEL
,这个配置项表示调试信息的等级,默认为Compiler default
。
另一个选项是Line tables only
,这种类型的调试信息允许获得带有函数名、文件名和行号的函数调用栈,但是不包含其他数据(比如局部变量和函数参数)。
Deployment Postprocessing
如果为 YES,在编译生成目标文件之后要进行后续的Strip处理;如果为 NO,则不会有后续处理;使用 Xcode Archive
进行编译,Deloyment Postprocessing
的值恒为YES;
Strip Linked Product
如果为 YES,则进行Strip符号;如果为 NO,则不进行Strip符号。如果Deployment Postprocessing
设置为NO,则此选项不生效。
当Strip Linked Product
为YES时我们可以看到Xcode在编译完成可执行文件后,有对可执行文件进行Strip的操作:
Deployment Postprocessing
和Strip Linked Product
都为YES才生效;去除的是可执行文件中的符号。
Symbol Table
中的信息。如下图所示去除局部符号和调试符号后,符号表里还有282个符号,类似于AFNetworkingTaskDidCompleteNotification
这样的全局符号还在:Symbol Table
中目标模块定义的全局、局部符号信息。如下图所示去除掉所有符号后,符号表里还有208个符号,剩下的就是一些C++符号之类的了:对可执行文件进行符号的strip,使用的是llvm的llvm-strip
命令,比如之前的几种Strip Style
的差别只是调用llvm-strip
的参数不一样而已。
/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
/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
/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掉了,所以你看到的调用堆栈都是二进制地址,而不是可读的函数名称。
上面我们说过没有对应的符号是因为没有符号被strip掉了,那我们只要保存好这份被strip掉的符号不就行了。在xcode的build setting里可以生成这份被strip掉的符号文件,也就是dSYM符号文件,如下图设置即可:
DeriveData
的目录下获取到dSYM文件:.achive
目录下(右键显示包内容)找到对应的dSYM
文件:这里我真机跑了一个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
。
把以上三个文件放到同一级目录:
export DEVELOPER_DIR="/Applications/XCode.app/Contents/Developer"
./symbolicatecrash HelloDemo_Example.crash HelloDemo_Example.app.dSYM > result.log
dSYM目录结构(右键打开包内容),显示如下图:
其中真正保存保存数据的是 DWARF
文件(DWARF结构), DWARF(Debuging With Arbitrary Format)
是ELF 和 Mach-O 等文件格式中用来存储和处理调试信息的标准格式。 DWARF 中的数据是高度 压 缩 的 , 可以通过dwarfdump
命令提取可读信息,比如提取关键的调试信息.debug_info
、.debug_line
。
dwarfdump --debug-info HelloDemo_Example.app.dSYM > debuginfo.txt
如下图我在类DSViewController
24行声明了NSMutableArray
变量:
然后在debuginfo.txt
,能看到响应的信息:
我们打开.carsh
文件,如下图所示红框内为崩溃的堆栈:
然后再往下就是对应线程的调用堆栈,我们往下拉找到HelloDemo_Example
的堆栈地址,红框内地址部分第一列为当前指令地址,第二列为模块基地址,第三列为运行时的偏移地址:
HelloDemo_Example
的所有行其实地址都相同,如图起始地址为0x100930000
。
因为崩溃信息里会包括基址信息,我们图中的21行基本就是main函数的起始地址,也就是基址。如果不是崩溃堆栈的话,可以手动获取基址信息。
// 获取HelloDemo_Example模块的基址
uintptr_t baseAddress = getBaseAddress("HelloDemo_Example");
// 获取指定模块的基地址
uintptr_t getBaseAddress(const char *moduleName) {
const struct mach_header *header = _dyld_get_image_header(0);
if (!header) {
return 0;
}
for (uint32_t i = 0; i < _dyld_image_count(); i++) {
if (strstr(_dyld_get_image_name(i), moduleName) != NULL) {
return (uintptr_t)_dyld_get_image_header(i);
}
}
return 0;
}
我们可以看到第一张图有两个地址是相近的,这两个地址都属于
HelloDemo_Example
,其中0x100937928
,我们下面能看到偏移量为31016 = 0x7928
。因为起始地址相同,另一个地址0x10093781c
的偏移量为0x10093781c-0x100930000 = 30748 = 0x781C
。
以上地址均为 app 发生崩溃时的运行地址,根据虚拟内存偏移地址不变的原理,只要知道符号表 TEXT 段的起始地址,加上偏移量就能得到崩溃地址对应符号表中的地址。
使用如下命令输出符号表中TEXT段起始位置为0x0000000100000000
otool -l HelloDemo_Example.app.dSYM/Contents/Resources/DWARF/HelloDemo_Example | grep __TEXT -C 5
--
nsects 0
flags 0x0
Load command 3
cmd LC_SEGMENT_64
cmdsize 1032
segname __TEXT
vmaddr 0x0000000100000000
vmsize 0x0000000000030000
fileoff 98304
filesize 116
maxprot 0x00000005
initprot 0x00000005
nsects 12
flags 0x0
Section
sectname __text
segname __TEXT
addr 0x0000000100007748
size 0x000000000001d240
offset 0
align 2^2 (4)
reloff 0
--
使用符号表基址+之前获取的崩溃堆栈偏移量,
0x0000000100000000+0x7928 = 0x100007928
,0x0000000100000000+0x781C = 0x10000781C
使用以下命令解析对应地址在符号表中的符号,如图为解析内容,我们能看到对应的文件以及函数。
dwarfdump HelloDemo_Example.app.dSYM --lookup 0x10000781C
或
atos -o HelloDemo_Example.app.dSYM/Contents/Resources/DWARF/HelloDemo_Example -arch arm64 -l 0x0000000100000000 0x10000781C
注:这里的基地址为0x0000000100000000,本地打的release包也是0x0000000100000000,需要实际打的线上release基地址才不同。
获取该地址对应的准确行数,这需要借助
debug_line
文件,使用以下代码得到debugline.txt
,然后我们就能找到地址0x10000781C
准确的行数了,如下图所示:
dwarfdump --debug-line HelloDemo_Example.app.dSYM > debugline.txt
uintptr_t baseAddress = getBaseAddress("GlobalPax");
NSLog(@"GlobalPax Base Address: 0x%lu", (unsigned long)baseAddress);
NSLog(@"----callStack----:%@",[NSThread callStackSymbols]);
GlobalPax Base Address: 0x4333338624
----callStack----:(
0 GlobalPax 0x000000010478d870 GlobalPax + 36657264
1 GlobalPax 0x00000001036804e4 GlobalPax + 18777316
2 libdispatch.dylib 0x00000001097fcb98 _dispatch_call_block_and_release + 32
3 libdispatch.dylib 0x00000001097fe7bc _dispatch_client_callout + 20
4 libdispatch.dylib 0x000000010980130c _dispatch_queue_override_invoke + 1056
5 libdispatch.dylib 0x0000000109812ae4 _dispatch_root_queue_drain + 404
6 libdispatch.dylib 0x00000001098134d8 _dispatch_worker_thread2 + 188
7 libsystem_pthread.dylib 0x00000001ed7878f8 _pthread_wqthread + 228
8 libsystem_pthread.dylib 0x00000001ed7840cc start_wqthread + 8
)
// 偏移量:36657264=0x22f5870
// 符号表基址:0x0000000100000000
// 0x0000000100000000+0x22f5870=0x1022F5870
dwarfdump GlobalPax.app.dSYM --lookup 0x1022F5870
或
// GlobalPax模块基址:0x4333338624
// 0x4333338624+0x22f5870=0x433562DE94
atos -o GlobalPax.app.dSYM/Contents/Resources/DWARF/GlobalPax -arch arm64 -l 0x4333338624 0x433562DE94
因为iOS是一门动态语言,我们可以通过类名、方法名等字符串动态的执行方法之类。因此可执行文件中一定会存有类名和方法名(不是在符号表中),所以恢复方案就是:
[NSThread callStackReturnAddresses]
获取调用栈的内存地址;根据以上原理我们知道因为OC是动态语言所以可以恢复Objective-C
的函数符号,但如果C++函数符号被Strip后,我们是没有办法恢复其符号的。