重定位符号表
案例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
是近址相对位移调用指令
,即:偏移地址 + 下一条指令地址 = 调用函数的所在地址
- 由于在链接时才会分配真实的虚拟内存地址,所以编译目标文件时,调用
test
和test_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)
- 以前两条指令为例,它们的
address
为00000049
和00000044
。在目标文件的代码段中,分别对应48: e8
和43: e8
后面的00 00 00 00
地址,表示这两个地址在链接时需要被替换
- 下面两条指令,对全局变量
global
赋值的占位,address
为0000003b
和00000031
。在目标文件的代码段中,分别对应39: c7 05
和2f: c7 05
后面的fc ff ff ff
地址。再后面的14 00 00 00
和15 00 00 00
,分别对应16进制
的20
和21
案例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
参数表示format
,b
为2进制
,x
为16进制
,d
为10进制
,o
为8进制
2进制
取反后地址为1000111
使用
e -f b -- 0b1000111 + 0b1
命令,将取反后2进制
进行+1
,将其还原为原码(int) $1 = 0b00000000000000000000000001001000
使用
e -f x -- 0b1001000
命令,将其转为16进制
(int) $2 = 0x00000048
- 有符号数,
16进制
的0xffffffb8
,f
开头为负数。所以转为原码应该为-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
两次赋值,偏移地址分别为0x4067
和0x405d
- 将偏移地址分别加上它们的下一条指令地址
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
-f
:DWARF
格式文件-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
方法运行项目,打印
ASLR
和test_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
源代码,需要准备带调试信息的dyld
、libdyld.dylib
、libclosured.dylib
,与系统做替换,⻛险较⼤,不建议使用方式二
:通过lldb
调试dylib
,推荐使用通过
lldb
调试dylib
,有两种方式设置断点:
br set -n dyldbootstrap::start -s dyld
通过-s
参数,在指定二进制文件上设置断点set set target.breakpoints-use-platform-avoid-list 0
lldb
保留了⼀个库列表,类似于白名单功能。列表中出现的库文件,无法直接按名称设置断点,⽽dyld
与libdyld.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::_main
→dyldbootstrap::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
段下,创建名称为__interpose
的Section
使用
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
段下名称为__interpose
的Section
中- 将两个指针类型成员变量赋值为
my_NSLog
和NSLog
的函数地址延用上述
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