mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 04:17:17 +00:00
519 lines
13 KiB
Markdown
519 lines
13 KiB
Markdown
# 动态调试
|
||
|
||
|
||
|
||
## Xcode 调试的原理
|
||
|
||
Xcode 是电脑端的程序,Xcode 使用 LLDB 进行调试。真机连接 Xcode 运行起来,点击屏幕,对应的事件处理方法里加了断点。手机是如何与 Xcode 断点连接同步的呢?
|
||
|
||
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeDebugWithiPhone.png" style="zoom:25%">
|
||
|
||
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
|
||
|
||
### 核心原因
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeDebugWithAnyAppiPhone.png" style="zoom:25%">
|
||
|
||
|
||
|
||
上面说了 `debugserver` 只能调试 Xcode 连接安装的程序。这句话不够严谨,Xcode 连接 iPhone 的时候,会自动将 `debugserver` 安装到 iPhone 上,但是权限会做收敛。具体表现就是权限 plist。
|
||
|
||
所以我们可以自行修改权限,重新签名即可:
|
||
|
||
- 将 `debugserver` 拷贝到电脑上
|
||
|
||
- 利用 ` ldid -e debugserver > debugserver.entitlements` 命令导出权限文件
|
||
|
||
- 打开 `debugserver.entitlements` 添加 `get-task-allow`、`task_for_pid-allow` 2个权限
|
||
|
||
```shell
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||
<plist version="1.0">
|
||
<dict>
|
||
<key>get-task-allow</key>
|
||
<true/>
|
||
<key>task_for_pid-allow</key>
|
||
<true/>
|
||
<key>com.apple.springboard.debugapplications</key>
|
||
<true/>
|
||
<key>com.apple.backboardd.launchapplications</key>
|
||
<true/>
|
||
<key>com.apple.backboardd.debugapplications</key>
|
||
<true/>
|
||
<key>com.apple.frontboard.launchapplications</key>
|
||
<true/>
|
||
<key>com.apple.frontboard.debugapplications</key>
|
||
<true/>
|
||
<key>seatbelt-profiles</key>
|
||
<array>
|
||
<string>debugserver</string>
|
||
</array>
|
||
<key>com.apple.diagnosticd.diagnostic</key>
|
||
<true/>
|
||
<key>com.apple.security.network.server</key>
|
||
<true/>
|
||
<key>com.apple.security.network.client</key>
|
||
<true/>
|
||
<key>com.apple.private.memorystatus</key>
|
||
<true/>
|
||
<key>com.apple.private.cs.debugger</key>
|
||
<true/>
|
||
</dict>
|
||
</plist>
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||
<plist version="1.0">
|
||
<dict>
|
||
<key>com.apple.springboard.debugapplications</key>
|
||
<true/>
|
||
<key>com.apple.backboardd.launchapplications</key>
|
||
<true/>
|
||
<key>com.apple.backboardd.debugapplications</key>
|
||
<true/>
|
||
<key>com.apple.frontboard.launchapplications</key>
|
||
<true/>
|
||
<key>com.apple.frontboard.debugapplications</key>
|
||
<true/>
|
||
<key>seatbelt-profiles</key>
|
||
<array>
|
||
<string>debugserver</string>
|
||
</array>
|
||
<key>com.apple.diagnosticd.diagnostic</key>
|
||
<true/>
|
||
<key>com.apple.security.network.server</key>
|
||
<true/>
|
||
<key>com.apple.security.network.client</key>
|
||
<true/>
|
||
<key>com.apple.private.memorystatus</key>
|
||
<true/>
|
||
<key>com.apple.private.cs.debugger</key>
|
||
<true/>
|
||
</dict>
|
||
</plist>
|
||
```
|
||
|
||
- 利用 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 调试指令
|
||
|
||
### 指令格式
|
||
|
||
指令格式为:`<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [arguments [argument...]]`
|
||
|
||
- `<command>`:命令
|
||
- `<subcommand>` : 子命令
|
||
- `<action>` :命令操作
|
||
- `<options>` :命令选项
|
||
- `<arguments>`:命令参数
|
||
|
||
|
||
|
||
比如给函数 sayHi 设置断点:`breakpoint set -n sayHi`,其中
|
||
|
||
- breakpoint 是 `<command>`
|
||
- set 是 `<action>`
|
||
- -n 是 `<options>`
|
||
- sayHi 是 `<arguments>`
|
||
|
||
|
||
|
||
### help 查看帮助
|
||
|
||
`help <command> <subcommand>`:用来查看某个指令和子指令 `<command> <subcommand>` 的说明。比如 `help breakpoint set`
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBHelpCommand.png" style="zoom:25%">
|
||
|
||
|
||
|
||
### expression
|
||
|
||
`expression <cmd-options> -- <expr>` 用于执行一个表达式
|
||
|
||
- `<cmd-options>` :命令选项
|
||
- `--`:命令选项结束符,表示所有的命令选项已经设置完毕,如果没有命令选项,`--` 可以省了
|
||
- `<expr>` : 需要执行的表达式
|
||
|
||
比如经常在断点的时候,想额外执行某个函数或者处理某个逻辑,举个例子。在 `touchesMoved` 方法的断点模式下,想修改 view 的背景颜色,此时不需要重新运行。利用 expression 执行指令即可
|
||
|
||
```swift
|
||
(lldb) expression self.view.backgroundColor = .red
|
||
```
|
||
|
||
- `expression`、`expression --` 和指令 print、p、call 的效果一样
|
||
|
||
- `expression -O --` 和指令 po 效果一样。比如
|
||
|
||
|
||
|
||
### 堆栈信息
|
||
|
||
`thread backtrace` :打印线程的堆栈信息。效果等同于 `bt`
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBThreadBacktrace.png" style="zoom:25%">
|
||
|
||
|
||
|
||
|
||
|
||
### 方法返回
|
||
|
||
`thread return [<expr>]` 让函数直接返回某个值,不会执行断点后面的代码
|
||
|
||
例如下面的代码,直接将函数的返回值进行修改了
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBThreadReturn.png" style="zoom:25%">
|
||
|
||
### frame variable
|
||
|
||
`frame variable [<variable-name>]` 打印当前栈帧变量
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBFrameVariable.png" style="zoom:25%">
|
||
|
||
|
||
|
||
### 调试指令
|
||
|
||
`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个断点。
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakPointSetSameSymbol.png" style="zoom:25%">
|
||
|
||
|
||
|
||
当有多个方法同名的时候,只对当前类设置断点,指令格式为 `breakpoint set -n "[类名 方法名]"`。
|
||
|
||
比如通过 `breakpoint set -n "[ViewController add:withB:]"` 对 ViewController 类的 `- (NSInteger)add:(NSInteger)a withB:(NSInteger)b` 方法设置断点
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakPointsSetWithSpecificClass.png" style="zoom:25%">
|
||
|
||
如果一个方法没有参数,也可以通过 `breakpoint set -n sayHi` 的方式设置断点
|
||
|
||
|
||
|
||
#### 函数地址
|
||
|
||
在逆向,调试别人的 App 的时候我们无法知道函数名称,所以给函数地址打断点就很重要了。
|
||
|
||
`breakpoint set -a 函数地址`。注意:函数地址是需要处理的,因为 iOS 有 `ASLR 技术`。
|
||
|
||
|
||
|
||
#### 正则表达式
|
||
|
||
`breakpoint set -r 正则表达式`,效果就是给所有函数名符合正则表达式的函数,设置断点。
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakpointSetRegx.png" style="zoom:25%">
|
||
|
||
#### 动态库
|
||
|
||
`breakpoint set -s 动态库 -n 函数名`
|
||
|
||
|
||
|
||
#### 列出所有断点
|
||
|
||
`breakpoint list` 用于列出所有的断点,每个断点都有自己的编号
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakPointList.png" style="zoom:25%">
|
||
|
||
|
||
|
||
#### 断点的删除、禁用、开启
|
||
|
||
`breakpoint disable 断点编号` ,比如 `breakpoint disable 2.1 2.2` 禁用了2个断点
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakpointDisable.png" style="zoom:25%">
|
||
|
||
`breakpoint delete 断点编号`,比如 `breakpoint delete 2`。
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakpointDelete.png" style="zoom:25%">
|
||
|
||
比较奇怪,断点开启、禁用是可以跟子序号的,比如2.1 2.2,而断点删除必须是一级序号
|
||
|
||
|
||
|
||
#### 断点指令信息
|
||
|
||
`breakpoint command add 断点编号`,该指令会给断点预先设置需要执行的命令,到触发断点时,就会按照指令添加的顺序执行。
|
||
|
||
指令可以添加多个,最后以 "DONE" 结束。
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakpointAddCommand.png" style="zoom:25%">
|
||
|
||
|
||
|
||
`breakpoint command list 断点编号` 用于查看该断点下的所有指令
|
||
|
||
`breakpoint command delete 断点编号` 用于删除该断点下的所有指令
|
||
|
||
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBBreakpointCommandList.png" style="zoom:25%">
|
||
|
||
|
||
|
||
|
||
|
||
### 内存断点
|
||
|
||
在内存数据发生改变时触发
|
||
|
||
`watchpoint set variable 变量`
|
||
|
||
在 `viewDidLoad` 中 通过 `watchpoint set variable self->_age` 给 age property 设置了断点。当改变的时候就触发断点
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBWatchpoint.png" style="zoom:25%">
|
||
|
||
|
||
|
||
`watchpoint set expression 变量地址`
|
||
|
||
在 `viewDidLoad` 中 通过 `watchpoint set expression 0x00007fcd20306a60` 给 age property 设置了断点。当改变的时候就触发断点
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBWatchpointExpression.png" style="zoom:25%">
|
||
|
||
|
||
|
||
`watchpoint list`
|
||
|
||
`watchpoint disable 断点编号`
|
||
|
||
`watchpoint enable 断点编号`
|
||
|
||
`watchpoint disable 断点编号`
|
||
|
||
`watchpoint delete 断点编号`
|
||
|
||
`watchpoint command add 断点编号`
|
||
|
||
`watchpoint command list 断点编号`
|
||
|
||
`watchpoint command delete 断点编号`
|
||
|
||
|
||
|
||
### image
|
||
|
||
#### image list
|
||
|
||
`image list` 列举所加载的模块信息
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBImageList.png" style="zoom:25%">
|
||
|
||
|
||
|
||
`image list -o -f` 打印出模块的偏移地址、全路径
|
||
|
||
|
||
|
||
#### image lookup
|
||
|
||
`image lookup -t 类型`:查找某个类型的信息
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBImageLookupT.png" style="zoom:25%">
|
||
|
||
|
||
|
||
`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。
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBImageLookupAddress.png" style="zoom:25%">
|
||
|
||
`image lookup -n 符号或者函数名`:查找某个符号或者函数的位置
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLDBImageLookupSymbol.png" style="zoom:25%">
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|