初始學《鏈接裝載與庫》

因爲工作原因,很久沒有寫博客,近期初略看了一個《鏈接裝載與庫》,書寫得很好,但因本人功力有現,好些地方還是不明白。做個小結先。相信以後再讀此書會有更多收穫。

 

 

 

第二章 編譯和鏈接

從原文件到可執行文件可心爲爲幾部:預編譯,編譯,彙編,靜態鏈接。

預編譯:由預編譯器完成。主要處理代碼中以“#”開始的預編譯指令。比如“include”"define"等,刪除所有註釋,插入文件標識。(生成.i文件)

編譯:對預處理完的文件進行詞法,語法,語義分析及優化,生成相應的彙編代碼文件(生成.cod,.s文件)。

彙編:將彙編代碼轉變爲機器可執行的指令。(生成目標文件.obj,.o文件)

鏈接:把目標文件組裝的過各就是鏈接。主要的工作是:地址空間分配,符號決議和重定位。

 

庫的本質:他是一組打包存放的目標文件。

重定位:地址修正的過程也叫重定位,每個要修正的地方叫重定位入口。

 

每三章 目標文件裏有什麼

文件格式

OMF-對象模型文件(Object Module File)

OMF是一大羣IT巨頭在n年制定的一種格式,在Windows平臺上很常見。大家喜歡的Borland公司現在使用的目標文件就是這種格式。

COFF – 通用對象文件格式(Common Object File Format)

MS和Intel在n年前用的也是OMF格式,現在都改投異側,用COFF格式了。

1.PE(Portable Executable)格式,是微軟Win32環境可執行文件的標準格式。

2.ELF-可執行及連接文件格式(Executable and Linking Format)

ELF格式,是linux通用格式,在非Windows平臺上使用得比較多,在Windows平臺基本上沒見過。

其它格式:Unix a.out和MS-Dos.COM格式。

 

這些格式文件歸類

1.可重定位文件。

用途:連接成爲執行文件或共享目標文件

實例:Linux的.o,Windows的.obj

2.可執行文件(一般沒有文件名)

3.共享目標文件

用途:1.作爲可執行文件一部分運行生成可執行文件。2生成新的目標文件。

例:linus .so, windows dll

4.核心轉儲文件(略)

用於保存運行信息。Linux 下core dump

(目的文件,動態靜態鏈接庫,可執行文件都按可執行文件格式存儲)

 

目標文件中的段:

ELF Header:描述文件的基本屬性,如程序入口,文件版本等。(正是這樣,病毒可能修改程序的入口地址)

Section Table:描述各個段的信息,如段句,升序,在文件中的偏移,讀寫權限等。

.test:指信段

.data段:已經初始化了的全局中靜態變量和局部靜態變量

.bss段:未初始化的全局變量和局部靜態變量。

.rodata段: 存放只讀據,const修飾的變量和字符串常量

.rel.text段:重定位表。如對printf的調用。在鏈接時使用重定位表對地址進行修正。

符號表:函數和變量統稱符號,函數或變量名就是符號名。符號值就是函數或變量的地址。特殊符號:_executable_start,_etext或 _etext或etext,_edata或edata,_end或end

字符串表

段表字符串表

.comment:編譯器版本信息

 

符號修飾與函數簽名

即函數的名稱修改方法,用於識別不用的函數。C++爲了實現以C的兼容,使用符號"extern "C""聲明的符號會按C的方式進行符號修飾。

弱符號與強符號

規則:1.強符號不允許多次定義。2.如果一個符號在霜凍個目標文件中是強符號,在其它中是弱符號,那麼選擇強符號。3.如果都是弱符號,那麼選擇空間最大的一個(這樣可能產生調用過程中的問題)。

弱引用與強引用

在鏈接時,沒有找到符號定義就報錯,稱爲強引用。

 

 

第四章 靜態鏈接

連接器一般採用“兩步鏈接”的方法進行鏈接。第一步進行空間和地址分配,第二步進行符號解析和重定位。

空間和地址分配

空間分配:這裏的空間分配只關心虛擬地址的空間分配,而不關心可執行文件的空間分配。空間分配採用相似段合併的方法,合 並後某個段內部的佈局是有輸入文件的順序決定的,當然收到連接腳本的控制。

地址分配:確定各個段的虛擬地址,包括BSS段。

空間分配需要的信息包括段的長度、屬性和位置(偏移)。

空間和地址分配遍結束後:有一個全局符號表,記錄所有目標文件的符號信息,包括符號定義和符號引用。合併後各個段的起始地址和長度確定

符號解析與重定位

符號解析:確定各個符號的地址。根據段的地址和符號在段中的偏移以及相同段的合併順序,可以確定各個符號的準確地址,從而更新符號表。每個要重定位的段都有一個重定位表。所有被引用的符號都不能是未定義的。

重定位:根據各個目標文件的重定位表信息,可以準確的定位文件中哪些地方需要進行重定位。

重定位表的結構如下:

Typedef struct{

           Elf32_Addr                r_offset;               //從定位入口的偏移,相對於作用段的偏移

           Elf32_Word               r_info;                 //入口的類型和符號

}Elf32_Rel;

符號解析結束,找到重定位入口,下一步做的就是指令修改。根據指令的尋址方式進行修改。

 

 

第六章 可執行文件裝載與進程

32位虛擬地址空間0到2的32次方減1(0到0xFFFFFFFF).

 

從操作系統看可執行文件裝載

進程的建立:一個進程的建立最關鍵的特徵是擁有獨立的虛擬地址空間。進程的建立分三步:

1 創建獨立的虛擬地址空間:創建虛擬地址空間實際上是建立虛擬地址空間到物理空間的映射,在i386的linux實際上就是創建一個頁目錄,或者稱爲頁表。

2 讀取可執行文件頭,建立虛擬空間與可執行文件的映射關係。當程序發生頁錯誤時,操作系統將從內存中分配一個物理頁,並將該缺頁從磁盤讀入內存。然後設置頁的映射關係。顯然,當缺頁時,操作系統需要知道程序當前需要的頁在磁盤中的哪個位置,此時指令的虛擬地址是知道的,這就需要建立一個虛擬地址到可執行文件之間的映射。這樣的一種數據結構稱爲VMA,它記錄了各個段對應的虛擬空間,並記錄該段在文件中的偏移。

3 CPU指令寄存器設置成可執行程序入口,啓動。

頁錯誤:當CPU執行某條指令,如果該指令所在頁是空頁,則發生段錯誤。操作系統捕獲,找到該指令所在的VMA,並計算出該頁在文件中的偏移,分配一個物理頁,將文件內容讀入內存,設置虛擬地址也物理地址的映射關係,即頁表。將控制權還給程序。

文件的鏈接視圖和執行視圖

對於相同的權限的段,合併到一起當作一個段進行映射。稱爲"Segment",和前面的section不一樣。一個是從裝的角度看(Segment),一個是鏈接的角度看(Section)。

 

段地址對齊

最簡單的情況是,各個Segment在物理內存中都分別映射,Segment起始地址都是4096整數倍(每頁4K)。這樣容易造成物理空間的浪費。

 

 

第七章 動態鏈接

爲什麼要動態鏈接

.節省內存和磁盤空間(共享代碼,但私用數據會有複本)。2.程序的升級。3.有利於程序的可擴展和兼容性

 

動態庫參與鏈接

gcc  –o  Program1  Program1.c  ./lib.so

Program1.c引用到lib.so中的foobar()函數,這裏foobar定義在共享對象中,連接器會將這個符號的引用標記爲一個動態鏈接的符號,不對其進行重定位,留到裝載時再進行。動態庫lib.so保存了完整的符號信息,把./lib.so作爲連接的輸入符號之一,連接器在解析符號時就知道foobar是定義在lib.so中的動態符號。如果foobar是定義在其他目標文件中的函數,鏈接器會按照靜態鏈接的規則進行重定位。

 

固定裝載地址的困擾

靜態共享庫:操作系統在某些特定的地區劃分一些地址塊,爲那些已知的模塊預留足夠的空間。

裝載時重定位(任意地址加載,方法一)

裝載時重定位是首先想到的解決共享對象任意地址裝載問題。在鏈接時,對所有絕對地址的引用都不盡興重定位,而是把這一步推遲到裝載時進行。

連接時重定位 vs 裝載時重定位(基址重置)這種方法對共享對象並不是很合適,因爲這種方法需要修改指令,而指令部分被多個進程共享,沒法做到一份指令被多個進程共享,因爲指令被重定位後對每個進程都是不同地。

但這種方法可以修改共享對象的數據部分,因爲數據部分都是進程私有的。

  地址無關代碼(任意地址加載,方法二)

    要想實現指令部分的進程共享,解決共享對象指令中對絕對地址的重定位問題,方法是將指令中那些需要被修改的部分分離出來,放在數據部分。這種方案稱爲地址無關代碼(PIC, Position-Independent Code)技術。

  模塊中四種類型的地址引用:

(1)模塊內部的函數調用、跳轉

(2)模塊內部的數據訪問,全局變量?、靜態變量

(3)模塊外部的函數調用、跳轉

(4)模塊外部的數據訪問,如其他模塊定義的全局變量

 

延遲綁定

動態鏈接速度慢的原因有:

(1)對於全局變量和靜態數據的訪問都要通過GOT表定位,再間接尋址。

(2)對於模塊間的調用也要先定位GOT,然後再進行間接跳轉。

(3)動態鏈接的鏈接工作在運行時完成。

主要的原因是(2)和(3),因爲模塊間的全局變量訪問比較少,多了耦合性就強。

  延遲綁定是指函數在第一次用到時才進行綁定,包括符號查找、重定位等。如果沒有用到則不進行綁定。這樣就節省了(3)的時間。

ELF採用PLT(Procedure Linkage Table)的方法來實現延遲綁定。這種方法是在GOT之上增加一層間接跳轉來實現的。每個外部函數在PLT中都有一個對應的項,調用函數並不是直接跳轉的GOT中進行定位,而是先跳轉到PLT中的對應項,如果是第一次調用該函數,PLT則先進行符號綁定,填充GOT表,再跳轉過去。否則直接跳轉到GOT進行定位。

 

 

第9章 Windows下的動態鏈接

Windes PE採用了與ELF不同的辦法,它採用的是裝載時重定位的方法。

 

 

第10章 內存

棧向低地址增長,堆向高地址增長,直到預留的空間被用完。

 

對於windows來說,每個線程默認棧大小是1MB,可以在createThread時指定。

堆棧幀一般包含以下幾個內容:

(1)傳入的參數

(2)返回的地址

(3)保存的寄存器(上下文)

(4)臨時變量

返回值<=4字節,值用eax返回,5-5字節,用eax和edx聯合返回(eax低4,edx高4),大於8字節:爲返回值開一個臨時空間,把地址傳給被調用函數,被調用函數爲它賦值並用eax返回它的地址。(如果調用函數要用到返回值,就用eax找到臨時對象,複製值)

 

1.malloc是一個運行庫的功能,系統“批發”給進程一塊較大的空間,程序通過malloc進行內存的管理。在linux上用mmap和windows中的VirtualAlloc相似,向系統申請空間。對爲x86來說,申請的大小必須是4096字節的整數倍。

2.winows堆管理函數:heapcreate,heapalloc,heapfree,heapdestroy.每個進程都有一個默認堆,默認大小爲1M.

3.堆在內存中是通過空閒鏈表,位圖,對象池等方式組織起來的。

 

 

第11章 運行庫

入口函數和程序初始化

程序的入口點實際上是一個程序的初始化和結束部分,它往往是運行庫的一部分。典型的程序運行步驟:

1.系統創建進程,把控制權交到程序入口,入口往往是運行庫的某個入口函數。

2.穰函數對運行庫和運行環境進行初始化,包括堆,I/O,線程,全局中變量構造等等。

3.初始完後,調用main,執行程序主體。

4.返回到入口函數,進行清理工作,包括全局變量,堆銷燬,關閉I/O,然後系統調用結束進程。

 

MSVC CRT入口函數

MSVC CRT默認的入口函數名爲mainCRTStartup, 它的總體流程是:

1.初始化和OS版本有關的全局變量。

2.初始化堆。alloc.

3.初始化I/O.

4.獲取命令行參數和環境變量。

5.初始化C庫一些數據。

6.調用main並記錄返回值。

7.檢查並並main返回值返回。

 

exit函數和return函數的主要區別是:

1)exit用於在程序運行的過程中隨時結束程序,其參數是返回給OS的。也可以這麼講:exit函數是退出應用程序,並將應用程序的一個狀態返回給OS,這個狀態標識了應用程序的一些運行信息。

main函數結束時也會隱式地調用exit函數,exit函數運行時首先會執行由atexit()函數登記的函數,然後會做一些自身的清理工作,同時刷新所有輸出流、關閉所有打開的流並且關閉通過標準I/O函數tmpfile()創建的臨時文件。

exit是系統調用級別的,它表示了一個進程的結束,它將刪除進程使用的內存空間,同時把錯誤信息返回父進程。通常情況:exit(0)表示程序正常, exit(1)和exit(-1)表示程序異常退出,exit(2)表示系統找不到指定的文件。在整個程序中,只要調用exit就結束。

 

運行庫與I/O

現代系統系統對系統資源都有嚴格的控制。以文件操作舉例:設置文件句柄可以防止用戶寫操作系統內核的文件對象,文件句柄總是和內核的文件對象相關聯的。內核可以通過句柄來計算出內核文件對象的地址。

在內核中,每一個進程都有一個私有的”打開文件表“,這個表是一個數組,每一個元素都指向一個崔的打開文件對象。而打開文件得到的fd是這個表的下標。(p329)

 

C/C++語言運行庫

支撐c語言運行的一系列函數所構成的集合稱爲運行時庫。C語言運行庫大致包含:啓動與退出,C標準函數,堆,語言實現調試。

參數格式:int printf(const char* format, ...);

cdecl調用慣例保證參數的正確清除,它是由調用方負責清除堆棧。

局部跳轉

C語言中一個爭議機制,它可以實現從一個函數休向別一個函數體跳轉。(p340)

 

glibc與MSVC CRT

C語言的運行庫是c程序和和不同操作系統平臺之間的抽象層,將不同的API抽象成相同的庫函數。但象線程和諧這樣的功能並不是標準c運行庫的一部分,glibc有pthread庫的pthread_create創建,而MSVCRT有_beginthread創建。所以事實上他們是標準C語言運行庫的超集。

 

層次關係圖(P402)

 

 

第12章 系統調用與API

系統調用原理

用戶態的程序一般是通過中斷來從用戶態切換到內核態。windows中的運行庫中通過調用windows api實現系統調用,windows API實質上是以DLL函數函數的形式暴露給應用程序開發者的。

SDK是Windows API DLL導出函數的聲明頭文件、導出庫、相關文件和工具的集合。Windows在API之上建立了很多應用模塊,如winine.dll。之所以引入API,是爲了隔離硬件結構的不同而導致的程序的兼容性問題。

 

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