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')

效果展示

如下所示,通过命令将两个实例的的虚函数表进行打印。 根据两份输出,我们可以很容易得出以下信息

  • BA 的子类 (推理过程:类B 部分函数指向了 A的实现,如A::TEST_B()
  • B 重写了TEST_A() 函数(推理过程:类A 存在TEST_A() 函数,但是 类 BTEST_A() 函数指向了B::TEST_A()
  • B 引入了 TEST_E() 函数 (推理过程:类A 不存在TEST_E() 函数,但是 类 BTEST_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