可執(zhí)行文件是怎么來的阔墩?(以C語言為例)
C代碼(.c) - 經(jīng)過編譯器預(yù)處理,編譯成匯編代碼(.asm) - 匯編器挪凑,生成目標代碼(.o) - 鏈接器段化,鏈接成可執(zhí)行文件(.out) - OS將可執(zhí)行文件加載到內(nèi)存里執(zhí)行
From C to running program
可執(zhí)行文件的創(chuàng)建
#include <stdio.h>
int main()
{
printf("hello world!\n");
}
1. 預(yù)處理
gcc -E -o hello.cpp hello.c -m32??? 預(yù)處理(文本文件)
預(yù)處理負責把include的文件包含進來及宏替換等工作
2. 編譯
gcc -x cpp-output -S -o hello.s hello.cpp -m32??? 編譯成匯編代碼(文本文件)
3. 匯編
gcc -x assembler -c hello.s -o hello.o -m32??? 匯編成目標代碼(ELF格式,二進制文件薪棒,有一些機器指令手蝎,只是還不能運行)
4. 鏈接
gcc -o hello hello.o -m32??? 鏈接成可執(zhí)行文件(ELF格式,二進制文件)
在hello可執(zhí)行文件里面使用了共享庫俐芯,會調(diào)用printf棵介,libc庫里的函數(shù)
gcc -o hello.static hello.o -m32 -static??? 靜態(tài)鏈接
把執(zhí)行所需要依賴的東西都放在程序內(nèi)部
hello 只有7k,hello.static卻有7百k吧史,因為它把需要C庫里邊的東西也放到可執(zhí)行文件里面來
可執(zhí)行文件怎樣變成一個運行的進程的邮辽?
要弄清楚這個問題,需要先弄清楚贸营,可執(zhí)行文件的內(nèi)部是怎樣的吨述?是怎樣描述可執(zhí)行文件的?
常見的文件格式
目標文件的格式ELF
EXECUTABLE? AND? LINKABLE? FORMAT? 可執(zhí)行的和可鏈接的格式(是文件格式的標準)
.o文件 和 可執(zhí)行文件钞脂,都是目標文件揣云,一般使用相同的文件格式
ABI和目標文件格式是什么關(guān)系
目標文件也叫做ABI,應(yīng)用程序二進制接口芳肌。實際上在目標文件里面灵再,它已經(jīng)是二進制兼容的格式。而什么叫二進制兼容呢亿笤?所謂的二進制兼容,就是指這個目標文件已經(jīng)是適應(yīng)某一種CPU體系結(jié)構(gòu)上的二進制的指令栋猖,比如在32位x86環(huán)境下編譯出來的目標文件净薛,鏈接成ARM上的可執(zhí)行文件,那肯定是不可以的
ELF文件里面三種目標文件
一個可重定位(relocatable)文件保存著代碼和適當?shù)臄?shù)據(jù)蒲拉,用來和其它的object文件一起來創(chuàng)建一個可執(zhí)行文件或者是一個共享文件(主要是.o文件)
一個可執(zhí)行(executable)文件保存著一個用來執(zhí)行的程序肃拜,該文件指出了exec(BA_OS)如何來創(chuàng)建程序進程映象(操作系統(tǒng)怎么樣把可執(zhí)行文件加載起來并且從哪里開始執(zhí)行)
一個共享object文件保存著代碼和合適的數(shù)據(jù)痴腌,用來被下面兩個鏈接器鏈接:(主要是.so文件)
第一個是鏈接編輯器(靜態(tài)鏈接)【請參看ld(SD_CMD)】,可以和其它的可重定位和共享object文件來創(chuàng)建其它的object
第二個是動態(tài)鏈接器燃领,聯(lián)合一個可執(zhí)行文件和其它的共享object文件來創(chuàng)建一個進程映象
ELF的目標文件格式
Object文件參與程序的鏈接(創(chuàng)建一個程序)和程序的執(zhí)行(運行一個程序)
一個ELF頭在文件的開始士聪,保存了路線圖(road map),描述了該文件的組織情況
程序頭表(Program header table)告訴系統(tǒng)如何來創(chuàng)建一個進程的內(nèi)存映像
Section頭表(Section header table)包含了描述文件Sections的信息猛蔽。每個Section在這個表中有一個入口剥悟,每個入口給出了該Section的名字,大小等信息
鏈接視圖曼库,有很多Section区岗,執(zhí)行視圖,有很多段(Segment)
大多數(shù)文件格式也都是這種模式毁枯,在頭記錄了一些元數(shù)據(jù)慈缔,用readelf命令來詳細查看ELF文件頭
可執(zhí)行程序加載的主要工作
當創(chuàng)建或增加一個進程映象的時候,系統(tǒng)在理論上將拷貝一個文件的段到一個虛擬的內(nèi)存段
可執(zhí)行文件的格式和進程的地址空間有一個映射關(guān)系
可執(zhí)行文件有一個頭部种玛,里面有一些關(guān)鍵信息藐鹤,Entry point Address,入口地址赂韵,即程序的起點教藻,0x8048300,后面有一些代碼右锨,數(shù)據(jù)
對于進程來講括堤,進程有一個進程地址空間,而對于32位x86體系結(jié)構(gòu)來講绍移,進程有4G的進程地址空間(邏輯地址)悄窃,3G以上的地址空間只能在內(nèi)核態(tài)下訪問,在用戶態(tài)的時候蹂窖,只能訪問0到3G的地址空間轧抗。
ELF可執(zhí)行文件加載到內(nèi)存的位置? 與? ELF可執(zhí)行文件加載到內(nèi)存中開始執(zhí)行的第一行代碼
ELF可執(zhí)行文件默認加載到內(nèi)存0x8048000這個位置,從這個位置開始加載瞬测。前面加載ELF可執(zhí)行文件的頭部信息横媚,但因不同文件大小不同,程序的實際入口為:0x8048x00月趟,圖例為0x8048300灯蝴,也就是說這個位置是程序的實際入口地址,即剛加載過可執(zhí)行文件的進程(一個進程加載了新的可執(zhí)行文件之后孝宗,開始執(zhí)行的入口點)穷躁,就是從這個地方開始執(zhí)行
簡略地來看,圖例里的文件是ELF的靜態(tài)鏈接文件因妇。靜態(tài)鏈接的時候问潭,會將所有代碼放在一個代碼段猿诸,把所有的鏈接都鏈接好了,所以從0x8048300開始一行行代碼執(zhí)行狡忙,壓棧出棧梳虽,把整個程序執(zhí)行完
而實際上如果需要用到共享庫,需要動態(tài)鏈接的話灾茁,會有多個代碼段窜觉,情況會更復(fù)雜(暫不研究)
裝載可執(zhí)行程序之前的工作最主要的是兩大部分:
1. 可執(zhí)行程序的文件格式
2. 可執(zhí)行程序的執(zhí)行環(huán)境
一般是通過shell程序啟動一個可執(zhí)行程序,shell程序具體做了什么删顶?而當啟動加載一個可執(zhí)行程序的時候竖螃,也就是發(fā)起一個系統(tǒng)調(diào)用execve,shell環(huán)境準備了哪些執(zhí)行的上下文環(huán)境(用戶態(tài)的執(zhí)行環(huán)境)
再看看execve系統(tǒng)調(diào)用怎么樣把一個可執(zhí)行文件在內(nèi)核態(tài)里面裝載起來逗余,裝載起來后又返回到用戶態(tài)(內(nèi)核態(tài)的執(zhí)行環(huán)境)
可執(zhí)行程序的執(zhí)行環(huán)境(Shell命令行特咆、main函數(shù)的參數(shù)與execve的參數(shù))
$ ls -l /usr/bin 列出/usr/bin下的目錄信息,Shell本身不限制命令行參數(shù)的個數(shù)录粱,命令行參數(shù)的個數(shù)受限于命令自身
例如腻格,int main(int argc, char *argv[])??? -- 愿意接收命令行參數(shù)
又如,int main(int argc, char *argv[], char *envp[])??? -- 愿意接收shell的環(huán)境變量啥繁,前兩個參數(shù)由用戶輸入命令的時候設(shè)定-l /usr/bin菜职,后一個是shell環(huán)境,shell程序自動加上
Shell會調(diào)用execve將命令行參數(shù)和環(huán)境參數(shù)傳遞給可執(zhí)行程序的main函數(shù)
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
庫函數(shù)exec*都是execve的封裝例程
命令行參數(shù)和環(huán)境變量是如何保存和傳遞的旗闽?
先函數(shù)調(diào)用參數(shù)傳遞酬核,再系統(tǒng)調(diào)用參數(shù)傳遞
Shell程序 -> execve -> sys_execve,然后在初始化新程序堆棧時拷貝進去
命令行參數(shù)和環(huán)境串都放在用戶態(tài)堆棧中
當fork一個子進程的時候适室,復(fù)制父進程嫡意,調(diào)用execve系統(tǒng)調(diào)用的時候,要加載的可執(zhí)行程序把原來的進程的環(huán)境覆蓋掉了捣辆,覆蓋掉之后它的用戶態(tài)堆棧也被清空了蔬螟,因為它是個新的程序要執(zhí)行,那么argv和envp是如何進入新程序的用戶態(tài)堆棧的汽畴?即命令行參數(shù)和環(huán)境變量是如何進入新程序的堆棧的旧巾?
在創(chuàng)建一個新的用戶態(tài)堆棧的時候,實際上是把命令行參數(shù)的內(nèi)容和環(huán)境變量的內(nèi)容通過指針的方式傳遞到execve系統(tǒng)調(diào)用的內(nèi)核處理函數(shù)忍些,然后內(nèi)核處理函數(shù)在創(chuàng)建可執(zhí)行程序新的用戶態(tài)堆棧的時候鲁猩,會把參數(shù)拷貝到用戶態(tài)堆棧里,初始化新的可執(zhí)行程序的上下文環(huán)境坐昙。所以绳匀,新的程序能從main函數(shù)開始,把對應(yīng)的參數(shù)接收過來炸客,然后執(zhí)行疾棵。但原先在調(diào)用execve時,參數(shù)只是壓在了shell程序當前進程的堆棧上痹仙,而這個堆棧在加載完新的可執(zhí)行程序之后是尔,已經(jīng)被清空了,內(nèi)核又創(chuàng)建了一個新進程的用戶態(tài)堆棧
如果僅僅只是加載一個靜態(tài)鏈接的可執(zhí)行程序的話开仰,只需要傳遞一些命令行參數(shù)拟枚,一些環(huán)境變量,可執(zhí)行程序就可以正常地工作众弓。但是對于絕大多數(shù)的可執(zhí)行程序來講恩溅,還有一些對動態(tài)鏈接庫的依賴,這個比較復(fù)雜谓娃。裝載時動態(tài)鏈接和運行時動態(tài)鏈接應(yīng)用舉例
動態(tài)鏈接分為可執(zhí)行程序裝載時動態(tài)鏈接和運行時動態(tài)鏈接脚乡,如下代碼演示了這兩種動態(tài)鏈接。
1. 準備.so文件(動態(tài)鏈接文件滨达,windows下是dll)
編譯成libshlibexample.so文件
$ gcc -shared shlibexample.c -o libshlibexample.so -m32
編譯成libdllibexample.so文件
$ gcc -shared dllibexample.c -o libdllibexample.so -m32
2. 以共享庫和動態(tài)加載共享庫的方式使用libshlibexample.so文件和libdllibexample.so文件
編譯main,注意這里只提供shlibexample的-L(庫對應(yīng)的接口頭文件所在目錄)和-l(庫名,如libshlibexample.so去掉lib和.so的部分)权悟,并沒有提供dllibexample的相關(guān)信息眨业,只是指明了-ldl
可執(zhí)行程序的裝載相關(guān)關(guān)鍵問題分析(execve和fork都是特殊一點的系統(tǒng)調(diào)用)
傳統(tǒng)的系統(tǒng)調(diào)用都是陷入到內(nèi)核態(tài)画株,然后再返回到用戶態(tài)辆飘,繼續(xù)執(zhí)行系統(tǒng)調(diào)用下面的指令
fork系統(tǒng)調(diào)用進入到內(nèi)核態(tài),兩次返回谓传,在父進程中蜈项,返回到父進程原來的位置繼續(xù)向下執(zhí)行,這個和傳統(tǒng)的系統(tǒng)調(diào)用是一樣的良拼。在子進程中战得,構(gòu)造了它的堆棧環(huán)境,子進程返回到特定的點庸推,是從ret_from_fork開始執(zhí)行然后返回到用戶態(tài)常侦,對于子進程來講比較特殊
execve系統(tǒng)調(diào)用,當前的可執(zhí)行程序在執(zhí)行贬媒,執(zhí)行到execve系統(tǒng)調(diào)用時候聋亡,陷入到內(nèi)核態(tài),在內(nèi)核里面际乘,用execve加載的可執(zhí)行文件坡倔,把當前進程的可執(zhí)行程序給覆蓋掉了,當execve系統(tǒng)調(diào)用返回的時候,已經(jīng)不是返回到原來的可執(zhí)行程序了罪塔,是新的可執(zhí)行程序的執(zhí)行起點投蝉,也就是main函數(shù)大致的位置,那么main函數(shù)的執(zhí)行環(huán)境征堪,也就需要我們來構(gòu)建好加載的新的可執(zhí)行程序的執(zhí)行環(huán)境
sys_execve內(nèi)核處理過程
當execve系統(tǒng)調(diào)用陷入到內(nèi)核里的時候瘩缆,system_call,調(diào)用了sys_execve()佃蚜,sys_execve內(nèi)部會解析可執(zhí)行文件格式庸娱,后面的調(diào)用順序:
do_execve -> do_execve_common ->? exec_binprm
search_binary_handler根據(jù)文件頭部信息尋找對應(yīng)的文件格式處理模塊,如下:
根據(jù)我們給出的文件名谐算,加載了文件的頭部熟尉,判斷文件是什么格式,在列表中尋找能夠解釋ELF格式的內(nèi)核模塊
對于ELF格式的可執(zhí)行文件fmt->load_binary(bprm);執(zhí)行的應(yīng)該是load_elf_binary其內(nèi)部是和ELF文件格式解析的部分需要和ELF文件格式標準結(jié)合起來閱讀
Linux內(nèi)核是如何支持多種不同的可執(zhí)行文件格式的洲脂??
當ELF文件格式出現(xiàn)的時候斤儿,觀察者就能自動執(zhí)行l(wèi)oad_elf_binary,但實際上是在retval = fmt->load_binary(bprm)執(zhí)行腮考,這個地方實際上是一種多態(tài)的機制雇毫,本質(zhì)上是一種觀察者模式
在load_elf_binary里有一個很關(guān)鍵的地方,start_thread
看一下start_thread????? /linux-3.18.6/fs/binfmt_elf.c#82
start_thread這個函數(shù)有一個pt_regs踩蔚,一個new_ip棚放,一個new_sp
pt_regs實際上就是內(nèi)核堆棧的棧底的那部分,發(fā)生系統(tǒng)調(diào)用int 0x80的時候馅闽,把eflags飘蚯、sp、ip都壓入到棧福也。那么新進程執(zhí)行的時候局骤,需要把它的起點位置給它替換掉
new_ip是怎么來的呢暴凑?
看一下load_elf_binary????? /linux-3.18.6/fs/binfmt_elf.c#571
975 start_thread(regs, elf_entry, bprm->p);
elf_entry峦甩,對于一個靜態(tài)鏈接的可執(zhí)行文件,就是可執(zhí)行文件里的Entry point address现喳,可執(zhí)行文件頭部定義的起點
在一個新的可執(zhí)行程序返回到用戶態(tài)之前凯傲,需要修改int 0x80壓入內(nèi)核堆棧的EIP,用新的可執(zhí)行程序的起點來修改嗦篱,但是對于動態(tài)鏈接的過程又更復(fù)雜一些冰单,先理解靜態(tài)鏈接的過程,大致是這樣
看一下系統(tǒng)調(diào)用sys_execve內(nèi)核處理過程
看一下sys_execve????? /linux-3.18.6/fs/exec.c#1604
看一下do_execve????? /linux-3.18.6/fs/exec.c#do_execve
看一下do_execve_common????? /linux-3.18.6/fs/exec.c#do_execve_common
打開要加載的可執(zhí)行文件灸促,然后加載文件頭部
1474 file = do_open_exec(filename);
創(chuàng)建結(jié)構(gòu)體诫欠,bprm
1481 bprm->file = file; 1482 bprm->filename = bprm->interp = filename->name;
把環(huán)境變量和參數(shù)都copy到結(jié)構(gòu)體里面
1505 retval = copy_strings(bprm->envc, envp, bprm); 1509 retval = copy_strings(bprm->argc, argv, bprm);
對可執(zhí)行文件的處理過程
1513 retval = exec_binprm(bprm);
看一下exec_binprm????? /linux-3.18.6/fs/exec.c#exec_binprm
尋找可執(zhí)行文件的處理函數(shù)
1416 ret = search_binary_handler(bprm);
看一下search_binary_handler????? /linux-3.18.6/fs/exec.c#1352
尋找能夠解釋當前可執(zhí)行文件的代碼模塊
看一下load_elf_binary????? /linux-3.18.6/fs/binfmt_elf.c#84
結(jié)構(gòu)體變量涵卵,對load_binary做了賦值
前面說過,elf_format這個結(jié)構(gòu)體變量要注冊到鏈表里面去荒叼,這是一個發(fā)布訂閱的架構(gòu)模式轿偎,或者叫觀察者模式,實際上是一個函數(shù)指針甩挫,在面向?qū)ο罄锾颍卸鄳B(tài)模式
在看load_elf_binary之前椿每,看elf_format這個結(jié)構(gòu)體變量是怎么進入到一個內(nèi)核的處理模塊里
看一下__init init_elf_binfmt????? /linux-3.18.6/fs/binfmt_elf.c#2198
注冊結(jié)構(gòu)體變量伊者,把結(jié)構(gòu)體變量注冊到鏈表里面去,這樣當出現(xiàn)一個ELF格式文件的時候间护,就到鏈表里面找專門處理ELF文件格式的模塊
重點關(guān)注ELF文件格式的解釋亦渗,看一下load_elf_binary????? /linux-3.18.6/fs/binfmt_elf.c#571
571static int load_elf_binary(struct linux_binprm *bprm)
(結(jié)合ELF文件格式)讀取文件頭部信息
626 retval = kernel_read(bprm->file, loc->elf_ex.e_phoff, 627 (char *)elf_phdata, size);
load_elf_binary主要的工作是把可執(zhí)行文件映射到進程的地址空間,ELF可執(zhí)行文件默認會被映射到0x8048000這個地址
816 error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, 817 elf_prot, elf_flags, 0);
需要動態(tài)鏈接的可執(zhí)行文件先加載連接器ld
所以后面在start_thread的時候就會有兩種可能
975 start_thread(regs, elf_entry, bprm->p);
如果是一個靜態(tài)鏈接文件的話汁尺,elf_entry就是指向Entry point address的位置0x8048x00
如果是一個需要依賴動態(tài)鏈接庫的話法精,需要ld鏈接器,elf_entry就是指向動態(tài)鏈接器的起點
可以簡單地理解痴突,start_thread實際上就是把返回到用戶態(tài)的位置從原來的int0x80的下一條指令變成了新加載的可執(zhí)行文件的Entry point address的位置0x8048x00
淺析動態(tài)鏈接的可執(zhí)行程序的裝載
對于一般的可執(zhí)行程序來講搂蜓,大多都是需要使用動態(tài)鏈接庫,動態(tài)鏈接庫最常見的就是libc辽装,動態(tài)鏈接器ld它也是libc的一部分帮碰。那么在動態(tài)鏈接的過程,內(nèi)核做了什么拾积?
ELF格式里面要依賴其它的動態(tài)鏈接庫殉挽,動態(tài)鏈接庫某一個.so本身(它也是一個ELF格式的文件)還可能會依賴其它的動態(tài)鏈接庫,因此實際上動態(tài)鏈接庫的依賴關(guān)系會形成一個圖拓巧。在解釋每一個ELF格式文件的時候斯碌,看它依賴了哪些動態(tài)鏈接庫,這樣它就會加載
那么誰負責加載呢肛度?
閱讀內(nèi)核代碼的時候傻唾,可以看到,當這個文件需要用elf_interpreter的話承耿,也就是說它需要依賴動態(tài)鏈接器來解釋這個ELF文件冠骄,那么它就需要加載load_elf_interp,實際上是加載動態(tài)連接器ld瘩绒,那么這時候Entry point address猴抹,也就是說在返回到用戶態(tài)的時候,它返回的就不是這個可執(zhí)行程序文件規(guī)定的起點锁荔,它返回的是動態(tài)連接器的程序入口蟀给,動態(tài)連接器負責解釋當前的可執(zhí)行文件蝙砌,看它里面依賴哪些動態(tài)鏈接庫,然后把那些動態(tài)鏈接庫一個一個加載進來跋理,加載進來之后再解釋加載進來的動態(tài)鏈接庫择克,看它這個動態(tài)鏈接庫還依賴哪些文件,這樣就有一個叫廣度遍歷的方法(即動態(tài)鏈接庫的裝載過程是一個圖的遍歷)前普,把所有的動態(tài)鏈接庫都裝載起來肚邢,裝載起來之后ld再負責把CPU的控制權(quán)移交給可執(zhí)行程序頭部規(guī)定的起點位置
那么從以上分析看出,動態(tài)鏈接的過程不是由內(nèi)核來完成的拭卿,主要是由動態(tài)鏈接器來完成的骡湖,動態(tài)鏈接器是libc的一部分,是在用戶態(tài)做的事情
Summary:
加載可執(zhí)行程序兩種方式
1. 靜態(tài)鏈接峻厚,直接執(zhí)行可執(zhí)行程序的入口 Entry point address(0x8048x00)
2. 需要動態(tài)鏈接响蕴,由ld動態(tài)鏈接這個可執(zhí)行程序
(完)