Files
knowledge-kit/Chapter1 - iOS/1.132.md
2024-04-27 13:01:58 +08:00

519 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 动态调试
## 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%">