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

13 KiB
Raw Blame History

动态调试

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-allowtask_for_pid-allow 2个权限

    <?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

expression

expression <cmd-options> -- <expr> 用于执行一个表达式

  • <cmd-options> :命令选项
  • --:命令选项结束符,表示所有的命令选项已经设置完毕,如果没有命令选项,-- 可以省了
  • <expr> 需要执行的表达式

比如经常在断点的时候,想额外执行某个函数或者处理某个逻辑,举个例子。在 touchesMoved 方法的断点模式下,想修改 view 的背景颜色,此时不需要重新运行。利用 expression 执行指令即可

(lldb) expression self.view.backgroundColor = .red
  • expressionexpression -- 和指令 print、p、call 的效果一样

  • expression -O -- 和指令 po 效果一样。比如

堆栈信息

thread backtrace :打印线程的堆栈信息。效果等同于 bt

方法返回

thread return [<expr>] 让函数直接返回某个值,不会执行断点后面的代码

例如下面的代码,直接将函数的返回值进行修改了

frame variable

frame variable [<variable-name>] 打印当前栈帧变量

调试指令

thread continuecontinuec:程序继续运行

thred step-overnextn :单步运行,把字函数当作整体一步执行

thread step-insteps:单步运行,遇到子函数会进入子函数

thread step-outfinish:直接执行完当前函数的所有代码,返回到上一个函数

sinis n:类似

  • s n 是源码级别

  • sini 是汇编指令级别

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 符号或者函数名:查找某个符号或者函数的位置