x86 中哪些 MOV 指令未使用或最少使用,可用于自定义 MOV 扩展

which MOV instructions in the x86 are not used or the least used, and can be used for a custom MOV extension

我正在 gem5 模拟器中对 X86 架构中的自定义 MOV 指令进行建模,为了测试它在模拟器上的实现,我需要使用内联汇编来编译我的 C 代码以创建一个二进制文件。但由于它是 GCC 编译器尚未实现的自定义指令,因此编译器会抛出错误。我知道一种方法是扩展 GCC 编译器以接受我的自定义 X86 指令,但我不想这样做,因为它更耗时(但以后会这样做)。

作为临时 hack(只是为了检查我的实施是否值得)。我想在模拟器中更改其基础 "micro ops" 的同时编辑已经存在的 MOV 指令,以欺骗 GCC 接受我的 "custom" 指令并进行编译。

因为它们有许多类型的 MOV 指令,可在 x86 架构中使用。因为它们是86架构中的各种MOV指令reference

因此我的问题是,哪个 MOV 指令使用最少,我可以编辑其底层微操作。假设我的工作量只包括整数,即很可能不会使用 xmm 和 mmx 寄存器,并且我的指令反映了 MOV 指令的相同实现。

您最好的选择是常规 mov,带有 GCC 永远不会自行发出的前缀。即创建一个新的 mov 编码,在任何其他 mov 前面包含一个强制性前缀。比如 lzcntrep bsr.

或者如果你正在修改 GCC 和 as,你可以添加一个新的助记符,它只使用 otherwise-invalid(在 64 位模式下)memory-source 的单字节操作码, memory-dest,以及 mov 的 immediate-source 版本。 AMD64 释放了几个操作码,包括像 AAM 这样的 BCD 指令,以及 push/pop 大多数段寄存器。 (x86-64 仍然可以 mov to/from Sreg,但每个方向只有 1 个操作码,而不是每个 Sreg 2 个用于推送 ds/pop ds 等)

Assuming my workload just includes integers i.e. most probably wont be using the xmm and mmx registers

XMM 的错误假设:GCC 积极使用 16 字节 movaps / movups 而不是一次复制 4 或 8 字节的结构。作为 small known-length memcpy 或 struct / array init 的内联扩展的一部分,在标量整数代码中找到矢量 mov 指令并不少见。此外,这些 mov 指令至少有 2 个字节的操作码(SSE1 0F 28 movaps,因此普通 mov 前面的前缀与您的想法相同)。

但是,关于 MMX 规则,您是对的。我认为现代 GCC 永远不会发出 movq mm0, mm1 或根本不使用 MMX,除非您使用 MMX 内在函数。绝对不是针对 64 位代码。

此外 mov to/from 控制寄存器 (0f 21/23 /r) or debug registers (0f 20/22 /r) 都是 mov 助记符,但 gcc 绝对不会单独发出任何一个。仅适用于 GP 寄存器操作数作为非调试或控制寄存器的操作数。所以这在技术上就是你标题问题的答案,但可能不是你真正想要的。


GCC 不解析其内联 asm 模板字符串,它只是将其包含在其 asm 文本输出中以在替换 %number 操作数后提供给汇编程序。所以 GCC 本身并不是使用内联 asm 发出任意 asm 文本的障碍。

并且您可以使用 .byte 发出任意机器代码。

也许一个好的选择是使用 0E 字节作为您将要进行 GEM 解码的特殊 mov 编码的前缀。 0E is push CS in 32-bit mode,64位模式下无效。 GCC 也永远不会发出。

或者只是一个 F2 repne 前缀; GCC 永远不会在 mov 操作码(不适用的地方)前发出 repne,只会发出 movs。 (F3 rep / repe 在 memory-destination 指令上使用时表示 xrelease,所以不要使用它。https://www.felixcloutier.com/x86/xacquire:xrelease 表示 F2 repne 是与 [ 一起使用时的 xacquire 前缀=44=]ed 指令,它不包含 mov 到内存中,所以它会被默默地忽略。)

像往常一样,不适用的前缀没有记录在案的行为,但实际上 CPU 不理解 rep / repne 的人会忽略它。一些未来的 CPU 可能会理解它意味着一些特殊的东西,而这正是你在 GEM.

上所做的

选择 .byte 0x0e; 而不是 repne; 可能是更好的选择,如果你想 防止不小心将这些前缀留在你 运行 的构建中CPU。 (在 64 位模式下它会 #UD -> SIGILL,或者在 32 位模式下通常会因为弄乱堆栈而崩溃。)但是如果你 do 希望能够 运行 真正的 CPU 上完全相同的二进制文件,具有相同的代码对齐和所有内容,然后忽略 REP 前缀是理想的。


在标准 mov 指令前使用前缀的优点是让汇编程序为您编码操作数:

template<class T>
void fancymov(T& dst, T src) {
    // fixme: imm -> mem  needs a size suffix, defeating template
    // unless you use Intel-syntax where the operand includes "dword ptr"
    asm("repne; movl  %1, %0"
#if 1
       : "=m"(dst)
       : "ri" (src)
#else
       : "=g,r"(dst)
       : "ri,rmi" (src)
#endif
       : // no clobbers
    );
}

void test(int *dst, long src) {
    fancymov(*dst, (int)src);
    fancymov(dst[1], 123);
}

(Multi-alternative 约束让编译器选择 reg/mem 目标或 reg/mem 源。实际上它更喜欢寄存器目标,即使这将花费另一条指令来执行它自己的存储,所以很糟糕。)

On the Godbolt compiler explorer,对于只允许memory-destination的版本:

test(int*, long):
        repne; movl  %esi, (%rdi)       # F2 E9 37
        repne; movl  3, 4(%rdi)      # F2 C7 47 04 7B 00 00 00
        ret

如果您希望它可用于加载,我认为您必须制作该函数的 2 个单独版本并在适当的情况下手动使用加载版本或存储版本,因为 GCC 似乎想要使用 reg ,尽可能注册。


或允许寄存器输出的版本(或另一个版本 returns 结果为 T,参见 Godbolt link):

test2(int*, long):
        repne; mov  %esi, %esi
        repne; mov  3, %eax
        movl    %esi, (%rdi)
        movl    %eax, 4(%rdi)
        ret