编译后的程序在 PC 上执行时会有不同的机器代码,Mac、Linux 等吗?

Would a compiled program have different machine codes when executed on PC, Mac, Linux etc?

我刚刚开始学习计算机和编程的基础知识。我了解到,在编译程序中,生成的机器代码特定于处理器类型及其指令集。我想知道的是,我有 Windows、OS X 和 Linux 所有 运行 在完全相同的硬件(特定于处理器)上,会从这个编译程序生成的机器代码在 OSes 中有所不同?机器代码 OS 是相关的还是它是所有 OS 中位和字节的完全相同的副本?

二进制文件通常不能跨系统移植。 Linux(和 Unix)使用 ELF executable format, macOS uses Mach-O and Windows uses PE.

你尝试的时候发生了什么?正如所回答的,支持的文件格式可能会有所不同,但您询问了机器代码。

同一个处理器核心的机器码当然是一样的。但是只有一部分代码是通用的

a=b+c:
printf("%u\n",a);

假设即使您使用针对相同 cpu 的相同编译器版本,但使用不同的操作系统(同一台计算机 运行 linux 然后稍后 windows)假设顶层 function/source 代码相同,则添加在理想情况下是相同的。

首先代码的入口点可能从一个 OS 到另一个不同,所以 linker 可能会使程序不同,对于位置相关的代码,固定地址将在二进制文件,您可以调用或不调用该机器码,但具体地址可能会导致不同的指令。 branch/jump 当然可能需要根据地址进行不同的编码,但在一个系统中,您可能有一种形式的分支,另一种可能需要蹦床才能从一个地方到达另一个地方。

然后是系统调用本身,没有理由假设操作系统之间的系统调用是相同的。这会使代码的大小等发生变化,这可能再次导致编译器或 linker 必须根据某些指令集的 jmp 目标的远近或地址是否可以是,做出不同的机器代码选择编码为立即数,还是您必须从附近的位置加载它然后间接分支到那个位置。

编辑

早在您开始 ponder/worry 了解同一平台或目标上不同操作系统上发生的情况之前。了解将程序组合在一起的基础知识,以及哪些东西可以更改机器代码。

很简单program/function

extern unsigned int dummy ( unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
    dummy(a+b+3);
    return(a+b+7);
}

先编译再反汇编

00000000 <fun>:
   0:   e92d4010    push    {r4, lr}
   4:   e0804001    add r4, r0, r1
   8:   e2840003    add r0, r4, #3
   c:   ebfffffe    bl  0 <dummy>
  10:   e2840007    add r0, r4, #7
  14:   e8bd4010    pop {r4, lr}
  18:   e12fff1e    bx  lr

那里实际上发生了很多事情。这是手臂,全尺寸(还不是拇指……)。 a 参数在 r0 中输入,b 在 r1 中,结果在 r0 中输出。 lr 基本上是 return 地址寄存器,所以如果我们调用另一个函数,我们需要将其保存(在堆栈上)同样,我们将重新使用 r0 来调用 dummy,事实上,根据这个调用约定,任何函数can modify/destroy r0-r3,所以编译器将需要处理我们的两个参数,因为我故意使用它们的方式与编译器可以将 a+b 优化到寄存器并将其保存在堆栈中的方式相同,实际上出于性能原因,毫无疑问,他们将 r4 保存在堆栈上,然后使用 r4 保存 a+b,您不能根据调用约定在函数中随意修改 r4,因此任何嵌套函数都必须保留它并且 return 使其处于 as found 状态,因此在调用其他函数时将 a+b 留在那里是安全的。

他们将 3 添加到我们在 r4 中的 a+b 总和并调用 dummy。当它 returns 时,他们将 7 加到 r4 中的 a+b 总和和 r0 中的 return 中。

从机器代码的角度来看,这还没有 linked 并且 dummy 是一个外部函数

   c:   ebfffffe    bl  0 <dummy>

我称它为 dummy 是因为当我们在这里使用它时,它除了 return 之外什么都不做,一个 dummy 函数。那里编码的指令显然是错误的分支到 fun 的开头是行不通的,这不是我们要求的递归。所以让 link 吧,至少我们需要声明一个 _start 标签来让 gnu linker 开心,但我想做的不止于此:

.globl _start
_start
    bl fun
    b .

.globl dummy
dummy:
    bx lr

和link输入地址0x1000产生了这个

00001000 <_start>:
    1000:   eb000001    bl  100c <fun>
    1004:   eafffffe    b   1004 <_start+0x4>

00001008 <dummy>:
    1008:   e12fff1e    bx  lr

0000100c <fun>:
    100c:   e92d4010    push    {r4, lr}
    1010:   e0804001    add r4, r0, r1
    1014:   e2840003    add r0, r4, #3
    1018:   ebfffffa    bl  1008 <dummy>
    101c:   e2840007    add r0, r4, #7
    1020:   e8bd4010    pop {r4, lr}
    1024:   e12fff1e    bx  lr

linker通过修改调用它的指令为dummy填写了地址,所以你可以看到机器码已经改变了。

    1018:   ebfffffa    bl  1008 <dummy>

取决于事物的距离或其他因素可以改变这一点,这里的bl指令有一个很长的范围但不是完整的地址space,所以如果程序足够大并且有很多调用者和被调用者之间的代码那么 linker 可能需要做更多的工作。由于不同的原因,我可以造成这种情况。手臂有手臂和拇指模式,你必须使用特定的指令才能切换,bl 不是其中之一(或者至少不是所有手臂)。

如果我在虚拟函数前面加上这两行

.thumb
.thumb_func
.globl dummy
dummy:
    bx lr

然后强制汇编程序生成缩略图指令并将虚拟标签标记为缩略图标签

00001000 <_start>:
    1000:   eb000001    bl  100c <fun>
    1004:   eafffffe    b   1004 <_start+0x4>

00001008 <dummy>:
    1008:   4770        bx  lr
    100a:   46c0        nop         ; (mov r8, r8)

0000100c <fun>:
    100c:   e92d4010    push    {r4, lr}
    1010:   e0804001    add r4, r0, r1
    1014:   e2840003    add r0, r4, #3
    1018:   eb000002    bl  1028 <__dummy_from_arm>
    101c:   e2840007    add r0, r4, #7
    1020:   e8bd4010    pop {r4, lr}
    1024:   e12fff1e    bx  lr

00001028 <__dummy_from_arm>:
    1028:   e59fc000    ldr r12, [pc]   ; 1030 <__dummy_from_arm+0x8>
    102c:   e12fff1c    bx  r12
    1030:   00001009    andeq   r1, r0, r9
    1034:   00000000    andeq   r0, r0, r0

因为在这种情况下 BX 需要切换模式,有趣的是手臂模式,虚拟是拇指模式,linker 非常好地为我们添加了一个蹦床功能,我称之为弹跳到从乐趣变成假人。 link 寄存器 (lr) 包含一个位,它告诉 return 上的 bx 要切换到哪个模式,因此那里没有额外的工作来修改虚拟函数。

如果内存中的两个函数之间有很大的距离,我希望 linker 也能为我们修补它,但只有尝试才能知道。

.globl _start
_start:
    bl fun
    b .


.globl dummy
dummy:
    bx lr


.space 0x10000000

唉,好吧

arm-none-eabi-ld -Ttext=0x1000 v.o so.o -o so.elf
v.o: In function `_start':
(.text+0x0): relocation truncated to fit: R_ARM_CALL against symbol `fun' defined in .text section in so.o

如果我们把一加变成一减:

extern unsigned int dummy ( unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
    dummy(a-b+3);
    return(a+b+7);
}

而且变得更复杂

00000000 <fun>:
   0:   e92d4070    push    {r4, r5, r6, lr}
   4:   e1a04001    mov r4, r1
   8:   e1a05000    mov r5, r0
   c:   e0400001    sub r0, r0, r1
  10:   e2800003    add r0, r0, #3
  14:   ebfffffe    bl  0 <dummy>
  18:   e2840007    add r0, r4, #7
  1c:   e0800005    add r0, r0, r5
  20:   e8bd4070    pop {r4, r5, r6, lr}
  24:   e12fff1e    bx  lr

他们不能再优化 a+b 结果所以更多的堆栈 space 或者在这个优化器的情况下,将其他东西保存在堆栈上以便在寄存器中腾出空间。现在你问为什么将 r6 压入堆栈?是不是被修改了?这个 abi 需要一个 64 位对齐的堆栈,这意味着压入四个寄存器来保存三个东西或者压入三个东西然后修改堆栈指针,因为这个指令集压入四个东西比获取另一条指令并执行它更便宜。

如果出于某种原因外部函数变为本地函数

void dummy ( unsigned int )
{
}
unsigned int fun ( unsigned int a, unsigned int b )
{
    dummy(a-b+3);
    return(a+b+7);
}

再次改变了事情

00000000 <dummy>:
   0:   e12fff1e    bx  lr

00000004 <fun>:
   4:   e2811007    add r1, r1, #7
   8:   e0810000    add r0, r1, r0
   c:   e12fff1e    bx  lr

既然 dummy 不使用传递的参数并且优化器现在可以看到它,那么没有理由浪费指令减去和添加 3,那都是死代码,所以删除它。我们不再调用 dummy,因为它是死代码,所以不需要在堆栈上保存 link 寄存器并保存参数,只需执行加法和 return.

static void dummy ( unsigned int x )
{
}
unsigned int fun ( unsigned int a, unsigned int b )
{
    dummy(a-b+3);
    return(a+b+7);
}

制作虚拟机 local/static 但没有人使用它

00000000 <fun>:
   0:   e2811007    add r1, r1, #7
   4:   e0810000    add r0, r1, r0
   8:   e12fff1e    bx  lr

上次实验

static unsigned int dummy ( unsigned int x )
{
    return(x+1);
}
unsigned int fun ( unsigned int a, unsigned int b )
{
    unsigned int c;
    c=dummy(a-b+3);
    return(a+b+c);
}

dummy 是静态的并被调用,但它在这里被优化为内联,所以没有调用它,所以外部人员不能使用它(静态),这个文件内部的任何人都不能使用它,所以有没有理由生成它。

编译器检查所​​有操作并对其进行优化。 a-b+3+1+a+b = a+a+4 = (2*a)+4 = (a<<1)+4; 为什么他们使用左移而不是仅仅添加 r0、r0、r0,不知道可能在管道中移位更快,或者它可能是无关紧要的,并且两者中的任何一个都一样好,编译器作者选择了这种方法,或者也许有点通用的内部代码解决了这个问题,在它进入后端之前,它已经被转换成一个班次而不是一个加法。

00000000 <fun>:
   0:   e1a00080    lsl r0, r0, #1
   4:   e2800004    add r0, r0, #4
   8:   e12fff1e    bx  lr

用于这些实验的命令行

arm-none-eabi-gcc -c -O2 so.c -o so.o
arm-none-eabi-as v.s -o v.o
arm-none-eabi-ld -Ttext=0x1000 v.o so.o -o so.elf
arm-none-eabi-objdump -D so.o
arm-none-eabi-objdump -D so.elf

关键是您可以自己做这些简单的实验,并开始了解编译器和 linker 在何时何地对机器代码进行修改的情况,如果您愿意的话想一想。然后意识到当我添加非静态虚拟函数(fun() 函数现在被更深地推入内存)时我在这里显示了什么,因为您添加了更多代码,例如从一个操作系统到下一个操作系统的 C 库可能会改变或者除了系统调用外可能大部分相同,因此它们的大小可能不同,导致其他代码可能在更大的地方移动 puts() 可能导致 printf() 位于不同的地址,所有其他因素保持不变。如果不喜欢静态,那么毫无疑问会有差异,只是用于在 linux 上查找 .so 文件或在 windows 上查找 .dll 的文件格式和机制解析它,连接点之间的运行时应用程序中对共享库的系统调用。应用程序 space 中共享库本身的文件格式和位置将导致 link 使用特定操作存根编辑的二进制文件不同。然后最终是实际的系统调用本身。