Skip to content

Instantly share code, notes, and snippets.

@quink-black
Last active March 28, 2021 03:51
Show Gist options
  • Save quink-black/eb7f6dfdf68afb1939b11e474310285c to your computer and use it in GitHub Desktop.
Save quink-black/eb7f6dfdf68afb1939b11e474310285c to your computer and use it in GitHub Desktop.
如何找到虚函数地址

如何找到虚函数地址

摘要

关于虚函数的实现、虚函数表的含义,已经有很多资料介绍。这里分析一个实际问题:虚函数寻址的编译实现,或者说如何找到虚函数的地址。弄清楚这一问题,有助于理解分析通过野指针调用虚函数时的行为表现。

  • 注意:本文分析虚函数具体实现,具体实现与编译器有关。测试环境包括Android + armv8 + clang, macOS + x64 + clang.

虚函数解引用的汇编

p->call();

p是对象指针,call()是它的虚函数。看下armv8的汇编代码:

    0x5555555c44 <+64>:  ldr    x9, [x8]
    0x5555555c48 <+68>:  ldr    x9, [x9, #0x10]
    0x5555555c4c <+72>:  mov    x0, x8
    0x5555555c50 <+76>:  blr    x9

过程解释如下:

  1. x8寄存器保存的是p的值,即对象的地址

  2. ldr x9, [x8]:把虚函数表的地址保存到x9. 虚函数表的地址保存在对象的开头8个字节(armv8架构).

  3. ldr x9, [x9, #0x10]:从虚函数表的第2个索引处(0x10 / 0x8 = 2),加载call()函数的实际地址,保存到x9。虚函数表的第0个、第1个索引处,是对象的析构函数的地址。是的,有两个析构函数地址,一个只执行析构,一个执行析构 + free

  4. mov x0, x8:把p对象指针作为隐含参数,传给call()

  5. blr x9: 跳转执行call()

由此可知,调用虚函数至少有两次解引用的过程。对于p为野指针的情况,两次ldr可能crash,也可能不会,blr可能跳转到非法地址处,也可能跳转到非text段执行非法指令。注意都只是可能,所以野指针带来的问题比空指针更棘手。

打印虚函数地址

现在来考虑如何打印出虚函数的地址。根据前面的汇编代码可知,还有一个遗留问题是如何拿到虚函数表的索引值。测试发现,虚函数的指针实际是虚函数表中的偏移量,实函数的指针是真实地址。

核心代码如下:

#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

#include <string>

class Base {
public:
    virtual ~Base() = default;

    template <typename T>
    union FunPtr {
        T fun_ptr;
        ptrdiff_t offset;
    };

    virtual void call() {
        FunPtr<void (Base::*)()> u = {};
        u.fun_ptr = &Base::call;
        dump(__PRETTY_FUNCTION__, u.offset);
    }

    void dumpConcreteFun() {
        FunPtr<void(Base::*)()> u = {};
        u.fun_ptr = &Base::dumpConcreteFun;
        printf("addr of %s 0x%zx\n", __PRETTY_FUNCTION__, u.offset);
    }

protected:
    void dump(const char *fun_name, ptrdiff_t offset) {
        void *vptr = *(void **)this;
        size_t index = offset / sizeof(void *);
        void *fun_real_addr = ((void **)vptr)[index];
        printf("%s, virtual function table index %zu, real addr %p\n", fun_name,
               index, fun_real_addr);

        Dl_info info = {};
        if (!dladdr(fun_real_addr, &info)) {
            printf("dladdr failed\n");
            return;
        }
        printf(
            "dladdr symbol name %s, exact addr %p, base addr %p, pc offset "
            "from real addr 0x%lx\n",
            info.dli_sname, info.dli_saddr, info.dli_fbase,
            (long)((char *)fun_real_addr - (char *)info.dli_fbase));
    }
};

class Base2 {
public:
    virtual ~Base2() = default;

    virtual void call2() {}
};

class Child : public Base, public Base2 {
public:
    ~Child() override { printf("%s\n", __PRETTY_FUNCTION__); }

    void call() override {
        printf("this addr in %s is %p\n", __PRETTY_FUNCTION__, this);
        FunPtr<void (Child::*)()> u = {};
        u.fun_ptr = &Child::call;
        dump(__PRETTY_FUNCTION__, u.offset);
        call2();
    }

    void call2() override {
        FunPtr<void (Child::*)()> u = {};
        u.fun_ptr = &Child::call2;
        dump(__PRETTY_FUNCTION__, u.offset);
    }
};

int main(int argc, char *argv[]) {
    auto p = new Base();
    p->dumpConcreteFun();
    p->call();
    delete p;

    p = new Child();
    p->call();
    delete p;

    return 0;
}

测试结果:

addr of void Base::dumpConcreteFun() 0x1022e6910
virtual void Base::call(), virtual function table index 2, real addr 0x1022e69f0
dladdr symbol name _ZN4Base4callEv, exact addr 0x1022e69f0, base addr 0x1022e3000, pc offset from real addr 0x39f0
this addr in virtual void Child::call() is 0x7ff457c05b40
virtual void Child::call(), virtual function table index 2, real addr 0x1022e6be0
dladdr symbol name _ZN5Child4callEv, exact addr 0x1022e6be0, base addr 0x1022e3000, pc offset from real addr 0x3be0
virtual void Child::call2(), virtual function table index 3, real addr 0x1022e6c60
dladdr symbol name _ZN5Child5call2Ev, exact addr 0x1022e6c60, base addr 0x1022e3000, pc offset from real addr 0x3c60
virtual Child::~Child()

可以看到:

  • 实函数指针是真实地址

  • 虚函数的指针是虚函数表中的偏移量, virtual function table index 2

  • 虚函数真实地址减去加载地址base address,可以得到函数符号的相对偏移量。拿到的结果可以与nm输出的结果相对照:

    0000000100003910 T Base::dumpConcreteFun()
    00000001000039f0 t Base::call()         ///////////
    0000000100003a40 T Base::dump(char const*, long)
    00000001000038f0 t Base::Base()
    0000000100003980 t Base::Base()
    00000001000039c0 t Base::~Base()
    00000001000039a0 t Base::~Base()
    0000000100003a30 t Base::~Base()
    0000000100003a30 t Base2::call2()
    0000000100003b70 t Base2::Base2()
    00000001000039c0 t Base2::~Base2()
    00000001000039a0 t Base2::~Base2()
    0000000100003a30 t Base2::~Base2()
    0000000100003be0 t Child::call()      ////////////
    0000000100003c60 t Child::call2()     ////////////
    0000000100003960 t Child::Child()
    0000000100003b10 t Child::Child()
    0000000100003bb0 t Child::~Child()
    0000000100003b90 t Child::~Child()
    0000000100003d00 t Child::~Child()
    

    也可以与dladdr拿到的实际函数地址相对照。但在Android armv8环境,dladdr返回了base address,但没返回符号地址。

  • 多重继承的情况,结果也是正确的

  • 用调试器查看虚函数表

    (lldb) p *(void**)p
    (void *) $0 = 0x0000000103845078
    (lldb) x/3a 0x0000000103845078
    0x103845078: 0x0000000103844b90 vtbl`Child::~Child() at vtbl.cpp:59
    0x103845080: 0x0000000103844bb0 vtbl`Child::~Child() at vtbl.cpp:59
    0x103845088: 0x0000000103844be0 vtbl`Child::call() at vtbl.cpp:61
    

其他

虚函数在虚函数表中的索引与虚函数声明的顺序有关。调整虚函数声明的顺序,会导致ABI不兼容。如果更新依赖的动态库后,发现函数调用错乱,可以猜想引用的头文件可能与实际动态库不符,需要更新头文件重新编译。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment