iOS高级强化--016:重定位符号表 & dSYM & dyld

重定位符号表

案例1:

查看可执行文件的代码段

创建test.m文件,写入以下代码:

int main(){
   return 0;
}

使用clang命令,生成可执行文件

clang test.m -o test

使用objdump --macho -d test命令,查看可执行文件的代码段

test:
(__TEXT,__text) section
_main:
100003fa0: 55  pushq   %rbp
100003fa1: 48 89 e5    movq    %rsp, %rbp
100003fa4: 31 c0   xorl    %eax, %eax
100003fa6: c7 45 fc 00 00 00 00    movl    $0, -4(%rbp)
100003fad: 5d  popq    %rbp
100003fae: c3  retq
  • 代码段由机器码和汇编指令构成,系统执行的是机器码,而汇编指令提供给开发者阅读
  • 第一列是链接器分配的虚拟内存地址,第二列是机器码指令
  • 执行Mach-O文件时,就是按照虚拟内存地址的排列顺序,依次执行机器码指令

案例2:

查看目标文件的代码段

打开test.m文件,写入以下代码:

void test(){
   
}

void test_1(){
   
}

int global = 10;

int main(){
   global = 20;
   global = 21;
   test();
   test_1();
   return 0;
}

使用clang命令,生成目标文件

clang -c test.m -o test.o

使用objdump --macho -d test.o命令,查看目标文件的代码段

test.o:
(__TEXT,__text) section
_test:
      0:  55  pushq   %rbp
      1:  48 89 e5    movq    %rsp, %rbp
      4:  5d  popq    %rbp
      5:  c3  retq
      6:  66 2e 0f 1f 84 00 00 00 00 00   nopw    %cs:_test(%rax,%rax)
_test_1:
     10:  55  pushq   %rbp
     11:  48 89 e5    movq    %rsp, %rbp
     14:  5d  popq    %rbp
     15:  c3  retq
     16:  66 2e 0f 1f 84 00 00 00 00 00   nopw    %cs:_test(%rax,%rax)
_main:
     20:  55  pushq   %rbp
     21:  48 89 e5    movq    %rsp, %rbp
     24:  48 83 ec 10 subq    $16, %rsp
     28:  c7 45 fc 00 00 00 00    movl    $_test, -4(%rbp)
     2f:  c7 05 fc ff ff ff 14 00 00 00   movl    $20, _global-4(%rip)
     39:  c7 05 fc ff ff ff 15 00 00 00   movl    $21, _global-4(%rip)
     43:  e8 00 00 00 00  callq   _test
     48:  e8 00 00 00 00  callq   _test_1
     4d:  31 c0   xorl    %eax, %eax
     4f:  48 83 c4 10 addq    $16, %rsp
     53:  5d  popq    %rbp
     54:  c3  retq
  • 代码段中的机器码和汇编代码的生成顺序,和源码中的顺序一致
  • 第一列虚拟内存地址变成了偏移量
  • main函数中调用test函数,前面e8为固定机器码,表示汇编callq指令
  • callq近址相对位移调用指令,即:偏移地址 + 下一条指令地址 = 调用函数的所在地址
  • 由于在链接时才会分配真实的虚拟内存地址,所以编译目标文件时,调用testtest_1函数,偏移地址都使用00 00 00 00对进行占位
  • 如果想在链接时,告诉链接器此指令需要替换真实的虚拟内存地址,必须将其存储到重定位符号表中

使用objdump --macho --reloc test.o命令,查看目标文件的重定位符号表

test.o:
Relocation information (__TEXT,__text) 4 entries
address  pcrel length extern type    scattered symbolnum/value
00000049 True  long   True   BRANCH  False     _test_1
00000044 True  long   True   BRANCH  False     _test
0000003b True  long   True   SIGNED4 False     _global
00000031 True  long   True   SIGNED4 False     _global
Relocation information (__LD,__compact_unwind) 3 entries
address  pcrel length extern type    scattered symbolnum/value
00000040 False quad   False  UNSIGND False     1 (__TEXT,__text)
00000020 False quad   False  UNSIGND False     1 (__TEXT,__text)
00000000 False quad   False  UNSIGND False     1 (__TEXT,__text)
  • 以前两条指令为例,它们的address0000004900000044。在目标文件的代码段中,分别对应48: e843: e8后面的00 00 00 00地址,表示这两个地址在链接时需要被替换
  • 下面两条指令,对全局变量global赋值的占位,address0000003b00000031。在目标文件的代码段中,分别对应39: c7 052f: c7 05后面的fc ff ff ff地址。再后面的14 00 00 0015 00 00 00,分别对应16进制2021

案例3:

查看可执行文件分配的虚拟内存地址

使用clang命令,生成可执行文件

clang test.m -o test

使用objdump --macho -d test命令,查看可执行文件分配的虚拟内存地址

test:
(__TEXT,__text) section
_test:
100003f60: 55  pushq   %rbp
100003f61: 48 89 e5    movq    %rsp, %rbp
100003f64: 5d  popq    %rbp
100003f65: c3  retq
100003f66: 66 2e 0f 1f 84 00 00 00 00 00   nopw    %cs:(%rax,%rax)
_test_1:
100003f70: 55  pushq   %rbp
100003f71: 48 89 e5    movq    %rsp, %rbp
100003f74: 5d  popq    %rbp
100003f75: c3  retq
100003f76: 66 2e 0f 1f 84 00 00 00 00 00   nopw    %cs:(%rax,%rax)
_main:
100003f80: 55  pushq   %rbp
100003f81: 48 89 e5    movq    %rsp, %rbp
100003f84: 48 83 ec 10 subq    $16, %rsp
100003f88: c7 45 fc 00 00 00 00    movl    $0, -4(%rbp)
100003f8f: c7 05 67 40 00 00 14 00 00 00   movl    $20, 16487(%rip)
100003f99: c7 05 5d 40 00 00 15 00 00 00   movl    $21, 16477(%rip)
100003fa3: e8 b8 ff ff ff  callq   _test
100003fa8: e8 c3 ff ff ff  callq   _test_1
100003fad: 31 c0   xorl    %eax, %eax
100003faf: 48 83 c4 10 addq    $16, %rsp
100003fb3: 5d  popq    %rbp
100003fb4: c3  retq
  • 此时分配了虚拟内存地址,但依然是偏移地址
  • 调用test函数,e8是机器码指令,b8 ff ff ff偏移地址,加上下一条指令地址,就能得到test函数的所在地址
  • macOS是小端模式,高位在右,低位在左。从右往左读取偏移地址0xffffffb8
  • 偏移地址0xffffffb8为补码,需要转为2进制,先取反再+1,将其转为原码

使用lldb命令,进入lldb终端。使用e -f b -- 0xffffffb8命令,将其转为2进制

(unsigned int) $0 = 0b11111111111111111111111110111000
  • -f参数表示formatb2进制x16进制d10进制o8进制
  • 2进制取反后地址为1000111

使用e -f b -- 0b1000111 + 0b1命令,将取反后2进制进行+1,将其还原为原码

(int) $1 = 0b00000000000000000000000001001000

使用e -f x -- 0b1001000命令,将其转为16进制

(int) $2 = 0x00000048
  • 有符号数,16进制0xffffffb8f开头为负数。所以转为原码应该为-0x48

计算函数地址:偏移地址 + 下一条指令地址 = 调用函数的所在地址,即:-0x48 + 0x100003fa8 = 0x100003F60,对应的正是test函数所在地址

案例4:

查看全局变量global的所在地址

使用objdump --macho -s test命令,查看全局变量global的所在地址

Contents of section __text:
100003f60 554889e5 5dc3662e 0f1f8400 00000000  UH..].f.........
100003f70 554889e5 5dc3662e 0f1f8400 00000000  UH..].f.........
100003f80 554889e5 4883ec10 c745fc00 000000c7  UH..H....E......
100003f90 05674000 00140000 00c7055d 40000015  .g@........]@...
100003fa0 000000e8 b8ffffff e8c3ffff ff31c048  .............1.H
100003fb0 83c4105d c3                          ...].
Contents of section __unwind_info:
100003fb8 01000000 1c000000 00000000 1c000000  ................
100003fc8 00000000 1c000000 02000000 603f0000  ............`?..
100003fd8 34000000 34000000 b63f0000 00000000  4...4....?......
100003fe8 34000000 03000000 0c000100 10000100  4...............
100003ff8 00000000 00000001                    ........
Contents of section __objc_imageinfo:
100004000 00000000 40000000                    ....@...
Contents of section __data:
100008000 0a000000
  • 最后的0x100008000,就是全局变量global的所在地址,0xa是初始化赋值的10

main函数中,对global两次赋值,偏移地址分别为0x40670x405d

  • 将偏移地址分别加上它们的下一条指令地址
  • 0x100003f99 + 0x4067 = 0x100008000
  • 0x100003fa3 + 0x405d = 0x100008000
dSYM

dSYM⽂件:按照DWARF格式保存调试信息的⽂件

DWARF格式:是⼀种被众多编译器和调试器使⽤的,⽤于⽀持源代码级别
调试的⽂件格式

如何将调试信息生成dSYM⽂件

  • 读取debug map
  • .o⽂件中加载__DWARF
  • 重新定位所有地址
  • 最后将全部的DWARF打包成dSYM Bundle

案例1:

生成带调式信息的目标文件

使用clang命令,-g参数,生成保留调式信息的目标文件

clang -g -c test.m -o test.o

使用objdump --macho -private-headers test.o命令,查看Mach header信息

  • 使用-g参数,在Mach-O文件中生成__DWARF段,它保存的就是调试信息

案例2:

查看可执行文件的调式信息

编译时,编译器会将调试信息放到Mach-O文件的__DWARF段中。链接时,链接器会将调试信息统一放到符号表中,然后脱去Mach-O中的__DWARF

使用clang命令,-g参数,生成保留调式信息的可执行文件

clang -g test.m -o test

查看可执行文件的Mach header信息,里面并不存在__DWARF

使用nm -pa test命令,查看符号表信息

0000000000000000 - 00 0000    SO /Users/zang/Zang/Spark/LG/10/1/
0000000000000000 - 00 0000    SO test.m
00000000606d54d5 - 03 0001   OSO /var/folders/jl/d06jlfkj2ws74_5g45kms07m0000gn/T/test-3abaee.o
0000000100003f60 - 01 0000 BNSYM
0000000100003f60 - 01 0000   FUN _test
0000000000000010 - 00 0000   FUN
0000000000000010 - 01 0000 ENSYM
0000000100003f70 - 01 0000 BNSYM
0000000100003f70 - 01 0000   FUN _test_1
0000000000000010 - 00 0000   FUN
0000000000000010 - 01 0000 ENSYM
0000000100003f80 - 01 0000 BNSYM
0000000100003f80 - 01 0000   FUN _main
0000000000000035 - 00 0000   FUN
0000000000000035 - 01 0000 ENSYM
0000000000000000 - 00 0000  GSYM _global
0000000000000000 - 01 0000    SO
0000000100000000 T __mh_execute_header
0000000100008000 D _global
0000000100003f80 T _main
0000000100003f60 T _test
0000000100003f70 T _test_1
                U dyld_stub_binder
  • __mh_execute_header以上,全部都是调试符号

案例3:

生成dSYM⽂件

使用clang命令,-g1参数,生成可执行文件时,将调试信息生成dSYM⽂件

clang -g1 test.m -o test

使用dwarfdump test.dSYM命令,取出并验证DWARF格式调试信息

test.dSYM/Contents/Resources/DWARF/test:   file format Mach-O 64-bit x86-64

.debug_info contents:
0x00000000: Compile Unit: length = 0x00000063 version = 0x0004 abbr_offset = 0x0000 addr_size = 0x08 (next unit at 0x00000067)

0x0000000b: DW_TAG_compile_unit
             DW_AT_producer   ("Apple clang version 12.0.0 (clang-1200.0.32.28)")
             DW_AT_language   (DW_LANG_ObjC)
             DW_AT_name   ("test.m")
             DW_AT_LLVM_sysroot   ("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk")
             DW_AT_APPLE_sdk  ("MacOSX.sdk")
             DW_AT_stmt_list  (0x00000000)
             DW_AT_comp_dir   ("/Users/zang/Zang/Spark/LG/10/1")
             DW_AT_APPLE_major_runtime_vers   (0x02)
             DW_AT_low_pc (0x0000000100003f60)
             DW_AT_high_pc    (0x0000000100003fb5)

0x00000033:   DW_TAG_subprogram
               DW_AT_low_pc   (0x0000000100003f60)
               DW_AT_high_pc  (0x0000000100003f66)
               DW_AT_name ("test")

0x00000044:   DW_TAG_subprogram
               DW_AT_low_pc   (0x0000000100003f70)
               DW_AT_high_pc  (0x0000000100003f76)
               DW_AT_name ("test_1")

0x00000055:   DW_TAG_subprogram
               DW_AT_low_pc   (0x0000000100003f80)
               DW_AT_high_pc  (0x0000000100003fb5)
               DW_AT_name ("main")

0x00000066:   NULL
  • 包含符号所在文件,文件所在目录,符号名称等信息

案例4:

使用dsymutil命令,将可执行文件的调试符号生成dSYM文件

dsymutil -f test -o test.dSYM
  • -fDWARF格式文件
  • -o:输出.dSYM文件
计算虚拟内存

案例1:

搭建TestInject测试项目

打开ViewController.m文件,写入以下代码:

@implementation ViewController

- (void)viewDidLoad {
   [super viewDidLoad];
   
   dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
       [self test_dwarf];
   });
}

- (void)test_dwarf {
   NSArray *array = @[];
   array[1];
}

@end
  • viewDidLoad方法中,延迟2秒调用test_dwarf函数
  • test_dwarf函数中,定义array空数组,强行访问下标1的元素
  • 运行项目,2秒后因数组越界异常退出

打开控制台,选择崩溃报告,找到TestInject进程名称

  • 显示完整的crash信息,包含设备信息,函数调用栈,程序使用的镜像信息等
  • 控制台中显示完整的crash信息,因为TestInject项目中包含了调试符号

TestInject项目脱去全部符号

  • 再次执行程序,2秒后异常退出

打开控制台,找到TestInject进程名称

  • 函数调用栈中,之前包含调试符号时,显示的方法名称,脱去符号后,变成了内存地址

在运⾏时调试的地址,实际上是虚拟地址 + ASLR = 调试地址。而dSYM⽂件中,保存的是没有偏移的,真实的虚拟地址

如果想把内存地址还原成符号,需要借助dSYM⽂件,并且需要将crash中的调试地址恢复为真实的虚拟地址

案例2:

将调试地址恢复为真实的虚拟地址

使用objdump --macho -private-headers TestInject命令,查看Mach header信息

Load command 0
     cmd LC_SEGMENT_64
 cmdsize 72
 segname __PAGEZERO
  vmaddr 0x0000000000000000
  vmsize 0x0000000100000000
 fileoff 0
filesize 0
 maxprot ---
initprot ---
  nsects 0
   flags (none)
  • vmsize记录的0x0000000100000000,就是Mach-O中的起首地址
  • 所以运行时的偏移地址,也要以0x100000000地址为基础进行偏移

在控制台的crash中,找到镜像信息

  • 第一条记录,就是当前Mach-O的镜像信息,但起首地址不是0x100000000,变成了0x1033d3000
  • 使用偏移后地址 - 起首地址 = ASLR,即:0x1033d3000 - 0x100000000 = 0x33D3000

crash中,提取一条错误信息

3   TestInject                          0x00000001033d4e80 TestInject + 7808
  • 使用调试地址 - ASLR = 虚拟地址,即:0x1033d4e80 - 0x33D3000 = 0x100001E80
查看调试信息

案例1:

使用真实的虚拟地址,查看调试信息

TestInject项目中,设置Debug模式生成dSYM文件

Run Script中,写入以下代码:

rm -rf -- "${SRCROOT}/../dSYM"
mkdir -p -- "${SRCROOT}/../dSYM"
cp -Rv -- ${BUILT_PRODUCTS_DIR}/*.dSYM "${SRCROOT}/../dSYM"
  • 删除dSYM目录
  • 创建dSYM目录
  • 将编译后目录中的.dSYM文件,拷贝到dSYM目录下

编译项目,在TestInject目录平级,创建dSYM目录,里面拷贝了编译链接后生成的.dSYM文件

使用dwarfdump --lookup 0x100001E80 TestInject.app.dSYM命令,查看调试信息

TestInject.app.dSYM/Contents/Resources/DWARF/TestInject:   file format Mach-O 64-bit x86-64
0x000489fe: Compile Unit: length = 0x0000024b version = 0x0004 abbr_offset = 0x0000 addr_size = 0x08 (next unit at 0x00048c4d)

0x00048a09: DW_TAG_compile_unit
             DW_AT_producer   ("Apple clang version 12.0.0 (clang-1200.0.32.28)")
             DW_AT_language   (DW_LANG_ObjC)
             DW_AT_name   ("/Users/zang/Zang/Spark/LG/10/2/TestInject/TestInject/ViewController.m")
             DW_AT_LLVM_sysroot   ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk")
             DW_AT_APPLE_sdk  ("iPhoneSimulator14.3.sdk")
             DW_AT_stmt_list  (0x0000ae85)
             DW_AT_comp_dir   ("/Users/zang/Zang/Spark/LG/10/2/TestInject")
             DW_AT_APPLE_major_runtime_vers   (0x02)
             DW_AT_low_pc (0x0000000100001cb0)
             DW_AT_high_pc    (0x0000000100001ea7)

0x00048b2d:   DW_TAG_subprogram
               DW_AT_low_pc   (0x0000000100001e40)
               DW_AT_high_pc  (0x0000000100001ea7)
               DW_AT_frame_base   (DW_OP_reg6 RBP)
               DW_AT_object_pointer   (0x00048b47)
               DW_AT_name ("-[ViewController test_dwarf]")
               DW_AT_decl_file    ("/Users/zang/Zang/Spark/LG/10/2/TestInject/TestInject/ViewController.m")
               DW_AT_decl_line    (26)
               DW_AT_prototyped   (true)
Line info: file 'ViewController.m', line 28, column 5, start line 26
  • --lookup:查看地址的调试信息。将显示出所在的目录、文件、函数等信息
  • 0x00000001033d4e80 TestInject恢复调试信息。包含函数名称、所属文件、所在目录、行号

案例2:

通过代码获取ASLR和函数真实的虚拟内存地址

打开ViewController.m文件,写入以下代码:

uintptr_t get_slide_address(void) {
   uintptr_t vmaddr_slide = 0;

   for (uint32_t i = 0; i < _dyld_image_count(); i++) {

       const char *image_name = (char *)_dyld_get_image_name(i);
       const struct mach_header *header = _dyld_get_image_header(i);
       if (header->filetype == MH_EXECUTE) {
           vmaddr_slide = _dyld_get_image_vmaddr_slide(i);
       }
       NSString *str = [NSString stringWithUTF8String:image_name];
      
       if ([str containsString:@"TestInject"]) {
           NSLog(@"Image name %s at address 0x%llx and ASLR slide 0x%lx.\n", image_name, (mach_vm_address_t)header, vmaddr_slide);
           break;
       }
   }

   return (uintptr_t)vmaddr_slide;
}
  • 获取镜像总数
  • 遍历镜像名称
  • 遍历mach header
  • 如果文件类型是可执行文件,通过系统提供的_dyld_get_image_vmaddr_slide方法获取ASLR
  • 如果当前镜像名称是TestInject,停止遍历,返回ASLR
- (void)getMethodVMA {
   IMP imp = (IMP)class_getMethodImplementation(self.class, @selector(test_dwarf));
   unsigned long imppos = (unsigned long)imp;
   unsigned long slide =  get_slide_address();
   unsigned long addr = imppos - slide;
   NSLog(@"真实的虚拟内存地址:0x%lx\n", addr);
}
  • 使用运行时方法,拿到函数IMP
  • 转为运行时地址
  • 获取ASLR
  • 计算:运行时地址 - ASLR = 真实虚拟内存地址
- (void)viewDidLoad {
   [super viewDidLoad];
   [self getMethodVMA];
}

- (void)test_dwarf {
   NSArray *array = @[];
   array[1];
}
  • viewDidLoad方法中,调用getMethodVMA方法

运行项目,打印ASLRtest_dwarf函数的真实虚拟内存地址

使用dwarfdump --lookup 0x100001d10 TestInject.app.dSYM命令,查看调试信息

TestInject.app.dSYM/Contents/Resources/DWARF/TestInject:   file format Mach-O 64-bit x86-64
0x0004a51c: Compile Unit: length = 0x0000021c version = 0x0004 abbr_offset = 0x0000 addr_size = 0x08 (next unit at 0x0004a73c)

0x0004a527: DW_TAG_compile_unit
             DW_AT_producer   ("Apple clang version 12.0.0 (clang-1200.0.32.28)")
             DW_AT_language   (DW_LANG_ObjC)
             DW_AT_name   ("/Users/zang/Zang/Spark/LG/10/2/TestInject/TestInject/ViewController.m")
             DW_AT_LLVM_sysroot   ("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk")
             DW_AT_APPLE_sdk  ("iPhoneSimulator14.3.sdk")
             DW_AT_stmt_list  (0x0000b267)
             DW_AT_comp_dir   ("/Users/zang/Zang/Spark/LG/10/2/TestInject")
             DW_AT_APPLE_major_runtime_vers   (0x02)
             DW_AT_low_pc (0x0000000100001b90)
             DW_AT_high_pc    (0x0000000100001de7)

0x0004a642:   DW_TAG_subprogram
               DW_AT_low_pc   (0x0000000100001d10)
               DW_AT_high_pc  (0x0000000100001d77)
               DW_AT_frame_base   (DW_OP_reg6 RBP)
               DW_AT_object_pointer   (0x0004a65c)
               DW_AT_name ("-[ViewController test_dwarf]")
               DW_AT_decl_file    ("/Users/zang/Zang/Spark/LG/10/2/TestInject/TestInject/ViewController.m")
               DW_AT_decl_line    (45)
               DW_AT_prototyped   (true)
Line info: file 'ViewController.m', line 45, column 0, start line 45
动态库源码调试

案例1:

搭建TestFramework项目

TestFramework是一个动态库项目,Debug模式不脱符号

打开TestExample.m文件,写入以下代码:

@implementation TestExample

- (void)lg_test:(id)e {
   NSLog(@"lg_test--");
}

@end

搭建TestLibrary项目

TestLibrary是一个App项目,项目中使用Pods导入TestFramework动态库

打开ViewController.m文件,写入以下代码:

@implementation ViewController

- (void)viewDidLoad {
   [super viewDidLoad];
   TestExample *example = [TestExample new];
   [example lg_test:nil];
}

@end

运行项目,在viewDidLoad方法中设置断点

  • 停留在viewDidLoad方法中

lldb中,使用br set -r lg_test(.*)命令,对动态库中的lg_test方法,设置断点

Breakpoint 2: where = TestFramework`-[TestExample lg_test:] + 60 at TestExample.m:20:5, address = 0x0000000104ce7ee4
  • TestExample.m中,成功设置1条断点

跳过viewDidLoad方法中的断点,成功来到TestFramework动态库的TestExample.m文件中,断点定位在lg_test方法处

作为动态库的开发者,即使不提供源码,也可以让调用者对其进行源码调试,只要保留动态库的调试符号即可

案例2:

禁止调用者对动态库源码调试

打开TestFramework项目,设置Debug模式剥离调试符号

打开TestLibrary项目,运行项目

  • 停留在viewDidLoad方法中

lldb中,使用br set -r lg_test(.*)命令,对动态库中的lg_test方法,设置断点

Breakpoint 2: where = TestFramework`-[TestExample lg_test:], address = 0x00000001027c7ea8
  • 只能设置断点,已经找不到源码文件了

跳过viewDidLoad方法中的断点,停留在lg_test方法的汇编代码

当动态库剥离调试符号,调用者无法对其进行源码调试

dyld

如何调试dyld

  • 方式一:编译出带调试信息的dyld。如果想调试dyld源代码,需要准备带调试信息的dyldlibdyld.dyliblibclosured.dylib,与系统做替换,⻛险较⼤,不建议使用
  • 方式二:通过lldb调试dylib,推荐使用

通过lldb调试dylib,有两种方式设置断点:

  • br set -n dyldbootstrap::start -s dyld
    通过-s参数,在指定二进制文件上设置断点
  • set set target.breakpoints-use-platform-avoid-list 0
    lldb保留了⼀个库列表,类似于白名单功能。列表中出现的库文件,无法直接按名称设置断点,⽽dyldlibdyld.dylib就在该列表上。可以通过上面的命令,暂时关闭白名单功能

案例1:

通过br命令,指定在dyld上设置断点

使用lldb -file test命令,将test可执行文件,包装成一个target

(lldb) target create "test"
Current executable set to '/Users/zang/Zang/Spark/LG/10/1/test' (x86_64).

使用br set -n dyldbootstrap::start -s dyld命令,指定在dyld上设置dyldbootstrap::start断点

Breakpoint 1: where = dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*), address = 0x0000000000001062
  • 断点设置成功

案例2:

关闭白名单,按名称设置断点

使用b dyldbootstrap::start命令,按名称设置断点

Breakpoint 2: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.
  • 因白名单功能,此时无法设置

关闭白名单

set set target.breakpoints-use-platform-avoid-list 0

使用b dyldbootstrap::start命令,再次设置断点

Breakpoint 3: where = dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*), address = 0x0000000000001062
  • 断点设置成功
  • 关闭白名单,仅对本次lldb生效

通过lldb调试dylib,⽆需查看代码、⼆进制⽂件,⽽是通过dyld提供的环境变量来控制dyld在运⾏过程中输出有⽤信息

dyld提供的环境变量

  • DYLD_PRINT_APIS:打印dyld内部⼏乎所有发⽣的调⽤
  • DYLD_PRINT_LIBRARIES:打印在应⽤程序启动期间正在加载的所有动态库
  • DYLD_PRINT_WARNINGS:打印dyld运⾏过程中的辅助信息
  • DYLD_*_PATH:显示dyld搜索动态库的⽬录顺序
  • DYLD_PRINT_ENV:显示dyld初始化的环境变量
  • DYLD_PRINT_SEGMENTS:打印当前程序的segment信息
  • DYLD_PRINT_STATISTICS:打印pre-main time
  • DYLD_PRINT_INITIALIZERS:显示都有initialiser

案例3:

使用DYLD_PRINT_APIS=1 ./test命令,打印dyld内部⼏乎所有发⽣的调⽤

_dyld_register_func_for_add_image(0x7fff72ea16e8)
_dyld_register_for_bulk_image_loads(0x7fff730f4da1)
_dyld_is_memory_immutable(0x7fff730e9cab, 36)

案例4:

使用DYLD_PRINT_LIBRARIES=1 ./test命令,打印在应⽤程序启动期间正在加载的所有动态库

dyld: loaded: <32757925-9BB0-3EFB-B539-0F42610DE648> /Users/zang/Zang/Spark/LG/10/1/./test
dyld: loaded:  /usr/lib/libSystem.B.dylib
dyld: loaded:  /usr/lib/system/libcache.dylib
dyld: loaded:  /usr/lib/system/libcommonCrypto.dylib
dyld: loaded: <49B8F644-5705-3F16-BBE0-6FFF9B17C36E> /usr/lib/system/libcompiler_rt.dylib
dyld: loaded: <3C481225-21E7-370A-A30E-0CCFDD64A92C> /usr/lib/system/libcopyfile.dylib
dyld: loaded: <60567BF8-80FA-359A-B2F3-A3BAEFB288FD> /usr/lib/system/libcorecrypto.dylib
dyld: loaded:  /usr/lib/system/libdispatch.dylib
...

案例5:

使用DYLD_PRINT_SEGMENTS=1 ./test命令,打印当前程序的segment信息

re-using existing shared cache (/private/var/db/dyld/dyld_shared_cache_x86_64h):
       0x7FFF2BCBA000->0x7FFF7F1F9FFF init=5, max=5 read execute
       0x7FFF8BCBA000->0x7FFF99D89FFF init=3, max=3 read write
       0x7FFFCBCBA000->0x7FFFE6D81FFF init=1, max=1 read
dyld: Main executable mapped /Users/zang/Zang/Spark/LG/10/1/./test
       __PAGEZERO at 0x00000000->0x100000000
           __TEXT at 0x101C45000->0x101C49000
     __DATA_CONST at 0x101C49000->0x101C4D000
           __DATA at 0x101C4D000->0x101C51000
       __LINKEDIT at 0x101C51000->0x101C55000
dyld: Using shared cached for /usr/lib/libSystem.B.dylib
           __TEXT at 0x7FFF6FE9F000->0x7FFF6FEA1000
           __DATA at 0x7FFF990DCCA0->0x7FFF990DCFC8
       __LINKEDIT at 0x7FFFCC27A000->0x7FFFE471167E
dyld: Using shared cached for /usr/lib/system/libcache.dylib
           __TEXT at 0x7FFF72C9C000->0x7FFF72CA2000
           __DATA at 0x7FFF99584610->0x7FFF99584738
       __LINKEDIT at 0x7FFFCC27A000->0x7FFFE471167E
...

案例6:

lldb中使用环境变量

使用lldb -file test命令,将test可执行文件,包装成一个target

(lldb) target create "test"
Current executable set to '/Users/zang/Zang/Spark/LG/10/1/test' (x86_64).

使用b main命令,为man函数设置断点

Breakpoint 1: where = test`main, address = 0x0000000100003f80

target设置环境变量

settings set target.env-vars DYLD_PRINT_APIS=YES

使用r命令,运行test可执行文件

_dyld_register_func_for_add_image(0x7fff72ea16e8)
_dyld_register_for_bulk_image_loads(0x7fff730f4da1)
_NSGetExecutablePath(...)
_dyld_is_memory_immutable(0x7fff730e9cab, 36)
Process 20919 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
   frame #0: 0x0000000100003f80 test`main
test`main:
->  0x100003f80 <+0>: pushq  %rbp
   0x100003f81 <+1>: movq   %rsp, %rbp
   0x100003f84 <+4>: subq   $0x10, %rsp
   0x100003f88 <+8>: movl   $0x0, -0x4(%rbp)
Target 0: (test) stopped.
  • 断住main函数之前,打印dyld内部⼏乎所有发⽣的调⽤

案例7:

Xcode中使用环境变量

使用Xcode打开项目,选择Edit Scheme...

选择Run -> Arguments,点击+,输入环境变量的Name-Value

运行项目,打印dyld内部⼏乎所有发⽣的调⽤

_dyld_register_func_for_add_image(0x1038878a3)
_dyld_register_for_bulk_image_loads(0x103bd73ff)
_NSGetExecutablePath(...)
_dyld_is_memory_immutable(0x103b98e19, 36)
dlopen_internal(/usr/lib/system/introspection/libdispatch.dylib, 0x00000010)
 dlopen_internal(/usr/lib/system/introspection/libdispatch.dylib) ==> 0x10198b220
dlsym_internal(0x10198b220, dispatch_introspection_versions)
 dlsym_internal(0x10198b220, dispatch_introspection_versions) ==> 0x10382ba00
dlsym_internal(0x10198b220, dispatch_introspection_hooks_install)
 dlsym_internal(0x10198b220, dispatch_introspection_hooks_install) ==> 0x10382400f
dlsym_internal(0x10198b220, dispatch_get_current_queue)
 dlsym_internal(0x10198b220, dispatch_get_current_queue) ==> 0x1037f2058
dlsym_internal(0x10198b220, dispatch_queue_get_label)
 dlsym_internal(0x10198b220, dispatch_queue_get_label) ==> 0x1037f999e
dlsym_internal(0x10198b220, dispatch_queue_offsets)
 dlsym_internal(0x10198b220, dispatch_queue_offsets) ==> 0x10382b588
...
dyld

dyld是什么?

  • dyld:动态链接程序,负责链接应用程序
  • libdyld.dylib:给我们的程序提供在Runtime期间能使⽤动态链接功能

dyld的⼯作是什么?

  • 执⾏⾃身初始化配置加载环境LC_DYLD_INFO_ONLY
  • 加载当前程序链接的所有动态库到指定的内存中LC_LOAD_DYLIB
  • 搜索所有的动态库,绑定需要在调⽤程序之前⽤的符号(⾮懒加载符号)LC_DYSYMTAB
  • 在间接符号表(indirect symbol table)中,将需要绑定的导⼊符号真实地址替换LC_DYSYMTAB
  • 向程序提供在Runtime时使⽤dyld的接⼝函数(存在libdyld.dylib中,由LC_LOAD_DYLIB提供)
  • 配置Runtime,执⾏所有动态库/image中使⽤的全局构造函数
  • dyld调⽤程序⼊⼝函数,开始执⾏程序LC_MAIN

dyld加载应用程序的过程

  • 调⽤fork函数,创建⼀个process(进程)
  • 调⽤execve或其衍⽣函数,在该进程上加载,执⾏我们的Mach-O⽂件
  • 将⽂件加载到内存
  • 开始分析Mach-O中的mach_header,以确认它是有效的Mach-O⽂件
  • 验证通过,根据mach_header解析load commands。根据解析结果,将程序各个部分加载到指定的地址空间,同时设置保护标记
  • LC_LOAD_DYLINKEN中加载dyld
  • dyld开始⼯作
  • 调用__dyld_start()函数,通知dyld开始工作
  • 调用dyldbootstrap::start函数,使dyld⾃身进⼊可运⾏状态
  • 调用dyld::_main函数,dyld的入口函数
  • 通过缓存检视,在共享缓存中查找。如果找到直接返回,否则继续后面的流程
  • 共享缓存中未找到,进入以下流程
  • 加载所有手动插入的动态库
  • 链接程序需要的动态库
  • 链接插入的库
  • 应用插入函数
  • 绑定符号
  • 通过instantiateMainExecutable,为主可执行文件创建映像
  • 调用当前程序与动态库的初始化构造函数
  • 通过LC_MAIN查找设置程序⼊⼝函数,将胶⽔地址设置成⼊⼝函数地址,否则胶⽔地址为0
  • 提供胶水地址,返回到dyld::_main函数中继续执行
  • 通过dyld::_maindyldbootstrap::start__dyld_start()dyld配置完成,把控制权交给可执⾏⽂件的⼊⼝函数main(),继续后面的流程

手动插入的动态库

创建动态库项目Inject,创建Inject.m文件,写入以下代码:

#import 

__attribute__((constructor))
static void customConstructor(int argc, const char **argv) {
    NSLog(@"Hello from dylib!\n");
}

创建App项目TestInject,在main函数中,写以下代码:

int main(int argc, char * argv[]) {
   NSString * appDelegateClassName;
   @autoreleasepool {
       appDelegateClassName = NSStringFromClass([AppDelegate class]);
       NSLog(@"main函数");
   }
   return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

编译Inject动态库项目,将编译后产物,从Inject.framework中拷贝到TestInject项目的根目录

TestInject项目中,选择Edit Scheme...,添加环境变量

  • DYLD_INSERT_LIBRARIES${SRCROOT}/Inject
  • 通过向宏DYLD_INSERT_LIBRARIES里写入动态库完整路径,就可以在执行文件加载时将该动态库插入

使用模拟器运行TestInject项目,输出以下内容:

TestInject[16363:3595746] Hello from dylib!
main函数
  • 在主程序main函数之前,加载插入的动态库,并执行构造函数

所以,插入的动态库和正常主程序链接的动态库是不一样的。插入的动态库需要添加环境变量,并提供动态库完整路径,才能对其进行插入

插入函数

创建动态库项目InjectFunction,创建InjectFunction.m文件,写入以下代码:

#import 

#define 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 \
   };

void my_NSLog(NSString *format, ...) {
   NSLog(@"InjectFunction---%@", format);
}

INTERPOSE(my_NSLog, NSLog);
  • INTERPOSE宏,实现了函数的HOOK
  • 实现原理:在__DATA段下,创建名称为__interposeSection

使用Xcode工具,查看宏展开后的代码

INTERPOSE宏展开后的代码:

__attribute__((used)) static struct {
   const void* replacement;
   const void* replacee;
} _interpose_NSLog __attribute__ ((section("__DATA, __interpose"))) = {
   (const void*) (unsigned long) &my_NSLog,
   (const void*) (unsigned long) &NSLog
};
  • 使用__attribute__((used))标记,避免未使用的变量报警告
  • 定义静态匿名结构体,包含两个指针类型成员变量
  • 实例化结构体对象_interpose_NSLog,将其加入到__DATA段下名称为__interposeSection
  • 将两个指针类型成员变量赋值为my_NSLogNSLog的函数地址

延用上述App项目TestInject,完成插入函数的使用。将动态库项目InjectFunction的编译后产物,从InjectFunction.framework中拷贝到TestInject项目的根目录

TestInject项目中,选择Edit Scheme...,对DYLD_INSERT_LIBRARIES环境变量添加第二个动态库参数

  • 参数之间使用:进行分割

使用模拟器运行TestInject项目,输出以下内容:

InjectFunction---Hello from dylib!
InjectFunction---main函数
  • 使用NSLog函数打印的内容,全部增加了InjectFunction---的前缀。因为它已经被InjectFunction动态库中my_NSLog函数替换了

所以,dyld就是通过MachO__DATA__interpose节中,读取应用插入函数。如果__DATA__interpose节存在,会根据里面的内容进行HOOK

你可能感兴趣的:(iOS高级强化--016:重定位符号表 & dSYM & dyld)