《程式設計師的自我修養》(一)——編譯與靜態連結

吳尼瑪發表於2017-12-31

簡介

溫故而知新

  • 電腦科學領域的任何問題都可以通過增加一個間接地中間層來解決。
  • 在UNIX中,硬體裝置的訪問形式跟訪問普通的檔案形式一樣;在Windows系統中,圖形硬體被抽象成了GDI,聲音和多媒體裝置被抽象成了DirectX物件,磁碟被抽象成了普通檔案系統,等等。
  • 如何將計算機上有限的實體記憶體分配給多個程式使用。整個想法是這樣的,我們把程式給出的地址看作是一種虛擬地址,然後通過某些對映的方法,將這個虛擬地址轉換成實際的實體地址。
  • 程式的對映方式:
    • 分段,基本思路是把一段與程式需要的記憶體大小的虛擬空間對映到某個地址空間。
    • 分頁,基本方法是把地址空間人為地等分成固定大小的頁,每一頁的大小又硬體決定,或硬體支援多種大小的頁,由作業系統選擇決定頁的大小。目前幾乎所有的PC上的作業系統都使用4KB大小的頁。幾乎所有的硬體都是採用一個叫MMU的部件來進行頁對映。
    • 在頁對映模式下,CPU發出的Virtual Address,即我們的程式看到的是虛擬地址。經過MMU轉換以後就變成了Physical Address。一般MMU都整合在CPU內部了,不會以獨立的部件存在。

《程式設計師的自我修養》(一)——編譯與靜態連結

靜態連結

編譯和連結

  • 預編譯(預處理):預編譯過程主要處理那些原始碼檔案中的以“#”開始的預編譯指令。C檔案預編譯後形成.i檔案,C++檔案編譯後副檔名是.ii。
  • 編譯:編譯過程就是把預處理完的檔案進行一系列詞法分析、語法分析、語義分析及優化後生產相應的彙編程式碼檔案,編譯後生成.s檔案。
  • 彙編:彙編器是將彙編程式碼轉變成機器可以執行的指令,經過彙編後生成.o檔案。
  • 連結:連結的主要內容就是把各個模組之間相互引用的部分都處理好,使得各個模組之間能夠正確地銜接。連結過程主要包括了地址和空間分配、符號決議和重定位等步驟。最終生成可執行檔案。

《程式設計師的自我修養》(一)——編譯與靜態連結

  • 編譯過程一般可以分為6步:掃描、語法分析、語義分析、原始碼優化、程式碼生成和目的碼優化。
    • 詞法分析:首先原始碼程式被輸入到掃描器,掃描器的任務很簡單,它只是簡單地進行詞法分析,運用一種類似於有限狀態機的演算法可以很輕鬆地將原始碼的字元序列分割成一系列的記號。
    • 語法分析:接下來語法分析器將對由掃描器產生的記號進行語法分析,從而產生語法樹(以表示式為節點的樹)。
    • 語義分析:編譯器所能分析的語義是靜態語義,所謂靜態語義是指在編譯期可以確定的語義,靜態語義通常包括宣告和型別的匹配,型別的轉換。
    • 中間語言生成:現代的編譯器有著很多層次的優化,往往在原始碼級別會有一個優化過程。原始碼優化器往往將整個語法樹轉換成中間程式碼,它是語法樹的順序表示,其實它已經非常接近目的碼了。
    • 目的碼生成與優化:程式碼生成器將中間程式碼轉換成目標機器程式碼,這個過程十分依賴於目標機器,因為不同的機器有著不同的字長、暫存器、整數資料型別和浮點數資料型別等。最後目的碼優化器對上述的目的碼進行優化,比如選擇合適的定址方式、使用位移來代替乘法運算、刪除多餘的指令等。

目標檔案裡有什麼

  • 現在PC平臺流行的可執行檔案格式主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format),它們都是COFF(Common file format)格式的變種。
  • 目標檔案中包括機器指令程式碼、資料、符號表、除錯資訊、字串等。一般目標檔案將這些資訊按不同屬性,以“段”的形式儲存。
  • 程式原始碼編譯後的機器指令經常被放在程式碼段裡,程式碼段常見的名字有“.code”或“.text”;全域性變數和區域性靜態變數資料經常放在資料段,資料段的一般名字都叫“.data”。
  • .bss段只是為未初始化的全域性變數和區域性靜態變數預留位置而已,它並沒有內容,所以它在檔案中也不佔據空間

《程式設計師的自我修養》(一)——編譯與靜態連結

  • ELF目標檔案的檔案頭描述了整個檔案的基本屬性,包括檔案是否可執行、是靜態連結還是動態連結及入口地址、目標硬體、目標作業系統、程式入口地址等。緊接著就是ELF檔案各個段,後面是與段有關的重要結構——段表,段表其實是一個描述檔案中各個段的陣列。段表描述了檔案中各個段的段名、段的長度、在檔案中的偏移位置、讀寫許可權及段的其他屬性等。 。
  • 編譯器還會將一些輔助性的資訊,諸如符號、重定位資訊等也按照段的方式存放在目標檔案中。

《程式設計師的自我修養》(一)——編譯與靜態連結

靜態連結

  • 現在的連結器空間分配的策略都是採用一種叫兩步連結的方法。

    • 第一步,空間與地址分配。掃描所有的輸入目標檔案,獲得它們的各個段的長度、屬性和位置,並且將輸入目標中的符號表中所有的符號定義和符號引用收集起來,統一放到一個全域性符號表。
    • 第二步,符號解析與重定位。使用上面第一步中收集到的所有資訊,讀取輸入檔案中段的資料、重定位資訊,並且進行符號解析與重定位、調整程式碼中的地址等。這一步是連結過程的核心,特別是重定位過程。
  • 對於可重定位的ELF檔案來說,它必須包含有重定位表,用來描述如何修改相應的段裡的內容。對於每個要重定位的ELF段都有一個對應的重定位表,而一個重定位表往往就是ELF檔案中的一個段,所以其實重定位表也可以叫重定位段。

  • 連結器的COMMON塊機制解決同一個符號定義在多個檔案中的問題(目前的連結器本身並不支援符號的型別,即變數型別對於連結器來說是透明的,它只知道一個符號的名字,並不知道型別是否一致)。這個問題解決的規則是,如果是弱符號(未初始化的全域性變數)則在最終連結後的輸出檔案中,該符號所指的變數大小以輸入檔案中最大的那個為準。如果其中有一個符號為強符號,那麼最終輸出結果中的符號所佔空間與強符號相同。

  • 如果要使兩個編譯器編譯出來的目標檔案能夠互相連結,那麼這兩個目標檔案必須滿足下面這些條件:採用同樣的目標檔案格式、擁有同樣的符號修飾標準、變數的記憶體分佈方式相同、函式的呼叫方式相同,等等。其中我們把符號修飾標準、變數記憶體佈局、函式呼叫方式等這些跟可執行程式碼二進位制相容性相關的內容稱為ABI(Application Binary Interface)。

  • 一個靜態庫可以簡單地看成一組目標檔案的集合,即很多目標檔案經過壓縮打包後形成的一個檔案。

  • VISUAL C++允許使用指令碼來控制整個連結過程,這種控制指令碼叫做模組定義檔案,它們的副檔名一般為.def。

Windows PE/COFF

  • 在Windows平臺,VISUAL C++編譯器產生的目標檔案使用COFF格式,而可執行檔案為PE格式。微軟對64位Windows平臺上的PE檔案結構稍微做了一些修改,這個新的檔案格式叫做PE32+。新的PE32+並沒有新增任何結構,最大的變化就是把那些原來32位的欄位變成64位。
  • COFF檔案是由檔案頭及後面的若干個段組成,再加上檔案末尾的符號表、除錯資訊的內容,就構成了COFF檔案的基本結構。COFF檔案的檔案頭部包括了兩部分,一個是描述檔案總體結構和屬性的映像頭,另外一個是描述該檔案中包含的段屬性的段表。
  • “.drectve 段”實際上是“Directive”的縮寫,它的內容是編譯器傳遞給連結器的指令,即編譯器希望告訴連結器應該怎樣連結這個目標檔案。
  • COFF檔案中所有以“.debug”開始的段都包含著除錯資訊。比如“.debug$S”表示包含的是符號相關的除錯資訊段;“.debug$P”表示包含預編譯標頭檔案相關的除錯資訊段;“.debug$T”表示包含型別相關的除錯資訊段。
  • PE檔案是基於COFF的擴充套件,它比COFF檔案多了幾個結構。最主要的變化有兩個:第一個是檔案最開始的部分不是COFF檔案頭,而是DOS MS可執行檔案格式的檔案頭和樁程式碼;第二個變化是原來的COFF檔案頭中的“IMAGE_FILE_HEADER”部分擴充套件成了PE檔案頭結構“IMAGE_NT_HEADERS”,這個結構包括了原來的“Image Header”及新增的PE擴充套件頭部結構。

《程式設計師的自我修養》(一)——編譯與靜態連結

相關文章