iOS中符号的那些事儿

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

dSYM 与 DWARF

对于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

Symbol

变量、函数都是符号。连接就是将各个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中的下标。

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。

参考资料

相关文章
相关标签/搜索