为什么汇编代码因我使用的反汇编程序而异

Why does assembly code differ depending on the disassembler I use?

本文关键字:反汇编程序 汇编 代码 为什么      更新时间:2023-10-16

我正在自学调试汇编语言;我是组装新手。我有一个非常简单的C++程序,我使用不同的反汇编器对其进行了 3 次反汇编:GDB、otool 和 godbolt.org。GDB 和 godbolt.org 生成的代码量大致相同(字处理器中的 1 页),尽管许多行不同。otool -tv 命令生成了大约 14 页代码,因此在 GDB 和 godbolt.org 输出方面存在许多差异。程序集代码太长,无法发布。我希望汇编代码输出彼此相同。为什么它们不同,哪个拆装器最好?

这是我C++程序:

#include <iostream>
int main () {
int a = 1;
int b = 2;
int c = 3;
a += b;
a = a + c;
std::cout << "Value of A is " << a << std::endl;
return 0;
}

程序集差异的示例:

GDB:

0x0000000100000f44 <+4>:    sub    $0x30,%rsp
0x0000000100000f48 <+8>:    mov    0x10c1(%rip),%rdi        # 0x100002010
0x0000000100000f4f <+15>:   lea    0xfb6(%rip),%rsi

Godbolt.org:

sub rsp, 16
mov DWORD PTR [rbp-4], 1
mov DWORD PTR [rbp-8], 2

Otool -tv 比其他代码多 13 页,因此存在明显的差异。

您遇到的差异不在于反汇编程序,而在于用于表示机器指令的语法。

汇编是一种非常低级的语言,其中机器指令助记符之间存在一对一的映射。前者是位序列,可能是可变长度的---就像 x86 架构的情况一样。这种表示由CPU直接解释,以执行与指令语义相关的工作。汇编语言是此类序列的">人类可读"表示。

基本上,您可以找到任何方法来表示相同的机器指令。这是程序集语法

众所周知,对于x86架构,存在两种不同的语法:AT&TIntel。您从GBD获得的输出是根据AT&T语法生成的,而您从 Godbolt.org 获得的输出是Intel的。

英特尔和AT&T的语法在外观上非常不同,可能这就是为什么你一直认为结果不一样的原因。实际上,这只是表示相同指令的不同方式。

同一架构组件的这两种">方言"在诞生时就有不同的目标。AT&T语法是在AT&T实验室开发的,用于支持为许多不同的CPU生成程序(参见:Jeff Duntermann,汇编语言逐步)。当时,AT&T在计算机历史上扮演着重要角色。AT&T(贝尔实验室)一直是Unix的源头---它的范式目前(尽管部分)由Linux---C编程语言以及我们今天继续使用的许多其他基本工具所承诺。

另一方面,英特尔语法已经开发出来,嗯...由英特尔为他们自己的 CPU。许多采用英特尔语法的人说,在英特尔 CPU 上操作时,它要整洁得多。很可能是这种情况,因为语法是专门为 CPU 支持的内容精心设计的。

虽然目前不再使用AT&T语法(至少据我所知)为x86以外的CPU编写程序,但语法的一些"罪魁祸首"是由它更"通用"产生的。

那么,要学习哪一个呢?我的选择将取决于你工作的环境。整个Unix生态系统(包括Linux和Mac OS)有一个直接使用该语法的工具链(如gas)。在Linux内核(和其他低级软件)中,你肯定会找到AT&T语法的内联汇编代码,以便与硬件进行交互。另一方面,Windows系统具有使用英特尔语法的工具链(例如nasm)。虽然编译时标志可以要求这些工具切换到其他语法(例如objdump-M标志),但习惯是采用"本机"语法。

关于问题中给出的具体例子,它们是"不兼容的",因为它们指的是反汇编代码的不同部分,因此两者之间的差异程度更高。 实际上,关于此 GDB 输出:

sub    $0x30, %rsp
mov    0x10c1(%rip), %rdi
lea    0xfb6(%rip), %rsi

相应的英特尔反汇编将是:

sub    rsp, 0x30
mov    rdi, QWORD PTR [rip+0x10c1]
lea    rsi, [rip+0xfb6]

另一方面,关于 Godbolt.org 输出:

sub rsp, 16
mov DWORD PTR [rbp-4], 1
mov DWORD PTR [rbp-8], 2

相应的AT&T拆解将是:

sub    $0x10,%rsp
movl   $0x1,-0x4(%rbp)
movl   $0x2,-0x8(%rbp)

如您所见,最大的区别(可能会引起很多头痛)与以下事实有关:AT&T语法首先放置源,然后放置目的地,而英特尔语法则相反。

汇编序列不是具有不同语法的等价物,它们只是不同,可能是由于使用了不同的编译器。

第一对:

sub $0x30,%rsp             ;rsp -= 0x30
sub rsp,16                 ;rsp -= 0x10

下一对:

mov 0x10c1(%rip),%rdi      ;rdi = [rip+0x10c1]  (loads a value)
mov DWORD PTR [rbp-4],1    ;[rbp+4] = 1  (stores an immediate value)

下一对:

lea    0xfb6(%rip),%rsi    ;rsi = rip+0xfb6   (loads an offset)
mov DWORD PTR [rbp-8],2    ;[rbp+8] = 2 (stores an immediate value)

这两个序列都不完整,但我认为这并不重要,因为显示的序列已经显示了差异。

因为源代码和程序集之间没有 1 对 1 的关系。 编译器可能会为以下语句生成相同的程序集:

x = x + 1

x++;

两者都将被编译成类似的东西

add dword ptr [rdi], 1

那么,当我们拆解它时,它应该拆解成哪一个?x = x+1还是x++?这几乎适用于程序的每个语句 - 如果有多种方式来表达源语言中发生的情况,并且效果相同,编译器可以选择将它们两者转换为相同的输出。 之后,您无法知道使用了哪一个。