【问题标题】:Set XMM register via address location for X86-64通过 X86-64 的地址位置设置 XMM 寄存器
【发布时间】:2021-11-22 08:20:53
【问题描述】:

我在内存中的某个地址有一个浮点值,我想通过使用该地址将 XMM 寄存器设置为该值。我正在使用asmjit

此代码适用于 32 位构建,并将 XMM 寄存器 v 设置为正确的值 *f

using namespace asmjit;
using namespace x86;

void setXmmVarViaAddressLocation(X86Compiler& cc, X86Xmm& v, const float* f)
{
   cc.movq(v, X86Mem(reinterpret_cast<std::uintptr_t>(f)));
}

但是,当我以 64 位编译时,我在尝试使用寄存器时遇到了段错误。这是为什么呢?

(是的,我的组装能力不是很强......请善待......我已经做了一天了......)

【问题讨论】:

  • 这会产生什么实际的机器代码?它是否可能会截断 64 位地址以适应 [disp32] 绝对寻址模式? x86-64 不能使用任意 64 位地址作为绝对直接内存操作数,除了 mov to/from RAX/EAX/AX/AL。通常,您希望对具有 2GiB 代码的静态数据使用 RIP 相对寻址。
  • 我不知道 AsmJIT,所以我宁愿不发布次优答案或只是猜测到底发生了什么。不,首先将 data 移动到 RAX 并不是我的意思,如果您最终想要 XMM0 中的数据,那么效率会降低。如果您确实需要使用 64 位绝对地址而不是普通的 RIP 相对地址来处理具有 2GiB 代码的数据(或者如果您可以将数据放在低 32 位中,则使用 32 位绝对地址,例如 Linux mmap(MAP_32BIT),您'想要mov reg, imm64,使用任何方便的整数寄存器。然后movq xmm0, [reg]
  • RIP 相对数据的意思是“相对于指令指针”——即嵌入到您的程序中。如果您动态加载数据,您可能无法使用它。
  • 我也不知道 asmjit,但是如果你想从内存中加载“a”浮点值,你应该使用movss 而不是movq(后者移动一个四字,即64 位)——两者都将剩余元素设置为零。但这可能不是您的段错误的原因。
  • 这似乎是旧版本的 asmjit,不再受支持。一般来说,JIT 编译中的规则是——如果你不知道你提供的地址是否可以通过 32 位有符号位移访问,那么不要使用这样的地址——将地址移动到注册将始终有效,所以我会推荐那。生成代码中的可访问地址通常通过标签引用。此外。当发生不好的事情时,AsmJit 总是返回一个错误,所以总是使用 ErrorHandler - 它会为你节省很多时间,尤其是在这种情况下。

标签: c++ assembly x86-64 sse asmjit


【解决方案1】:

最简单的解决方案是避免ptr() 中的绝对地址。原因是 x86/x86_64 需要 32 位位移,这对于任意用户地址并不总是可能的 - 位移是通过使用当前指令指针和目标地址来计算的 - 如果差异在有符号 32 位整数之外该指令不可编码(这是架构约束)。

示例代码:

using namespace asmjit;

void setXmmVarViaAddressLocation(x86::Compiler& cc, x86::Xmm& v, const float* f)
{
    x86::Gp tmpPtr = cc.newIntPtr("tmpPtr");
    cc.mov(tmpPtr, reinterpret_cast<std::uintptr_t>(f);
    cc.movq(v, x86::ptr(tmpPtr));
}

如果您想针对没有问题的 32 位模式优化此代码,则必须先检查目标架构,例如:

using namespace asmjit;

void setXmmVarViaAddressLocation(x86::Compiler& cc, x86::Xmm& v, const float* f)
{
    // Ideally, abstract this out so the code doesn't repeat.
    x86::Mem m;
    if (cc.is32Bit() || reinterpret_cast<std::uintptr_t>(f) <= 0xFFFFFFFFu) {
        m = x86::ptr(reinterpret_cast<std::uintptr_t>(f));
    }
    else {
        x86::Gp tmpPtr = cc.newIntPtr("tmpPtr");
        cc.mov(tmpPtr, reinterpret_cast<std::uintptr_t>(f);
        m = x86::ptr(tmpPtr);
    }

    // Do the move, now the content of `m` depends on target arch.
    cc.movq(v, x86::ptr(tmpPtr));
}

这样你可以在 32 位模式下保存一个寄存器,这总是很宝贵的。

【讨论】:

  • x86-64 可以直接使用 32 位绝对地址,以及 RIP 相对地址。像 GAS .intel_syntax noprefix 模式:movss xmm0, [RIP + 0x1234]movss xmm0, [0x401234](长 1 个字节)。因此,如果您的数据恰好处于低 32 位,但您正在 JITing 到一个不是的缓冲区,那么这是一个选项。在使用笨重且速度较慢的mov reg, imm64 回退之前,检查地址是否以一种或另一种方式直接可用可能很有用。 (除非您需要寄存器中的地址来循环遍历数组。)
  • 或 JIT 代码,如果您在调用者中有地址,则将其作为函数 arg 获取地址,而不是将其嵌入。 (不适合常量和其他东西,但对遍历数组的函数有意义。)
  • 是的,您可以使用 32 位绝对地址,在这种情况下,您可以在代码示例中添加一个额外的分支来添加对它的支持。但是,在 64 位代码中依赖绝对 32 位地址并不是很方便。避免此类问题的最佳方法是使用临时寄存器 - 无论地址如何,无论 JIT 代码重定位到何处,它都将始终有效。
  • 当然它总是有效的,但 JITing 的全部意义在于性能,如果你不尝试使 高效 asm 成为你的牺牲品。 mov reg, imm64 本身有 10 个字节长,这是一条额外的指令。更糟糕的是,从 SnB 系列 CPU 的 uop 缓存中获取具有 imm64 的 uop 需要一个额外的周期,因此它对前端吞吐量的影响可能超出您的预期。 (请参阅 Agner Fog 的微架构指南;agner.org/optimize)所以这是您希望避免的最坏情况。
  • 我已将代码更改为使用绝对地址,以防它是 32 位地址。但是,我认为讨论我们没有见过的代码的性能是没有意义的。这样的代码很可能永远不会处于循环中,所以我并没有真正看到负面影响。通常,在编写 JIT 时,您希望编写尽可能安全的代码,因为只有大量的操作系统和配置控制着安全性。如果您的地址空间是随机的,那么确保您的代码重新定位到可以相对访问传递的指针的可到达区域可能会很棘手。
猜你喜欢
  • 2016-12-25
  • 1970-01-01
  • 2011-01-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-16
  • 1970-01-01
相关资源
最近更新 更多