调试信息生成过程探究
- 第一个工程
- clang test.m -o test -> .m生成可执行文件
- objdump --macho -d test -> 查看代码段 -> 汇编执行(虚拟内存地址+执行的指令)
- clang -c test.m -o test.o -> 生成.o文件
- objdump --macho -d test.o -> 查看.o文件 -> 与可执行文件不同的是(偏移量 + 执行的执行)
- 新增加test(),test_1()的函数, 并在mian里面调用 -> 重新生成.o文件并objdump来重新查看
- main的汇编里面 -> callq -> 前面都是e8开头, e8是固定机器码(代表callq指令)
- e8 后面 -> 进地址相对位移调用指令(偏移量)
- 偏移量 + 根据下一条指令地址 -> 就是这条指令的调用函数
- 此时偏移量都是00 00 00 00, 但是我们test,test_1明明有 -> 此时函数调用的并是不真实地址 -> 链接的时候, 分配虚拟内存地址 -> 需要告诉链接器,要把真实地址填进来
- test -> 重定位符号表 -> 告诉链接器,要把真实地址填进来 -> 在链接时
2.objdump --macho --reloc test.o
image.png
- 图片红框的address -> 需要重定位的地方
3.clang test.m -o test -> 生成可执行文件
- objdump -> 查看生成的可执行文件
- 此时的可执行文件, 已经分配了虚拟内存地址
- macho地址要从右往左看(小端)
image.png
4.估码 -> 源码 -> 所以有的1相反然后加1, b8 ff ff ff -> 只需要看b8取反并+1就行
- lldb
- e -f b -- 0xb8 -> e, 执行一个表达式/f(format)/b(binary二进制)
- e -f b -- 0xb8 + 1 (e -f b -- 01000111 + 1) -> 真可爱,居然放弃计算,直接用计算器了 -> 0x48
- 验证了偏移地址 + 下一条指令的地址 -> 就是本条函数的调用地址
4.objdump --macho -s test -> 显示当前所有二进制的内容
5.汇编就是这样找函数的 偏移量 + 下条指令的地址 = 函数或对象的真实地址
调试信息dSYM
dSYM文件就是保存按DWARF格式保存调试信息的文件
DWARF是一种被众多编译器和调试器使用的用于支持源代码级别 调试的调试文件格式。
怎么生成dSYM文件的
- 读取
debug map
- 从.o文件中加载__DWARF
- 重新定位所有地址
- 最后将全部的
DWARF
打包成dSYM Bundle
研究流程
- clang -g -c test.m -o test.o -> 生成.o目标文件, -g 生成调试信息(__DEARF)
- objdump --macho --private -headers test.o
- __DEARF -> 这个段就是保持的调试信息 -> 对应上面的第二条( 从.o文件中加载__DWARF)
- strip的时候会将这个段删掉 -> 把所有的调试信息放在符号表中
- clang -g test.m -o test -> 再编译成可执行文件
- objdump --macho --private -headers test
- nm -pa test -> 查看符号表,此时调试信息已经放在符号表里面了
image.png
- clang -g1 test.m -o test -> 生成可执行文件的同时, 生成dSYM文件脚本
- dwarfdump test.dSYM -> 查看该文件
- 其实就是链接的时候, 将调试符号的信息抽取出来,生成dSYM文件来保存调试符号的信息(文件,文件名称,符号的名称,目录)
崩溃日志与dSYM
- 工程运行, 数组越界 -> 崩溃, 控制台打印日志
- 打开控制台 -> 崩溃报告 -> 刚才运行的崩溃日志
- Xcode控制台打印的很清晰 -> 是因为有保存当前项目的调试符号
- 如果想不保存 -> 脱符号
Build Settings -> Deployment Postprocessing -> Yes Strip Style -> Debug -> All Symbols 再运行 -> 再去看控制台 -> 此时哪些VC, 以及方法都会变成地址, 而没有名字 怎么还原?
- 拿到macho的地址, 拿偏移后的地址 - 偏移量 = 没有偏移的地址
- 我们运行时调试到的地址实际上是: 调试地址 = 虚拟地址 + ASLR
- dSYM文件内保存的是没有偏倚的虚拟地址还是偏移后的地址? -> 没有偏移的
- e -f x -- 偏移的地址 - 偏移量 -> 会拿到一个地址
- 注意项目的脚本 Build Phases -> Copy dSYM的代码
- 终端进到该文件夹 -> dwarfdump --lookup 上一步算出的地址 TestInject.app.dSYM -> 查看该地址对应的信息 -> 找到了崩溃时的方法
image.png
Mach-o格式解析
重定位符号表作用
虚拟内存地址与ASLR与dSYM文件关系 /.crash文件符号恢复原理探究
- 打开工程3-ASLR与dSYM
- 获取ASLR
uintptr_t get_slide_address(void) { uintptr_t vmaddr_slide = 0; // 使用的所有的二进制文件 = ipa + 动态库 // ASLR Macho 二进制文件 image 偏移 for (uint32_t i = 0; i < _dyld_image_count(); i++) { // 遍历的是那个image名称 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; } } // ASLR返回出去 return (uintptr_t)vmaddr_slide; } - (void)getMethodVMA { // 运行中的地址(偏移) IMP imp = (IMP)class_getMethodImplementation(self.class, @selector(test_dwarf)); unsigned long imppos = (unsigned long)imp; unsigned long slide = get_slide_address(); // 运行中的地址(偏移) - ASLR = 真正的虚拟内存地址 unsigned long addr = imppos - slide; }
然后开启DEARF Build Settings -> dw -> Debug information Format -> Debug -> DEARF with dSYM File 检查是否脱调试符号 -> Build Settings -> Deployment -> Deployment Postprocessing -> NO(默认的,不脱调试符号) 运行, 断点获取addr -> 转16进制 -> e -f x -- 地址 (也可以用计算器试试) dwarfdump --lookup 上一步算出的地址 TestInject.app.dSYM
dSYM文件的作用
- 打开代码调试工程
- 使用了自己的TestFramework文件
- TestFramework.podspec文件里面的写法
if ENV['Source'] -> 如果pod install时, 引入的是源码,否则就是编译好的.framework文件 终端来到PodFile文件夹下 -> pod install -> 引入的是framework 如果想引入源码 Source=1 pod install -> 让这个变量为真
- 然后运行工程, 通过终端下断点
image.png
- 下断点后, 再往下走, 发现并没有进源码里面, 你的framework里面保存的有完整的调试信息的话, 你是可以进到源码里面的
- TestFramework.framework -> show in finder -> cd到该文件夹下
- nm -pa TestFramework
- 发现因为是里面的调试信息的目录路径不对
- 可以把源码放到上面的路径下试试 -> 最终重新编译的framewrok,意味着里面的路径也会重新生成
- 注意framework的脚本 -> 脚本最终会把编译的产物放在Products目录下
- 然后按上面的步骤,重新pod install, 运行项目, 下断点(注意写法时通过正则下的断点) -> 下断点成功后, 继续运行 -> 进入了framework组件的源码
- 思路:组件化或者二进制化的时候 -> 通过控制你的二进制文件有没有调试信息 -> 来达到调试源码的目的
视频5
dyld的调试与作用
如果想调试dyld源代码,需要准备带调试信息的dyld/libdyld.dylib/ libclosured.dylib,与系统做替换,⻛险较大。
lldb保留了 一个库列表,避免在按名称设置断点时出现问题,而dyld与
libdyld.dylib就在该列表上。
有两种方式在可以强制在dyld上设置断点:
- br set -n dyldbootstrap::start -s dyld
- -s 在指定的二进制文件里设置断点
- set set target.breakpoints-use-platform-avoid-list 0
- 注重这种设置只在当前这次运行中生效
- 这种是通过lldb提供的环境变量
- 因为在库列表里的不能设置断点, 以防跟我们常用的冲突, 这个改环境变量就是将白名单禁掉(就是上面的库列表)
image.png
dyld提供的环境变量
image.png
使用举例:
image.png
image.png
以上设置环境变量在Xcode中的设置步骤为:
- Edit Scheme
- Arguments
- Environment Variables
image.png
程序加载流程
image.png
objdump --macho --private -headers test -> 查看Macho的信息
- dyld: 动态链接程序
- libdyld.dylib: 给我们的程序提供在Runtime期间能使用动态链接功能
- dyld做了什么 ->
image.png
dyld加载流程
图注: 白色线往下走, 黄色线往上走
image.png
image.png
image.png
- 正则下断点
image.png
插入动态库与插入函数
插入测试动态库流程分析
- Inject.m -> 只有一个打印函数
- 编译包装成一个动态库
- 新打开一个工程
- 插入(图中没有打钩, 需要打勾)
image.png
- 然后运行工程, 发现控制台打印的有插入的动态库(这里其实是逆向的知识点)
插入函数
这个是dyld给我提供的hook函数, 以下方法就是调用NSLog时, 改为调用my_NSLog
// 1.在__DATA 创建__interpose这个section #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 \ }; // hook function // 国内大厂 面源码 // 国外 实际应用 void my_NSLog(NSString *format, ...) { NSLog(@"InjectFunction---%@", format); } // hook function INTERPOSE(my_NSLog, NSLog);
- InjectFunction
- my_NSLog替换NSLog -> dyld提供的宏
- Preprocess -> 可以查看转化后代码
image.png
__attribute__((used))
因为该方法没有使用 -> 告诉编译器,你要不管,这是我私下使用的,不要给我报警告
__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 };;
- 上面的代码其实就是声明一个结构体类型,并同时在定义个结构体, 然后实例化了这个结构体
- 然后把创建的结构体放在了__DATA __interpose section
- 我们dyldy就知道从__interpose 这个section里面调用你插入的函数
- 然后编译, 将编译后的可执行文件, 直接通过Xcode配置来实现插入(截图为插入多个动态库的写法,通过:分割开)
image.png