我基本上是在学习如何在X86架构中编写自己的指令,但为了做到这一点,我理解了它们是如何被解码和解释成一种低级语言的。
通过举一个简单的mov指令的例子,并使用.byte符号,我想详细了解指令是如何解码的,
我的简单代码如下:
#include <stdio.h>
#include <iostream>
int main(int argc, char const *argv[])
{
int x{5};
int y{0};
// mov %%eax, %0
asm (".byte 0x8b,0x45,0xf8\n\t" //mov %1, eax
".byte 0x89, 0xC0\n\t"
: "=r" (y)
: "r" (x)
);
printf ("dst value : %d\n", y);
return 0;
} 当我使用objdump分析它是如何被分解成机器语言时,我得到了以下输出:
000000000000078a <main>:
78a: 55 push %ebp
78b: 48 dec %eax
78c: 89 e5 mov %esp,%ebp
78e: 48 dec %eax
78f: 83 ec 20 sub $0x20,%esp
792: 89 7d ec mov %edi,-0x14(%ebp)
795: 48 dec %eax
796: 89 75 e0 mov %esi,-0x20(%ebp)
799: c7 45 f8 05 00 00 00 movl $0x5,-0x8(%ebp)
7a0: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp)
7a7: 8b 45 f8 mov -0x8(%ebp),%eax
7aa: 8b 45 f8 mov -0x8(%ebp),%eax
7ad: 89 c0 mov %eax,%eax
7af: 89 45 fc mov %eax,-0x4(%ebp)
7b2: 8b 45 fc mov -0x4(%ebp),%eax
7b5: 89 c6 mov %eax,%esi
7b7: 48 dec %eax
7b8: 8d 3d f7 00 00 00 lea 0xf7,%edi
7be: b8 00 00 00 00 mov $0x0,%eax
7c3: e8 78 fe ff ff call 640 <printf@plt>
7c8: b8 00 00 00 00 mov $0x0,%eax
7cd: c9 leave
7ce: c3 ret 关于objdump的这个输出,为什么指令7aa: 8b 45 f8 mov -0x8(%ebp),%eax重复了两次,这背后有什么原因吗,或者我在使用.byte表示法时做错了什么?
发布于 2020-03-24 09:22:54
其中之一是编译器生成的,因为您要求GCC在选择寄存器时为您提供输入。这就是"r"(x)的意思。并且您在编译时禁用了优化(默认-O0),因此它实际上将x存储到内存中,然后在您的asm语句之前重新加载它。
你的代码不需要对内存的内容或者指向的地方做任何假设。
因为你使用的是89 c0 mov %eax,%eax,所以你的asm语句唯一安全的约束是输入和输出的显式寄存器约束, 强制编译器选择它。如果你在启用优化的情况下编译,你的代码完全崩溃,因为你欺骗了编译器关于你的代码实际做了什么。
// constraints that match your manually-encoded instruction
asm (".byte 0x89, 0xC0\n\t"
: "=a" (y)
: "a" (x)
);没有强制要求GCC为"m"源或"=m"目标操作数选择特定寻址模式的约束,因此您需要要求特定寄存器中的输入/输出。
如果你想对自己的mov指令进行不同于标准mov的编码,请参见which MOV instructions in the x86 are not used or the least used, and can be used for a custom MOV extension -你可能想在常规的mov操作码之前使用前缀,这样你就可以让汇编程序为你编码寄存器和寻址模式,比如.byte something; mov %1, %0。
查看编译器-生成asm输出(gcc -S,而不是.o或可执行文件的反汇编)。然后您可以看到哪些指令来自asm语句,哪些指令是由GCC发出的。
如果您没有在asm模板中显式引用某些操作数,但仍希望查看编译器选取的内容,则可以在asm注释中使用它们,如下所示:
asm (".byte 0x8b,0x45,0xf8 # 0 = %0 1 = %1 \n\t"
".byte 0x89, 0xC0\n\t"
: "=r" (y)
: "r" (x)
);而gcc会帮你填上它,这样你就可以看到它希望你读写的是什么操作数。(带有g++ -m32 -O3的Godbolt)。我把你的代码放在void foo(){}中,而不是main中,因为GCC -m32认为它需要重新对齐main顶部的堆栈。这使得代码更难理解。
# gcc-9.2 -O3 -m32 -fverbose-asm
.LC0:
.string "dst value : %d\n"
foo():
subl $20, %esp #,
movl $5, %eax #, tmp84
## Notice that GCC hasn't set up EBP at all before it runs your asm,
## and hasn't stored x in memory.
## It only put it in a register like you asked it to.
.byte 0x8b,0x45,0xf8 # 0 = %eax 1 = %eax # y, tmp84
.byte 0x89, 0xC0
pushl %eax # y
pushl $.LC0 #
call printf #
addl $28, %esp #,
ret还要注意的是,如果你编译为64位,它可能会选择%esi作为寄存器,因为printf希望它的第二个参数在那里。因此,"a"而不是"r"约束实际上很重要。
如果你给一个必须在函数调用中存活下来的变量赋值,你可以让32位的GCC使用一个不同的寄存器;然后,GCC会选择一个调用保留的寄存器,比如EBX,而不是EAX。
https://stackoverflow.com/questions/60822941
复制相似问题