本文介绍了iOS开发中常见的符号及堆栈符号化等内容。html
对于dSYM,iOS开发应该都比较熟悉了。ios
编译器在编译过程(即把源代码转换成机器码)中,会生成一份对应的Debug符号表。Debug符号表是一个映射表,它把每个编译好的二进制中的机器指令映射到生成它们的每一行源代码中。这些Debug符号表要么被存储在编译好的二进制中,要么单独存储在Debug Symbol文件中(也就是dSYM文件):通常来讲,debug模式构建的App会把Debug符号表存储在编译好的二进制中,而release模式构建的App会把Debug符号表存储在dSYM文件中以节省二进制体积。c++
在每一次的编译中,Debug符号表和App的二进制经过构建时的UUID相互关联。每次构建时都会生成新的惟一标识UUID,不论源码是否相同。仅有UUID保持一致的dSYM文件,才能用于解析其堆栈信息。git
DWARF,即 Debug With Arbitrary Record Format ,是一个标准调试信息格式,即调试信息。单独保存下来就是dSYM文件,即 Debug Symbol File 。使用MachOView打开一个二进制文件,就能看到不少DWARF的section,如 __DWARF,__debug_str, __DWARF,__debug_info, __DWARF,__debug_names 等。github
线上的App没有dSYM,因此对于一些线上的崩溃,须要对应正确的dSYM才能进行堆栈符号化。如 Firebase 和 Bugly 平台都须要上传dSYM文件才能符号化堆栈信息。shell
/xxxxxx/Pods/Crashlytics/iOS/Crashlytics.framework/upload-symbols -a 75ef2a0601e7b1071aed828d01b73ebdda95f3b9 -p ios ./MyApp.dSYM
复制代码
其中,-a参数即指定了UUID。swift
变量、函数都是符号。连接就是将各个mach-o文件收集并连接在一块儿的过程,连接的过程就须要读取符号表。而使用Xcode进行调试的时候,也会经过符号表将符号和源文件映射起来。xcode
如二进制main中用到了二进制A中的函数a,即main经过符号在A中找到该函数的实现。二进制A维护本身的符号表。使用nm工具能够查看二进制中的符号信息。bash
struct nlist_64 存储了符号的数据结构。而符号的name不在符号表中,而在 String Table 中,由于全部的字符串都存储在那里。须要根据 n_strx 找到符号的name位于 String Table 中的下标位置,才能找到正确的符号名,即字符串。数据结构
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */ // 符号的name在String Table中的下标。
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
复制代码
注意这个n_strx字段,即为符号的名字在String Table中的下标。
符号表存储了符号信息。ld和dyld都会在link的时候读取符号表,
二进制中的全部字符串都存储在 String Table 中。
使用strings命令能够查看二进制中的能够打印出来的字符串,String Table里边的字符串固然也在其中了。
strings - find the printable strings in a object, or other binary, file
复制代码
动态符号表,Dynamic Symbol Table ,其中 仅存储了符号位于Symbol Table中的下标 ,而非符号数据结构,由于符号的结构仅存储在 Symbol Table 而已。
使用 otool 命令能够查看动态符号表中的符号位于符号表中的下标。所以动态符号也叫作 Indirect symbols。
➜ swift-hello git:(master) ✗ otool -I swift-hello.out
swift-hello.out:
Indirect symbols for (__TEXT,__stubs) 9 entries address index 0x0000000100000eec 10 0x0000000100000ef2 11 0x0000000100000ef8 15 0x0000000100000efe 16 0x0000000100000f04 17 0x0000000100000f0a 18 0x0000000100000f10 19 0x0000000100000f16 21 0x0000000100000f1c 22 Indirect symbols for (__DATA_CONST,__got) 5 entries address index 0x0000000100001000 12 0x0000000100001008 13 0x0000000100001010 14 0x0000000100001018 20 0x0000000100001020 23 Indirect symbols for (__DATA,__la_symbol_ptr) 9 entries address index 0x0000000100002000 10 0x0000000100002008 11 0x0000000100002010 15 0x0000000100002018 16 0x0000000100002020 17 0x0000000100002028 18 0x0000000100002030 19 0x0000000100002038 21 0x0000000100002040 22 复制代码
上边的otool命令输出中,有 Indirect symbols for (__DATA,__la_symbol_ptr) 9 entries 。 __la_symbol_ptr 是懒加载的符号指针,即第一次使用到的时候才加载。
section_64的结构中有个reserved字段,若该section是 __DATA,__la_symbol_ptr ,则该reserved1字段存储的就是该 __la_symbol_ptr 在Dynamic Symbol Table中的偏移量,也能够理解为下标。
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
复制代码
查找 __la_symbol_ptr 的符号流程以下:
__non_la_symbol_ptr 也是相似的原理,非懒加载。
二进制加载的时候,对于使用到的符号,先经过一系列的关系查找到 lazy symbol 和 non lazy symbol ,将函数符号定位到其函数实现,两者绑定起来的过程就是符号绑定。
这里主要参考nm的命令帮助,以及大神的博客 深刻理解Symbol。
nm命令用于显示二进制的符号表。该命令有两个版本,咱们经常使用的nm其实是 llvm-nm 。
nm显示的符号表,即每一个二进制文件的 nlist 结构中的符号表。
As of Xcode 8.0 the default nm(1) tool is llvm-nm(1). For the most part nm(1) and llvm-nm(1) have the same options; notable exceptions include -f, -s, and -L as described below. This document explains options common between the two commands as well as some historically relevant options supported by nm-classic(1). More help on options for llvm-nm(1) is provided when running it with the --help option.
nm displays the name list (symbol table of nlist structures) of each object file in the argument list. In some cases, as with an object that has had strip(1) with its -T option used on the object, that can be different than the dyld information. For that information use dyldinfo(1).
If an argument is an archive, a listing for each object file in the archive will be produced. File can be of the form libx.a(x.o), in which case only symbols from that member of the object file are listed. (The parentheses have to be quoted to get by the shell.) If no file is given, the symbols in a.out are listed.
Each symbol name is preceded by its value (blanks if undefined). Unless the -m option is specified, this value is followed by one of the following characters, representing the symbol type: U (undefined), A (absolute), T (text section symbol), D (data section symbol), B (bss section symbol), C (common symbol), - (for debugger symbol table entries; see -a below), S (symbol in a section other than those above), or I (indirect symbol). If the symbol is local (non-external), the symbol's type is instead represented by the corresponding lowercase letter. A lower case u in a dynamic shared library indicates a undefined reference to a private external in another module in the same library.
If the symbol is a Objective-C method, the symbol name is +-[Class_name(category_name) method:name:], where `+' is for class methods, `-' is for instance methods, and (category_name) is present only when the method is in a category.
复制代码
使用nm命令能够查看mach-o文件的符号信息,如:
➜ codes git:(master) ✗ nm main
0000000000000000 T _main
U _printf
复制代码
大小字母表示全局符号,小写表示本地符号。这里的U表示undefined,即未定义的外部符号。
对于Swift代码生成的二进制文件,nm执行的输出以下:
➜ swift-hello git:(master) ✗ nm swift-hello.out
0000000100002050 b _$s4main4name33_9D2E62AE399B1FA0EBB6EEB3A775C624LLSSvp
0000000100000c40 t _$s4main8sayHelloyyF
U _$sSS19stringInterpolationSSs013DefaultStringB0V_tcfC
U _$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC
U _$sSSN
0000000100000e70 t _$sSSWOh
U _$sSSs20TextOutputStreamablesWP
U _$sSSs23CustomStringConvertiblesWP
U _$ss26DefaultStringInterpolationV06appendC0yyxs06CustomB11ConvertibleRzs20TextOutputStreamableRzlF
U _$ss26DefaultStringInterpolationV13appendLiteralyySSF
U _$ss26DefaultStringInterpolationV15literalCapacity18interpolationCountABSi_SitcfC
0000000100000e90 t _$ss26DefaultStringInterpolationVWOh
U _$ss27_allocateUninitializedArrayySayxG_BptBwlF
U _$ss5print_9separator10terminatoryypd_S2StF
0000000100000eb0 t _$ss5print_9separator10terminatoryypd_S2StFfA0_
0000000100000ed0 t _$ss5print_9separator10terminatoryypd_S2StFfA1_
U _$sypN
0000000100000fa0 s ___swift_reflection_version
0000000100002048 d __dyld_private
0000000100000000 T __mh_execute_header
0000000100000bf0 T _main
U _swift_bridgeObjectRelease
U _swift_bridgeObjectRetain
U dyld_stub_binder
复制代码
能够看出,相对比较复杂,但也是符合上边讲到的命名规则的。
nm -g 能够仅查看全局符号(external symbol)。
By default, Xcode just leaves every symbol in a library visible, unless it is obviously private (like static functions or inlined ones, or in Swift ones declared internal or private). But there is a setting to reverse that: “Symbols Hidden by Default” (Clang flag -fvisibility=hidden).
项目中的符号默认都是可见的。可使用 -fvisibility=hidden 使得符号被隐藏。也可使用clang的 attribute 来单独设置符号的可见性,如:
//符号可被外部连接
__attribute__(( visibility("default") )) void foo( void );
//符号不会被放到Dynamic Symbol Table里,意味着不能够再被其余编译单元连接
__attribute__(( visibility("hidden") )) void bar( int x );
复制代码
参考自 深刻理解Symbol。
版权声明:本文为CSDN博主「黄文臣」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处连接及本声明。 原文连接:blog.csdn.net/Hello_Hwc/a…
默认的符号是 strong symbol 的,且必须有对应实现,且符号不能重名。
而 weak symbol 是一种能够不包含相应函数实现的符号,即容许符号在运行的时候不存在。strong的能够覆盖weak的符号。
使用场景:
extern void demo(void) __attribute__((weak_import));
if (demo) {
printf("Demo is not implemented");
}else{
printf("Demo is implemented");
}
复制代码
符号断点在有些调试场景下很是实用:
(lldb) breakpoint set -F "-[UIViewController viewDidAppear:]"
Breakpoint 2: where = UIKitCore`-[UIViewController viewDidAppear:], address = 0x00007fff46b03dab
复制代码
image lookup命令能够在调试时查看符号相关信息:
# 查看符号的定义
image lookup -t symbol_name
# 查看符号的位置
image lookup -s symbol_name
复制代码
符号绑定,就是将符号名与其实际地址绑定起来的操做,如将函数名与函数体的地址绑定起来。
看这段Swift代码:
# swift-hello.swift
private let name = "Chris"
func sayHello() {
print("Hello \(name)")
}
sayHello()
复制代码
使用命令 swiftc swift-hello.swift -o swift-hello.out ,生成可执行文件为 swift-hello.out ,查看其符号信息:
➜ swift-hello git:(master) ✗ xcrun dyldinfo -bind swift-hello.out
bind information:
segment section address type addend dylib symbol
__DATA_CONST __got 0x100001020 pointer 0 libSystem dyld_stub_binder
__DATA_CONST __got 0x100001000 pointer 0 libswiftCore _$sSSN
__DATA_CONST __got 0x100001008 pointer 0 libswiftCore _$sSSs20TextOutputStreamablesWP
__DATA_CONST __got 0x100001010 pointer 0 libswiftCore _$sSSs23CustomStringConvertiblesWP
__DATA_CONST __got 0x100001018 pointer 0 libswiftCore _$sypN
复制代码
-bind参数输出的符号都是已经bind好了的,即属于 __DATA_CONST __got section的。里边的 dyld_stub_binder 就是执行bind操做的工具。
而实际上,大部分的外部符号,在第一次使用的时候才会bind,这就是 __la_symbol_ptr 。使用参数 -lazy_bind 能够查看。
➜ swift-hello git:(master) ✗ xcrun dyldinfo -lazy_bind swift-hello.out
lazy binding information (from lazy_bind part of dyld info):
segment section address index dylib symbol
__DATA __la_symbol_ptr 0x100002000 0x0000 libswiftCore _$sSS19stringInterpolationSSs013DefaultStringB0V_tcfC
__DATA __la_symbol_ptr 0x100002008 0x003C libswiftCore _$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC
__DATA __la_symbol_ptr 0x100002010 0x0089 libswiftCore _$ss26DefaultStringInterpolationV06appendC0yyxs06CustomB11ConvertibleRzs20TextOutputStreamableRzlF
__DATA __la_symbol_ptr 0x100002018 0x00F2 libswiftCore _$ss26DefaultStringInterpolationV13appendLiteralyySSF
__DATA __la_symbol_ptr 0x100002020 0x012E libswiftCore _$ss26DefaultStringInterpolationV15literalCapacity18interpolationCountABSi_SitcfC
__DATA __la_symbol_ptr 0x100002028 0x0186 libswiftCore _$ss27_allocateUninitializedArrayySayxG_BptBwlF
__DATA __la_symbol_ptr 0x100002030 0x01BC libswiftCore _$ss5print_9separator10terminatoryypd_S2StF
__DATA __la_symbol_ptr 0x100002038 0x01EE libswiftCore _swift_bridgeObjectRelease
__DATA __la_symbol_ptr 0x100002040 0x020F libswiftCore _swift_bridgeObjectRetain
复制代码
能够看到,这里的符号所有都属于 __DATA __la_symbol_ptr 这个section,即 lazy bind 的。
fishhook其实就是利用了符号绑定的原理,使用符号重绑定(rebind),将指定函数符号的实现定位到本身定义的新的函数实现,以达到hook C语言函数的目的。
ld是静态连接器,将不少源文件编译生成的 .o 文件,进行连接而已。
dylib这一类动态库使用dyld进行连接。
dlopen和dlsym 是iOS系统提供的一组API,能够在运行时加载动态库和动态得获取符号,不过线上App不容许使用。
extern NSString *myDyFunc(void);
void *handle = dlopen("my.dylib", RTLD_LAZY);
NSString *(*myFunc)(void) = dlsym(RTLD_DEFAULT,"myDyFunc");
NSString *result = myFunc();
复制代码
从博客 深刻理解Symbol 中看到dyld能够用于hook。不过iOS禁用,只能用于MacOS和模拟器。
都知道C函数hook能够用fishhook来实现,但其实dyld内置了符号hook,像malloc history等Xcode分析工具的实现,就是经过dyld hook和malloc/free等函数实现的。这里经过dyld来hook NSClassFromString,注意dyld hook有个优势是被hook的函数仍然指向原始的实现,因此能够直接调用。
做者提供的示例代码以下:
#define DYLD_INTERPOSE(_replacement,_replacee) \ __attribute__((used)) static struct{\ const void* replacement;\ const void* replacee;\ } _interpose_##_replacee \ __attribute__ ((section ("__DATA,__interpose"))) = {\ (const void*)(unsigned long)&_replacement,\ (const void*)(unsigned long)&_replacee\ };
Class _Nullable hooked_NSClassFromString(NSString *aClassName){
NSLog(@"hello world");
return NSClassFromString(aClassName);
}
DYLD_INTERPOSE(hooked_NSClassFromString, NSClassFromString);
复制代码
静态库 *.a 文件不会被连接,而是直接使用 ar 。相似于 tar 命令。
假设有另一个可执行程序 F 和可执行程序 E 一样须要引用 foo 函数:E 和 F 都引用静态库 S,那么 E 和 F 编译完成后都会有对应的 foo 函数代码,foo 函数代码有两份。 E 和 F 都引用动态库 D,那么 E 和 F 编译完成后,只须要在运行时引用动态库 D 的 foo 函数代码便可执行,foo 函数代码只有动态库 D 中的一份。
关于堆栈符号化,只要注意App、UUID、dSYM对应起来便可。
符号化的过程,即在指定的二进制对应的dSYM中,根据crash中堆栈的地址信息,查找出符号信息,即调用函数便可。
dwarfdump命令能够获取dSYM文件的uuid,也能够进行简单的查询。
dwarfdump --uuid dSYM文件
dwarfdump --lookup [address] -arch arm64 dSYM文件
复制代码
使用mfind用于在Mac系统中定位dSYM文件,如:
mdfind "com_apple_xcode_dsym_uuids == E30FC309-DF7B-3C9F-8AC5-7F0F6047D65F"
复制代码
使用symbolicatecrash命令,能够将crash文件进行符号化。
首先经过命令找到symbolicatecrash,以后把symbolicatecrash单独拷贝出来便可使用(或者建立一个软链接也能够)。
find /Applications/Xcode.app -name symbolicatecrash -type f
复制代码
使用方式以下:
./symbolicatecrash my.crash myDSYM > symbolized.crash
复制代码
若出现下边错误,则将 export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer 加到bashrc中便可。
Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line 69.
复制代码
若是有出现 No symbolic information found,可能跟是否开启Bitcode有关。开启bitcode,则Xcode会生成多个dSYM文件;若关闭bitcode,则只会产生一个。具体内容能够查看博客 ios bitcode 机制对 dsym 调试文件的影响。
有些时候,某些个别符号的dSYM文件须要单独从其余地方拿到,如:
0x1001f263c _hidden#1_ + 26172 (__hidden#18_:33)
复制代码
这时候可能须要用到atos命令了。
使用atos命令,能够对单个地址进行符号化。运行shell命令 xcrun atos -o [dwarf文件地址] -arch arm64 -l [loadAddress] [instructionAddress] 。
xcrun atos -o app.dSYM/Contents/Resources/DWARF/MyApp -arch arm64 -l -l 0x1006b4000 0x0000000100d382a8
复制代码
实际上仅经过符号在对应mach-o中的 offset 便可符号化,可假设 loadAddress 为1,计算 instructionAddress = offset + loadAddress 。atos命令不接受直接传递offset地址,很奇怪。且loadAddress不能为0。
xcrun atos -o app.dSYM/Contents/Resources/DWARF/MyApp -arch arm64 -l 0x1 0xF781
复制代码
其中,0xF781即为loadAddress为0x1的状况下,经过offet计算获得的instructionAddress。