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

时间:2017-04-16 17:34:55

标签: compilation cross-platform machine-code

我刚刚开始学习计算机和编程的基础知识。我已经意识到,在编译的程序中,生成的机器代码特定于处理器及其指令集的类型。我想知道的是,我说Windows,OS X和Linux都运行在完全相同的硬件上(特定处理器),从这个编译程序生成的机器代码是否会因操作系统而异?是否依赖于机器代码OS,或者它是否是所有操作系统中位和字节的完全相同的副本?

2 个答案:

答案 0 :(得分:2)

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

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

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

假设即使你使用相同的编译器版本针对相同的cpu但使用不同的操作系统(运行linux的同一台计算机,然后是后来的Windows),假设顶级功能/源代码相同,添加理想情况相同。

首先关闭代码的入口点可能因OS而异,因此链接器可能会使程序不同,对于位置相关的代码,固定地址最终会以二进制形式存在,您可以调用该机器代码,但具体地址可能会导致不同的指令。分支/跳转可能必须根据当然的地址进行不同的编码,但在一个系统中,您可能有一种形式的分支,另一个可能需要蹦床才能从一个地方到另一个地方。

然后有系统调用自己,没有理由认为操作系统之间的系统调用是相同的。这可能会使代码的大小不同等,这可能导致编译器或链接器必须根据jmp目标对某些指令集的接近程度或远程程度来选择不同的机器代码,或者可以将地址编码为立即数或你是否必须从附近的位置加载它然后间接分支到那个。

修改

在您开始思考/担心同一平台或目标上的不同操作系统上发生的情况之前很久。了解将程序放在一起的基础知识,以及可以改变机器代码的类型。

一个非常简单的程序/功能

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中输入r,在r1中输入b,在r0中输出。 lr基本上是返回地址寄存器,所以如果我们调用另一个函数我们需要保存它(在堆栈上)同样我们将重新使用r0来调用dummy,实际上这个调用约定任何函数都可以修改/销毁r0-r3,所以编译器需要处理我们的两个参数,因为我故意以相同的方式使用它们,编译器可以将a + b优化到寄存器并将其保存在堆栈中,实际上出于性能原因无疑是毫无疑问的,他们将r4保存在堆栈上,然后使用r4保存a + b,你不能在基于调用约定的函数中随意修改r4,所以任何嵌套函数都必须保留它并在找到状态下返回它,因此,在调用其他功能时,只需将+ b留在那里是安全的。

他们在r4中为我们的a + b和加3,并调用dummy。当它返回时,它们在r4中的a + b和中加7,并在r0中返回。

从机器代码的角度来看,这还没有链接,虚拟是一个外部功能

   c:   ebfffffe    bl  0 <dummy>

我称之为虚拟,因为当我们在这里使用它时,它只会返回一个虚函数。在那里编码的指令明显错误的分支到乐趣的开头不会起作用,这不是我们要求的递归。所以让我们链接它,至少我们需要声明一个_start标签以使gnu链接器满意,但我想做更多的事情:

.globl _start
_start
    bl fun
    b .

.globl dummy
dummy:
    bx lr

并且输入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

链接器通过修改调用它的指令填充虚拟地址,因此您可以看到机器代码已更改。

    1018:   ebfffffa    bl  1008 <dummy>

根据事物的距离或其他因素可以改变这一点,这里的bl指令有一个很长的范围而不是完整的地址空间,所以如果程序足够大并且调用者之间有很多代码和然后,链接器可能需要做更多的工作。由于不同的原因,我可以导致这一点手臂有手臂和拇指模式,你必须使用特定的指令才能切换,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在这种情况下需要切换模式而乐趣是手臂模式而虚拟是拇指模式,链接器非常好地为我们添加了一个蹦床功能,我称之为反弹以从乐趣变为虚拟。链接寄存器(lr)包含一个位,告诉bx返回哪个模式要切换到,所以没有额外的工作来修改虚函数。

如果内存中的两个函数之间有很大距离,我希望链接器也能为我们打补丁,但是在你尝试之前你永远都不会知道。

.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结果以便更多的堆栈空间,或者在这个优化器的情况下,将其他东西保存在堆栈中以便在寄存器中腾出空间。现在你问为什么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

由于虚拟不使用传递的参数,优化器现在可以看到它,因此没有理由浪费指令减去和添加3,即所有死代码,所以删除它。我们不再调用虚拟,因为它是死代码所以不需要将链接寄存器保存在堆栈上并保存参数只需添加并返回。

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

制作虚拟本地/静态而没有人使用它

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 <&lt; 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

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

答案 1 :(得分:1)

二进制文件通常不能跨系统移植。 Linux(和Unix)使用ELF可执行格式,macOS使用Mach-O,Windows使用PE