打印 c++ 的虚函数表可以快速的帮助我们了解 c++ 父类与子类的 override 关系。 但是,lldb 目前却只支持常用的变量或者地址打印功能。所以,我们通过自定义 vt 实现打印虚函数表的诉求。
本文假设您已经对 lldb 相关的 API 有所了解,您可以阅读一下文章快速了解相关知识。
lldb 入坑指北(1)-给Xcode批量添加启用&禁用断点功能
lldb 入坑指北(2)- 15行代码搞定二进制与源码映射
因为 C++ 标准并没有规定虚函数如何设计,所以,本文以 Itanium ABI
标准为例进行讲解。
根据该标准,我们可以得到以下重要信息:
ptr to vtbl
)下面,我们先感受一个实际的例子。
struct A {
virtual void f();
};
struct B : A {
virtual void f();
virtual void g();
};
struct C {
virtual void h();
};
struct D : A, C {
virtual void f();
virtual void h();
};
上述 Demo 的虚函数表布局如下所示。
根据以上标准,打印虚函数工作就变得异常简单。我们只需要按照以下步骤依次进行即可实现目的。
下面,我们详细讲解一下代码的实现步骤。
PointerByteSize = 8
# 函数调用入口,假设我们在 Xcode 的 lldb 中执行了 `vt yout` 命令
def pvtable(debugger, command, result, internal_dict):
# 获取环境用于后续的代码执行
target = debugger.GetSelectedTarget()
process = target.GetProcess()
# 执行 x/a &yout 并返回结果
interpreter = lldb.debugger.GetCommandInterpreter()
returnObject = lldb.SBCommandReturnObject()
interpreter.HandleCommand('x/a &' + command, returnObject)
# print('x/a ' + command)
output = returnObject.GetOutput()
# 命令结果
# 0x7ffeefbfe350: 0x0000000103dce2b0 dsymutil`vtable for llvm::yaml::Output + 16
print("output-1: %s" % output)
# &this 会报错,需要特殊处理
# error: invalid start address expression.
# error: address expression "&this" evaluation failed
if output == None:
# 执行 x/a this 并返回结果
interpreter.HandleCommand('x/a ' + command, returnObject)
output = returnObject.GetOutput()
print("output-2: %s" % output)
if output:
# 将上述命令结果通过正则分解
groupList = re.match(r'(.*) (.*vtable) for (.*) \+ (.*)', output, re.M)
print(groupList)
print(groupList.group(0))
# 获取前面的地址信息
# 0x7ffeefbfe350: 0x0000000103dce2b0
print(groupList.group(1))
# 获取中间信息
# dsymutil`vtable
print(groupList.group(2))
# 获取类型
# llvm::yaml::Output
print(groupList.group(3))
# 获取结尾的 偏移
# 16
print(groupList.group(4))
else:
print('Oops!!!');
return;
# 将地址信息切割并取出最后一个地址,该地址即符号表的第一个函数位置
# first vtable
objAddressStr = groupList.group(1).split().pop()
print("objAddressStr: %s" % objAddressStr)
# 将地址从字符串转为 int
objAddress = int(objAddressStr, 16)
print("objAddress: %s" % objAddress)
# p (void *)*((unsigned long *)*(unsigned long *)test + 0)
# p (void *)*((unsigned long *)*(unsigned long *)&test + 0)
error = lldb.SBError()
# 获取类型
# llvm::yaml::Output
typename = groupList.group(3)
# print("typename: %s" % typename)
vtblSymbol = 'vtable for ' + typename
# print("\"" + vtblSymbol + "\"")
# 查找符合
# image lookup -r -v -s "vtable for llvm::yaml::Output"
symbols = target.FindSymbols(vtblSymbol)
for sc in symbols:
print("sc: %s" % sc)
# 获取 virtual table 的范围
startP = sc.symbol.GetStartAddress().GetLoadAddress(target)
endP = sc.symbol.GetEndAddress().GetLoadAddress(target)
# 先跳过偏移量;groupList.group(4)
# skip 16
startP = startP + 16;
while startP < endP:
# 执行 x/a objAddress,获取该地址的内容
interpreter.HandleCommand('x/a ' + str(objAddress), returnObject)
# 打印结果
# 0x103dce3a0: 0x000000010208f1e0 dsymutil`llvm::yaml::Output::getNodeKind() at YAMLTraits.cpp:838
output = returnObject.GetOutput()
print(output)
objAddress = objAddress + PointerByteSize;
startP = startP + PointerByteSize;
def __lldb_init_module(debugger, internal_dict):
debugger.HandleCommand(
'command script add vt -f pvtable.pvtable')
如下所示,通过命令将两个实例的的虚函数表进行打印。 根据两份输出,我们可以很容易得出以下信息
B
是 A
的子类 (推理过程:类B
部分函数指向了 A
的实现,如A::TEST_B()
)B
重写了TEST_A()
函数(推理过程:类A
存在TEST_A()
函数,但是 类 B
的 TEST_A()
函数指向了B::TEST_A()
)B
引入了 TEST_E()
函数 (推理过程:类A
不存在TEST_E()
函数,但是 类 B
的 TEST_A()
函数指向了B::TEST_E()
)(lldb) vt a
0x100002048: 0x00000001000011d0 ++`A::TEST_B() at main.cpp:17
0x100002050: 0x00000001000011e0 ++`A::TEST_C() at main.cpp:26
0x100002058: 0x00000001000011f0 ++`A::TEST_A() at main.cpp:27
(lldb) vt ptrB
0x100002080: 0x00000001000011d0 ++`A::TEST_B() at main.cpp:17
0x100002088: 0x00000001000011e0 ++`A::TEST_C() at main.cpp:26
0x100002090: 0x0000000100001260 ++`B::TEST_A() at main.cpp:32
0x100002098: 0x0000000100001270 ++`B::TEST_E() at main.cpp:31
(lldb)
说明:
0x100002098
该地址保存了虚函数的地址)0x0000000100001270
)B::TEST_E() at main.cpp:31
)目前业界 lldb 相关的工具非常少,目前最流行的工具库 Chisel 也主要面向 iOS 开发者提供常用的命令。 为此,作者特地分享了一些私人实用的命令,希望能帮助大家更好的进行开发和调试。
安装教程如下:
command script import /path/to/lldb.py
https://itanium-cxx-abi.github.io/cxx-abi/abi.html#layout https://releases.llvm.org/5.0.0/docs/TypeMetadata.html