快樂Linux —— 6. 編譯鏈接裝載

參考:

《程序員自我修養》《深入理解計算機系統》

https://blog.csdn.net/baidu_41667019/category_8404116.html

https://www.tutorialspoint.com/compiler_design/compiler_design_overview.htm

關於鏈接詳細可以看看這兩個博文:

https://blog.csdn.net/baidu_41667019/article/details/84789564 符號和符號解析

https://blog.csdn.net/baidu_41667019/article/details/84872940 重定位

簡述

​ 整個程序從寫好到執行過程如下,下面簡單介紹這個過程

預處理

  • 展開並刪除所有#define

  • 處理所有條件編譯

  • 將#include頭文件插入

  • 刪除所有的註釋

  • 添加行號和文件名標識,以便產生調試用的行號信息和用於編譯器產生編譯錯誤信息

  • 保留#pragma ,因爲編譯器要使用

    !!!!.c .cpp 經過預處理器後生成 .i 文件

編譯

在編譯原理這本書裏,編譯過程最終輸出有三種:

  1. 輸出使用絕對地址的機器語言程序。
  2. 輸出可重定位的機器語言程序。
  3. 輸出彙編程序。

一般而言,常見編譯器都是生成彙編代碼,所以下文我們默認爲第三種情況進行討論。

在整個過程中符號表錯誤處理程序貫穿整個編譯過程。

下面就表達式 array[index] = (index + 4) * (2 + 6) 做簡要分析。

  1. 詞法分析 有限狀態機

    將源代碼中的字符掃描爲一個個記號(token),記號一般可分爲以下幾類: 關鍵字標識符字面量特殊符號(運算符號)。將分析到的符號放到合適的地方(標識符放到符號表,字面量放到文字表等)

  2. 語法分析 上下文無關語法

    將掃描到的表達式生成語法樹,判斷語法是否正確。

  3. 語義分析

    • 分析語義是否有意義(靜態語義)例如 兩個指針相乘語法是正確的但是沒有意義的,會在這個階段排查出來。

      靜態語義指在編譯期就可以確定的語義,包括聲明和類型匹配,類型轉換等。

      動態語義只有在運行期才能夠確定。動態語義包括運行時發生除0錯誤等。

    • 對語法分析生成的語法樹進行進一步更新。添加類型標識,添加類型轉換節點等。

  4. 中間代碼生成 與 優化

    語法樹的順序表示,有很多種類型,常見爲三地址碼。

    爲什麼要生成中間代碼?

    • 爲了優化代碼,而原有的語法樹不利於修改,所以將語法樹順序表示轉化爲中間代碼。
    • 將編譯器分爲前端和後端,前端生成與機器無關的代碼,後端將中間代碼轉換成目標機器代碼。簡單來說,就是現在有一個跨平臺的編譯器,那麼它只需要開發一個前端編譯器,再根據不同平臺開發不同後端編譯器。
  5. 目標代碼生成 與 優化

    !!!到這一步生成的代碼就與特定的運行環境和目標機器有關了。

    生成: 將中間代碼轉換成彙編代碼,依賴於目標機器,因爲不同機器有着不同字長,寄存器,整數數據類型等等。

    優化: 刪除多餘的指令,用位移代替乘法運算等。

彙編

將編譯器生成的彙編代碼翻譯成相應的機器指令(01 指令序列)。

生成各個section。

根據編譯過程中彙總的符號表 生成 鏈接過程的符號表

  • 編譯時的符號表包括局部自動變量,因爲要進行語法語義判斷和代碼優化等工作。
  • 而鏈接過程中符號表沒有局部自動變量,因爲這些變量是在運行時在棧上管理,跟鏈接所做的工作無關。

鏈接

到這塊,建議先看看生成的可重定位文件內容: https://www.cnblogs.com/starrys/p/11911064.html

整個鏈接所作的工作可以簡單概括下面幾句:目標文件純粹是字節塊的集合,這些塊中包含程序代碼和程序數據,和引導鏈接器和加載器的數據結構。鏈接器將這些塊鏈接起來,確定被連接塊的運行時位置,並且修改代碼和數據塊中的各種位置。鏈接器對目標機器瞭解甚少。主要有兩個工作 符號解析重定位

符號解析

符號解析的目的就是 關聯符號的定義與引用。將每個引用的符號與可重定位文件的符號表中一個確定的符號定義關聯起來。

對於第三種 局部符號由編譯器確保它們只有一個定義。鏈接過程中主要處理第一種和第二種全局符號。

對於 定義在本模塊被其他模塊引用全局符號 和 定義在其他模塊被本模塊引用全局符號 有以下:

  • 在編譯時,編譯器向每個全局符號附加強 弱屬性,經過彙編器把這個屬性隱含在可重定位文件的符號表裏。
    • 強符號 包括 函數 和 已初始化的全局變量。
    • 弱符號 包括 未初始化的全局變量。
  • 用以下規則處理多重定義的符號名:
    • 不允許多個同名強符號。
    • 如果有強符號和弱符號同名,那麼選擇強符號。
    • 當多個弱符號同名,則從其中任選一個。

從這點可以看出爲什麼在ELF可重定位文件中 未初始化的全局變量例如 x(弱符號) 會被分配到 COMMON塊中,因爲編譯器沒有辦法預測鏈接器會使用x的哪個定義,所以把它分配到COMMON塊中,把決定權交給鏈接器。而已初始化全局變量因爲是強符號所以可以直接放到.data 或 .bss 。

重定位

重定位工作有以下:

  • 重定位節

    將可重定位文件中所有相同類型的section 合併成一個。

  • 重定位符號定義

    給符號和指令分配虛擬地址。使得程序中每條指令和全局變量都有唯一的虛擬內存地址。

  • 重定位符號引用

    修改代碼和數據中對每個符號的引用,使它們指向程序的虛擬地址。這個過程需要可重定位目標文件中的 .rel section。

裝載

爲了節省內存,可以利用程序運行的局部性原理,把程序經常需要的信息駐留到內存,不太常用的存放在磁盤中。

有兩種動態裝載的方式:

  • 覆蓋裝入 已被淘汰

    簡單來說,就是再寫一個管理模塊調用的代碼,假如有A,B兩個程序模塊,且兩模塊間不會互相調用,當運行A模塊時,從磁盤中加載A到內存上,當A沒在運行並且要運行B時,把B覆蓋到原來A所在內存上。相當於同一塊內存由多個不同時刻獨立模塊共享。但這樣做只能對模塊間獨立有效,並且,每次裝載都要從磁盤讀數據,用時間換空間的做法。把利用內存的任務交給了程序員。

  • 頁映射 常用

    將內存和磁盤上的指令和數據按特定大小劃分,將一個單位稱爲頁,之後裝載和操作的單位就是頁。一般來說一個頁的大小爲4k。當需要哪個頁的數據時,將其裝載到內存上。當內存上可用頁被佔滿後,根據FIFO 或者LUR 算法更換頁。這塊簡單看看頁映射的圖。

當運行可執行文件時,發生了什麼?

在 shell 終端輸入 ./可執行文件 時,當鍵入回車鍵後,shell 知道我們已經輸入完命令,檢測到不是內置命令,所以將其當作一個可執行文件。然後通過調用加載器,將可執行文件中的代碼和數據從磁盤複製到內存中,然後通過跳轉到程序的第一條指令來運行該程序。

其中裝載的過程:

  • 創建虛擬地址空間。

    虛擬空間的頁映射,這個頁映射機制其實就是一個映射函數,創建虛擬地址空間不是創建空間,而是創建這個映射函數所需要的數據結構。分配一個空的頁目錄就可以。

  • 讀取可執行文件頭,並建立虛擬空間與可執行文件的映射關係。

    這一步所做的是虛擬空間與可執行文件的映射關係。當發生頁錯誤時,操作系統從可執行文件中讀取頁,並將它裝載到物理內存上。

    若沒有這一步,如果發生了頁錯誤時,不知道要裝載可執行文件中哪個頁。

  • 將cpu的指令寄存器設置成可執行文件的入口地址,啓動運行。

    這個入口地址就是可執行文件頭部保存的 Entry point address

詳細的裝載過程中的指令請見 《程序員自我修養》 p173

頁錯誤: 當程序開始執行時,發現某個頁面是空頁面,觸發了異常中斷,被操作系統捕捉執行錯誤處理程序,錯誤處理程序藉助上面裝載第二步建立的數據結構,找到了缺失的空白頁在文件中的偏移,然後再在物理頁上新開闢一頁,建立該頁與文件中的頁映射關係,最後返回中斷現場,繼續執行。

當段的數目增多時,就會產生空間浪費的問題,因爲ELF文件被映射時,每個section在映射時如果都是以頁進行映射,並且section大小與頁大小取模有剩餘,那麼多餘出來的也會佔一頁。而一個ELF文件中有很多section,這就容易翻車。

由此引出了 segment ,還記得上一節 ELF 文件中的segment 嗎?

當站在操作系統的角度看可執行文件的裝載,發現它並不關心各個section的具體內容,而主要關心的是段的權限。於是乎,我們可以將多個權限相同的section 一起裝載,減少內存碎片。這些section就稱爲 segment.

VMA , 頁 , segment ,section 之間是什麼關係?

  • 頁就是內存和磁盤管理數據的單位。一般大小4k。

  • VMA (Virtual Memory Area)就是進程虛擬空間的一塊區域,故名思意,就是一塊區域。

  • section就是 ELF 文件中的 .data .text 等等。

  • segment 就是 在裝載可執行文件時,將多個權限相同的 section 看作一個segment。

  • 一個segment 包含多個section,映射到虛擬內存上時可能要花費多個頁。

    一個VMA根據大小,可能包含一個segment,也可能連一個section 都不夠。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章