# 动态调试
## 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 符号或者函数名`:查找某个符号或者函数的位置