C ++虚拟表查找 - 如何搜索和替换
c++ virtual table lookup - how does it search & replace
让我们看看下面的例子:
class Base{
virtual string function1(){ return "Base - function1"; };
virtual string function2(){ return "Base - function2"; };
};
class Derived : public Base {
virtual string function2(){ return "Derived - function2"; };
virtual string function1(){ return "Derived - function1"; };
string function3() { return "Derived - function3"; };
};
虚值表结构就像
Base-vTable
-----------------------
name_of_function address_of_function
function1 &function1
function2 &function2
-----------------------
-----------------------
Derived-vTable
-----------------------
name_of_function address_of_function
function1 &function1
function2 &function2
还是
Base-vTable
-----------------------
Offset function
+0 function1
+4 function2
-----------------------
-----------------------
Derived-vTable
-----------------------
Offset function
+0 function1
+4 function2
如果是后者?那么偏移量是多少呢?它在哪里使用?
函数名:它是混乱的函数名吗?如果它被篡改了,那么基名和派生名将不匹配,虚参表查找将无法工作。编译器对所有虚函数名进行了修改,所以它必须是一个修改过的名称,这是否意味着base &如果是虚函数,派生函数也是一样的。
虚表只是函数指针的数组,就像第二个代码片段一样。编译器将对虚函数的调用转换为通过指针的调用,例如
Base * b = /* ... */;
b->function2();
被翻译成
b->__vtable[1]();
,其中我使用名称__vtable
来引用虚拟表(注意,虚拟表通常不能直接访问)。
表中表项的顺序由类中函数声明的顺序决定。记住,类定义在调用点总是可用的。
我正在解释下面的代码。我想这会让你明白
Base *p = new Derived;
p->function2();
在编译时,创建了VTable, Base类的VTable与派生类的VTable相同。我的意思是两者都有两个功能正如你在第一种情况中提到的。编译器插入代码初始化正确对象的vptr。
当编译器看到语句p->function2();时,它不会绑定到被调用的函数,因为它只知道Base对象。从Base类的VTable中可以知道function2的位置(这里是VTable中的第二个位置)。
在运行时,类派生的VTable被赋值给vtr。调用VTable中第二个位置的函数。
最简单的清除方法是查找实际实现。
考虑以下代码:
struct Base { virtual void foo() = 0; };
struct Derived { virtual void foo() { } };
Base& base();
void bar() {
Base& b = base();
b.foo(); // virtual call
}
现在,将此输入Clang的Try Out页面以获得LLVM IR:
; ModuleID = '/tmp/webcompile/_6336_0.bc'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
target triple = "x86_64-unknown-linux-gnu"
%struct.Base = type { i32 (...)** }
define void @_Z3barv() {
%1 = tail call %struct.Base* @_Z4basev()
%2 = bitcast %struct.Base* %1 to void (%struct.Base*)***
%3 = load void (%struct.Base*)*** %2, align 8
%4 = load void (%struct.Base*)** %3, align 8
tail call void %4(%struct.Base* %1)
ret void
}
declare %struct.Base* @_Z4basev()
既然我想你可能还不知道IR,让我们一点一点地复习。
首先是一些你不应该担心的事情。它标识了为其编译的体系结构(处理器和系统),以及它的属性。
; ModuleID = '/tmp/webcompile/_6336_0.bc'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
target triple = "x86_64-unknown-linux-gnu"
然后,LLVM学习类型:
%struct.Base = type { i32 (...)** }
从结构上分析了类型。所以这里我们只得到Base
将由单个元素i32 (...)**
组成:这实际上是"臭名昭著的"v表指针。为什么是这种奇怪的类型?因为我们将在v表中存储许多不同类型的函数指针。这意味着我们将拥有一个异构数组(这是不可能的),因此我们将其视为"泛型"未知元素的数组(以标记我们确保存在什么),并且在实际使用它之前,由应用程序将指针转换为适当的函数指针类型(或者更确切地说,如果我们在C或c++中,则IR要低得多)。
跳到结尾:
declare %struct.Base* @_Z4basev()
这声明了一个函数(_Z4basev
,名称是混乱的),它返回一个指向Base
的指针:在IR中引用和指针都用指针表示。
好的,让我们看看bar
(或_Z3barv
,因为它是混乱的)的定义。有趣的事情就在这里:
%1 = tail call %struct.Base* @_Z4basev()
调用base
,返回一个指向Base
的指针(返回类型总是在调用位置精确,更容易分析),这存储在一个名为%1
的常量中。
%2 = bitcast %struct.Base* %1 to void (%struct.Base*)***
一个奇怪的位强制转换,将我们的Base*
转换为指向奇怪东西的指针…实际上,我们正在获取v表指针。它没有被"命名",我们只是在类型的定义中确保它是第一个元素。
%3 = load void (%struct.Base*)*** %2, align 8
%4 = load void (%struct.Base*)** %3, align 8
首先加载v表(由%2
指向),然后加载函数指针(由%3
指向)。此时,%4
因此为&Derived::foo
。
tail call void %4(%struct.Base* %1)
最后,我们调用函数,并将this
元素传递给它,在这里明确表示。
第二种情况——假设指针占用4字节(32位机器)
函数名永远不会存储在可执行文件中(除调试外)。虚表只是一个函数指针的向量,可由运行中的代码直接访问。
在类中添加a虚函数时,编译器创建一个隐藏指针(称为v-ptr)作为类的成员。[你可以通过sizeof(类)来检查它,它增加了sizeof(指针)]此外,编译器内部在构造函数的开头添加了一些代码,将v-ptr初始化为类的v表的基偏移量。现在当这个类由其他类派生出来时这个v-ptr也由派生类派生出来。对于派生类,这个v-ptr被初始化为派生类的v表的基偏移量。而且我们已经知道,各个类的v表将存储它们各自版本的虚函数的地址。[注意,如果虚函数没有在派生类中被重写,那么函数在层次结构中的基本版本或最派生版本(对于多级继承)的地址将存储在v表中]。因此,在运行时,它只是通过这个v-ptr调用函数。因此,如果基类指针存储基类对象,则v-ptr的基类版本开始起作用。由于它指向v表的基本版本,因此将自动调用函数的基本版本。派生对象也是如此。
这实际上取决于编译器,标准没有指定内存表示如何工作。标准规定多态性必须始终有效(即使在inline
函数的情况下,就像您的一样)。您的函数可以内联,这取决于上下文和编译器的聪明程度,因此有时call
或jmp
甚至可能不会出现。然而,在大多数编译器上,最可能遇到的是第二种变体。
对于您的情况:
class Base{
virtual string function1(){ return "Base - function1"; };
virtual string function2(){ return "Base - function2"; };
};
class Derived : public Base {
virtual string function2(){ return "Derived - function2"; };
virtual string function1(){ return "Derived - function1"; };
};
假设你有:
Base* base = new Base;
Base* derived = new Derived;
base->function1();
derived->function2();
对于第一次调用,编译器将获得Base
的vftable
的地址,并调用vftable
中的第一个函数。对于第二次调用,vftable
驻留在不同的位置,因为对象实际上是Derived
类型。它搜索第二个函数,从遇到函数的vftable开始跳转到偏移量(即vftable + offset
—最有可能是4字节,但同样取决于平台)。
- 搜索并替换结构向量中的值
- 如何搜索矢量元素并替换它
- 插入到二叉搜索树中.节点替换
- 如何替换二叉搜索树中的节点
- 对 std::string 执行正则表达式搜索和替换
- Eclipse IDE,正则表达式搜索和替换
- 在 c++ 中搜索和替换 txt 文件中的字符串
- 使用格式化结果解析搜索/替换头文件
- 搜索并替换为指针
- C++:搜索并写入文件以替换字符串
- 在文本文件中搜索和替换
- 替换特定位置后出现的所有搜索字符串
- 在文件中搜索 printf 语句并替换为字符串值
- 搜索和替换字符数组 c++ 中出现的所有字符串
- 在 ARM 上快速搜索/替换 8 位数组中匹配的单个字节
- 在 C++ 中搜索和替换 c 样式字符串
- 单次通过搜索和替换
- 正则表达式搜索和替换C++中的组?
- C ++虚拟表查找 - 如何搜索和替换
- 搜索/替换boost正则表达式