gcc - x86 中哪些 MOV 指令未使用或使用最少,可用于自定义 MOV 扩展
问题描述
我在 gem5 模拟器中对 X86 架构中的自定义 MOV 指令进行建模,为了在模拟器上测试它的实现,我需要使用内联汇编编译我的 C 代码以创建二进制文件。但由于它是一条自定义指令,在 GCC 编译器中没有实现,编译器会报错。我知道一种方法是扩展 GCC 编译器以接受我的自定义 X86 指令,但我不想这样做,因为它更耗时(但之后会这样做)。
作为临时黑客(只是为了检查我的实施是否值得)。我想在模拟器中更改其底层“微操作”的同时编辑已经 MOV 指令,以欺骗 GCC 接受我的“自定义”指令并进行编译。
因为它们是 x86 架构中可用的多种 MOV 指令。因为它们是 86 架构参考中的各种 MOV 指令。
因此,我的问题是,哪条 MOV 指令使用最少,我可以编辑它的底层微操作。假设我的工作量只包括整数,即很可能不会使用 xmm 和 mmx 寄存器,并且我的指令反映了 MOV 指令的相同实现。
解决方案
您最好的选择是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 进行解码。 0E
在push CS
32 位模式下,在 64 位模式下无效。GCC 也永远不会发射。
或者只是一个 F2repne
前缀;GCC 永远不会在操作码(它不适用的地方)repne
前面发出,只有. (F3 /在用于内存目标指令时表示 xrelease,所以不要使用它 。https ://www.felixcloutier.com/x86/xacquire:xrelease 表示 F2 repne 是与ed 指令一起使用时的 xacquire 前缀,它不包含在内存中,因此它将在那里被默默地忽略。)mov
movs
rep
repe
lock
mov
像往常一样,不适用的前缀没有记录的行为,但实际上 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