iOS中符号的那些事儿

8,341 阅读15分钟

本文介绍了iOS开发中常见的符号及堆栈符号化等内容。

dSYM 与 DWARF

对于dSYM,iOS开发应该都比较熟悉了。

编译器在编译过程(即把源代码转换成机器码)中,会生成一份对应的Debug符号表。Debug符号表是一个映射表,它把每一个编译好的二进制中的机器指令映射到生成它们的每一行源代码中。这些Debug符号表要么被存储在编译好的二进制中,要么单独存储在Debug Symbol文件中(也就是dSYM文件):一般来说,debug模式构建的App会把Debug符号表存储在编译好的二进制中,而release模式构建的App会把Debug符号表存储在dSYM文件中以节省二进制体积。

在每一次的编译中,Debug符号表和App的二进制通过构建时的UUID相互关联。每次构建时都会生成新的唯一标识UUID,不论源码是否相同。仅有UUID保持一致的dSYM文件,才能用于解析其堆栈信息。

DWARF,即 Debug With Arbitrary Record Format ,是一个标准调试信息格式,即调试信息。单独保存下来就是dSYM文件,即 Debug Symbol File 。使用MachOView打开一个二进制文件,就能看到很多DWARF的section,如 __DWARF,__debug_str, __DWARF,__debug_info, __DWARF,__debug_names 等。

线上的App没有dSYM,所以对于一些线上的崩溃,需要对应正确的dSYM才能进行堆栈符号化。如 Firebase 和 Bugly 平台都需要上传dSYM文件才能符号化堆栈信息。

/xxxxxx/Pods/Crashlytics/iOS/Crashlytics.framework/upload-symbols -a 75ef2a0601e7b1071aed828d01b73ebdda95f3b9 -p ios ./MyApp.dSYM

其中,-a参数即指定了UUID。

Symbol

变量、函数都是符号。链接就是将各个mach-o文件收集并链接在一起的过程,链接的过程就需要读取符号表。而使用Xcode进行调试的时候,也会通过符号表将符号和源文件映射起来。

如二进制main中用到了二进制A中的函数a,即main通过符号在A中找到该函数的实现。二进制A维护自己的符号表。使用nm工具可以查看二进制中的符号信息。

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中的下标。

Symbol Table

符号表存储了符号信息。ld和dyld都会在link的时候读取符号表,

String Table

二进制中的所有字符串都存储在 String Table 中。

使用strings命令可以查看二进制中的可以打印出来的字符串,String Table里边的字符串当然也在其中了。

strings - find the printable strings in a object, or other binary, file

Dynamic Symbol Table

动态符号表,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 

__la_symbol_ptr

上边的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 的符号流程如下:

  1. 如果LC是 __DATA,__la_symbol_ptr ,则读取其中的 reserved1 字段,即得到了该 __la_symbol_ptrDynamic Symbol Table 中的起始地址或者下标。
  2. __la_symbol_ptr 进行遍历,就得到其中每个symbol对应于 Dynamic Symbol Table 中的下标。即当前遍历下标 idx + reserverd1
  3. 通过 Dynamic Symbol Table ,找到符号对应于 Symbol Table 中的下标。
  4. 通过 Symbol Table ,找到符号名对应于 String Table 中的下标(即 nlist_64 中的 n_strx 字段),即得到符号名了。
  5. 最终,都是需要到 String Table 中,通过符号对应的下标,才能查找到符号名的。

__non_la_symbol_ptr

__non_la_symbol_ptr 也是类似的原理,非懒加载。

二进制加载的时候,对于使用到的符号,先通过一系列的关系查找到 lazy symbolnon lazy symbol ,将函数符号定位到其函数实现,二者绑定起来的过程就是符号绑定。

符号命名规则

这里主要参考nm的命令帮助,以及大神的博客 深入理解Symbol

  1. C语言的符号,直接在函数名前加下划线即可。如 myFunc 函数的符号为 _myFunc
  2. C++支持命名空间、函数重载等,为了避免冲突,所以对符号做了Symbol Mangling操作。如 __ZN11MyNameSpace7MyClass6myFuncEd 中,***_ZN*** 是开头部分,后边紧接着 命名空间的长度及命名空间,类名的长度及类名,函数名的长度及函数名 ,以 E 结尾,最后则是参数类型,如i为int,d为double。
  3. Objective-C的符号类似于:***_OBJC_CLASS_$_MyViewController*** ,***_OBJC_CLASS_$_MyObject*** 等。
  4. Swift的符号名,有点类似于C++的规则。如函数sayHello对应的符号名为 _s4main8sayHelloyyF*** 。以 ***_s 或者 _$ss 开头,紧接着是 4main 表示二进制名称?待查证。再接着的就是 8sayHello 即函数名的长度及函数名。最后的 yyF 不清楚。。。???

nm命令

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 );

符号的weak和strong

参考自 深入理解Symbol

版权声明:本文为CSDN博主「黄文臣」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/Hello_Hwc/a…

默认的符号是 strong symbol 的,且必须有对应实现,且符号不能重名。

weak symbol 是一种可以不包含相应函数实现的符号,即允许符号在运行的时候不存在。strong的可以覆盖weak的符号。

使用场景:

  1. 依赖注入:用weak symbol提供默认实现,外部可以提供strong symbol把实现注入进来,可以用来做依赖注入。
  2. weak linking:用来实现版本兼容。比如一个动态库的某些特性只有iOS 10以上支持,那么这个符号在iOS 9上访问的时候就是NULL的,这种情况就可以用就可以用weak linking
extern void demo(void) __attribute__((weak_import));
if (demo) {
    printf("Demo is not implemented");
}else{
    printf("Demo is implemented");
}

Xcode中的符号断点

符号断点在有些调试场景下非常实用:

(lldb) breakpoint set -F "-[UIViewController viewDidAppear:]"
Breakpoint 2: where = UIKitCore`-[UIViewController viewDidAppear:], address = 0x00007fff46b03dab

LLDB查看符号

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 的。

  1. __la_symbol_ptr 中的指针,会指向 __stub_helper
  2. 第一次调用该函数的时候,使用 dyld_stub_binder 把指针绑定到函数的实现。
  3. 而汇编代码调用函数的时候,直接调用 __DATA, __la_symbol_ptr 指针指向的地址。

fishhook其实就是利用了符号绑定的原理,使用符号重绑定(rebind),将指定函数符号的实现定位到自己定义的新的函数实现,以达到hook C语言函数的目的。

链接

静态链接器ld

ld是静态链接器,将很多源文件编译生成的 .o 文件,进行链接而已。

动态加载器dyld

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();

使用dyld来进行hook

从博客 深入理解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 命令。

  1. ld链接静态库(.a文件)的时候,只有该文件中的符号被引用到的时候,该符号才会写入到最终的二进制文件中,否则会被丢弃。
  2. 使用静态库的时候,二进制直接将静态库中相应的符号相关的代码数据拷贝到二进制中,这也使得二进制体积增大。且静态库更新时需要重新编译二进制。而二进制是可以单独运行。
  3. 使用动态库的时候,则二进制在编译时仅确定动态库中有其使用到的符号实现即可,而不会拷贝任何动态库中的符号相关代码数据。二进制运行的时候,还需要动态库,即运行时调用到某个函数时,还需要去动态库中查找函数相应的实现。动态库更新时,不需要重新编译二进制。

假设有另外一个可执行程序 F 和可执行程序 E 同样需要引用 foo 函数:E 和 F 都引用静态库 S,那么 E 和 F 编译完成后都会有对应的 foo 函数代码,foo 函数代码有两份。 E 和 F 都引用动态库 D,那么 E 和 F 编译完成后,只需要在运行时引用动态库 D 的 foo 函数代码即可执行,foo 函数代码只有动态库 D 中的一份。

参考:About macOS & iOS symbol

符号化工具及命令

关于堆栈符号化,只要注意App、UUID、dSYM对应起来即可。

  1. uuid是二进制的唯一标识,通过它找到对应的dSYM和DWARF文件
  2. dSYM包含了符号信息,其中就有DWARF。
  3. crash记录着原始的调用堆栈信息。

符号化的过程,即在指定的二进制对应的dSYM中,根据crash中堆栈的地址信息,查找出符号信息,即调用函数即可。

dwarfdump

dwarfdump命令可以获取dSYM文件的uuid,也可以进行简单的查询。

dwarfdump --uuid dSYM文件
dwarfdump --lookup [address] -arch arm64 dSYM文件

mfind

使用mfind用于在Mac系统中定位dSYM文件,如:

mdfind "com_apple_xcode_dsym_uuids == E30FC309-DF7B-3C9F-8AC5-7F0F6047D65F"

symbolicatecrash

使用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

使用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。

参考资料