6

根据 MSDN 文档,当使用类函数的默认__thiscall调用约定时,“this”指针存储在 ECX 中。尽管在翻译常规 C++ 代码时肯定会出现这种情况,但我在尝试使用内联汇编访问“this”时遇到了问题。

这是测试程序:

#include <cstdio>

class TestClass
{
    long x;

    public:
        inline TestClass(long x):x(x){}

    public:
        inline long getX1(){return x;}
        inline long getX2()
        {
            _asm
            {
                mov eax,dword ptr[ecx]
            }
        }
};
int main()
{
    TestClass c(42);

    printf("c.getX1() = %d\n",c.getX1());
    printf("c.getX2() = %d\n",c.getX2());

    return 0;
}

两个 Get 函数翻译如下:

?getX1@TestClass@@QAEJXZ (public: long __thiscall TestClass::getX1(void)):
  00000000: 8B 01              mov         eax,dword ptr [ecx]
  00000002: C3                 ret

?getX2@TestClass@@QAEJXZ (public: long __thiscall TestClass::getX2(void)):
  00000000: 8B 01              mov         eax,dword ptr [ecx]
  00000002: C3                 ret

我认为可以肯定地说这两个功能是相同的。然而,这是程序的输出:

c.getX1() = 42
c.getX2() = 1

显然,当调用第二个 Get 函数时,“this”不会存储在 ECX 中,所以我的问题是:如何确保包含内联汇编的类函数遵循调用约定和/或以与常规/非内联相同的方式调用职能?

编辑:主要功能是这样翻译的:

_main:
  00000000: 51                 push        ecx
  00000001: 6A 2A              push        2Ah
  00000003: 68 00 00 00 00     push        offset $SG3948
  00000008: E8 00 00 00 00     call        _printf
  0000000D: 83 C4 08           add         esp,8
  00000010: 8D 0C 24           lea         ecx,[esp]
  00000013: E8 00 00 00 00     call        ?getX2@TestClass@@QAEJXZ
  00000018: 50                 push        eax
  00000019: 68 00 00 00 00     push        offset $SG3949
  0000001E: E8 00 00 00 00     call        _printf
  00000023: 33 C0              xor         eax,eax
  00000025: 83 C4 0C           add         esp,0Ch
  00000028: C3                 ret
4

5 回答 5

8

不知道你是不是看错文档了,还是写的不好,但不代表指针__thiscall存储 在ECX中;这意味着指向对象的指针 在 ECX中传递。在较大的函数中,我看到它在函数的不同位置从一个寄存器移动到另一个寄存器,在某些情况下,我看到它溢出到内存中。你不能指望它在 ECX 中。它的位置可以根据函数中的其他代码以及传递给编译器的优化标志而改变。this

在您的情况下,由于您的函数是内联的,并且可能已经内联,因此问题变得更加复杂。(除了 _asm可能会抑制内联。)常量传播(一种非常简单且广泛使用的优化技术)几乎肯定意味着您的调用c.getX1()将只使用42,没有函数调用,也无法访问c任何内容。

通常,内联汇编器是一个棘手的问题,正是因为您不知道编译器使用什么寄存器来做什么。通常,除了实际的汇编器指令外,还会有指令告诉编译器您使用哪些寄存器和哪些变量,您将能够在汇编器中引用变量本身以及其他此类信息。除非您使用这些,否则您可以对内联汇编器做出非常非常少的假设。

但是每个编译器都有自己的规则。通常使用特殊的语法。mov eax, [cx].x例如,或者mov eax, x,可能是 Microsoft 内联汇编程序需要的东西。无论如何,编译器不可能从您所写的内容中推断出您正在访问c.x. 而且由于所有其他用途都已通过常量传播消除,因此它甚至会生成一个非常糟糕的编译器 variable c

编辑:

FWIW:Microsoft 内联汇编器的文档位于 http://msdn.microsoft.com/en-us/library/4ks26t93%28v=vs.71%29.aspx。我没有详细看它,但是有一节是关于“在 __asm 块中使用 C 或 C++ 符号”。这可能会解释如何x以编译器知道x已被访问的方式访问内联汇编程序。

于 2012-08-30T10:21:59.160 回答
4

问题是,根据我对 MS 编译器的了解,编译器不知道它[ecx]与 相同this->x,因此编译器不知道正在访问成员变量(通过函数跟踪数据流很棘手)。

编译器已经优化了对对象构造函数的调用,并内联了getX1将传递给构造函数的常量。这意味着在调用时未正确构造对象,getX2因为从编译器的角度来看,该函数getX2不会访问任何成员,因此不需要正确构造它。在 MS 编译器中,我还没有看到告诉编译器正在使用成员变量的方法,即使在使用[ecx]TestClass.x.

而且,已经多次提到inline的内容通常被编译器忽略,编译器对何时最好内联代码有更好的了解。在这种情况下,带有_asm块的函数没有被内联,其他函数被内联/重写。

于 2012-08-30T10:31:33.603 回答
2

this实际上存储在ecx中,至少是如果对象没有被优化掉的地址:

00000010: 8D 0C 24           lea         ecx,[esp]

问题是优化器并不真正理解汇编代码,所以代码正确性的责任在你身上。它只是删除了该对象,因为它看到它可以将其内联42printf调用中,例如:

printf("c.getX1() = %d\n",42);

为了使它工作,定义getX2noinline

long __declspec(noinline) getX2() { ... }

这使得优化器将其视为一个完整的黑盒,因此它不会对它是否访问c对象做出任何假设。这确实对我有用,但没有记录。

相反,我建议您不要在 MSVC 中使用内联汇编,它甚至不支持 64 位编译。改用 MASM,这也将消除未来的挫败感。

于 2012-08-30T10:04:32.030 回答
1

由于您已内联该函数,编译器不知道它必须ecx在调用 asm 代码之前正确设置,尤其是当函数的其余部分不使用任何对象属性或方法时。

尝试非inline先声明和定义方法。getX2在不同的翻译单元中定义非内联非成员函数可能会更好,因此编译器的优化机会受到限制。

于 2012-08-30T09:50:12.920 回答
1

这就是我设法使函数正确工作的方法(即在 ECX 中传递“this”):

测试类.hpp

class TestClass
{
    long x;

    public:
        inline TestClass(long x):x(x){}

    public:
        long getX1();
        long getX2();
};

测试类.cpp

#include "testclass.hpp"

long TestClass::getX1()
{
    return x;
}
long TestClass::getX2()
{
    _asm
    {
        mov eax,dword ptr[ecx]
    }
}

测试main.cpp

#include <cstdio>
#include "testclass.hpp"

int main()
{
    TestClass c(42);

    printf("c.getX1() = %d\n",c.getX1());
    printf("c.getX2() = %d\n",c.getX2());

    return 0;
}

输出

c.getX1() = 42
c.getX2() = 42

问题是 MSVC 2010 中的内联类函数不一定遵循 MSDN 指定的调用约定。我不认为这是一个错误,但如果您打算在内联函数中使用内联汇编,您至少应该意识到这一点。我的建议是你不要这样做。如果您需要在类函数中进行内联汇编,请将声明和实现分开。

于 2012-08-30T11:05:22.370 回答