1.obj檔案
程式員編寫程式,其實就是編寫出一個2進制(binary)檔案。假如我們聲明一個變量char c,也就是聲明需要一個8bit的空間,那麼就需要向系統聲明豫留8bit的空間,怎麼做到這一點呢?就是編譯一個特殊的2進制檔案--obj檔案,用gcc編譯的C語言得到的執行檔案,裡面不僅包含CPU指令,還有很多别的資訊在裡面,它有很多格式COFF、ELF……等等,在最後一道編譯過程中,連結器(linker)ld會加載一堆資訊進入可執行檔案。例如,當有多個編譯後等待連結的.o這種可重定位(relocatable)檔案,既然這些檔案裡面參數或者函數名的相對位置隻是本身所在.o檔案的相對位置,就有一些資訊要告訴連結編輯器(link editor)怎麼修改section的内容,來做relocate,也就是做位址的重新參照以便合成一個新的可執行檔案。
一個obj檔案有兩個重要時期,一個是正在連結(link)的時候,也就是處在
硬碟(disk)裡的時候;一個是正在執行的時候,當然這時它位于記憶體裡。我們平時說的ld linker其實叫link editor,最後編譯的步驟ld把該有的資訊寫進可執行檔案。如果是static link就會去找libxxx.a的函數庫檔案,把想要的程式代碼片段拷貝一份進可執行檔案,并且做成relocation後,把跳來跳去的參照寫進可執行檔案,這個檔案就可以執行。
2、動态連結檔案
相對于靜态連結(static link)拷貝原有的程式代碼進可執行檔案,動态連結不那樣做,link editor把一些資訊寫進可執行檔案而已。例如,需要的程式庫名、函數名等,最後執行的時候,必須呼叫dynamic linker來做program
intepreter,dynamic linker會根據需要的函數庫名稱,把想要的函數名字創造一個可執行的image放到記憶體,是以執行有動态連結的執行檔案,最後通常都是由OS的exec系列的system call與dynamic linker如ld.so聯合完成。
dynamic linker通常會做如下工作:
(1)把可執行檔案的内容加載到process image
(2)把shared obj需要的東西加載到process image
(3)完成relocation
本來這些obj檔案裡面的虛拟位址應該和檔案的位址有相對應的偏移(offset),而檔案首位址通常是0x08040800,這是絕對虛拟位址,但它隻适合可執行檔案,例如Linux extuable file通常是:
file offset virtual address
----------- ----------------
0x0 0x08048000
0x100 0x08048100
shared obj函數庫裡的程式代碼必須為位置無關代碼Position
Independent Code (PIC),也就是說它的位址可能會随不同process而有不同,例如,一個程式隻用了libc.so、ld-linux.so,通常這時候lib.so是從0x40017000開始的,但如果另一個程式多用一個libm.so,那麼libc.so從0x40034000開始兩個的printf參照(reference),就會有不同的位址,是以這種動态函數庫的内部資料就要說明這些code是PIC。
3、ELF檔案
(1)簡介
現在最常用的是一種叫ELF格式(executable and linkable format)的執行檔案,ELF定義了一些變量與資訊使得動态連結更有彈性,一個ELF的2進制檔案按照spec 1.1版的說法有6種,下列是較常見的:
relocatable:它就是編譯時産生的.o檔案,包含了代碼和資料(這些資料是和其 他 重定位檔案和共享的object檔案一起連接配接時使用的)
executable:它就是最後的可執行檔案,包含了代碼和資料shared obj:它就是在/lib /usr/lib下那些可動态連結的函數庫檔案,包含了代碼和資料(這些資料是在連接配接時候被連接配接器ld和運作時動态連接配接器使用的)
core:Core Dump時産生的檔案,包含了一堆garbage資料
注意:這些ELF檔案已經是廣義的2進制檔案,不單指可執行檔案。
(2)ELF組成
一個ELF obj檔案随它存在的時期有不一樣的需求群組成名字,在要連結linking時期位于硬碟,包含了:
ELF header
program header table (可以不要)
section 0
section 1
section 2
section 3
section ...
section n
section header table
ELF header放了ELF定義的一些ELF格式識别字串(俗稱magic number),還有obj檔案(shared obj,relocatable或者executable)這些一般(general)的資訊;program header table是描述了段(segment)資訊的結構數組和一些為程式運作準備的資訊。segement和section不大一樣,是屬于程式執行時期的元素,是以在程式執行時期是必要的,在連結時期是不必要的,是以如果程式不做連結動作,隻要有program header table就可以;section header table就是一個索引表,來記錄各個section的索引,sections就是把需要的資料根據屬性用途分門别類後的小集合,有.bss .data .init .debug .dynamic .fini .text………,其中比較重要的有:
.text
裡面儲存真的CPU指令
.bss
儲存沒有initialize的data
主要是聲明的global與static變量
.data
儲存initialize的data
寫程式用到的函數名,變量名分布在多個source code目錄裡時,需要一個
參照(reference)的資訊做連接配接這些名字,symbol是着被給linker來做連接配接用的,因為obj檔案分散存在,要把這些obj檔案的代碼集合起來,就要靠symbol
來辨識,string table存有很多行字串,每行字串用NULL來分開,每行字串就是symbol和section的名字。symbol table是一張表,存有将來要定址或重新定址所要的symbol定義和參照資訊。shared lib的obj檔案還有.dynsym這個section,裡面存有dynamic symbol table,動态連結的時候使用。另外,如果将來的程式要用debug工具調試,編譯時要加-g這個選項,它會根據sumbol
和string table放進debug多需要的資訊給obj檔案,這樣的資訊現在大都用一種叫stab的格式存放,這同時也會讓執行檔案大小增加到将近3倍。
在ELF不同的檔案型态裡,ELF定義的資訊該有的都有,header section……隻是裡面的值或有不同而已。
Unix/Linux通常從一個_start函數開始而不是從main開始,_start後來會調用main,是以如果要精簡程式,就不要用gcc編譯,直接彙編用_start就可以了(^_^)。另外像section header table如果不需要做連結也可以不要,還有可執行檔案的symbol table等,其實這些可以全部不要,不過要用彙編并同GAS來生成可執行檔案。其實還有很多東西,這就是為什麼即使根本沒有調用任何函數,做成的動态檔案,用ldd看一定有ld-linux.so libc.so了。
而一個存在記憶體中的process image,如下所示:
ELF header
program header table
segment 0
segment 1
segment 2
segment ...
segment n
section header table (可以不要)
Segment有Text,Data等,根據OS定義不同,Text根據存在硬碟檔案裡的.txt .fini等section來的,Data段根據.data .bss等section來的,一個segment通常包含了 一個或一個以上的section,這些section在程式員角度來看更顯的重要。
在支援ELF的系統上,一個程式是由可執行檔案或者加上一些shared obj檔案組成。為了執行這樣的程式,系統使用那些檔案建立程序的記憶體映像。為了使一個ELF檔案裝載到記憶體,必須有一個program header table(該program header table 是一個描述段資訊的結構數組和一些為程式運作準備的資訊)。這裡有幾個在ELF文檔中定義的比較特别的sections。以下這些是對程式特别有用的:
.fini
儲存程序終止代碼指令
是以,當一個程式正常退出時,系統安排執行這個section中的代碼
.init
儲存可執行指令,它構成了程序的初始化代碼
是以,當一個程式開始運作時,在main函數被調用前(C語言稱為main),
系統安排執行這個section中的代碼
.init和.fini sections的存在有着特别的目的。假如一個函數放到.init section,在main函數執行前系統就會執行它。同理,假如一個函數放到.fini section,在main函數傳回後該函數就會執行。該特性被C++編譯器使用,完成全局的構造和析構函數功能。
當ELF可執行檔案被執行,系統将在把控制權交給可執行檔案前裝載是以相關
的共享object檔案。構造正确的.init和.fini sections,構造函數和析構函數将以正确的次序被調用。
Unix/Linux的虛拟記憶體使用有這樣的範圍:
user area
0x0 ~ 0x0bffffff ->; 3GB
kernel area
0x0c000000 ~ 0xffffffff ->; 1GB
以下面程式代碼為例:
int global;
static int func1 (void)
{
static int b;
int *c;
int d;
func2();
return 1;
}
int func2 (void)
{
int c;
static int d;
return 2;
}
int main(void)
{
int a;
static int b;
int init = 3;
func1();
return 3;
}
那麼從一個Linux執行檔案在記憶體中看起來是這個樣子:
i386 Linux的執行image
Virtual Address Allocation
|----------------------------------|0x0
| |-----------------------------| |
| | | |
| | Thread stack | |
| |------------------------------| |
| |
| |------------------------------| |0x08048000 Text
| | executable | | Data
| | | | ……
| | | |
| | | |
| | | |
| |------------------------------| |
| |
| |------------------------------| |0x40000000 ld-linux.so
| | | | libm.so
| | shared LIB | | libc.so
| | | |
| | Stack | |
| | | |
3GB| |----------------------------- | |
| |
| |------------------------------| |0xc0000000
| | | |
| | Kernel Code and Data | |
| | | |
| |-------------------------------| |
4GB|--------------------------------- |0xffffffff
其中0x08048000 ~ 0x40000000 ~ 0xc0000000是這樣存在的。
從C角度看的image:
0x08048000
|--------------------------------------------------------|
| |--------------------------------------------------| |
| | main() | |
| | xxxx Text | |
| | func1 (instrction) | |
| | xxxx | |
| | func2 | |
| | xxxx | |
| |-------------------------------------------------| |
| |
| |-------------------------------------------------| |
| | int global Data | |
| | static int b(main) static int b(func1) | |
| | static int c(func2) | |
| |-------------------------------------------------| |
| |
| |-------------------------------------------------| |
| | malloc(int) Heap | |
| |-------------------------------------------------| |
| | |
| | |
| /|/ |
|--------------------------------------------------------|
| 0x40000000 |
| |
| |
|--------------------------------------------------------|
| /|/ |
| | |
| | |
| |-------------------------------------------------| |
| | func2 int c Stack 2 | |
| |-------------------------------------------------| |
| |
| |-------------------------------------------------| |
| | func1 int b Stack 1 | |
| |-------------------------------------------------| |
| |
| |-------------------------------------------------| |
| | main() argv[0] argv[1] … | |
| |-------------------------------------------------| |
|--------------------------------------------------------|
0xbfffffff
是以可以清楚的知道不同變量(global,static or auto)的生命周期(storage class),和不同變量的有效範圍(scope)。
Kernel code和data當然存在記憶體中,是以實際上都還要經過page table
轉成實際位址。在0x0~ 0xbfffffff中的page table,每個process有不同page
table,但在0xc0000000以下的page table,則都一樣。