在這裡,我想任何人做程式設計相關的人都應該至少接觸過某種程式語言,接觸過程式的編譯,執行過自己原始碼產生的可執行檔案。對於可執行檔案我想不得不提需要關心的應該直接首先是兩個:
1.可執行檔案的產生
2.可執行檔案的執行
目標機器可執行的一段指令流和所用到的資料流。更確說,其實應該還有一些額外的資訊(我們暫時稱它為檔案資訊),但這些在不同的情況下有所差異,我們可以暫時不考慮,比如在裸機下我們往特定地址燒錄的.bin(不包含額外的資訊),cpu是可執行的。在linux系統下的elf檔案(包含很多系統執行時所需的檔案相應資訊),也是可執行的。還有我們大學時期往8位元51微控制器燒寫的.hex(包含特定的地址資訊),總的說來,檔案資訊都是更好地為指令流在特定環境下順利執行服務的。
什麼是目標機器可執行
簡單來說,就是目標機器cpu能夠正確地把一串0和1解析成正確的指令,再簡單來說就是譯碼單元可解析。實際我們cpu完成了從機器碼到自身可執行具體指令的轉換(這是需要對應的具體硬體電路有相應實現支援的)。
可執行檔案的詳細產生過程
這裡以C語言為例,這裡簡單闡述高階語言到可執行檔案的整個過程。
比如檔名叫a.c,首先我們會將.c檔案進行預處理,預處理是將很多宏做展開,條件編譯的處理,去掉註釋資訊,標頭檔案的插入。這裡不多贅述。
我們會根據C語言規則做詞法分析,語法分析,語意分析,機器無關中間程式碼產生與優化,機器相關目的碼的產生與優化。這裡我都把每個流程用一句話去簡單說明。
詞法分析:
把程式語句拆分成一個一個token。比如 a=b+5; token: a ,= , b, +, 5, ;,就把一個句子拆成一個一個詞彙
語法分析:
比如a=+;
這就過不了語法分析,這裡 =賦值符兩邊的token ,a +的性質不同。
語意分析:
float a =1.1111;
int *b = a;
語法分析只完成了表示式層次的語法檢查,但是並不知道表示式是否有意義。這裡把int型別的指標賦值做浮點的強制轉換,這就是符合語法分析,但是不符合語意分析的。
機器無關中間程式碼的產生與優化:
這裡是將語法樹轉換為中間程式碼,比如三地址碼,其實就是將樹形結構轉換為順序的一條一條表示式結構。但是又沒有具體到具體的暫存器和指令,這裡是可優化但是又機器無關的。這也是常說的編譯器的前端。
機器相關目的碼的產生與優化:
這裡是把中間程式碼轉換到組合,優化之後,轉換為目標機器程式碼,具體到具體的暫存器,具體的硬體支援。比如不同機器的暫存器數量不同,有的有乘法運算單元有的沒有,等等。
連結我想用一個比喻來說明,假設可執行檔案的產生是一個拼積木圖的過程。比如我們拼一個老虎,我們編譯實際就是區組裝這個老虎的各個部分,比如它的頭,它的尾巴.....當我們拿到已經拼好的各個部位的時候,這其實並不是一副完整的拼圖也不是一隻完整的老虎,我們還需要最後一步,去把每個部分與其他部分銜接拼成一隻完整的老虎,這種銜接頁跟積木拼接一樣需要嚴絲合縫,不是簡單的把老虎身子放在頭的下面就能草草了事,這個嚴絲合縫的銜接過程就是連結,(圖紙就是連結指令碼)由此也可以看出連結的本質——把所有的東西放在它該放的位置。編譯階段最多隻能在某個模組的內部,把它放在相對這個模組的正確位置,比如眼睛相對頭的位置肯定是可以確定的,但是頭在整副圖中的絕對位置,眼睛在整副圖中絕對位置在編譯階段是無法確定的。
上面打了個比喻,個人感覺很貼切,這裡還是比較客觀的給出連結階段所完成的事情:
符號決議
簡單來說,符號決議簡單來說,就是若干個中間檔案根據各自符號表去他人表內找到唯一存在的自己所需的外部符號的定義(當然定義肯定不是定義在表內,只是記錄在案)。當我第一次看到符號表的時候,我感覺豁然開朗,因為我突然明白,a檔案怎麼在b檔案去找到它要的東西,符號表是在中間檔案生成的,也就是編譯時生成,雖然符號決議沒有在編譯時完成,但是這個符號表卻是連結階段能夠完成符號決議的關鍵因素,實在是太巧妙了,這也回答了static 修飾的變數為什麼能夠做到外部不可存取,該變數在符號表中的屬性可以去做限制。也看到別人總結的很好:
本質上整個符號表只是想表達兩件事:
我能提供給其它檔案使用的符號
我需要其它檔案提供給我使用的符號
段整合和程式碼的重定位
將各個中間檔案的對應內容根據連結指令碼放在對應的位置(對應的段中),也就是說可執行檔案的各個段是怎麼由中間檔案組成的。確定程式執行時的地址,因為在中間檔案中所使用的函數和全域性變數地址在執行時的最終地址我們在編譯時是無法確定的,通常連結時會給出,通過引數或者連結指令碼指定,我們一般會在中間檔案中先暫時把地址設定為0,然後把這些需要重定位的函數和變數記錄在某些段中(實際上是兩個rl.txt rl.data),最後我們會去修正這些地址(在指令機器碼中修正)。
地址和空間分配
這裡我感覺是指的是劃分每段的分佈位置還有大小,就經常在連結指令碼裡看見的那樣,全域性變數,函數的地址分配。只有分配了,才能有確定的地址,有了確定的地址才能去做修正。
在總結完這些後,我更加驚訝於計算機前輩們的無處不在的睿智,而且一直以來,我自己覺得計算機問題,應該是先問自己,再參考別人。就是當我們需要做到某個功能時,你會怎麼去做,所有的解決方法和理論都是伴隨問題誕生的。這裡只是簡單討論了一個可執行程式的編譯連結過程,程其中很多重要的細節沒有講到。我會在後續的文章中繼續討論,這也是在部落格園的第一篇部落格,希望自己能一直堅持寫下去。