为啥 Solaris 汇编器生成的机器代码与这里的 GNU 汇编器不同?
Posted
技术标签:
【中文标题】为啥 Solaris 汇编器生成的机器代码与这里的 GNU 汇编器不同?【英文标题】:Why does the Solaris assembler generate different machine code than the GNU assembler here?为什么 Solaris 汇编器生成的机器代码与这里的 GNU 汇编器不同? 【发布时间】:2013-07-31 14:25:42 【问题描述】:我为 amd64 编写了这个小汇编文件。代码的作用对于这个问题并不重要。
.globl fib
fib: mov %edi,%ecx
xor %eax,%eax
jrcxz 1f
lea 1(%rax),%ebx
0: add %rbx,%rax
xchg %rax,%rbx
loop 0b
1: ret
然后我开始在 Solaris 和 Linux 上组装然后反汇编它。
Solaris
$ as -o y.o -xarch=amd64 -V y.s
as: Sun Compiler Common 12.1 SunOS_i386 Patch 141858-04 2009/12/08
$ dis y.o
disassembly for y.o
section .text
0x0: 8b cf movl %edi,%ecx
0x2: 33 c0 xorl %eax,%eax
0x4: e3 0a jcxz +0xa <0x10>
0x6: 8d 58 01 leal 0x1(%rax),%ebx
0x9: 48 03 c3 addq %rbx,%rax
0xc: 48 93 xchgq %rbx,%rax
0xe: e2 f9 loop -0x7 <0x9>
0x10: c3 ret
Linux
$ as --64 -o y.o -V y.s
GNU assembler version 2.22.90 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.22.90.20120924
$ objdump -d y.o
y.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <fib>:
0: 89 f9 mov %edi,%ecx
2: 31 c0 xor %eax,%eax
4: e3 0a jrcxz 10 <fib+0x10>
6: 8d 58 01 lea 0x1(%rax),%ebx
9: 48 01 d8 add %rbx,%rax
c: 48 93 xchg %rax,%rbx
e: e2 f9 loop 9 <fib+0x9>
10: c3 retq
生成的机器码怎么不一样? Sun as 为mov %edi,%ecx
生成8b cf
,而gas 为相同的指令生成89 f9
。这是因为在 x86 下对同一条指令进行编码的方式多种多样,还是这两种编码真的有特别的区别?
【问题讨论】:
Encoding ADC EAX, ECX - 2 different ways to encode?, What is the “.s” suffix in x86 instructions? 还相关:x86 XOR opcode differences re:reg、reg 指令的两种操作码选择、操作码中的模式(方向和大小位)以及相关问答的更多链接。 【参考方案1】:一些 x86 指令有多种编码来做同样的事情。特别是,任何作用于两个寄存器的指令都可以交换寄存器,并反转指令中的方向位。
给定的汇编器/编译器选择哪一个取决于作者选择的工具。
【讨论】:
你不能假设同一个程序集在应该接受相同语法的不同汇编程序中生成相同的机器代码,这真的很酷。 你真的不能假设它产生了什么,只要它的作用是一样的。见鬼,它可以决定将a + b
实现为a - (0 - b)
。
假设汇编器总是为我请求的指令选择最短的编码是否安全?
从技术上讲,我认为不是,但在大多数情况下,是的。这里的问题是重定位(如对其他函数的调用)。这些通常可以用 1 或 2 字节偏移量表示,但重定位要求它是 4 字节。
我记得 A86 汇编器使用这些类型的指令替代编码来生成唯一的“指纹”以检测未经授权的使用。 :-)【参考方案2】:
您没有为mov
、xor
和add
操作指定操作数大小。这会产生一些歧义。 GNU 汇编器手册i386 Mnemonics 提到了这一点:
如果指令没有指定后缀,那么 as 会尝试根据目标寄存器操作数(约定的最后一个)来填充缺失的后缀。 [...]。 请注意,这与 AT&T Unix 汇编器不兼容,后者假定缺少助记符后缀意味着操作数过长。
这意味着 GNU 汇编器选择不同 - 它会选择带有指定目标操作数的 R/M 字节的操作码(因为目标大小是已知/隐含的),而 AT&T 则选择 R/M 字节所在的操作码指定源操作数(因为隐含操作数大小)。
我已经完成了这个实验,并在您的汇编源代码中给出了明确的操作数大小,并且它不会改变 GNU 汇编器输出。但是,上面文档的另一部分,
可以通过可选的助记符后缀指定不同的编码选项。当从一个寄存器移动到另一个寄存器时,`.s' 后缀在编码中交换 2 个寄存器操作数。
哪个可以用;以下源代码,使用 GNU as
,为我创建了您从 Solaris as
获得的操作码:
.globl fib
fib: movl.s %edi,%ecx
xorl.s %eax,%eax
jrcxz 1f
leal 1(%rax),%ebx
0: addq.s %rbx,%rax
xchgq %rax,%rbx
loop 0b
1: ret
【讨论】:
您参考的手册部分指出,如果从寄存器中不清楚大小,则存在歧义,而寄存器到寄存器的移动则不是这种情况。手册指出了诸如 mov $12,(%eax) 之类的情况,其中不清楚大小的含义,因为它可能是一个字节、一个单词、一个长字或一个四字 mov。 @FUZxxl 你提到的情况,mov $..., (%...)
not 有一个“目标寄存器操作数”(根据文档)。如果没有大小(和/或对操作数大小的默认假设),它总是模棱两可。
是的。我确实提到了这个作为一个模棱两可的指令的例子。 mov %eax,%ebx 然而并不模棱两可。
这个答案的整个前半部分都是假的,似乎是基于对 x86 机器码的误解。 ModRM 字节指定both 寄存器操作数。一个在 /r 字段中,一个在 /m 字段中(模式字段指定寄存器模式,而不是内存操作数,因此没有 SIB 字节或 disp8 或 disp32)。您引用的部分并不意味着汇编器如何选择哪个操作数是/ r,哪个是/ m,仅与操作数大小有关。答案的.s
部分肯定很有趣,不过,我不知道!
@Peter Cordes:你说得对,这里是操作码歧义而不是隐式大小:0x8b
是带有 mem 或注册为 target 的 MOV
(即'store' if to-mem),而 0x8c
是带有 mem 的 MOV
或注册为 source (即,如果 from-mem 则为 'load')。对于 reg-reg 传输,有两条指令可以做同样的事情。然而,虽然 R/M 字节在这两种情况下都指定了 reg-reg(前两位设置),但操作数是相反的,0x8b
的源寄存器是0x89
的目标寄存器,反之亦然。第一个 0xCF
是 11-001-111
而第二个 0xF9
是 11-111-001
。以上是关于为啥 Solaris 汇编器生成的机器代码与这里的 GNU 汇编器不同?的主要内容,如果未能解决你的问题,请参考以下文章
汇编指令对应的机器码 ,问 为啥这个汇编指令对应的是这个机器码?