你寫的代碼是如何跑起來的
大家好,我是飛哥!
今天,我們來思考一個簡單的問題一個程序如何在Linux上運行
我們以宇宙中最簡單的Hello World程序為例。
# includeltstdio.hgtintmainprintf,return0
寫完代碼后,我們簡單編譯一下,然后就可以在shell命令行啟動了。
#gccmain.c—ohelloworld#。/helloworldHello,世界!
那么編譯,啟動,運行的過程中發(fā)生了什么呢今天我們就來仔細看看
首先,了解可執(zhí)行文件的格式
源代碼編譯完成后,會生成一個可執(zhí)行的程序文件我們先來了解一下編譯后的二進制文件是什么樣子的
首先,讓我們使用file命令來檢查這個文件的格式。
# filehelloworldhelloworld:elf 64—bitLSBexecutable,x86—64,版本1,...
file命令給出了這個二進制文件的摘要信息,其中ELF 64位LSB可執(zhí)行文件表示這個文件是ELF格式的64位可執(zhí)行文件X86—64表示此可執(zhí)行文件支持的cpu體系結(jié)構(gòu)
LSB的全稱是Linux Standard Base,是一個Linux標(biāo)準(zhǔn)規(guī)范它的目的是制定一系列標(biāo)準(zhǔn)來增強Linux發(fā)行版的兼容性
ELF的全稱是可執(zhí)行可鏈接格式,是一種二進制文件格式Linux下的目標(biāo)文件,可執(zhí)行文件和CoreDump都是按照這種格式存儲的
ELF文件由四部分組成,即ELF文件頭,程序頭表,段和段頭表。
接下來,我們分幾個板塊逐一介紹。
1.1 ELF文件頭
ELF文件頭記錄了整個文件的屬性信息原來的二進制很不方便觀察但是,我們有一個方便的工具——readelf,可以幫助我們查看elf文件中的各種信息
我們先來看看編譯后的可執(zhí)行文件的ELF文件頭,可以使用—file—header選項查看。
# readelf—file—header helloworldelfheader:Magic:7f 454 c 46020101000000000000000 class:elf 64 data:2 ' s complement,little endian version:1OS/ABI:UNIX—SystemVABIVersion:0 type:EXECMachine:advancedmicrodevices x86—64 version:0x 1 entry point address:0x 401040 startofprogramheaders:64和CORE。
入口地址:程序的入口地址,顯示入口在0x401040。
這個頭的大小:ELF文件頭的大小,這里顯示為占用64個字節(jié)。
以上字段是ELF頭中對ELF的整體描述此外,ELF頭包含關(guān)于程序頭和節(jié)頭的描述信息
程序頭的開始:表示程序頭的位置。
程序頭的大小:每個程序頭的大小
節(jié)目頭數(shù):總共有多少個節(jié)目頭。
節(jié)頭的開始:表示節(jié)頭的開始位置。
節(jié)標(biāo)題的大小:每個節(jié)標(biāo)題的大小
章節(jié)標(biāo)題的數(shù)量:有多少章節(jié)標(biāo)題。
1.2程序標(biāo)題表
在介紹程序頭表之前,先介紹一對類似的概念ELF文件中的Segment和Section。
ELF文件中最重要的單元是一個接一個的節(jié)每個部分由編譯器鏈接器生成,有不同的用途比如編譯器會把我們寫的代碼編譯好放進去文本部分,并將全局變量放入數(shù)據(jù)或bss部分
但是對于操作系統(tǒng)來說,它并不關(guān)注具體的節(jié)是什么,它只關(guān)注這個內(nèi)容應(yīng)該加載到內(nèi)存中的權(quán)限是什么,比如讀,寫,執(zhí)行等權(quán)限屬性因此,具有相同權(quán)限的Section可以放在一起形成一個段,便于操作系統(tǒng)更快地加載
由于Segment和Section翻譯成中文,它們的意思過于接近,很難理解所以在這篇文章里,我直接用了原來的Segment和Section的概念,而不是翻譯成段落或小節(jié),太混亂了
節(jié)目頭表作為所有節(jié)目段的頭信息來描述所有節(jié)目段
使用readelf工具的— program—headers選項來分析和查看存儲在該區(qū)域中的內(nèi)容。
# readelf—program—headershelloworldelfpiletypeisexecentrypoint 0x 401040 there are 11 program headers,startingatoffset 64 program headers:typeoffsetvirtaddrphysadrfilesizemsizflagsalignphdr 0x 00000000000040000000004000000000000000040000000000000000000000000000000000000...0001 . interp 02 . interp . note . GNU . build—id . note . ABI—tag . GNU . hash . dyn sym . dynstr . GNU . version . GNU . version _ r . rela . dyn . rela . PLT . text . fini 04 . rodata . eh _ frame _ HDR . eh _ frame 05 . init _ array . fini _ array . dynamic . got . PLT . data . BSS 06 . dynamic 07 . note . GNU .
上面的結(jié)果顯示總共有11個程序頭。
對于每個段,輸出Offset,VirtAddr和描述當(dāng)前段的其他信息Offset表示當(dāng)前段在二進制文件中的起始位置,F(xiàn)ileSiz表示當(dāng)前段的大小Flag表示當(dāng)前段的權(quán)限類型,R表示全部,E表示可執(zhí)行,W表示可寫
在底部,它還顯示了每個部分由哪些部分組成例如,第03節(jié)由四個部分組成初始化plt文字菲尼
1.3章節(jié)標(biāo)題表
與程序頭表不同,段頭表直接描述每個段它們都描述了不同的部分,但是它們有不同的目的,一個用于加載,另一個用于鏈接
使用readelf工具的— section—headers選項來解析和查看存儲在該區(qū)域中的內(nèi)容。
# readelf—section—header shell worldthere 30 section headers,startingatoffset 0x5b 10:section headers:NameTypeAddressOffsetSizeEntSizeFlagsLinkInfoAlign....... textprogbits 0000000000401040000000104000000000000175000000000000000 ax 0016。......data progbits 00000000004040200000000302000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000......KeytoFlags:W,A,X,M,S,I,L,O,G,T,C,x,o,E,l,p
結(jié)果顯示,文件中有30個部分,每個部分在二進制文件中的位置由Offset列指示部分的大小由大小列反映
在這30個部分中,每個部分都有獨特作用我們寫的代碼會放到節(jié)里編譯成二進制指令后的文本此外,我們看到文本部分顯示地址為00000000401040回想一下,我們在ELF文件的頭文件中看到,入口點地址顯示的入口地址是0x401040這表明程序的入口地址是文本段
還有另外兩個部分值得注意:數(shù)據(jù)和bss代碼中的全局變量數(shù)據(jù)在編譯后會占據(jù)這兩段中的一些位置如下面的簡單代碼所示
//未初始化的內(nèi)存區(qū)域位于bss段intdata1//初始化的內(nèi)存區(qū)域位于數(shù)據(jù)段intdata2 = 100//代碼位于intmain1.4在供進一步查看的文本段
接下來,我們想看看前面提到的程序條目0x401040,看看它是什么這一次,讓我們在nm命令的幫助下,仔細看看可執(zhí)行文件中的符號及其地址信息—n選項用于按地址而不是按名稱對顯示的符號進行排序
# nm—nhelloworldw _ _ gmon _ start _ _ U _ _ libc _ start _ mainGLIBC _ 2 . 2 . 5 uprintfglibc _ 2 . 2 . 50000000000401040t _ start 0000000000401126 main
從上面的輸出可以看出,程序條目0x401040指向_start函數(shù)的地址在這個函數(shù)執(zhí)行一些初始化操作之后,我們的入口函數(shù)main將被調(diào)用,它位于地址0x401126
二。用戶進程創(chuàng)建過程概述
我們寫好的代碼編譯生成可執(zhí)行程序后,下一步就是用shell加載運行了一般來說,shell進程通過fork+execve加載并運行新的進程簡單加載helloworld命令的shell的核心邏輯如下
//shell代碼示例int main)PID = fork(),If(pid0)//如果在進程中//使用exec系列函數(shù)加載運行可執(zhí)行文件execve ( "Hello World ",argv,envp),其他
shell進程首先通過fork系統(tǒng)調(diào)用創(chuàng)建一個進程然后在子進程中調(diào)用execve來加載執(zhí)行的程序文件,然后就可以調(diào)用到程序文件的運行入口來運行這個程序了
fork系統(tǒng)調(diào)用在內(nèi)核入口處的kernel/fork.c下。
//file:kernel/fork . csyscall _ define 0 return do _ fork(SIGCHLD,0,0,NULL,NULL),
在do_fork的實現(xiàn)中,核心是一個copy_process函數(shù),它復(fù)制父進程生成一個新的task_struct。
//file:kernel/fork . clong do _ fork//復(fù)制一個task_struct到struct task _ struct * p,p=copy_process(clone_flags,stack_start,stack_size,child_tidptr,NULL,trace),//將子任務(wù)加入就緒隊列,等待調(diào)度器調(diào)度wake _ up _ new _ task(p),
在copy_process函數(shù)中為新進程申請task_struct,用當(dāng)前進程自己的地址空間,命名空間等初始化新進程,并為其申請工藝pid
//file:kernel/fork . cstatics tructtask_struct * copy _ process//復(fù)制進程task _ struct結(jié)構(gòu)struct task _ struct * p,p=dup_task_struct(當(dāng)前),//process核心元素初始化retval = copy _ files (clone _ flags,p),retval=copy_fs(clone_flags,p),retval=copy_mm(clone_flags,p),retval = copy _ namespaces(clone _ flags,p),//申請pidampamp設(shè)置進程號PID = alloc _ PID(p—n proxy—PID _ ns),p—PID = PID _ NR(PID),p—tgid = p—PID,
執(zhí)行后,進入wake_up_new_task,讓新進程等待調(diào)度器調(diào)度。
但是,fork系統(tǒng)調(diào)用只能根據(jù)當(dāng)前shell進程復(fù)制一個新進程這個新進程中的代碼和數(shù)據(jù)仍然與原始shell進程中的代碼和數(shù)據(jù)完全相同
要加載并運行另一個程序,比如我們編譯的helloworld程序,您需要使用execve系統(tǒng)調(diào)用。
三。Linux可執(zhí)行加載程序
其實Linux只能加載ELF這種可執(zhí)行文件格式,不能寫死當(dāng)它啟動時,它將加載它支持的所有可執(zhí)行文件的解析器并使用格式雙向鏈表來保存所有解析器內(nèi)存中格式雙向鏈表的結(jié)構(gòu)如下圖所示
我們以ELF的loader elf_format為例,看看這個loader是怎么注冊的在Linux中,每個加載器由一個linux_binfmt結(jié)構(gòu)表示它指定了用于加載二進制可執(zhí)行文件的load_binary函數(shù)指針,以及用于加載崩潰文件的core_dump函數(shù)
//file:include/Linux/bin fmts . hstructlinux _ binfmtint(struct Linux _ bin PRM *),int(* load _ shlib)(struct file *),int(* core _ dump)(structcoredump _ params * cprm),,
ELF的loader ELF _ format指定了具體的加載函數(shù),例如load_binary成員指向具體的load_elf_binary函數(shù)這是ELF裝載的入口
//file:fs/bin fmt _ elf . cstaticstructlinux _ binfmtelf _ format =模塊= THIS _模塊,load_binary=load_elf_binary,load_shlib=load_elf_library,核心轉(zhuǎn)儲=elf核心轉(zhuǎn)儲,
register_binfmt將在初始化期間注冊加載程序elf_format。
//file:fs/bin fmt _ elf . cstaticint _ _ init init _ elf _ binfmtregister _ bin fmt(amp,elf _ format),return0
而register_binfmt就是把加載器掛在全局加載器list—formats全局鏈表中。
//file:fs/exec . cstaticlist _ HEAD,void _ _ register _ bin fmt(struct Linux _ bin fmt * fmt,intinsert)插入。list _ add(amp,fmt—左側(cè),amp格式):list _ add _ tail(amp,fmt—左側(cè),amp格式),
Linux支持除elf文件格式之外的其他格式在源碼目錄中搜索register_binfmt,可以找到Linux操作系統(tǒng)支持的所有加載器格式
# grep—r " register _ bin fmt " * fs/bin fmt _ flat . c:register _ bin fmt,fs/bin fmt _ elf _ FD pic . c:register _ bin fmt(amp,elf _ FD pic _ format),fs/bin fmt _ som . c:register _ bin fmt(amp,som _ format),fs/bin fmt _ elf . c:register _ bin fmt(amp,elf _ format),fs/bin fmt _ aout . c:register _ bin fmt(amp,aout _ format),fs/bin fmt _ script . c:register _ bin fmt(amp,script _ format),fs/bin fmt _ em86 . c:register _ bin fmt(amp,em86 _ format),
以后Linux在加載二進制文件時會遍歷格式鏈表,根據(jù)要加載的文件格式查詢合適的加載器。
四。execve加載用戶程序
加載可執(zhí)行文件的具體工作由execve系統(tǒng)調(diào)用完成。
系統(tǒng)調(diào)用將讀取用戶輸入的可執(zhí)行文件名稱,參數(shù)列表和環(huán)境變量,并開始加載和運行用戶指定的可執(zhí)行文件系統(tǒng)調(diào)用的位置在fs/exec.c文件中
//file:fs/exec . csys call _ define 3 struct filename * path = getname(filename),do_execve(path—name,argv,envp)int do _ exec ve()returndo _ exec ve _ common(filename,argv,envp),
execve系統(tǒng)調(diào)用了do_execve_common函數(shù)我們來看看這個函數(shù)的實現(xiàn)
//file:fs/exec . cstaticindo _ exec ve _ common//Linux _ bin PRM結(jié)構(gòu)用于保存加載二進制文件時使用的參數(shù)structlinux _ binprm * bprm//1申請并初始化brm對象值bprm = kzaloc (sizeof (* bprm),GFP _ kernel),bprm—file =,bprm—filename =,bprm _ mm _ init(bprm)bprm—argc = count(argv,MAX _ ARG _ STRINGS),bprm—envc=count(envp,MAX _ ARG _ STRINGS),prepare _ bin PRM(bprm),//2遍歷查找合適的二進制加載器search _ binary _ handler(bprm),
在該功能中申請和初始化brm對象的具體工作可以如下圖所示。
在這個函數(shù)中,已經(jīng)完成了三項工作。
1.使用kzalloc申請linux_binprm內(nèi)核對象這個內(nèi)核對象用于保存加載二進制文件時使用的參數(shù)應(yīng)用后,參數(shù)對象被初始化
其次,在bprm_mm_init中將申請一個全新的mm_struct對象,它將被保留給新的進程。
第三,為新進程的堆棧申請一頁虛擬內(nèi)存空間,記錄堆棧指針。
第四,讀取二進制文件的前128個字節(jié)。
讓我們看一下與初始化堆棧相關(guān)的代碼。
//file:fs/exec . cstaticint _ _ bprm _ mm _ init bprm—VMA = VMA = kmem _ cache _ zal loc(VM _ area _ cachep,GFP _ KERNEL),VMA—VM _ end = STACK _ TOP _ MAX,VMA—VM _ start = VMA—VM _ end—PAGE _ SIZE,bprm—p = VMA—VM _ end—sizeof(void *),
在上面的函數(shù)中,申請了一個vma對象,vm_end指向STACK_TOP_MAX(靠近地址空間頂部的一個位置),在vm_start和vm_end之間留出一個頁面大小也就是說,默認(rèn)情況下,堆棧的大小為4KB最后,堆棧的指針被記錄到bprm—gt,p中等
再看一下prepare_binprm在這個函數(shù)中,從文件頭中讀取128個字節(jié)這樣做的原因是為了讀取二進制文件的文件頭,以便于后期判斷其文件類型
//file:include/uapi/Linux/bin fmts . h # defineBINPRM _ BUF _ SIZE 128//file:fs/exec . cint prepare _ binprmmemset(bprm—BUF,0,bin PRM _ BUF _ SIZE),returnkernel_read(bprm—file,0,bprm—buf,bin PRM _ BUF _ SIZE),
在申請并初始化brm對象值后,最后使用search_binary_handler函數(shù)遍歷系統(tǒng)中注冊的加載器,嘗試解析并加載當(dāng)前的可執(zhí)行文件。
在3.1節(jié)中,我們介紹了系統(tǒng)的所有加載程序都注冊在格式全局鏈表中search_binary_handler函數(shù)的工作過程是遍歷全局鏈表,根據(jù)二進制文件頭中攜帶的文件類型數(shù)據(jù)找到解析器找到調(diào)用解析器加載二進制文件的函數(shù)
//file:fs/exec . cint search _ binary _ handler fortry = 0,try2try++list_for_each_entry(fmt,ampformats,LH)int(* fn)(struct Linux _ bin PRM *)= fmt—load _ binary,retval = fn(bprm),//如果加載成功,則返回If(retval = 0)return retval,//加載失敗繼續(xù)循環(huán)以嘗試加載
上面代碼中的list_for_each_entry是遍歷格式的全局鏈表,遍歷時判斷每個鏈表元素是否有l(wèi)oad_binary函數(shù)如果有,調(diào)用它并嘗試加載它
回想一下3.1可執(zhí)行文件加載程序的注冊對于ELF文件加載器elf_format,load_binary函數(shù)的指針指向load_elf_binary
//file:fs/bin fmt _ elf . cstaticstructlinux _ binfmtelf _ format =模塊= THIS _模塊,
然后加載工作會進入load_elf_binary函數(shù)這個函數(shù)很長可以說,所有的程序加載邏輯都體現(xiàn)在這個函數(shù)中根據(jù)這個功能的主要工作,我分以下五個小部分給大家介紹
在介紹的過程中,為了表達清楚,我會稍微調(diào)整一下源代碼的位置,可能和內(nèi)核源代碼的行順序不一樣。
4.1 ELF文件頭讀取
在load_ELF_binary中,將首先讀取ELF文件的頭。
文件頭包含了當(dāng)前文件格式類型等一些數(shù)據(jù),所以在讀取文件頭后會做出一些合法性判斷如果不合法,退出并返回
//file:fs/bin fmt _ ELF . cstaticintload _ ELF _ binary//4.1 ELF文件頭解析//定義結(jié)構(gòu)標(biāo)題并申請內(nèi)存保存ELF文件頭structstructelfhdrelf _ exstructelfhdrinterp _ elf _ ex* locloc=kmalloc(sizeof(*loc),GFP _ KERNEL),//獲取二進制頭loc—gt,elf _ ex = *((structelfhdr *)bprm—gt,buf),//頭上做一系列合法性判斷,退出if(loc—gt,elf_ex.e_type!= ET _ EXECampamp...)gotoout...4.2程序標(biāo)題讀取
程序頭的數(shù)目記錄在ELF文件的頭中,緊接在ELF文件頭之后的是程序頭表這樣內(nèi)核就可以讀出所有的程序頭
//file:fs/bin fmt _ elf . cstaticintload _ elf _ binary//4.1 elf文件頭解析//4.2ProgramHeader讀取//elf_ex.e_phnum保存程序頭個數(shù)//然后根據(jù)program header size size of(struct elf _ phdr)//計算所有程序頭大小,讀入size = loc—elf _ exe _ phnum * sizeof(struct elf _ phdr),elf_phdata=kmalloc(size,GFP _ KERNEL),kernel_read(bprm—file,loc—elf_ex.e_phoff,(char*)elf_phdata,size),4.3清空父進程繼承的資源
fork系統(tǒng)調(diào)用創(chuàng)建的進程包含了原進程的很多信息,比如舊的地址空間,信號表等等這些新程序在運行時毫無用處,所以需要清理
工作包括初始化新進程的信號表,應(yīng)用新的地址空間對象等。
//file:fs/bin fmt _ Elf . cstaticintload _ Elf _ binary//4.1 Elf文件頭解析//4.2ProgramHeader讀取//4.3清除父進程繼承的資源retval = flush _ old _ exec(bprm),current—mm—start _ stack = bprm—p,
清空父進程繼承的資源后,直接將之前準(zhǔn)備的進程棧的地址空間指針設(shè)置為mm對象以便將來可以使用該堆棧
4.4執(zhí)行分段加載
接下來,加載程序會將ELF文件中所有加載類型的段加載到內(nèi)存中使用elf_map在虛擬地址空間中分配虛擬內(nèi)存最后,適當(dāng)?shù)卦O(shè)置虛擬地址空間mm_struct中的地址空間相關(guān)指針,如start_code,end_code,start_data和end_data
我們來看看具體的代碼:
//file:fs/bin fmt _ Elf . cstaticintload _ Elf _ binary//4.1 Elf文件頭解析//4.2ProgramHeader讀取//4.3清除父進程繼承的資源//4.4執(zhí)行段加載進程//遍歷可執(zhí)行文件的program header for(I = 0,elf _ ppnt = ppnt iltloc—gt,elf _ ex.e _ phnum++,elf _ ppnt++)//只加載LOAD類型的段,否則跳過if(elf_ppnt—p_type!=PT_LOAD)繼續(xù),//為段建立內(nèi)存mmap,將程序文件的內(nèi)容映射到虛擬內(nèi)存空間//這樣以后就可以訪問程序中的代碼和數(shù)據(jù)了
其中l(wèi)oad_bias是要加載到內(nèi)存中的段的基址。這個參數(shù)有幾種可能性
值為0表示直接根據(jù)ELF文件中的地址在內(nèi)存中映射。
為了將該值與整數(shù)頁的開頭對齊,物理文件對于可執(zhí)行文件的大小來說可能足夠緊湊,而不考慮對齊問題可是,為了高效地運行,操作系統(tǒng)需要在整數(shù)頁的開始處加載段
4.5數(shù)據(jù)存儲應(yīng)用放大器,堆初始化
因為進程的數(shù)據(jù)段需要寫權(quán)限,所以需要使用set_brk系統(tǒng)調(diào)用為數(shù)據(jù)段申請?zhí)摂M內(nèi)存。
//file:fs/bin fmt _ Elf . cstaticintload _ Elf _ binary//4.1 Elf文件頭解析//4.2ProgramHeader讀取//4.3清除父進程繼承的資源//4.4執(zhí)行段加載進程//4.5申請數(shù)據(jù)內(nèi)存amp初始化retval=set_brk(elf_bss,elf _ brk),
set_brk函數(shù)中做了兩件事:第一件是為數(shù)據(jù)段申請?zhí)摂M內(nèi)存,第二件是初始化進程堆的開始指針和結(jié)束指針。
//file:fs/bin fmt _ ELF . cstaticintset _ brk//1為數(shù)據(jù)段申請?zhí)摂M內(nèi)存start = ELF _ page align(start),end = ELF _ page align(end),if(end start)unsignedlongaddr,addr=vm_brk(start,end—start),//2初始化堆的指針current—mm—start _ brk = current—mm—brk = end,return0
因為程序初始化時堆還是空的因此,當(dāng)堆指針被初始化時,堆的start_brk address _ brk和end地址brk都被設(shè)置為相同的值
4.6跳轉(zhuǎn)到程序入口執(zhí)行。
程序的入口地址記錄在ELF文件的頭中在非動態(tài)鏈接加載的情況下,入口地址是這樣的
但是如果是動態(tài)鏈接,也就是說有INTERP類型的段,這個動態(tài)鏈接器會先加載運行,然后回調(diào)到程序的代碼入口地址。
# readelf—program—headershelloworldProgramHeaders:typeoffsetvirtaddrphysadrfilesizmemsizflagsaligninterp0x 0000000000002 a 80000000000004002 a 80000000000000001 c 0x 0000000000000001 c 0x 0 x 1
對于動態(tài)加載器類型,您需要首先將動態(tài)加載器加載到地址空間中。
加載完成后,計算動態(tài)加載程序的入口地址下面我展示這段代碼,不耐煩的同學(xué)可以跳過反正只要知道這里是一個程序的入口地址就行了
//file:fs/bin fmt _ Elf . cstaticintload _ Elf _ binary//4.1 Elf文件頭解析//4.2ProgramHeader讀取//4.3清除父進程繼承的資源//4.4執(zhí)行段加載//4.5申請數(shù)據(jù)內(nèi)存amp初始化//4.6跳轉(zhuǎn)到程序入口執(zhí)行//第一次遍歷programheadertable//只對PT_INTERP類型的段進行預(yù)處理//該段保存動態(tài)加載器在文件系統(tǒng)中的路徑信息for(I = 0,iltloc—gt,elf _ ex.e _ phnum++) ...//第二次遍歷programheadertable并做一些特殊處理elf _ ppnt = elf _ phdatafor(I = 0,iltloc—gt,elf _ ex.e _ phnum++,elf _ ppnt++)...//如果程序中指定了動態(tài)鏈接器,則讀出動態(tài)鏈接器程序if(elf_interpreter)// Load并返回動態(tài)鏈接器代碼段elf_entry=load_elf_interp的地址(amploc—gt,interp_elf_ex,解釋器,放大器,interp_map_addr,load _ bias),//計算動態(tài)鏈接器入口地址elf _ entry+= loc—gt,interp _ elf _ ex.e _ entryelse elf _ entry = loc—gt,elf _ ex.e _ entry//跳轉(zhuǎn)到門戶啟動start _ thread (regs,elf _ entry,bprm—gt,p),...五.總結(jié)
看似簡單的一行helloworld代碼,但要想看清楚它的運行過程,需要很大的內(nèi)功。
本文首先帶領(lǐng)大家認(rèn)識和理解二進制可運行ELF文件格式ELF文件由四部分組成,即ELF文件頭,程序頭表,段和段頭表
當(dāng)Linux初始化時,所有受支持的加載程序都將在一個全局鏈表中注冊對于ELF文件,其加載器在內(nèi)核中定義為elf_format,其二進制加載入口為load_elf_binary函數(shù)
一般來說,shell進程通過fork+execve加載并運行新的進程執(zhí)行fork系統(tǒng)調(diào)用的作用是創(chuàng)建一個新的進程但是,fork創(chuàng)建的新進程的代碼和數(shù)據(jù)與原來的shell進程完全相同要加載并運行另一個程序,需要使用execve系統(tǒng)調(diào)用
在execve系統(tǒng)調(diào)用中,將首先應(yīng)用一個linux_binprm對象在初始化linux_binprm的過程中,會申請一個全新的mm_struct對象,并為新進程預(yù)留還將為新進程的堆棧準(zhǔn)備一頁虛擬內(nèi)存還會讀取可執(zhí)行文件的前128個字節(jié)
下一步是調(diào)用ELF加載器的load _ ELF _ binary函數(shù)進行實際加載。將大致執(zhí)行以下步驟:
ELF文件頭解析
程序頭讀取
清空父進程繼承的資源,使用新的mm_struct和新的堆棧。
執(zhí)行段加載,將ELF文件中加載類型的所有段加載到虛擬內(nèi)存中。
為數(shù)據(jù)段申請內(nèi)存,初始化堆的開始指針。
最后計算,跳轉(zhuǎn)到程序入口執(zhí)行。
當(dāng)用戶進程啟動時,我們可以通過proc偽文件查看進程中的每個段。
# cat/proc/46276/maps 00400000—00401000 r—p 000000000 FD:01396999/root/work _ temp/hello world 00401000—00402000 r—XP 00001000 FD:01396999/root/work _ temp/hello world 00402000—004030000......7 f 01231 c 0000—7 f 01231 c 1000 r—p 0002 a 000 FD:011182554/usr/lib 64/LD—2.32 . so 7 f 01231 c 1000—7 f 01231 c 3000 rw—p 0002 b 000 FD:011182554/usr/lib 64/LD—2.32 . so 7 ffdf 059000......
雖然這篇文章很長,但它仍然只是列出了一般的加載和啟動過程如果你在以后的工作和學(xué)習(xí)中遇到了想找出來的問題,可以按照本文的思路,在源代碼中找到具體的問題,然后幫你找到工作中問題的解決方案
最后,細心的讀者可能會發(fā)現(xiàn),在這個例子中,加載一個新程序來運行的過程實際上存在一些浪費fork系統(tǒng)調(diào)用首先復(fù)制父進程的大量信息,當(dāng)execve加載可執(zhí)行程序時,重新賦值因此,在實際的shell程序中,一般使用vfork它的工作原理和fork的基本相同,不同的是它會復(fù)制較少execve系統(tǒng)調(diào)用中不使用的信息,從而提高加載性能
免責(zé)聲明:此文內(nèi)容為本網(wǎng)站轉(zhuǎn)載企業(yè)宣傳資訊,僅代表作者個人觀點,與本網(wǎng)無關(guān)。僅供讀者參考,并請自行核實相關(guān)內(nèi)容。




