鏈接加載原理及 ELF 文件格式

作者簡介:

偉林,中年碼農,從事過電信、手機、安全、芯片等行業,目前依舊從事 Linux 方向開發工作,個人愛好 Linux 相關知識分享。

原理概述

爲什麼要研究鏈接和加載?寫一個小的 main 函數用戶態程序,或者是一個小的內核態驅動 ko,都非常簡單。但是這一切都是在 gcc 和 linux 內核的封裝之上,你只是實現了別人提供的一個接口,至於程序怎樣啓動、怎樣運行、怎樣實現這些機制你都一無所知。接着你會對程序出現的一些異常情況束手無策,對內核代碼中的一些用法不能理解,對 makefile 中的一些實現不知所云。所以這就是我們要研究鏈接和加載的目的:明白程序的映像文件是怎麼組織的,程序啓動是怎麼實現的,相關的機制是怎麼聯繫在一起的。“你應當瞭解真相,真相會使你自由”。

鏈接和加載 (linker and loader): linker 即鏈接器,它負責將多個. c 編譯生成的. o 文件,鏈接成一個可執行文件或者是庫文件;loader 即加載器,它原本的功能很單一隻是將可執行文件的段拷貝到編譯確定的內存地址即可,但是有了動態鏈接庫以後,部分的外部庫引用符號在加載的時候纔會得到解析,所以加載也要處理鏈接器的相同操作重定位。

這方面的資料乍一看起來非常晦澀難懂,其實根本的功能非常簡單:鏈接和加載的最核心的內容就是重定位。鏈接器負責將多個. o 文件鏈接重定位成一個大文件,而加載器再將這個大文件重定位到一個進程空間當中去。

在 linux 環境下,鏈接和加載的機制最終有一個載體來承擔,這個載體就是 elf 文件。所以從研究 elf 文件格式入手,是理解鏈接和加載原理的好方法。

本文檔描述的鏈接和加載主要針對用戶程序而言,在操作系統的鏈接和加載和這裏有些不同,因爲如果你編譯一個內核,在加載內核的時候又有誰來做動態加載呢?關於內核實現的不同以後再在專門文檔中描述。

重定位原理

前面已經說過鏈接和加載的核心內容就是重定位,所以開篇先用通俗易懂的語言來闡明重定位的原理。

符號表 (Symbol Table):

符號表就是一張字符符號和地址的對應表,例如使用 “nm file“、”readelf -s file “等命令可以讀出一個 elf 文件的符號表。符號表的作用就是一個助記符,用一個字符串來標示某些抽象的地址,它能標示的地址有代碼地址和數據地址,代碼地址包括函數名、跳轉標號,數據地址包括全局變量。

符號表的組織如下圖所示:

從以上描述中可以看出,符號表的作用就是將符號名稱和地址進行綁定。而綁定的根本目的就是方便對符號的引用,在符號值發生改變的時候,不需要去手工改動源代碼中對符號引用的地方,而這種改動是由鏈接程序在重新生成執行文件時自動完成的。

重定位表 (Relocation):

有了符號表,就需要有人對符號表進行引用,在程序的執行過程中對全局變量的引用、跳轉、調用函數,這些都涉及到相應的符號引用。符號和其引用是一對多的關係,一個符號可能被代碼中多處引用。因爲符號值改變的時候,也需要對所有引用符號的地方的代碼進行修改,所以需要還有一張表來記錄符號表的引用關係,這就是重定位表:

從上圖可見,重定位表項用來記錄鏈接和加載的過程中需要重新定位的位置,在各個段位置發生改變而引起符號地址改變時,根據重定位表來修改符號引用的值。

GOT 表 (Global Offset Table):

前面的符號表和重定位表已經滿足編譯和鏈接過程中的重定位需求。同樣加載的過程中還需要重定位操作,需要將外部鏈接庫中的函數和變量和本程序中的引用鏈接起來,但是由於加載過程中代碼已經處於運行狀態,使用鏈接過程中同樣的重定位手段有些不合適。鏈接的重定位是通過重定位表直接修改代碼來完成的,但是代碼在運行過程中再去修改代碼會帶來很多問題和風險。

所以加載過程中的重定位,使用了一種改良的重定位手段:即通過兩張間接訪問表來屏蔽掉重定位帶來的對代碼的修改,訪問外部數據使用 GOT,訪問外部程序使用 PLT。這樣可鏈接出位置無關代碼 PIC(Position Independent Code),需要重定位時只需要修改 GOT 和 PLT 的值,而不需要去改動可執行代碼。

GOT 表用來做數據重定位的原理如上圖所示。

PLT 表 (Procedure Linkage Table):

從上一節可知,加載過程中的重定位爲了避免對代碼的修改,引入了 GOT 來屏蔽對數據的訪問,同理對外部代碼的訪問也是可以用 GOT 來訪問的。但是爲了實現動態鏈接的特性,即使用的時候才鏈接,不使用時可以不用鏈接,對外部代碼的訪問引入了一個新的表項 PLT。

elf 文件

相關背景

Elf 文件格式,是現有 linux 環境下最流行的可執行文件格式,在 elf 文件存儲的信息之上,實現了相應的鏈接和加載特性。 Linux 環境下可執行文件格式的發展歷史是:a.out -> coff -> xcoff -> elf。 Windows 環境下可執行文件格式的發展歷史是:dos com/exe -> pe-coff。

elf 文件格式

Linux 環境下,三種類型的執行文件都可以使用 elf 格式來表示:可重定位文件(即編譯生成但是未連接的文件)、動態庫文件、可執行文件。

Elf 文件提供了兩種文件解析的視角,鏈接視角和動態加載視角。鏈接視角使用 section 的概念來解析文件,主要關注鏈接過程的使用;動態加載視角使用 segment 的概念來解析文件,主要關注加載和動態鏈接的實現。

整個文件的組織框圖如上所示,ELF 頭描述了 section header table 和 program header table 的起始位置、表項大小和個數。根據 section header table 來尋址相應的 section,根據 program header table 來尋址相應的 segment,可以看到一般是一個 segment 包含多個 section。

Elf 文件的原理已經在上一章中闡述,elf 的具體文件格式詳細描述可以參考參考資料中的 “Executable and Linking Format (ELF) Specification “。這裏不再詳細描述,只是記一些 Specification 上沒有的概要和重點理解。

  1. 加載視角的 “PT_LOAD “類型 segment:
  1. 加載視角的 “PT_INTERP“類型 segment:

3. 加載視角的 “PT_DYNAMIC “類型 segment:

### 相關工具

Linux 下可以操作 elf 文件的有以下工具:

a.readelf
“readelf –a file“讀出elf文件的所有信息。

b.nm
“nm file“讀出elf文件的符號表信息。

c.objdump
“objdump –d file“反彙編出elf文件中包含可執行代碼的section,elf命令中功能最強大的一個。

d.objcopy
轉換elf文件爲bin或者其他格式的文件,編譯內核的時候會使用到。

e.strip
去掉elf文件中符號表和調試信息,對elf文件進行減肥。

f.addr2line
將絕對地址,轉換成調試信息中的源文件行號。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/W1jUTSnM8CCBAbj-psdAbw