跨平台程序集 ((x64 || x86) && (Microsoft x64 || SystemV))

Posted

技术标签:

【中文标题】跨平台程序集 ((x64 || x86) && (Microsoft x64 || SystemV))【英文标题】:Cross-platform assembly ((x64 || x86) && (Microsoft x64 || SystemV)) 【发布时间】:2019-02-25 17:53:24 【问题描述】:

我正在尝试编写一些代码,以了解更多关于汇编和 JIT 编译器之类的知识。到目前为止,我已经能够提出一个 XOR 函数,理论上它应该可以在 Windows 和 Linux 环境中的 x86 或 x64 机器上工作。

假设我理解正确,[RE]AX 寄存器用于保存整数返回值,而[RE]DX 是用于在函数之间传递整数的可用寄存器之一。我选择不严格遵循 ABI 并使用 [RE]AX 传递第一个参数,因为它保存了 MOV 指令而不影响结果。

是否有更好(更优雅或更高效)的方式来生成跨平台程序集,或者我在开发此程序时是否犯了任何错误?

#include <cstdint>
#include <iostream>

template<typename TInput>
static auto Xor(TInput const highPart, TInput const lowPart) 
    constexpr bool is16Bit = (std::is_same<TInput, int16_t>::value || std::is_same<TInput, uint16_t>::value);
    constexpr bool is32Bit = (std::is_same<TInput, int32_t>::value || std::is_same<TInput, uint32_t>::value);
    static_assert(is16Bit || is32Bit, "type must be a member of the type family: [u]int16, 32_t");

    if constexpr (is16Bit) 
        uint16_t result;

        #if (defined(__linux__) || defined(__unix__) || defined(_WIN32))
            asm volatile ("xorw %%dx, %%ax;" : "=a" (result) : "a" (highPart), "d" (lowPart));
        #else
            #error "Unsupported platform detected."
        #endif

        return result;
    
    else if constexpr (is32Bit) 
        uint32_t result;

        #if (defined(__linux__) || defined(__unix__) || defined(_WIN32))
            asm volatile ("xorl %%edx, %%eax;" : "=a" (result) : "a" (highPart), "d" (lowPart));
        #else
            #error "Unsupported platform detected."
        #endif

        return result;
    


#define HIGH_PART 4;
#define LOW_PART 8;

int main() 
    int16_t const a = HIGH_PART;
    int16_t const b = LOW_PART;
    int16_t const c = Xor(a, b);

    uint32_t const x = HIGH_PART;
    uint32_t const y = LOW_PART;
    uint32_t const z = Xor(x, y);

    std::cout << c << "\n";
    std::cout << z << "\n";
    getchar();

    return 0;

以下是如何改进的示例;通过“提升”result 变量和if defined(...) 检查在constexpr 检查之上,我们可以使事情更通用。

template<typename T>
static auto Xor(T const highPart, T const lowPart) 
    constexpr bool is16Bit = (std::is_same<T, int16_t>::value || std::is_same<T, uint16_t>::value);
    constexpr bool is32Bit = (std::is_same<T, int32_t>::value || std::is_same<T, uint32_t>::value);
    static_assert(is16Bit || is32Bit, "type must be a member of the type family: [u]int16, 32_t");

    #if !(defined(__linux__) || defined(__unix__) || defined(_WIN32))
        #error "Unsupported platform detected."
    #endif

    T result;

    if constexpr (is16Bit) 
        asm volatile ("xorw %%dx, %%ax;" : "=a" (result) : "a" (highPart), "d" (lowPart));
    
    else if constexpr (is32Bit) 
        asm volatile ("xorl %%edx, %%eax;" : "=a" (result) : "a" (highPart), "d" (lowPart));
    

    return result;

【问题讨论】:

int16_t c = a^b; 和 gcc 用于目标平台。我的意思是我不确定你在问什么:a)在处理算术的原始机器代码中,win / linux之间没有区别,那是在ABI和服务调用中(这里无关)b)不清楚64b在哪里进入这个以及如何,到目前为止,Q.c)xorw 的编码方式因当前 CPU 模式(16b 模式与 32b/64b 模式)不同而不同,我认为没有明智的方法可以同时满足这两种模式......尝试同时满足 32 和 64 目标平台时会出现类似问题,那么您的目标是什么? @Ped7g 使用哪些寄存器的机器没有区别,但如果我希望我生成的程序“与其他人一起玩”,那么我应该坚持目标平台的 ABI,不?这里的目标是最终发出机器代码,而不是使用 GCC 编译 XOR 表达式。标题中列出了我的潜在目标:x64、x86、Linux 和 Windows。 @Ped7g 正如第一句话所述,我正在获得知识和理解;不是试图击败现代编译器(还没有!),只是想体验他们处理的所有不同的复杂性。 我也不确定那些 if 16b else if 32b ...为什么不简单地定义两个具有适当参数类型的函数并让编译器根据使用的类型选择正确的一个...在现代 x86 世界中,即使使用 16b 值,执行 xorl 也更有意义(从性能的角度来看),这取决于它们的来源以及将它们读入寄存器的方式以及在 xor 之后如何继续计算。 .. 但在现代 x86 中,每个单个操作编译的整个概念没有多大意义,这将带来整体解决方案的解释器级性能。 对不起,我离开这里,我不明白你的问题..也许其他人会更好地理解你。 :) 【参考方案1】:

您不能让编译器在 64 位模式下在 EAX/RAX 中传递函数 arg。在 32 位模式下,您可以使用 gcc "regparm" 调用约定,如 __attribute__((regparm(3))) int my_func(int,int); 以该顺序在 EAX、ECX、EDX 中传递参数。 (因此编译器需要在 EAX 中具有函数 arg 的内联汇编之前的 mov)。

或者您可以使用 __attribute__((sysv_abi)) 声明您的函数以始终使用 SysV ABI,即使在 Windows 上编译时也是如此。但这只有在所有调用者都由 GCC/clang/ICC 而不是 MSVC 编译的情况下才有效。而且在 32 位模式下更糟; i386 System V 调用约定是废话:传递堆栈上的所有参数,并且在 edx:eax 中只返回 int64_t,而不是 2 成员 64 位结构。

调用sysv_abi 函数也可能是一个ms_abi 函数来保存/恢复所有xmm6..15,除非sysv_abi 函数调用可以内联和优化掉。所以总的来说,如果该函数还没有大量使用 XMM regs 并保存/恢复其中的大部分,那么这可能是一个糟糕的计划。


使用固定寄存器输入/输出约束通常没有用,除非您使用带有隐式寄存器的指令(例如,如果您不能使用 BMI2 shlx / shrx,则在 cl 中进行移位计数) .

让编译器使用"r""+r" 约束进行寄存器分配。 (或"=r""0" 匹配约束)因此您的函数可以有效地内联,而不管值在哪里。对于可以是寄存器或 32 位立即数的输入,也可以使用 "re"。甚至"rem" 用于也可以是内存的输入。但是,如果您重复使用输入,最好让编译器在 asm 之前为您加载它。

另见https://***.com/tags/inline-assembly/info

硬编码寄存器分配部分违背了使用内联 asm 而不是编译器必须调用而不是内联的独立 asm 函数的目的。

查看编译器为您的代码生成的 asm 以了解它生成的周围代码,以及它如何通过选择操作数填充模板。

还要注意"r" 为 16 位类型选择 16 位寄存器,为 32 位类型选择 32 位寄存器,所以所有这些类型调整的东西基本上是不必要的。 (尽管取决于您的输入是如何写入的,使用 32 位 xor 可能比 16 位 xor 更好,如果稍后读取完整的 32 位或 64 位寄存器,可能会避免部分寄存器停顿。但是如果您的输入寄存器是使用 16 位操作数大小编写,然后在 P6 系列 CPU 上,32 位异或会创建部分寄存器停顿。)您可以覆盖为 "xor %0" 模板替换填充的大小,用 "%k0" 替换 32 -位大小等。见x86 Operand Modifiers in the GCC manual。

【讨论】:

已在godbolt 中进行测试,使用与我的本地计算机相同的设置,它似乎完全按照我的要求生成。只是好奇这是如何工作的,因为你的第一句话让我相信它不应该...... @Kittoes0124:我说的是 function 参数。内联后,您没有使用任何函数参数,只是 mov-immediate 的编译时常量。因此,您所看到的只是内联汇编击败持续传播的效果。 (另请注意,您在 Godbolt 上使用 intel-syntax 模式编译,因此您的 asm 是编译器生成的 Intel 语法和您的一行 AT&T 的混合体。那不会汇编。)无论如何,godbolt.org/z/YXo6ZW 显示了一个测试调用者将自己的函数 args 传递给 Xor()。并显示删除 volatile 让两个 Xor(a,b) 调用 CSE 以重用一个。 啊,谢谢;这极大地解决了问题。也许更好的问题是“我怎样才能更彻底地打败编译器?”我的目标是在我打败它后尝试实现诸如持续传播之类的东西。 @Kittoes0124:使用 gcc,您可以使用 if(__builtin_constant_p(a)) 询问编译器您的输入是否为编译时常量。 (如果是这样,请使用纯 C 将常量传播留给编译器。)使用 asm 基本上没有希望这样做,除非您使用 asm .if 宏来检查操作数是数字还是寄存器名称,带有字符串比较宏条件。不过,这听起来很傻。

以上是关于跨平台程序集 ((x64 || x86) && (Microsoft x64 || SystemV))的主要内容,如果未能解决你的问题,请参考以下文章

x64 应用程序可以使用 x86 程序集吗?反之亦然?

无法为 x64 和 x86 加载文件或程序集 'CefSharp.Wpf;只有一个作品

『开源重编译』System.Data.SQLite.dll 自适应 x86 x64 AnyCPU 重编译

DllImport 自动选择x64或x86 dll

x86平台转x64平台关于内联汇编不再支持的解决

X86和X86_64和X64有什么区别?