【问题标题】:Loading XMM registers from address location从地址位置加载 XMM 寄存器
【发布时间】:2016-12-25 11:24:01
【问题描述】:

我正在尝试使用 32 位操作系统上的 XMM0 128 位寄存器从 char 指针数组加载/存储内存。

我尝试的很简单:

int main() {
    char *data = new char[33];
    for (int i = 0; i < 32; i++)
        data[i] = 'a';
    data[32] = 0;
    ASM
    {
        movdqu xmm0,[data]
    }

    delete[] data;
}

问题是这似乎不起作用。第一次调试得到的Win32应用程序:

xmm0 = 0024F8380000000000F818E30055F158

第二次调试我得到了:

xmm0 = 0043FD6800000000002C18E3008CF158

所以这行一定有什么东西:

movdqu xmm0,[data]

我尝试改用这个:

movdqu xmm0,data

但我得到了相同的结果。

我认为问题在于我复制了地址而不是地址处的数据。但是xmm0 寄存器中显示的值对于 32 位地址来说太大了,所以它必须从另一个地址复制内存。

我还尝试了一些在互联网上找到的其他说明,但结果相同。

是我传递指针的方式还是我对 xmm 基础知识有误解?

我们将不胜感激。

即使我找到了解决方案(终于在三个小时后),我仍然想要一个解释:

ASM
    {
        push eax
        mov eax,data
        movdqu xmm0,[eax]
        pop eax
    }

为什么要将指针传递给 32 位寄存器?

【问题讨论】:

  • 注意data是一个指针。
  • 您能否尝试一下是否可以直接使用局部变量char data[33]; 而不是带有指针的new/delete,就像在[data] 的原始帖子中一样?我现在无法调试,但我认为这可能有效,因为我可以想象编译的源代码。目前让我感到困惑的是,C++ 与char *data 的区别是什么。从 C++ 的角度来看,它们看起来是等价的。我可能忽略了一些东西。 (在第二个版本中,mov eax,data 被编译为mov eax,[data],对吧?)
  • x86 没有“内存间接”寻址模式。您正在将指针加载到xmm0。由于xmm0 比指针大,因此您也在读取内存中超出指针存储位置的垃圾字节。
  • 它适用于非指针。是的,它被编译为 mov eax,[data] (Ped7g)。嗯,装配时实际上是什么“数据”?也许我误解了 ASM 如何威胁“数据”。我认为“数据”受到威胁,因为它在 C++ 中受到威胁。它似乎将“数据”变量的指针返回到“数据”指针地址,而不是像在 C++ 中那样。这个解释在我看来是合乎逻辑的
  • 推荐的方法是 mm_loadu_si128 内在函数。

标签: c++ assembly sse cpu-registers


【解决方案1】:
#include <iostream>

int main()
{
    char *dataptr = new char[33];
    char datalocal[33];
    dataptr[0] = 'a';   dataptr[1] = 0;
    datalocal[0] = 'a'; datalocal[1] = 0;
    printf("%p %p %c\n", dataptr, &dataptr, dataptr[0]);
    printf("%p %p %c\n", datalocal, &datalocal, datalocal[0]);
    delete[] dataptr;
}

输出:

0xd38050 0x7635bd709448 a
0x7635bd709450 0x7635bd709450 a

正如我们所见,动态指针data实际上是一个指针变量(0x7635BD709448处的32位或64位),包含指向堆的指针0xD38050

局部变量直接是一个 33 个字符长的缓冲区,分配在地址 0x7635BD709450

datalocal 也可用作char * 值。

我有点困惑这是什么正式的 C++ 解释。在编写 C++ 代码时,这感觉很自然,并且 dataptr[0] 是堆内存中的第一个元素(即取消引用 dataptr 两次),但在汇编程序中您会看到 dataptr 的真实性质,它是指针的地址多变的。所以你首先要通过mov eax,[data]加载堆指针=用0xD38050加载eax,然后你可以通过[eax]0xD38050的内容加载到XMM0中。

对于局部变量,没有带有其地址的变量; datalocal 符号已经是第一个元素的地址,所以 movdqu xmm0,[data] 将在那时起作用。

在“错误”的情况下你仍然可以做movdqu xmm0,[data];从 32 位变量加载 128 位不是 CPU 的问题。它只会继续读取 32 位以外的内容并读取属于其他变量/代码的另外 96 位。如果您在内存边界附近并且这是应用程序的最后一个内存页面,它将在无效访问时崩溃。


在 cmets 中多次提到对齐。这是一个有效的观点;要通过movdqu 访问内存,它应该对齐。检查您的 C++ 编译器内在函数。对于 Visual Studio,这应该可以工作:

__declspec(align(16)) char datalocal[33];
char *dataptr = _aligned_malloc(33, 16);
_aligned_free(dataptr);

关于我的 C++ 解释:也许我从一开始就搞错了。

dataptr 是 dataptr 符号的值,也就是那个堆地址。然后dataptr[0] 正在解除对堆地址的引用,访问已分配内存的第一个元素。 &amp;dataptrdataptr 值的地址。这也适用于dataptr = nullptr; 之类的语法,您将 nullptr 值存储到 dataptr 变量中,而不是覆盖 dataptr 符号地址。

对于datalocal[],访问纯datalocal 基本上没有意义,就像在datalocal = 'a'; 中一样,因为它是一个数组变量,所以您应该始终提供[] 索引。而&amp;datalocal就是这样一个数组的地址。纯 datalocal 是一个别名快捷方式,可以更轻松地使用数组等进行点数学运算,还具有 char * 类型,但如果纯 datalocal 会引发语法错误,它仍然可以编写 C++ 代码(使用&amp;datalocal 表示指针,datalocal[..] 表示元素),它完全符合dataptr 逻辑。

结论:您的示例从一开始就错了,因为在汇编语言中[data] 正在加载data 的值,这是指向new 返回的堆的指针。

这是我自己的解释,现在有C++专家会从正式的角度把它撕成碎片……:)))

【讨论】:

  • 在大多数情况下(例如passing as a function arg,或与+[] 等运算符一起使用时),数组就像指针一样工作。但是,该地址并未存储在任何地方。它更像是一个直接常数。或堆栈指针的编译时常数偏移量。但是指针变量确实实际上将指针存储在内存或寄存器中。顺便说一句,&amp;datalocal 会发出警告,但会编译为与&amp;datalocal[0] 相同的代码。 godbolt.org/g/05S5XS
  • 我认为movdqu 是非对齐访问?如果是这样,则不需要对齐。如果已知它是对齐的,那么我建议movdqa
【解决方案2】:

您的代码的问题是data 是一个指针。汇编代码movdqu xmm0,[data]data 地址处的16 个字节加载到寄存器xmm0 中。这意味着 4 或 8 个字节包含指针的值以及内存中随后的任何字节。你很幸运指针地址在内存中正确对齐,否则你会遇到分段错误。没有任何东西可以保证这种对齐。

使用自动数组char data[33]; 的替代方案将解决寻址问题(movqdu 将从数组中加载数据)但不是对齐问题,您仍然可能会遇到冲突,具体取决于编译器如何将数组与自动对齐贮存。同样,不能保证正确对齐。

您找到的解决方案可能是一个不错的方法,但与malloc() 不同,我不确定new 返回的指针是否对任何对齐都有效。

这应该适用于所有情况:

#include <stdlib.h>

int main(void) {
    char *data = malloc(33);
    for (int i = 0; i < 32; i++) {
        data[i] = 'a';
    }
    data[32] = 0;
    __asm {
        mov    eax,  data
        movdqu xmm0, [eax]
    }
    free(data);
    return 0;
}

正如 Peter Cordes 所评论的那样,对这种事情使用内在函数要好得多,即mm_loadu_si128。有两个主要原因:首先,64 位构建不支持内联汇编,因此通过使用内部函数,您的代码变得更加可移植。其次,编译器在优化内联汇编方面做得相对较差,特别是往往会进行大量无意义的内存存储和加载。编译器在优化内在函数方面做得更好,这使您的代码运行得更快(这是使用内联汇编的重点!)。

【讨论】:

  • 很抱歉没有放弃但没有15个代表:X
  • @user2377766:那应该很快 ;-)
  • 不要在 inline-asm 语句中使用 push/pop。 MSVC 读取您的 asm 并保存/恢复您使用的任何寄存器。更重要的是,根本不要为此使用 MSVC 内联汇编。使用内在函数可以获得更好的结果。
  • @PeterCordes 感谢您的建议,但我真的开始感到恶心,因为 asm 的目的不仅用于编码,还用于代码破解。我真的错过了为什么 VS2016 禁用 x64 内联汇编。有了这个世界上所有无用的东西,他们为什么要禁用编程的本质?朝这个方向发展的 22 世纪的人们可能甚至不知道编程是什么,只是 bcs 之上的一些层可能更有用?
  • @user2377766: Wonder when 'intrinsics' and C++ will be inappropriate:根据很多人的说法,它们已经在很多用例中。我讨厌现在很多软件包都是臃肿的垃圾堆,它们使用的内存和 CPU 比它们应该使用的多数百倍(例如 Web 浏览器、图形桌面)。我希望人们花更多的时间来提高软件的效率,而不是更快地生产出更多的废话。但是花时间通过手动矢量化进行微优化通常是不合理的;编译器不断改进,大规模优化很重要。
猜你喜欢
  • 1970-01-01
  • 2023-03-27
  • 1970-01-01
  • 2012-06-28
  • 1970-01-01
  • 2017-10-16
  • 1970-01-01
  • 2017-03-30
  • 2016-08-02
相关资源
最近更新 更多