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 的虚函数表布局如下所示。

img

根据以上标准,打印虚函数工作就变得异常简单。我们只需要按照以下步骤依次进行即可实现目的。

  • 通过实例指针找到对应的类型
  • 通过该类型找到唯一的虚函数表
  • 遍历虚函数表,并打印对应的函数指针

实现代码

下面,我们详细讲解一下代码的实现步骤。

    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[2] 也主要面向 ios 开发者提供常用的命令。为此,作者特地分享了一些私人实用的命令,希望能帮助大家更好的进行开发和调试。

安装教程如下:

  • 下载 lldb_tool[3]

  • 创建文件 ~/.lldbinit,并添加以下代码

    command script import /path/to/lldb.py

扩展阅读

TypeMetadata[4]

广告时间

参考资料

[1]

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 快速创建个人博客系统

Shell 脚本踩坑指北

避坑指北CentOS 6源码编译安装Python3.7+

react Hook踩坑指北—一文解决你所有关于setState的疑惑

babel实践:真实gulp项目支持ES6转译ES5的跳坑指北

Rust LLDB 调试入门指北