lldb 入坑指北(3) - 打印 c++ 实例的虚函数表
时间:2022-07-28
本文章向大家介绍lldb 入坑指北(3) - 打印 c++ 实例的虚函数表,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
前言
打印 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
) - 第三列代表代码函数所在 module的位置 + 函数所在源码位置(
B::TEST_E() at main.cpp:31
)
One More
目前业界 lldb 相关的工具非常少,目前最流行的工具库 Chisel 也主要面向 iOS 开发者提供常用的命令。 为此,作者特地分享了一些私人实用的命令,希望能帮助大家更好的进行开发和调试。
安装教程如下:
- 下载 lldb_tool
- 创建文件 ~/.lldbinit,并添加以下代码
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
- Android6.0源码分析之蓝牙显示接收到的文件
- Android中应用调用系统权限
- Android5.0以后隐式启动ServiceBug
- Android6.0源码分析之录音功能(一)
- Android6.0源码开发之修改默认音量default及max和min
- Android源码开发之添加/删除系统应用
- 按键事件处理
- Android6.0锁屏源码分析之界面布局分析
- Android6.0源码分析之menu键弹出popupwindow菜单流程分析
- Android中初步自定义view
- Android中View研究自学之路 Android6.0源码分析之View(一)Android6.0源码分析之View(二)
- Android蓝牙配对弹出框过程分析 Android蓝牙配对弹出框过程分析
- Android6.0之修改或者查看系统属性值
- linux下Android7.0多用户编译问题
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 百度黄埔学院:交通枢纽高密人流下的防疫筛查解决方案技术理解(1)
- 百度黄埔学院:交通枢纽高密人流下的防疫筛查解决方案技术理解(2)
- 百度黄埔学院:十行代码高效完成深度学习POC
- 【数据相关】目标检测中的数据标注及格式转换代码
- 科学与艺术的融合:遗传算法绘制蒙娜丽莎
- “工业听诊”中多声源事件检测与定位
- 工业党福利:使用PaddleX高效实现指针型表计读取系列文章(2)
- 【三维点云系列】PCL点云库之数据文件与IO操作
- Jvm故障处理工具
- 递增子序列
- redis学习(二)
- You-Get 使用方法
- 接口测试 Mock 实战 | 结合 jq 完成批量化的手工 Mock
- 在Angular应用的child Component里同时使用@Input和@Output
- Angular应用里的@Input和@Output注解使用方法介绍