llvm学习(十八):lldb脚本demo

从零开始的 lldb 脚本开发。系列第四篇(估计是最后一篇),介绍一些常用情景。

简易测试

根据之前的介绍,有 IDE 加持查阅 API 会很方便,但是每次都要改 python 文件、重新加载、查看效果,非常费劲。lldb 支持交互式的 python Interpreter,输入 script 即可进入,并且自动绑定下面 lldb 这几个变量,可以快速测试 API 的效果。

  • lldb.debugger
  • lldb.target
  • lldb.process
  • lldb.thread
  • lldb.frame

举例:测试 lldb.frame.regs 的输出效果。

1
2
b main
r
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> print(lldb.frame.regs)
General Purpose Registers = {
rax = 0x0000000100003fa0
rbx = 0x0000000000000000
rcx = 0x00007ffeefbff670
rdx = 0x00007ffeefbff510
........
}
Floating Point Registers = {
fctrl = 0x037f
......
}
Exception State Registers = (trapno = 0x00000003, err = 0x00000000, faultvaddr = 0x0000000100003fa0)
>>>

访问寄存器

访问PC、SP

PC、SP是最常用的寄存器,断点触发的情况下有效,断点触发时可以获取到栈帧 lldb.SBFrame。断点触发时注册 stop-hook 可以拿到 lldb.SBExecutionContext,否则就只能通过 lldb.debugger 一层一层去找。

示范如下:

1
2
frame:lldb.SBFrame = lldb.debugger.GetSelectedTarget().GetProcess().GetSelectedFrame()
print(f"PC:{frame.pc}, SP:{frame.sp}")

访问通用寄存器

同样,lldb.SBFrame信息里包含全部寄存器的信息,lldb.SBFrame.GetRegisters 返回 lldb.SBValueList,lldb 将寄存器分为了三类(可能更多或更少),General Purpose RegistersFloating Point RegistersException State Registers,每类都是一个 lldb.SBValue ,里面存放数个寄存器条目,底层使用 c++ 指针。

python 的 API 只提供一个迭代器,这个设计下,使用列表、没有使用字典,导致直接访问指定名字的寄存器非常麻烦,例如访问 rax,需要使用如下的代码:

1
2
3
4
5
frame:lldb.SBFrame = lldb.debugger.GetSelectedTarget().GetProcess().GetSelectedFrame()
for reg in frame.register['General Purpose Registers']:
if reg.name == 'rax':
print(f"rax = {reg.value:#x}")
break

或者使用 python 高阶函数

1
2
frame:lldb.SBFrame = lldb.debugger.GetSelectedTarget().GetProcess().GetSelectedFrame()
next((x.value for x in frame.register['General Purpose Registers'] if x.name == 'rax'), None)

访问内存

可以通过 lldb.SBTarget 或者 lldb.SBProcess 访问内存,二者 API 是不一致的,前者 API 需要构造,后者的 API 更加友好。例如有:

  • ReadMemory
  • WriteMemory
  • ReadCStringFromMemory
  • ReadUnsignedFromMemory
  • ReadPointerFromMemory

举例:

1
2
3
4
5
6
7
8
>>> error = lldb.SBError()
>>> lldb.process.ReadMemory(0, 4, error)
>>> error.success
False
>>> lldb.process.ReadMemory(0x100003fa0, 4, error)
b'UH\x89\xe5'
>>> error.success
True

执行lldb命令

单纯执行命令、不考虑执行情况和返回值的话,可以直接使用 lldb.SBDebugger.HandleCommand

需要获取执行情况和返回值,需要用到 lldb.SBDebugger.GetCommandInterpreter 拿到 SBCommandInterpreter,调用 lldb.SBCommandInterpreter.HandleCommand

1
2
3
res = lldb.SBCommandReturnObject()
status = lldb.debugger.GetCommandInterpreter().HandleCommand('bt', res)
print(f"{status=}, {res.Succeeded()=}, {res.GetOutput()=}, {res.GetError()=}")

处理错误

lldb 抛出异常的情况较少,提供的 API 经常需要传递新建的 lldb.SBError 用来检测 API 是否运行异常,一般情况下,SBError success 时,API 返回真实的数据,SBError fail 时,API 返回一个 None。

实际开发中,需要注意处理异常的 case,例如上面的访问内存就有用到 lldb.SBError

小demo

效果:使用 demo.py 文件,每次断点断下来时,打印 sp 的值。

1
command script import /tmp/demo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from typing import Dict

import lldb

class StopHook:
def __init__(self, target: lldb.SBTarget, extra_args: lldb.SBStructuredData, _dict: Dict):
print("Construct StopHook")

def handle_stop(self, exe_ctx: lldb.SBExecutionContext, stream: lldb.SBStream):
print(f"SP={exe_ctx.frame.sp:#x}")


def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict):
debugger.HandleCommand("target stop-hook add -P demo.StopHook")

结语

lldb 整体的设计是非常优雅的,结构合理,兼容性好,开发中最大的困难就是对 API 的不够熟悉,很多功能不知道要调用什么 API,明明自带的命令有这种功能,但却找不到对应的 API。然后就得去翻源码找线索,整个过程比较消耗精力和消耗耐心。

最后,我计划为 pwndbg 添加 lldb 的支持,有想一起开发的小伙伴欢迎联系我,需要 py3 编码严谨、简洁、规范。(估计没人。。。)