首页 > 解决方案 > x86 中哪些 MOV 指令未使用或使用最少,可用于自定义 MOV 扩展

问题描述

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

作为临时黑客(只是为了检查我的实施是否值得)。我想在模拟器中更改其底层“微操作”的同时编辑已经 MOV 指令,以欺骗 GCC 接受我的“自定义”指令并进行编译。

因为它们是 x86 架构中可用的多种 MOV 指令。因为它们是 86 架构参考中的各种 MOV 指令。

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

标签: gccx86inline-assemblymachine-codegem5

解决方案


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

或者,如果您正在修改 GCC 和as,您可以添加一个新的助记符,该助记符仅对mov. AMD64 释放了几个操作码,包括像 AAM 这样的 BCD 指令,以及推送/弹出大多数段寄存器。(您仍然mov可以往返 Sreg,但每个 Sreg 不会浪费 1 个操作码。)

假设我的工作量只包括整数,即很可能不会使用 xmm 和 mmx 寄存器

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

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

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


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

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

也许一个不错的选择是使用一个0E字节作为特殊mov编码的前缀,您将专门对 GEM 进行解码。 0Epush CS32 位模式下,在 64 位模式下无效。GCC 也永远不会发射。

或者只是一个 F2repne前缀;GCC 永远不会在操作码(它不适用的地方)repne前面发出,只有. (F3 /在用于内存目标指令时表示 xrelease,所以不要使用它 。https ://www.felixcloutier.com/x86/xacquire:xrelease 表示 F2 repne 是与ed 指令一起使用时的 xacquire 前缀,它不包含在内存中,因此它将在那里被默默地忽略。)movmovsreprepelockmov

像往常一样,不适用的前缀没有记录的行为,但实际上 CPU 不理解rep/repne忽略它。一些未来的 CPU 可能会将其理解为特殊的含义,而这正是您使用 GEM 所做的。

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


在标准指令前使用前缀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);
}

(多替代约束让编译器选择 reg/mem 目标或 reg/mem 源。在实践中,它更喜欢寄存器目标,即使这将花费另一条指令来进行自己的存储,所以这很糟糕。)

在 Godbolt 编译器资源管理器上,对于只允许内存目标的版本:

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

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


或者使用允许寄存器输出的版本(或将结果返回为的另一个版本T,请参阅 Godbolt 链接):

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

推荐阅读