2017-04-16 35 views
0

我刚刚开始学习计算机和编程的基本原理。我已经明白,在编译的程序中,生成的机器代码是特定于处理器类型和它们的指令集的。我想知道的是,我的Windows,OS X和Linux都运行在完全相同的硬件上(具体的处理器),从这个编译后的程序生成的机器代码是否会在不同的操作系统上有所不同?机器代码是否依赖操作系统,或者它是否是所有操作系统中完全相同的位和字节副本?在PC,Mac,Linux等上执行时,编译好的程序是否有不同的机器码?

回答

2

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

当然,相同处理器内核的机器代码是相同的。但是,只有代码的某个百分比是通用

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

假设即使您正在使用针对相同的CPU,但使用不同的操作系统相同的编译器版本(同一台计算机上运行的Linux后来窗)的添加是理想的假设顶级函数/源代码是相同的。

首先关闭代码的入口点可能因操作系统而异,所以连接器可能会使程序不同,对于依赖位置的代码,固定地址会以二进制形式结束,您可以调用该机器代码或不,但具体的地址可能会导致不同的指示。分支/跳转可能必须根据当然的地址进行不同的编码,但在一个系统中,您可能有一种形式的分支,另一种形式的分支可能需要一个蹦床来从一个地方到另一个地方。

然后有系统调用自己,没有理由认为系统调用操作系统之间是相同的。这可能会使代码大小不同等等,这又会导致编译器或链接器必须根据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,b中的r1,r0中的结果。lr基本上是返回地址寄存器,所以如果我们调用另一个函数,我们需要保存它(在堆栈中),同样我们将重新使用r0来调用dummy,实际上,对于这个调用约定,任何函数都可以修改/销毁r0-r3,所以编译器需要处理我们的两个参数,因为我有意使用它们,编译器可以将a + b优化成一个寄存器并将其保存在堆栈中,实际上出于性能原因毫无疑问,他们将r4保存在堆栈中,然后使用r4保存a + b,但不能在基于调用约定的函数中随意修改r4,所以任何嵌套函数都必须保留它并以找到状态返回,因此在调用其他功能时只保留一个+ b是安全的。

他们在r4中给我们的a + b总和加3,然后称之为假人。当它返回时,它们将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指令在这里有很长的范围内,但不完整的地址空间,因此,如果程序是足够大,并有大量的代码在调用者和被调用者之间,那么链接器可能不得不做更多的工作。由于不同的原因,我可以这样做。手臂有手臂和拇指模式,你必须使用特定的指令才能切换,而不是其中之一(或至少不是所有的手臂)。

如果我在虚设功能

.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,即所有死码,所以删除它。由于它是死代码,所以我们不再调用dummy,所以不需要将链接寄存器保存在堆栈上并保存参数只需添加并返回。

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); 
} 

假人是静态的,并呼吁,但这里优化,内联,所以没有呼叫它,所以外部人都不能使用它(静态),也没有任何人在这个文件内使用它,所以没有理由生成它。

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

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 

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

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

+0

指出的机器代码的结果特别感兴趣,谢谢您的回答。所以,实际上,机器码不仅取决于处理器,还取决于平台操作系统(系统调用,编译器程序,链接器程序等),所有这些都在最终的相似或不同中发挥作用机器代码将在纸上看? – mathin

+0

是的,一切都在程序的构建中发挥着作用。即使其他所有内容都是相同的,系统调用自身的期望也会因操作系统而异,基本上就是应用程序层与内核层进行通信的确切机制/指令。所以你会有这些差异。 –

+0

如果您问的是指令集是否相同,是的,假设相同的处理器指令集是相同的。就像英语的生物学课本和英语的社会研究课本使用相同的英文字母。 –

1

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

+0

感谢有关二进制文件的信息。它帮助我理解了它的一部分。我对@old_timer – mathin

相关问题