【问题标题】:Why do we need "this pointer adjustor thunk"?为什么我们需要“这个指针调整器重击”?
【发布时间】:2011-03-29 17:38:23
【问题描述】:

我从here 读到了有关调整器的信息。这里引用一段话:

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

这就是调节器重击的地方 在。

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

更新

这是我的最新理解:

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

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

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

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

所以上述推理得出的结果是:

必须确保每个功能 在 vtable 中必须收到 对应的接口指针作为它的 “这个”参数。

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

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

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

【问题讨论】:

    标签: c++ com


    【解决方案1】:

    去掉问题中的 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; }
    };
    

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

    现在考虑用法:

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

    base 指针或引用调用函数时会出现问题。如果虚拟调度机制发现最终的overrider在base中,那么this指针必须引用base对象,如b-&gt;bar,其中隐含的this指针与存储在b。现在,如果最终覆盖器位于派生类中,与b-&gt;foo() 一样,this 指针必须与找到最终覆盖器的类型的子对象的开头对齐(在本例中为 derived)。

    编译器所做的是创建一段中间代码。当调用虚拟分派机制时,在分派到derived::foo 之前,中间调用获取this 指针并将偏移量减去derived 对象的开头。此操作与向下转换static_cast&lt;derived*&gt;(this) 相同。请记住,此时this 指针的类型为base,因此它最初是偏移的,这实际上返回了原始值&amp;d

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

    【讨论】:

      【解决方案2】:

      Here's 一位设计师关于 MSVC 内部结构的文章。它解释了 MSVC 实现的许多其他细节。您可能还想在 OpenRCE 上查看my article,了解它在汇编中的外观。

      【讨论】:

      • 您好,感谢您提供指向 MSVC 内部的链接。您能否提供您在 OpenRCE 上的文章的链接?谢谢。
      • 感谢您的奖励!这是文章:openrce.org/articles/full_view/23
      【解决方案3】:

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

      是的,这就是它的全部内容。

      【讨论】:

      • 感谢您的回答。我用一些想法更新了我的帖子,我想听听任何 cmets。谢谢。
      【解决方案4】:

      是的,this 对于查找对象开始的位置至关重要。你写在你的代码中:

      variable = 10;
      

      其中variable 是成员变量。首先,它属于哪个对象?它属于this指针所指向的对象。所以其实是

      this->variable = 10;
      

      现在 C++ 需要生成能够真正完成这项工作的代码 - 复制数据。为了做到这一点,它需要知道对象开始和成员变量之间的偏移量。约定是this 总是指向对象开始,所以偏移量可以是常量:

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

      【讨论】:

      • 感谢您的回答。我已经更新了我上面的帖子。任何 cmets 都深表感谢。
      【解决方案5】:

      我认为重要的是要指出,在 C++ 中没有像“接口指针”这样的实体或任何类似的实体。它充其量只是建立在受限抽象类的概念之上,但仍然是一个类。因此,适用于类成员和处理“this”的所有规则仍然适用。 因此,原则上,接口类必须像给定类型的独立类一样运行,而不管它们的功能和最终继承层次如何。

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

      【讨论】:

        猜你喜欢
        • 2014-06-05
        • 1970-01-01
        • 2019-01-23
        • 2014-04-07
        • 2016-11-14
        • 1970-01-01
        • 2013-10-18
        • 1970-01-01
        • 2011-10-04
        相关资源
        最近更新 更多