作者簡介:
偉林,中年碼農,從事過電信、手機、安全、芯片等行業,目前依舊從事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上沒有的概要和重點理解。
加載視角的“PT_LOAD “類型segment:
表明可加載到内存中的段,一般程序都包含兩個此種類型的段.data、.text
加載視角的“PT_INTERP“類型segment:
指定動态加載程序,即我們用 “ldd“命令看到的動态加載器
加載視角的“PT_DYNAMIC “類型segment:
相當于動态加載的一個入口段,指定了動态加載和鍊接需要的各種數據段的地址和類型。DT_NEEDED、DT_SONAME、DT_RPATH表項承載的是編譯時指定的一些依賴庫和搜索路徑等等。
相關工具
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将絕對地址,轉換成調試信息中的源文件行号。
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!