繁体   English   中英

为什么我们需要“this pointer adjustor thunk”?

[英]Why do we need "this pointer adjustor thunk"?

我从这里读到有关调节器的信息。 这是一些引述:

现在,只有一个 QueryInterface 方法,但是有两个条目,每个 vtable 一个。 请记住,vtable 中的每个 function 都接收相应的接口指针作为其“this”参数。 这对 QueryInterface (1) 来说很好; 它的接口指针与对象的接口指针相同。 但这对 QueryInterface (2) 来说是个坏消息,因为它的接口指针是 q,而不是 p。

这就是调节器的用武之地。

我想知道为什么“ vtable 中的每个 function 都接收相应的接口指针作为其“this”参数”? 它是接口方法用于在 object 实例中定位数据成员的唯一线索(基址)吗?

更新

这是我最新的理解:

其实我的问题不是这个参数的用途,而是为什么要用对应的接口指针作为这个参数。 抱歉我的含糊不清。

除了将界面指针用作对象布局中的定位器/立足点之外。 当然还有其他方法可以做到这一点,只要您是组件的实现者。

但是对于我们组件的客户来说,情况并非如此。

当组件以 COM 方式构建时,我们组件的客户对我们组件的内部结构一无所知。 客户端只能获取接口指针,而 this 正是将作为this参数传递到接口方法中的指针 在这种期望下,编译器只好根据这个特定的this指针来生成接口方法的代码。

因此,上述推理导致的结果是:

必须确保 vtable 中的每个 function 都必须接收相应的接口指针作为其“ this ”参数。

在“this pointer adjustor thunk”的情况下,单个 QueryInterface() 方法存在 2 个不同的条目,换句话说,可以使用 2 个不同的接口指针来调用 QueryInterface() 方法,但编译器只生成 1 个副本查询接口()方法。 因此,如果其中一个接口被编译器选择为 this 指针,我们需要将另一个接口调整为所选择的接口。 这就是这款调节器的诞生之处。

顺便说一句,如果编译器可以生成 2 个不同的 QueryInterface() 方法实例怎么办? 每一个都基于相应的接口指针。 这不需要调整器 thunk,但需要更多空间来存储额外但相似的代码。

BTW-2:似乎有时一个问题从实现者的角度缺乏合理的解释,但从用户的角度可以更好地理解。

从这个问题收走了COM的一部分, this指针调节thunk是一段代码,使得确保每个函数获取this指针指向的具体类型的子对象。 该问题出现了多重继承,其中基础对象和派生对象未对齐。

请考虑以下代码:

struct base {
   int value;
   virtual void foo() { std::cout << value << std::endl; }
   virtual void bar() { std::cout << value << std::endl; }
};
struct offset {
   char space[10];
};
struct derived : offset, base {
   int dvalue;
   virtual void foo() { std::cout << value << "," << dvalue << std::endl; }
};

(并且无视初始化的缺乏)。 derivedbase子对象不与对象的开始对齐,因为在[1]之间存在offset 当一个指向derived的指针被转换为指向base的指针(包括隐式转换,但没有重新解释会导致UB和潜在死亡的转换)时,指针的值会被偏移,因此(void*)d != (void*)((base*)d)对于derived类型的假定对象d

现在考虑一下用法:

derived d;
base * b = &d; // This generates an offset
b->bar();
b->foo();

base指针或引用调用函数时会出现问题。 如果虚拟调度机制发现最终置换器是在base ,则指针this必须提及的base对象,如b->bar ,其中,所述隐式this指针是存储在同一地址b 现在,如果最终覆盖在派生类中,与b->foo()this指针必须与找到最终覆盖的类型的子对象的开头对齐(在本例中为derived )。

编译器所做的是创建一段中间代码。 当调用虚拟调度机制时,在调度到derived::foo ,中间调用接受this指针并将偏移量减去derived对象的开头。 此操作与downcast static_cast<derived*>(this) 请记住,此时, this指针的类型为base ,因此它最初是偏移的,这有效地返回原始值&d

[1] 即使在接口的情况下也存在偏移 - 在Java / C#意义上:仅定义虚拟方法的类 - 因为它们需要将表存储到该接口的vtable。

是一篇关于MSVC内部设计师的文章。 它解释了MSVC实现的许多其他细节。 您可能还想查看我在OpenRCE上关于它在装配中的外观的文章。

它是接口方法用于在对象实例中定位数据成员的唯一线索(基址)吗?

是的,这就是真的。

是的, this对于找到对象的起始位置至关重要。 你写下你的代码:

variable = 10;

其中variable是成员变量。 首先,它属于哪个对象? 它属于this指针指向的对象。 实际上就是这样

this->variable = 10;

现在C ++需要生成能够实现工作的代码 - 复制数据。 为了做到这一点,它需要知道对象start和成员变量之间的偏移量。 惯例是, this始终指向对象的开始,因此偏移量可以是常量:

*(reinterpret_cast<int*>( reinterpret_cast<char*>( this ) + variableOffset ) ) = 10; //assuming variable is of type int

我认为重要的是要指出在C ++中没有“接口指针”这样的实体或者接近它的任何东西。 它最多建立在受限制的抽象类的概念上,但仍然是一个类。 因此,适用于班级成员和处理“此”的所有规则仍然适用不变。 所以主要是接口类必须表现为给定类型的独立类,而不管它们的功能和最终的继承层次结构。

我们可以使用虚方法调用机制来获取(接口)基类公开的对象的实际(动态类型)。 它是如何完成的是具体实现,包括虚拟方法表和“调整器thunks”等概念。 通常编译器可以使用其初始的'this'指针来定位VMT,然后使用给定函数的实际实现,并通过最终调整'this'指针来调用它。 如果基类的内存布局不同于在多重继承的情况下我们持有的引用的派生布局,则通常需要thunk调整来执行最终调用。

没有足够的代表发表评论,但 OP 中的链接已损坏。 我假设内容现在在这里可用:

https://devblogs.microsoft.com/oldnewthing/20040206-00/?p=40723

任何人都可以更新 OP 并删除这个答案吗?

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM