LLDB 动态调试任意APP

LLDB 动态调试

LLDB的Xcode默认的调试器,它与LLVM编译器一起,带给我们更丰富的流程控制和数据检测的调试功能。平时用Xcode运行程序,实际走的都是LLDB。熟练使用LLDB,可以让debug事半功倍。

LLDB 用法

指令的格式

1
2
3
4
5
6
# <command>: 命令
# <subcommand>: 子命令
# <action>: 命令操作
# <options>: 命令选项
# <arguments>: 命令参数
<command> [<subcommand> [<subcommand>]] <action> [-option [option-value]] [argument [argument...]]

比如给test函数设置断点

1
2
3
4
5
breakpoint set -n test 
# breakpoint -- <command>
# set -- <action>
# -n -- <options>
# test -- <arguments>

常用LLDB命令

help命令

1
2
help + <command>
# help breakpoint

执行表达式

1
2
3
4
5
6
7
expression <cmd-options>--<expr>
# 执行一个表达式
# <cmd-options>: 命令选项
# -- 命令选项结束符,标识所有的命令已经设置完毕,如果没有命令选项,--可以省略
# <expr>: 需要执行的表达式
# eg:
# expression self.view.backgroundColor = [UIColor redColor]

注:

expressionexpression —和指令printpcall的效果一样,执行代码并打印对象和内存地址

expression、-O — 和指令po效果一样,相当于%@ 方式打印出object.description

po $寄存器 可直接打印寄存器地址指向的内容

x/<类型> 地址 可读取内存内容 eg:x/s $x1 可读取x1寄存器的字符串

流程控制

thread backtrack (简写bt) 打印线程的堆栈信息
thread return <expr> 让函数直接返回某个值,不会执行断点后面的代码
frame variable [<variable-name>] 打印当前栈帧的变量
thread continuec : 程序继续执行
thread step-overnextn : 单步运行,把子函数当做整体一步执行
thread step-insteps : 单步运行,遇到子函数会进入子函数
thread step-outfinish :直接执行完当前函数所有代码,返回上一个函数
sinisn类似 (指令级别的单步运行)
thread step-inst-over 简写:nexti、ni
thread step-inset 简写:stepi、si
sn是源码级别
sini是汇编指令级别

断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# breakpoint set 缩写 br s
breakpoint set -a 函数地址
breakpoint set -n 函数名 (注:调试第三方程序时不能使用函数名打断点)
# breakpoint set -n test
# breakpoint set -n touchesBegan:withEvent: # 没有指定类,会将所有类中的该方法都加断点
# breakpoint set -n "-[ViewController touchesBegan:withEvent]"

breakpoint set -r 正则表达式 (会将所有符合正则表达式的方法都加断点)
breakpoint set -s 动态库 -n 函数名

breakpoint list # 列出所有的断点(每个断点都有自己的编号)
breakpoint disable 断点编号 # 禁用断点
breakpoint enable 断点编号 # 启用断点
breakpoint delete 断点编号 # 删除断点

断点命令

1
2
3
4
5
6
7
8
9
10
11
12
breakpoint command add 断点编号 # 给断点预习设置需要执行的命令,到触发断点时,就会按顺序执行
# --------eg--------
(lldb) breakpoint set -n "-[ViewController touchesBegan:withEvent:]" # 给touchesBegan方法加一个断点
(lldb) breakpoint list # 查看断点编号
(lldb) breakpoint command add 断点编号 # 给断点编号2增加命令
> po self # 断点处要执行的代码
> p self.view.backgroundColor = [UIColor redColor] # 断点处要执行的代码
> DONE # 输入DONE结束插入命令
# --------end-------

breakpoint command list 断点编号 # 查看某个断点设置的命令
breakpoint command delete 断点编号 # 删除某个断点设置的命令

内存断点

在内存数据发生改变的时候触发,开发中可以设置内存断点,然后使用bt查看调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
watchpoint set variable 变量
# 属性不可以self.age这样设置 需要self->_age
# watchpoint set variable self->_age

watchpoint set expression 内存地址
# watchpoint set expression &(self->age)
# watchpoint set expression 0x0000001012

watchpoint list # 查看断点列表
watchpoint disable 断点编号 # 设置断点disable
watchpoint enable 断点编号 # 设置断点enable
watchpoint delete 断点编号 # 删除断点
watchpoint command add 断点编号 # 设置断点命令
watchpoint command list 断点编号 # 查看断点命令
watchpoint command delete 断点编号 # 删除断点命令

模块查找

1
2
3
4
5
6
7
8
9
image lookup # 模块查找

image lookup -t 类型 # 查找某个类型的信息
# image lookup -t ViewController
image lookup -a 内存地址 # 根据内存地址查找在模块中的位置(代码行号)
image lookup -n 符号或者函数名 # 查找某个符号或者函数的位置

image list # 列出所加载的模块信息
image list -o -f # 打印出模块的偏移地址、全路径

寄存器

memory read

读取所有寄存器的值

1
2
3
4
5
6
# memory read/[数量][格式][字节数] [内存地址]
# x/[数量][格式][字节数] [内存地址]

# 格式: x->16进制; d->10进制; f->浮点数
# 字节大小: b->byte 1字节; h->half word 2字节; w->word 4字节; g->giant word 8字节
x/3xw 0x10010

memory write 寄存器 值

给某个寄存器写入值

1
2
# memory write [内存地址] [数值]
memory write 0x0000010 10

$+寄存器可直接访问对象

  • po $x0 打印方法调用者

  • x/s $x1 打印方法名

  • po $x2 打印参数 (以此类推,x3,x4也可能是参数)

    如果是非arm64, 寄存器就是r0,r1,r2

小技巧

  • 敲Enter,会自动执行上次的命令
  • 绝大多数命令都可以使用缩写

动态调试其他App

给debugserver增加权限

  • 默认情况下,/Developer/usr/bin/debugserver缺少一定的权限,只能调试Xcode安装的APP,无法调试其他APP(比如来自APP Store的APP)
  • 如果希望调试其他APP,需要对debugserver重新签名,签上2个调试相关的权限
    • get-task-allow
    • task_for_pid-allow
  • 将重签名的debugserver 放入 /usr/bin 目录

签名方法:

  • 通过ldid签名
1
2
3
$ ldid -e debugserver > debugserver.entitlements # 将debugserver权限导出
# debugserver.entitlements 新增两个权限
$ ldid -Sdebugserver.entitlements debugserver # 重新给debugserver签上权限
  • 通过codesign签名
1
2
3
4
5
6
# 查看权限信息
$ codesign -d --entitlements - debugserver
# 签名权限
$ codesign -f -s - --entitlements debugserver.entitlements debugserver
# 或者简写为
$ codesing -fs- --entitlements debugserver.entitlements debugserver

让debugserver附加到某个APP进程

1
2
3
4
5
6
7
# 方式一 将debugserver附加在一个已启动的进程上
$ debugserver IP:端口号 -a 进程
# *:端口号 使用iPhone的某个端口启动debugserver服务 *标识主机地址
# -a 进程 输入APP的进程信息 (进程ID或进程名称)

# 方式二 使用debugserver 启动App
$ debugserver -x auto *:端口号 APP的可执行文件路径

LLDB连接debugserver

  1. 启动LLDB
1
2
$ lldb
(lldb)
  1. 连接debugserver服务
1
(lldb) process connect connect: //手机IP地址: debugserver服务端口号
  1. 使用LLDB的c命令让程序先继续执行
1
(lldb) c
  1. 接下来就可以用LLDB命令调试APP了
  2. 退出连接
1
(lldb) process detach

断点调试

因调试第三方程序时不能使用函数名打断点,只能使用函数地址打断点

1
2
3
4
5
6
7
8
9
# 因ASLR原因,需要先获取偏移地址
(lldb) image list -o -f
# 打断点
breakpoint set -a 函数地址+偏移地址

# 使用$寄存器名可直接访问寄存器=寄存器的值
po $x0 # 打印方法调用者
x/s $x1 # 打印方法名
po $x2 # 打印参数 (以此类推,x3,x4也可能是参数)

遇到的问题

lldb连接debugserver时报错 error: rejecting incoming connection from ::ffff:127.0.0.1 (expecting ::1)

1
2
3
4
5
6
sirde-iPhone:~ root# debugserver_arm64 *:10011 -a neteasemusic
debugserver-@(#)PROGRAM:LLDB PROJECT:lldb-900.3.87
for arm64.
Attaching to process neteasemusic...
Listening to port 10011 for a connection from *...
error: rejecting incoming connection from ::ffff:127.0.0.1 (expecting ::1)

解决办法

使用ip地址替换 * 和localhost

手机端:

1
sirde-iPhone:~ root# debugserver 127.0.0.1:10011 -a neteasemusic

电脑端:

1
(lldb) process connect connect://127.0.0.1:10011

mac lldb提示ImportError: cannot import name _remove_dead_weakref

1
2
3
4
5
6
7
8
➜  ~ lldb
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/copy.py", line 52, in <module>
import weakref
File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/weakref.py", line 14, in <module>
from _weakref import (
ImportError: cannot import name _remove_dead_weakref

查看是否安装了python和python@2

1
brew list

remove 多余的 python@2

1
brew remove python@2 --ignore-dependencies

debugserver 在 iOS 12遇到的坑

因iOS12为不完美越狱,开发中总会遇到各种困难 kill:9,可通过如下方法解决

  1. 从/Developer/usr/bin/debugserver取出debugserver (或使用其他工具复制到电脑)

    1
    2
    cp /Developer/usr/bin/debugserver  /var/root/
    scp root@ip:/var/root/debugserver ./
  2. 对debugserver进行瘦身,抽取arm64架构二进制文件

    1
    lipo -thin arm64 debugserver  -output debugserver_arm64
  3. 复制debugserver_arm64到手机usr/bin目录 (可使用其他工具复制)

    1
    scp debugserver_arm64 root@ip:/usr/bin/debugserver_arm64
  4. 用ldid对debugserver进行签名 (也可在在电脑中签名后复制到手机)

    1
    2
    // uncOver越狱会提供debugserver.xml在/usr/share/entitlements/debugserver.xml
    ldid -S/usr/share/entitlements/debugserver.xml /usr/bin/debugserver_arm64
  5. 使用inject命令 (貌似是添加信任)

    1
    inject /usr/bin/debugserver_arm64