lldb 源码映射的技术实现

2020/05/25 Objective-C

一、需求

由于我们的项目比较大,编译一次大概需要一个多小时。对于线上问题,需要先切换分支,然后编译,然后再进行调试,定位问题,时间比较长,因此,有没有一种方案,在线上出问题后,能够立马进行远吗级别 的调试,定位问题。

二、方案

针对以上需求,提出了一种方案

在每次发版时先编译一个debug 版本,当有线上问题发生时,直接下载对应的debug 版本,将debug 版本的.app 文件放到product 目录中,执行run without building,便可直接进行调试,定位问题

三、问题

1、业务组同学端上的代码与线上发生问题的版本代码极大概率上是不一样的,这样导致业务组同学只能查看汇编代码而加大了调试难度

2、debug版本的.app 文件中是不包含符号信息的,

$ dwarfdump testRuntime.app/testRuntime 
testRuntime.app/testRuntime:	file format Mach-O 64-bit x86-64

.debug_info contents:

如何获取符号信息

四、解决问题

针对以上问题,有以下解决思路

1、添加符号信息

a 在调试时,将符号信息导入到lldb 中

(lldb) add-dsym ~/Downloads/Archive/testRuntime.dSYM
symbol file '~/Downloads/Archive/testRuntime.dSYM/Contents/Resources/DWARF/testRuntime' has been added to '~/Library/Developer/Xcode/DerivedData/testRuntime-bbzzorffuqwpioemkhzriysmacxo/Build/Products/Debug-iphonesimulator/testRuntime.app/testRuntime'

b 源码映射

(lldb) settings set target.source-map ~/Downloads/testRuntime /Users/haibing6/Downloads/testRuntime

2、获取dwarf 文件

两种方式

使用xcode 生成

或者

dsymutil ~/Downloads/Archive/testRuntime.app/testRuntime -o /Users/haibing6/Downloads/Archive/testRuntime.dSYM

附:关于dwarf 文件

$ dwarfdump ~/Downloads/Archive/testRuntime.dSYM/Contents/Resources/DWARF/testRuntime > ts.mm

可以看到

如:DW_comp_dir,DW_AT_name 等属性

四、实施

1、流程图

2、实施步骤

  • Jenkins服务器提供dwarf 文件、app文件,以及Jenkins 源码路径,以分支作为标识符,压缩提供下载服务

  • 客户端根据分支,下载对应的.app文件以及dwarf文件,解压后将APP文件拷贝到对应的target 目录,使用如下命令查找target目录以及对应的架构:

      xcodebuild -workspace ~/work/testruntime/testruntime/testruntime/testruntime.xcworkspace  \
      -scheme testruntime \
      -sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/  \
      -configuration Debug \
      -showBuildSettings
    

    这里会有需要的一切

  • 打开xcode ,在xcode红随便打个断点, 执行run without building

  • 启动lldb,attached target 后在lldb 中执行 command script import WBLoadSymbols.py

五、收益

附:思考

我们可不可以直接在.lldbinit文件中直接command script import WBLoadSymbols.py

要回答这个问题,首先要了解调试器工作原理

首先,调试器能做什么

  • 调试器可以启动某些进程,然后对其进行调试,或者attached 一个已存在的进程。

  • 调试器可以单步运行代码,设置断点然后运行程序,检查变量的值以及跟踪调用栈。

  • 调试器可以执行表达式并在被调试进程的地址空间中调用函数,修改程序行为

其次,如何做到以上

  • 我们知道调试器和被调试程序是两个互不相干的进程,在用户模式下是无法读取另一个进程的地址空间的
  • 但是Linux 系统提供一种方式ptraceptrace 系统调用提供了一种方法,通过这种方法,一个进程可以观察和控制另一个进程的执行,并检查和更改被跟踪进程的内存和寄存器。ptrace是一个功能众多且相当复杂的工具。定义如下: int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);

    第一个参数是request,系统提供一组值,如PT_TRACE_ME,PT_CONTINUE,PT_ATTACH

    第二个参数指定进程ID

    第三个参数是地址

    第四个参数是指向数据的指针

    当被调试程序被中断时,操作系统发出信号,调试进程就会去检查这个事件

三,断点

关于 int 3 指令

可以简单的理解断点就是通过CPU 的特殊指令–int 3 来实现的。int 就是x86 体系结构中的“陷阱指令”–对预定义的中断处理例程的调用。x86 支持int 指令带有一个8位的操作数,用来指定所发生的中断号(如最常见的svc 0x80)。int 3 指令产生一个特殊的单字节操作码(CC),这是用来调用调试异常处理例程的(这个单字节形式非常有价值,因为这样可以用过一个端点来替换掉第一个字节,包括其他的单字节指令也一样,而不会覆盖到其他的操作码)

使用int3 指令

一旦被调试进程执行到int 3 指令时,操作系统就将它暂停,在Linux平台上,会给调试器进程发送一个sigtrap 信号。

通过int 3 指令在调试器中设定断点

要在被调试进程中的某个目标地址上设定一个端点,调试器需要做下面两件事情:

1、保存目标地址上的数据

2、将目标地址上的第一个字节替换成int 3 指令

然后,当调试器向操作系统请求开始运行进程时(通过PT_CONTINUE),进程最终一定会碰到int 3 指令,此时,进程停滞,操作系统发送一个信号,这时就是调试器再次出马的时候,接收到被跟踪进程停止的信号,然后调试器要做一下几件事:

1、在目标地址上用原来的指令替换掉int 3 指令

2、将被跟踪进程的指令指针(指令指针寄存器,x86 中是IP ,arm 中PC,存储是下一条指令的偏移地址)向后递减1 。这是必须的,因为指令指针指向的已经是执行过的int 3 之后的下一条指令。

3、由于进程此时仍然是停止的,用户可以同被调试进程进行某种形式的交互。这里调试器可以让你查看变量的值,检查调用栈等等

4、当用户希望进程继续运行时,调试器负责将断点再次加到目标地址上(由于在第一步中已经被移除了,被替换回来了),除非用户取消断点

四、调试信息

  • 调试段
  • 定位函数
  • 定位变量
  • 定位到行号

这些信息豆村

参考:

[1]:探索 DWARF 调试格式信息

[2]:DWARF Debugging Information Format

[3]:Debugging Mono binaries with LLDB

[4]:调试器工作原理:第一部分 基础

[5]:How debuggers work: Part 1 - Basics

Search

    Post Directory