2017-04-16 39 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

相關問題