MachO与lldb (10)

调试信息生成过程探究

  1. 第一个工程
  2. clang test.m -o test -> .m生成可执行文件
    1. objdump --macho -d test -> 查看代码段 -> 汇编执行(虚拟内存地址+执行的指令)
  3. clang -c test.m -o test.o -> 生成.o文件
    1. objdump --macho -d test.o -> 查看.o文件 -> 与可执行文件不同的是(偏移量 + 执行的执行)
    2. 新增加test(),test_1()的函数, 并在mian里面调用 -> 重新生成.o文件并objdump来重新查看
    3. main的汇编里面 -> callq -> 前面都是e8开头, e8是固定机器码(代表callq指令)
      1. e8 后面 -> 进地址相对位移调用指令(偏移量)
        1. 偏移量 + 根据下一条指令地址 -> 就是这条指令的调用函数
        2. 此时偏移量都是00 00 00 00, 但是我们test,test_1明明有 -> 此时函数调用的并是不真实地址 -> 链接的时候, 分配虚拟内存地址 -> 需要告诉链接器,要把真实地址填进来
        3. test -> 重定位符号表 -> 告诉链接器,要把真实地址填进来 -> 在链接时
      2. objdump --macho --reloc test.o
        image.png
        1. 图片红框的address -> 需要重定位的地方
      3. clang test.m -o test -> 生成可执行文件
        1. objdump -> 查看生成的可执行文件
        2. 此时的可执行文件, 已经分配了虚拟内存地址
        3. macho地址要从右往左看(小端)
          image.png
        4. 估码 -> 源码 -> 所以有的1相反然后加1, b8 ff ff ff -> 只需要看b8取反并+1就行
          1. lldb
          2. e -f b -- 0xb8 -> e, 执行一个表达式/f(format)/b(binary二进制)
          3. e -f b -- 0xb8 + 1 (e -f b -- 01000111 + 1) -> 真可爱,居然放弃计算,直接用计算器了 -> 0x48
          4. 验证了偏移地址 + 下一条指令的地址 -> 就是本条函数的调用地址
      4. objdump --macho -s test -> 显示当前所有二进制的内容
      5. 汇编就是这样找函数的 偏移量 + 下条指令的地址 = 函数或对象的真实地址

调试信息dSYM

dSYM文件就是保存按DWARF格式保存调试信息的文件
DWARF是一种被众多编译器和调试器使用的用于支持源代码级别 调试的调试文件格式。

怎么生成dSYM文件的

  1. 读取debug map
  2. 从.o文件中加载__DWARF
  3. 重新定位所有地址
  4. 最后将全部的DWARF打包成dSYM Bundle

研究流程

  1. clang -g -c test.m -o test.o -> 生成.o目标文件, -g 生成调试信息(__DEARF)
  2. objdump --macho --private -headers test.o
    1. __DEARF -> 这个段就是保持的调试信息 -> 对应上面的第二条( 从.o文件中加载__DWARF)
    2. strip的时候会将这个段删掉 -> 把所有的调试信息放在符号表中
  3. clang -g test.m -o test -> 再编译成可执行文件
  4. objdump --macho --private -headers test
  5. nm -pa test -> 查看符号表,此时调试信息已经放在符号表里面了
    image.png
  6. clang -g1 test.m -o test -> 生成可执行文件的同时, 生成dSYM文件脚本
  7. dwarfdump test.dSYM -> 查看该文件
    1. 其实就是链接的时候, 将调试符号的信息抽取出来,生成dSYM文件来保存调试符号的信息(文件,文件名称,符号的名称,目录)

崩溃日志与dSYM

  1. 工程运行, 数组越界 -> 崩溃, 控制台打印日志
  2. 打开控制台 -> 崩溃报告 -> 刚才运行的崩溃日志
  3. Xcode控制台打印的很清晰 -> 是因为有保存当前项目的调试符号
  4. 如果想不保存 -> 脱符号
    1. Build Settings -> Deployment Postprocessing -> Yes
    2. Strip Style -> Debug -> All Symbols
    3. 再运行 -> 再去看控制台 -> 此时哪些VC, 以及方法都会变成地址, 而没有名字
    4. 怎么还原?
  5. 拿到macho的地址, 拿偏移后的地址 - 偏移量 = 没有偏移的地址
    1. 我们运行时调试到的地址实际上是: 调试地址 = 虚拟地址 + ASLR
    2. dSYM文件内保存的是没有偏倚的虚拟地址还是偏移后的地址? -> 没有偏移的
  6. e -f x -- 偏移的地址 - 偏移量 -> 会拿到一个地址
    1. 注意项目的脚本 Build Phases -> Copy dSYM的代码
    2. 终端进到该文件夹 -> dwarfdump --lookup 上一步算出的地址 TestInject.app.dSYM -> 查看该地址对应的信息 -> 找到了崩溃时的方法
      image.png

Mach-o格式解析

重定位符号表作用

虚拟内存地址与ASLR与dSYM文件关系 /.crash文件符号恢复原理探究

  1. 打开工程3-ASLR与dSYM
  2. 获取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;
}

  1. 然后开启DEARF
  2. Build Settings -> dw -> Debug information Format -> Debug -> DEARF with dSYM File
  3. 检查是否脱调试符号 -> Build Settings -> Deployment -> Deployment Postprocessing -> NO(默认的,不脱调试符号)
  4. 运行, 断点获取addr -> 转16进制 -> e -f x -- 地址 (也可以用计算器试试)
  5. dwarfdump --lookup 上一步算出的地址 TestInject.app.dSYM

dSYM文件的作用

  1. 打开代码调试工程
  2. 使用了自己的TestFramework文件
  3. TestFramework.podspec文件里面的写法
    1. if ENV['Source'] -> 如果pod install时, 引入的是源码,否则就是编译好的.framework文件
    2. 终端来到PodFile文件夹下 -> pod install -> 引入的是framework
    3. 如果想引入源码 Source=1 pod install -> 让这个变量为真
  4. 然后运行工程, 通过终端下断点
    image.png
  5. 下断点后, 再往下走, 发现并没有进源码里面, 你的framework里面保存的有完整的调试信息的话, 你是可以进到源码里面的
  6. TestFramework.framework -> show in finder -> cd到该文件夹下
    1. nm -pa TestFramework
    2. 发现因为是里面的调试信息的目录路径不对
    3. 可以把源码放到上面的路径下试试 -> 最终重新编译的framewrok,意味着里面的路径也会重新生成
    4. 注意framework的脚本 -> 脚本最终会把编译的产物放在Products目录下
    5. 然后按上面的步骤,重新pod install, 运行项目, 下断点(注意写法时通过正则下的断点) -> 下断点成功后, 继续运行 -> 进入了framework组件的源码
  7. 思路:组件化或者二进制化的时候 -> 通过控制你的二进制文件有没有调试信息 -> 来达到调试源码的目的

视频5

dyld的调试与作用

如果想调试dyld源代码,需要准备带调试信息的dyld/libdyld.dylib/ libclosured.dylib,与系统做替换,⻛险较大

lldb保留了 一个库列表,避免在按名称设置断点时出现问题,而dyld与
libdyld.dylib就在该列表上。
有两种方式在可以强制在dyld上设置断点:

  1. br set -n dyldbootstrap::start -s dyld
    1. -s 在指定的二进制文件里设置断点
  2. set set target.breakpoints-use-platform-avoid-list 0
    1. 注重这种设置只在当前这次运行中生效
    2. 这种是通过lldb提供的环境变量
    3. 因为在库列表里的不能设置断点, 以防跟我们常用的冲突, 这个改环境变量就是将白名单禁掉(就是上面的库列表)
      image.png

dyld提供的环境变量

image.png

使用举例:


image.png
image.png

以上设置环境变量在Xcode中的设置步骤为:

  1. Edit Scheme
  2. Arguments
  3. Environment Variables


    image.png

程序加载流程

image.png
  1. objdump --macho --private -headers test -> 查看Macho的信息
  2. dyld: 动态链接程序
    1. libdyld.dylib: 给我们的程序提供在Runtime期间能使用动态链接功能
  3. dyld做了什么 ->
    image.png

dyld加载流程

图注: 白色线往下走, 黄色线往上走

image.png

image.png

image.png
  1. 正则下断点
    image.png

插入动态库与插入函数

插入测试动态库流程分析

  1. Inject.m -> 只有一个打印函数
    1. 编译包装成一个动态库
  2. 新打开一个工程
    1. 插入(图中没有打钩, 需要打勾)
      image.png
  3. 然后运行工程, 发现控制台打印的有插入的动态库(这里其实是逆向的知识点)

插入函数

这个是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);
  1. InjectFunction

  2. my_NSLog替换NSLog -> dyld提供的宏

  3. Preprocess -> 可以查看转化后代码
    image.png
    1. __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 };;
  1. 上面的代码其实就是声明一个结构体类型,并同时在定义个结构体, 然后实例化了这个结构体
  2. 然后把创建的结构体放在了__DATA __interpose section
  3. 我们dyldy就知道从__interpose 这个section里面调用你插入的函数
  4. 然后编译, 将编译后的可执行文件, 直接通过Xcode配置来实现插入(截图为插入多个动态库的写法,通过:分割开)
    image.png

你可能感兴趣的:(MachO与lldb (10))