lldb 入坑指北 - 打印 c++ 实例的虚函数表
Posted 酷酷的哀殿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了lldb 入坑指北 - 打印 c++ 实例的虚函数表相关的知识,希望对你有一定的参考价值。
前言
准备工作
本文假设您已经对 lldb 相关的 API 有所了解。
虚函数表的原理
因为 C++ 标准并没有规定虚函数如何设计,所以,本文以 Itanium ABI
标准为例进行讲解。
根据 Itanium cxx abi - layout[1],我们可以得到以下重要信息:
-
每个类的虚函数表都是唯一的。 -
每个类的实例都会携带一个隐藏的指针,该指针会指向该类的虚函数表( 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
) -
第三列代表 代码函数所在 module 的位置 + 函数所在源码位置( B::TEST_E() at main.cpp:31
)
One More
目前业界 lldb 相关的工具非常少,目前最流行的工具库 Chisel[2] 也主要面向 ios 开发者提供常用的命令。为此,作者特地分享了一些私人实用的命令,希望能帮助大家更好的进行开发和调试。
安装教程如下:
-
下载 lldb_tool[3]
-
创建文件
~/.lldbinit
,并添加以下代码command script import /path/to/lldb.py
扩展阅读
TypeMetadata[4]
广告时间
参考资料
Itanium cxx abi - layout: https://itanium-cxx-abi.github.io/cxx-abi/abi.html#layout
[2]Chisel: https://github.com/facebook/chisel
[3]lldb_tool: https://github.com/sunbohong/lldb_tool
[4]TypeMetadata: https://releases.llvm.org/5.0.0/docs/TypeMetadata.html
以上是关于lldb 入坑指北 - 打印 c++ 实例的虚函数表的主要内容,如果未能解决你的问题,请参考以下文章
三分钟入坑指北 🔜 Docsify + Serverless Framework 快速创建个人博客系统
react Hook踩坑指北—一文解决你所有关于setState的疑惑