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,則都一樣。