# 动态调试 ## Xcode 调试的原理 Xcode 是电脑端的程序,Xcode 使用 LLDB 进行调试。真机连接 Xcode 运行起来,点击屏幕,对应的事件处理方法里加了断点。手机是如何与 Xcode 断点连接同步的呢? Xcode 编译器:GCC -> LLVM Xcode 调试器:GDB -> LLDB - `debugServer` 存放在:`/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/12.2/DeveloperDiskImage.dmg/usr/bin/debugserver` - 当 Xcode 识别到手机设备时,Xcode 会自动将 `debugserver` 安装到 iPhone 上。`/Developer/usr/bin/debugserver` - 一般情况下,Xcode 只可以调试通过 Xcode 安装的 App ## 动态调试任意 App ### 核心原因 上面说了 `debugserver` 只能调试 Xcode 连接安装的程序。这句话不够严谨,Xcode 连接 iPhone 的时候,会自动将 `debugserver` 安装到 iPhone 上,但是权限会做收敛。具体表现就是权限 plist。 所以我们可以自行修改权限,重新签名即可: - 将 `debugserver` 拷贝到电脑上 - 利用 ` ldid -e debugserver > debugserver.entitlements` 命令导出权限文件 - 打开 `debugserver.entitlements` 添加 `get-task-allow`、`task_for_pid-allow` 2个权限 ```shell get-task-allow task_for_pid-allow com.apple.springboard.debugapplications com.apple.backboardd.launchapplications com.apple.backboardd.debugapplications com.apple.frontboard.launchapplications com.apple.frontboard.debugapplications seatbelt-profiles debugserver com.apple.diagnosticd.diagnostic com.apple.security.network.server com.apple.security.network.client com.apple.private.memorystatus com.apple.private.cs.debugger com.apple.springboard.debugapplications com.apple.backboardd.launchapplications com.apple.backboardd.debugapplications com.apple.frontboard.launchapplications com.apple.frontboard.debugapplications seatbelt-profiles debugserver com.apple.diagnosticd.diagnostic com.apple.security.network.server com.apple.security.network.client com.apple.private.memorystatus com.apple.private.cs.debugger ``` - 利用 ldid `ldid -S debugserver.entitlements debugserver` 进行重签 自动安装的 `debugserver` 存放目录为 `Device/Developer/usr/bin` 下的,但是这个目录是只读的。我们没办法将重签后的 `debugserver` 拖放到该位置。 但以后的使用场景是,在电脑终端 `sh ~/login.sh` 登录到手机后,在命令行模式下使用 `debugserver AppProcessName`,所以 `debugserver` 就需要安装(拖放 )到 `Device/usr/bin` 此时还是无法使用 `debugserver` ,需要修改权限 `chmod +x /usr/bin/debugserver ` ### debugserver 附加到某个 App 进程 `debugserver *:端口号 -a 进程 ` - `*:端口号` :使用 iPhone 的某个端口启动 `debugserver` 服务(只要不是保留端口号就可以) - `进程`:输入 App 的进程信息(进程 ID 或者进程名称) 比如:`debugserver *:10011 -a Wechat` ### Mac 上启动 LLDB,远程连接 iPhone 上的 debugserver - 启动 LLDB,直接在终端输入 `lldb` - 连接 debugserver 服务:`process connect connect://手机 IP 地址:debugserver 服务端口号 `。其中 `connect://` 代表协议 - 第二步连接成功后,iPhone 的进程暂时就暂停了,下断点的状态,此时需要使用 LLDB 的 c 命令让程序先继续运行:`c` - 接下来就用 LLDB 常规命令调试 App ### 通过 debugserver 启动 App `debugserver -x auto *:端口号 App 的可执行文件路径` ## LLDB 调试指令 ### 指令格式 指令格式为:` [ [...]] [-options [option-value]] [arguments [argument...]]` - ``:命令 - `` : 子命令 - `` :命令操作 - `` :命令选项 - ``:命令参数 比如给函数 sayHi 设置断点:`breakpoint set -n sayHi`,其中 - breakpoint 是 `` - set 是 `` - -n 是 `` - sayHi 是 `` ### help 查看帮助 `help `:用来查看某个指令和子指令 ` ` 的说明。比如 `help breakpoint set` ### expression `expression -- ` 用于执行一个表达式 - `` :命令选项 - `--`:命令选项结束符,表示所有的命令选项已经设置完毕,如果没有命令选项,`--` 可以省了 - `` : 需要执行的表达式 比如经常在断点的时候,想额外执行某个函数或者处理某个逻辑,举个例子。在 `touchesMoved` 方法的断点模式下,想修改 view 的背景颜色,此时不需要重新运行。利用 expression 执行指令即可 ```swift (lldb) expression self.view.backgroundColor = .red ``` - `expression`、`expression --` 和指令 print、p、call 的效果一样 - `expression -O --` 和指令 po 效果一样。比如 ### 堆栈信息 `thread backtrace` :打印线程的堆栈信息。效果等同于 `bt` ### 方法返回 `thread return []` 让函数直接返回某个值,不会执行断点后面的代码 例如下面的代码,直接将函数的返回值进行修改了 ### frame variable `frame variable []` 打印当前栈帧变量 ### 调试指令 `thread continue`、`continue`、`c`:程序继续运行 `thred step-over`、`next`、`n` :单步运行,把字函数当作整体一步执行 `thread step-in`、`step`、`s`:单步运行,遇到子函数会进入子函数 `thread step-out`、`finish`:直接执行完当前函数的所有代码,返回到上一个函数 `si` 、`ni` 和 `s` `n`:类似 - `s` `n` 是源码级别 - `si`、`ni` 是汇编指令级别 ### breakpoint 设置断点的指令 #### 函数名 `breakpoint set -n "函数名"`,但可能存在多个断点,因为同样方法名称的方法,都会被设置断点 比如 Person 类和 ViewController 类,都有 `- (NSInteger)add:(NSInteger)a withB:(NSInteger)b` 方法。` breakpoint set -n "add:withB:"` 指令设置了2个断点。 当有多个方法同名的时候,只对当前类设置断点,指令格式为 `breakpoint set -n "[类名 方法名]"`。 比如通过 `breakpoint set -n "[ViewController add:withB:]"` 对 ViewController 类的 `- (NSInteger)add:(NSInteger)a withB:(NSInteger)b` 方法设置断点 如果一个方法没有参数,也可以通过 `breakpoint set -n sayHi` 的方式设置断点 #### 函数地址 在逆向,调试别人的 App 的时候我们无法知道函数名称,所以给函数地址打断点就很重要了。 `breakpoint set -a 函数地址`。注意:函数地址是需要处理的,因为 iOS 有 `ASLR 技术`。 #### 正则表达式 `breakpoint set -r 正则表达式`,效果就是给所有函数名符合正则表达式的函数,设置断点。 #### 动态库 `breakpoint set -s 动态库 -n 函数名` #### 列出所有断点 `breakpoint list` 用于列出所有的断点,每个断点都有自己的编号 #### 断点的删除、禁用、开启 `breakpoint disable 断点编号` ,比如 `breakpoint disable 2.1 2.2` 禁用了2个断点 `breakpoint delete 断点编号`,比如 `breakpoint delete 2`。 比较奇怪,断点开启、禁用是可以跟子序号的,比如2.1 2.2,而断点删除必须是一级序号 #### 断点指令信息 `breakpoint command add 断点编号`,该指令会给断点预先设置需要执行的命令,到触发断点时,就会按照指令添加的顺序执行。 指令可以添加多个,最后以 "DONE" 结束。 `breakpoint command list 断点编号` 用于查看该断点下的所有指令 `breakpoint command delete 断点编号` 用于删除该断点下的所有指令 ### 内存断点 在内存数据发生改变时触发 `watchpoint set variable 变量` 在 `viewDidLoad` 中 通过 `watchpoint set variable self->_age` 给 age property 设置了断点。当改变的时候就触发断点 `watchpoint set expression 变量地址` 在 `viewDidLoad` 中 通过 `watchpoint set expression 0x00007fcd20306a60` 给 age property 设置了断点。当改变的时候就触发断点 `watchpoint list` `watchpoint disable 断点编号` `watchpoint enable 断点编号` `watchpoint disable 断点编号` `watchpoint delete 断点编号` `watchpoint command add 断点编号` `watchpoint command list 断点编号` `watchpoint command delete 断点编号` ### image #### image list `image list` 列举所加载的模块信息 `image list -o -f` 打印出模块的偏移地址、全路径 #### image lookup `image lookup -t 类型`:查找某个类型的信息 `image lookup -a 地址`:根据内存地址查找在模块中的位置 举例:声明一个数组,只有5个元素,但通过下标6来访问数组的时候 crash 了,假设我们代码很长,crash 后想知道具体是哪一行代码造成了 crash,怎么办呢? 我们项目叫做 Demo111,那么 crash 堆栈中第4行有个地址 `0x000000010d534dfc`,可以通过该地址来分析具体的 crash 位置。通过 `image lookup -a 0x000000010d534dfc` 可以知道 `Summary: Demo111 -[ViewController touchesBegan:withEvent:] + 108 at ViewController.m:29:18` 是在 ViewController 的29行处发生了 crash。 `image lookup -n 符号或者函数名`:查找某个符号或者函数的位置