偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

從Execve到進(jìn)程運(yùn)行:ELF加載的內(nèi)核實(shí)現(xiàn)詳解

系統(tǒng) Linux
在 Linux 的世界里,ELF 文件就像是一個(gè)神秘的 “魔法盒子”,承載著程序從源代碼到可執(zhí)行實(shí)體的關(guān)鍵信息,而 Linux 進(jìn)程則是這些信息在系統(tǒng)中鮮活運(yùn)行的體現(xiàn)。從 ELF 文件的結(jié)構(gòu)剖析,到它如何通過(guò)編譯鏈接成為可執(zhí)行文件,再到 Linux 進(jìn)程的創(chuàng)建以及 ELF 文件在其中的加載過(guò)程,每一個(gè)環(huán)節(jié)都充滿(mǎn)了奇妙的技術(shù)細(xì)節(jié)。

在 Linux 的世界里,ELF 文件就像是一個(gè)神秘的 “魔法盒子”,承載著程序從源代碼到可執(zhí)行實(shí)體的關(guān)鍵信息,而 Linux 進(jìn)程則是這些信息在系統(tǒng)中鮮活運(yùn)行的體現(xiàn)。從 ELF 文件的結(jié)構(gòu)剖析,到它如何通過(guò)編譯鏈接成為可執(zhí)行文件,再到 Linux 進(jìn)程的創(chuàng)建以及 ELF 文件在其中的加載過(guò)程,每一個(gè)環(huán)節(jié)都充滿(mǎn)了奇妙的技術(shù)細(xì)節(jié)。

理解從 ELF 文件到 Linux 進(jìn)程的轉(zhuǎn)化過(guò)程,對(duì)于 Linux 開(kāi)發(fā)者、系統(tǒng)管理員以及對(duì)計(jì)算機(jī)底層原理感興趣的技術(shù)愛(ài)好者來(lái)說(shuō),都具有重要的意義。它不僅有助于我們優(yōu)化程序的編譯、鏈接和運(yùn)行效率,還能在程序出現(xiàn)問(wèn)題時(shí),通過(guò)深入分析 ELF 文件和進(jìn)程狀態(tài),快速定位和解決問(wèn)題。例如,在調(diào)試程序時(shí),了解 ELF 文件中的符號(hào)表和重定位信息,可以幫助我們準(zhǔn)確地找到程序中的錯(cuò)誤代碼位置;在系統(tǒng)性能優(yōu)化方面,掌握進(jìn)程的內(nèi)存布局和動(dòng)態(tài)鏈接機(jī)制,可以讓我們更好地管理系統(tǒng)資源,提高程序的運(yùn)行效率。今天,就讓我們一起揭開(kāi)從 ELF 文件到 Linux 進(jìn)程的神秘面紗,深入了解它們之間的奇妙轉(zhuǎn)化。

一、ELF 文件:Linux 世界的 “靈魂容器”

1.1什么是 ELF 文件

ELF,全稱(chēng)是 Executable and Linkable Format,即可執(zhí)行與可鏈接格式 ,是 Linux 下二進(jìn)制文件的標(biāo)準(zhǔn)格式。就像 Windows 系統(tǒng)中大家熟悉的.exe 文件是可執(zhí)行程序的載體一樣,ELF 文件在 Linux 系統(tǒng)中承擔(dān)著同樣的角色,并且功能更為豐富。

ELF 文件不僅僅局限于可執(zhí)行文件,它還涵蓋了多種類(lèi)型:

  1. 可執(zhí)行文件:這是我們?nèi)粘J褂玫某绦颍热绯R?jiàn)的命令行工具ls、grep等,或者是我們自己編譯生成的可執(zhí)行程序。當(dāng)我們?cè)诮K端輸入命令運(yùn)行它們時(shí),系統(tǒng)就會(huì)依據(jù) ELF 文件的內(nèi)容將其轉(zhuǎn)化為運(yùn)行中的進(jìn)程。
  2. 可重定位文件:通常是編譯過(guò)程中生成的目標(biāo)文件,以.o 為擴(kuò)展名。這類(lèi)文件包含了機(jī)器代碼和數(shù)據(jù),但它們的地址是相對(duì)的,還需要經(jīng)過(guò)鏈接過(guò)程才能最終成為可執(zhí)行文件 。比如我們編寫(xiě)一個(gè)簡(jiǎn)單的 C 程序,使用gcc -c命令編譯后生成的.o 文件就是可重定位文件。
  3. 共享對(duì)象文件:也就是我們常說(shuō)的動(dòng)態(tài)鏈接庫(kù),以.so 為擴(kuò)展名,類(lèi)似于 Windows 下的.dll 文件。共享對(duì)象文件可以在多個(gè)程序之間共享代碼和數(shù)據(jù),大大節(jié)省了系統(tǒng)資源。許多大型軟件項(xiàng)目都會(huì)依賴(lài)各種共享庫(kù),比如圖形界面程序可能依賴(lài)于 GTK 庫(kù),科學(xué)計(jì)算程序可能依賴(lài)于 BLAS、LAPACK 等數(shù)學(xué)庫(kù)。
  4. 核心轉(zhuǎn)儲(chǔ)文件:當(dāng)程序出現(xiàn)異常崩潰時(shí),操作系統(tǒng)會(huì)生成一個(gè)核心轉(zhuǎn)儲(chǔ)文件,它記錄了程序崩潰時(shí)的內(nèi)存狀態(tài)、寄存器值等信息,對(duì)于開(kāi)發(fā)者調(diào)試程序非常有幫助。通過(guò)分析核心轉(zhuǎn)儲(chǔ)文件,我們可以找到程序崩潰的原因,比如空指針引用、數(shù)組越界等問(wèn)題。

1.2 ELF 的 “身份證”

在 Linux 系統(tǒng)中,判斷一個(gè)文件是否為 ELF 格式其實(shí)非常簡(jiǎn)單,使用file命令就可以輕松做到。例如,我們想要查看/bin/ls這個(gè)文件,在終端輸入file /bin/ls,得到的輸出結(jié)果開(kāi)頭如果是 “ELF”,那就說(shuō)明它是 ELF 格式的文件 ,如/bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, ... 。

從更底層的原理來(lái)講,ELF 文件有一個(gè)獨(dú)特的標(biāo)識(shí) —— 魔數(shù)(Magic Number)。通過(guò)hexdump -C -n 16 /bin/ls命令查看/bin/ls文件的前 16 個(gè)字節(jié),會(huì)看到類(lèi)似00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|的輸出 ,其中最開(kāi)始的7f 45 4c 46就是 ELF 文件的魔數(shù),45 4c 46對(duì)應(yīng)的正是 ASCII 碼中的 “ELF” 三個(gè)字母,而前面的7f是一個(gè)特殊字符 。操作系統(tǒng)在識(shí)別文件時(shí),首先就會(huì)檢查這四個(gè)字節(jié),如果匹配,就可以確定它是一個(gè) ELF 文件,所以說(shuō)魔數(shù)就像是 ELF 文件的 “身份證” 。

1.3 ELF 文件的內(nèi)部結(jié)構(gòu)剖析

ELF 文件的結(jié)構(gòu)就像是一座精心規(guī)劃的大廈,每個(gè)部分都有其獨(dú)特的功能和作用,共同協(xié)作使得程序能夠順利地從磁盤(pán)走向內(nèi)存,最終在 CPU 上運(yùn)行。

圖片圖片

(1)文件頭(ELF Header)

文件頭就像是這本書(shū)的封面和目錄,位于 ELF 文件的開(kāi)頭,固定占據(jù)一定的字節(jié)數(shù)(32 位系統(tǒng)通常是 52 字節(jié),64 位系統(tǒng)通常是 64 字節(jié) )。它包含了許多關(guān)鍵信息,這些信息是理解整個(gè) ELF 文件的基礎(chǔ)。

  • 魔數(shù)(Magic Number):文件頭的前 4 個(gè)字節(jié)是魔數(shù),固定為 0x7f 45 4c 46,其中 45 4c 46 分別對(duì)應(yīng) ASCII 碼中的 'E'、'L'、'F',前面的 0x7f 是一個(gè)特殊字符。魔數(shù)就像是 ELF 文件的 “身份證”,操作系統(tǒng)在加載文件時(shí),首先會(huì)檢查這個(gè)魔數(shù),以確定該文件是否為合法的 ELF 文件。
  • 文件類(lèi)型:它表明了該 ELF 文件屬于可執(zhí)行文件、可重定位文件、共享對(duì)象文件還是核心轉(zhuǎn)儲(chǔ)文件等。通過(guò)這個(gè)信息,系統(tǒng)可以知道如何處理這個(gè)文件。
  • 目標(biāo)機(jī)器架構(gòu):告知系統(tǒng)該 ELF 文件是為哪種 CPU 架構(gòu)編譯的,比如常見(jiàn)的 x86_64、ARM 等。不同的 CPU 架構(gòu)有不同的指令集和寄存器結(jié)構(gòu),了解目標(biāo)機(jī)器架構(gòu)能確保系統(tǒng)正確地解釋和執(zhí)行 ELF 文件中的代碼。
  • 入口點(diǎn)地址:程序執(zhí)行的起始地址,當(dāng) ELF 文件被加載到內(nèi)存并準(zhǔn)備執(zhí)行時(shí),CPU 會(huì)從這個(gè)地址開(kāi)始讀取和執(zhí)行指令。
  • 程序頭表和節(jié)區(qū)頭表的偏移:這兩個(gè)偏移量分別指示了程序頭表和節(jié)區(qū)頭表在 ELF 文件中的位置。通過(guò)這些偏移量,系統(tǒng)可以方便地找到并解析這兩個(gè)重要的表。

(2)程序頭表(Program Header Table)

程序頭表就像是給操作系統(tǒng)的指南,它由一系列的程序頭(Program Header)組成,每個(gè)程序頭描述了一個(gè)段(Segment)的信息。這些段告訴操作系統(tǒng)如何將程序加載到內(nèi)存中執(zhí)行。只有可執(zhí)行文件和共享庫(kù)中存在程序頭表,目標(biāo)文件中是沒(méi)有的。

每個(gè)程序頭是一個(gè)結(jié)構(gòu)體,在 64 位系統(tǒng)中通常是 Elf64_Phdr 結(jié)構(gòu) ,包含以下重要字段:

  • 段類(lèi)型(p_type):指示該段的類(lèi)型,常見(jiàn)的類(lèi)型有 LOAD(可加載段,包含代碼或數(shù)據(jù))、INTERP(指定程序解釋器,即動(dòng)態(tài)鏈接器的路徑)、DYNAMIC(包含動(dòng)態(tài)鏈接相關(guān)信息)等。
  • 段標(biāo)志(p_flags):描述段的權(quán)限和屬性,如可讀(R)、可寫(xiě)(W)、可執(zhí)行(E)等。比如代碼段通常具有可讀和可執(zhí)行權(quán)限,數(shù)據(jù)段可能具有可讀和可寫(xiě)權(quán)限。
  • 文件偏移(p_offset):表示該段在 ELF 文件中的起始偏移位置,通過(guò)這個(gè)偏移量,系統(tǒng)可以在文件中準(zhǔn)確找到該段的內(nèi)容。
  • 虛擬地址(p_vaddr):段被加載到內(nèi)存后的虛擬地址,操作系統(tǒng)會(huì)根據(jù)這個(gè)地址將段映射到相應(yīng)的內(nèi)存區(qū)域。
  • 文件大?。╬_filesz):段在 ELF 文件中的大小,即實(shí)際存儲(chǔ)在文件中的字節(jié)數(shù)。
  • 內(nèi)存大?。╬_memsz):段在內(nèi)存中占用的大小,有些段在內(nèi)存中可能需要額外的空間,比如.bss 段在文件中通常不占用空間,但在內(nèi)存中需要為未初始化的變量分配空間。

在進(jìn)程創(chuàng)建過(guò)程中,操作系統(tǒng)會(huì)讀取程序頭表,找到所有類(lèi)型為 LOAD 的段,并按照段的信息將它們加載到內(nèi)存中,為進(jìn)程的運(yùn)行做好準(zhǔn)備。

(3)節(jié)區(qū)頭表(Section Header Table)

節(jié)區(qū)頭表是給鏈接器、調(diào)試器等工具看的指南,它描述了 ELF 文件中各個(gè)節(jié)區(qū)(Section)的信息。節(jié)區(qū)頭表由多個(gè)節(jié)區(qū)頭(Section Header)組成,每個(gè)節(jié)區(qū)頭是一個(gè)結(jié)構(gòu)體,包含了節(jié)區(qū)的名稱(chēng)、類(lèi)型、地址、大小、偏移量等信息。

通過(guò)節(jié)區(qū)頭表,鏈接器可以在鏈接過(guò)程中準(zhǔn)確地找到各個(gè)目標(biāo)文件中的節(jié)區(qū),并將它們合并和重定位,生成最終的可執(zhí)行文件或共享庫(kù)。調(diào)試器也可以利用節(jié)區(qū)頭表中的信息,獲取調(diào)試符號(hào)、源代碼行號(hào)等調(diào)試信息,方便開(kāi)發(fā)者調(diào)試程序。

節(jié)區(qū)(Sections)與段(Segments)

節(jié)區(qū)是 ELF 文件存儲(chǔ)的基本單位,針對(duì)鏈接器而言;段是運(yùn)行時(shí)內(nèi)存的基本單位,針對(duì)加載器而言。可以把段看作是一個(gè)或多個(gè)功能相似的節(jié)區(qū)的集合。常見(jiàn)的節(jié)區(qū)有:

  • .text:代碼節(jié)區(qū),存放程序的可執(zhí)行代碼,通常具有可讀和可執(zhí)行權(quán)限。
  • .data:已初始化數(shù)據(jù)節(jié)區(qū),存儲(chǔ)已經(jīng)初始化的全局變量和靜態(tài)變量。
  • .bss:未初始化數(shù)據(jù)節(jié)區(qū),用于存放未初始化的全局變量和靜態(tài)變量,在 ELF 文件中,.bss 節(jié)區(qū)通常不占用實(shí)際的磁盤(pán)空間,因?yàn)樗恍枰趦?nèi)存中為這些未初始化變量分配空間。
  • .rodata:只讀數(shù)據(jù)節(jié)區(qū),存放只讀數(shù)據(jù),比如字符串常量、常量數(shù)組等 。
  • .symtab:符號(hào)表節(jié)區(qū),記錄了程序中定義和引用的符號(hào)信息,包括函數(shù)名、變量名、全局變量、局部變量等,以及它們的地址、類(lèi)型等。符號(hào)表在鏈接過(guò)程中起著關(guān)鍵作用,鏈接器通過(guò)符號(hào)表來(lái)解析外部符號(hào)的引用。
  • .strtab:字符串表節(jié)區(qū),保存了符號(hào)表中符號(hào)的名字以及其他一些字符串信息。由于字符串的長(zhǎng)度不一,為了方便管理,將所有字符串集中存儲(chǔ)在這個(gè)節(jié)區(qū),通過(guò)偏移量來(lái)訪問(wèn)具體的字符串。

這些節(jié)區(qū)會(huì)根據(jù)其功能和屬性被劃分到不同的段中,比如.text 節(jié)區(qū)通常會(huì)被包含在代碼段中,.data 和.bss 節(jié)區(qū)會(huì)被包含在數(shù)據(jù)段中 。

二、從 ELF 文件到 Linux 進(jìn)程

2.1ELF 文件的編譯與鏈接

在 Linux 系統(tǒng)中,從我們編寫(xiě)的源代碼到最終生成可執(zhí)行的 ELF 文件,需要經(jīng)過(guò)編譯和鏈接兩個(gè)重要階段。這兩個(gè)階段就像是一場(chǎng)精密的制造過(guò)程,將人類(lèi)可讀的代碼轉(zhuǎn)化為計(jì)算機(jī)能夠理解和執(zhí)行的指令。

(1)編譯階段:當(dāng)我們使用 C、C++ 等編程語(yǔ)言編寫(xiě)好源代碼后,首先要進(jìn)行的就是編譯。以 C 語(yǔ)言為例,我們通常使用 GCC(GNU Compiler Collection)編譯器。假設(shè)我們有一個(gè)簡(jiǎn)單的 C 程序hello.c:

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

使用命令gcc -c hello.c進(jìn)行編譯,這里的-c選項(xiàng)表示只進(jìn)行編譯,不進(jìn)行鏈接。編譯過(guò)程主要分為以下幾個(gè)步驟:

  • 預(yù)處理:預(yù)處理器首先對(duì)源代碼進(jìn)行處理,它會(huì)展開(kāi)頭文件(比如這里的stdio.h),處理宏定義(如果有宏的話(huà)),移除注釋等。經(jīng)過(guò)預(yù)處理后的代碼會(huì)包含所有展開(kāi)后的內(nèi)容,方便后續(xù)的編譯操作。
  • 編譯:編譯器將預(yù)處理后的代碼轉(zhuǎn)化為匯編代碼。對(duì)于上面的hello.c,會(huì)生成對(duì)應(yīng)的hello.s匯編文件。匯編代碼是一種低級(jí)的人類(lèi)可讀代碼,它與機(jī)器指令有著緊密的對(duì)應(yīng)關(guān)系,不同的 CPU 架構(gòu)有不同的匯編語(yǔ)言。
  • 匯編:匯編器將匯編代碼進(jìn)一步轉(zhuǎn)化為目標(biāo)文件,通常以.o 為擴(kuò)展名,這里會(huì)生成hello.o。目標(biāo)文件是一種中間格式,它包含了機(jī)器代碼,但這些代碼的地址還不是最終可執(zhí)行的地址,是相對(duì)地址,并且還需要解決外部符號(hào)的引用等問(wèn)題 。目標(biāo)文件中除了機(jī)器代碼外,還包含了符號(hào)表、重定位信息等。符號(hào)表記錄了程序中定義和引用的符號(hào)(如函數(shù)名、變量名)及其相關(guān)信息;重定位信息則用于在鏈接階段調(diào)整代碼和數(shù)據(jù)的地址,使其能夠正確地運(yùn)行。

(2)鏈接階段:鏈接的主要作用是將多個(gè)目標(biāo)文件以及所需的庫(kù)文件合并成一個(gè)可執(zhí)行文件。鏈接分為靜態(tài)鏈接和動(dòng)態(tài)鏈接兩種方式。

①靜態(tài)鏈接:在靜態(tài)鏈接過(guò)程中,鏈接器會(huì)將程序所依賴(lài)的所有靜態(tài)庫(kù)文件(通常以.a 為擴(kuò)展名)中的相關(guān)代碼和數(shù)據(jù)直接拷貝到生成的可執(zhí)行文件中。這樣生成的可執(zhí)行文件是一個(gè)獨(dú)立的文件,不依賴(lài)外部的庫(kù)文件就可以運(yùn)行。例如,我們有兩個(gè)源文件main.c和func.c,func.c中定義了一個(gè)函數(shù)add,main.c中調(diào)用了這個(gè)函數(shù):

// func.c
int add(int a, int b) {
    return a + b;
}
// main.c
#include <stdio.h>
int add(int a, int b);

int main() {
    int result = add(3, 5);
    printf("The result is: %d\n", result);
    return 0;
}

首先分別編譯這兩個(gè)文件:gcc -c main.c和gcc -c func.c,生成main.o和func.o。然后進(jìn)行靜態(tài)鏈接:gcc -o main main.o func.o,這里的-o選項(xiàng)指定生成的可執(zhí)行文件名為main。在這個(gè)過(guò)程中,鏈接器會(huì)將func.o中的add函數(shù)代碼直接合并到main可執(zhí)行文件中。靜態(tài)鏈接的優(yōu)點(diǎn)是可執(zhí)行文件的運(yùn)行比較獨(dú)立,不依賴(lài)外部庫(kù),移植性好;缺點(diǎn)是生成的可執(zhí)行文件體積較大,因?yàn)樗怂幸蕾?lài)的庫(kù)代碼,如果多個(gè)程序都依賴(lài)同一個(gè)庫(kù),會(huì)導(dǎo)致磁盤(pán)空間和內(nèi)存的浪費(fèi)。

②動(dòng)態(tài)鏈接:動(dòng)態(tài)鏈接則是在程序運(yùn)行時(shí)才加載和鏈接所需的共享庫(kù)文件(以.so 為擴(kuò)展名)。鏈接器在鏈接時(shí)并不會(huì)將共享庫(kù)的代碼直接拷貝到可執(zhí)行文件中,而是記錄下對(duì)共享庫(kù)中符號(hào)的引用信息。當(dāng)程序運(yùn)行時(shí),動(dòng)態(tài)鏈接器(通常是/lib/ld-linux.so.2等)會(huì)根據(jù)這些引用信息,在系統(tǒng)中找到對(duì)應(yīng)的共享庫(kù)文件,并將其加載到內(nèi)存中,然后完成符號(hào)的解析和重定位,使程序能夠正確地調(diào)用共享庫(kù)中的函數(shù)。

繼續(xù)以上面的例子,如果add函數(shù)是在一個(gè)共享庫(kù)libfunc.so中定義的,我們可以這樣進(jìn)行動(dòng)態(tài)鏈接:首先編譯生成共享庫(kù)gcc -shared -fPIC -o libfunc.so func.c,其中-shared表示生成共享庫(kù),-fPIC表示生成位置無(wú)關(guān)代碼(Position - Independent Code),這是共享庫(kù)所必需的。然后編譯main.c并鏈接共享庫(kù)gcc -o main main.o -L. -lfunc,這里-L.表示在當(dāng)前目錄查找?guī)煳募?lfunc表示鏈接名為libfunc.so的共享庫(kù)。動(dòng)態(tài)鏈接的優(yōu)點(diǎn)是節(jié)省磁盤(pán)空間和內(nèi)存,多個(gè)程序可以共享同一個(gè)共享庫(kù);缺點(diǎn)是程序的運(yùn)行依賴(lài)于共享庫(kù),如果共享庫(kù)的版本不兼容或者缺失,可能會(huì)導(dǎo)致程序運(yùn)行出錯(cuò)。

2.2Linux 進(jìn)程的創(chuàng)建

當(dāng) ELF 文件準(zhǔn)備就緒后,它還需要經(jīng)歷從磁盤(pán)到內(nèi)存,從靜態(tài)文件到動(dòng)態(tài)進(jìn)程的轉(zhuǎn)變,才能真正在系統(tǒng)中運(yùn)行起來(lái),與我們進(jìn)行交互。在 Linux 系統(tǒng)中,創(chuàng)建進(jìn)程最常用的方式是使用fork函數(shù)。fork函數(shù)是一個(gè)系統(tǒng)調(diào)用,它的作用是創(chuàng)建一個(gè)新的進(jìn)程,這個(gè)新進(jìn)程被稱(chēng)為子進(jìn)程,而調(diào)用fork的進(jìn)程則是父進(jìn)程。子進(jìn)程幾乎是父進(jìn)程的一個(gè)副本,它會(huì)繼承父進(jìn)程的大部分資源,如文件描述符、內(nèi)存空間(通過(guò)寫(xiě)時(shí)拷貝技術(shù)實(shí)現(xiàn),父子進(jìn)程在寫(xiě)入數(shù)據(jù)前共享同一塊物理內(nèi)存,當(dāng)有一方進(jìn)行寫(xiě)入操作時(shí)才會(huì)復(fù)制一份物理內(nèi)存)、信號(hào)處理方式等,但也有一些不同,比如進(jìn)程 ID(PID)不同,父進(jìn)程 ID(PPID)不同等。fork函數(shù)的返回值很特別,在父進(jìn)程中,它返回子進(jìn)程的 PID;在子進(jìn)程中,它返回 0;如果創(chuàng)建進(jìn)程失敗,它返回一個(gè)負(fù)數(shù)。

下面是一個(gè)簡(jiǎn)單的示例代碼:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        perror("fork error");
        return 1;
    } else if (pid == 0) {
        // 子進(jìn)程
        printf("I am the child process, my pid is %d, my parent's pid is %d\n", getpid(), getppid());
    } else {
        // 父進(jìn)程
        printf("I am the parent process, my pid is %d, the child's pid is %d\n", getpid(), pid);
    }
    return 0;
}

在這個(gè)例子中,當(dāng)fork函數(shù)被調(diào)用后,系統(tǒng)會(huì)創(chuàng)建一個(gè)子進(jìn)程。父進(jìn)程和子進(jìn)程從fork函數(shù)返回后,根據(jù)返回值的不同,分別執(zhí)行不同的代碼塊。這種機(jī)制使得我們可以方便地創(chuàng)建多進(jìn)程程序,實(shí)現(xiàn)并發(fā)處理任務(wù)。例如,在一個(gè) Web 服務(wù)器程序中,父進(jìn)程可以負(fù)責(zé)監(jiān)聽(tīng)端口,接受客戶(hù)端連接,然后創(chuàng)建子進(jìn)程來(lái)處理每個(gè)客戶(hù)端的請(qǐng)求,這樣可以同時(shí)處理多個(gè)客戶(hù)端的并發(fā)請(qǐng)求,提高服務(wù)器的性能。

2.3 ELF 文件的加載

當(dāng)我們?cè)诮K端中輸入命令運(yùn)行一個(gè) ELF 可執(zhí)行文件時(shí),系統(tǒng)會(huì)啟動(dòng)一個(gè)新的進(jìn)程,并將 ELF 文件加載到這個(gè)進(jìn)程的內(nèi)存空間中。具體的加載流程如下:

  1. 創(chuàng)建新進(jìn)程:首先,通過(guò)fork函數(shù)創(chuàng)建一個(gè)新的子進(jìn)程。這個(gè)子進(jìn)程繼承了父進(jìn)程的一些環(huán)境信息,如當(dāng)前工作目錄、用戶(hù) ID 等。
  2. 加載 ELF 文件:子進(jìn)程調(diào)用exec函數(shù)族(如execve等)來(lái)執(zhí)行 ELF 文件。exec函數(shù)會(huì)替換當(dāng)前進(jìn)程的代碼段、數(shù)據(jù)段、堆和棧等,將 ELF 文件的內(nèi)容加載到內(nèi)存中。在加載過(guò)程中,會(huì)讀取 ELF 文件的程序頭表,根據(jù)程序頭表中描述的段信息,將各個(gè)段(如代碼段、數(shù)據(jù)段、只讀數(shù)據(jù)段等)加載到內(nèi)存的相應(yīng)位置。例如,程序頭表中會(huì)指定代碼段的虛擬地址、文件偏移、大小等信息,加載器會(huì)根據(jù)這些信息將代碼段從 ELF 文件中讀取到內(nèi)存中對(duì)應(yīng)的虛擬地址處。
  3. 動(dòng)態(tài)鏈接(如果是動(dòng)態(tài)鏈接的 ELF 文件):如果 ELF 文件是動(dòng)態(tài)鏈接的,在加載過(guò)程中還會(huì)涉及到動(dòng)態(tài)鏈接的步驟。首先,加載器會(huì)根據(jù) ELF 文件中INTERP段指定的路徑,找到動(dòng)態(tài)鏈接器(通常是/lib/ld-linux.so.2等),并將其加載到內(nèi)存中。然后,動(dòng)態(tài)鏈接器會(huì)解析 ELF 文件中的動(dòng)態(tài)鏈接信息,找到并加載程序所依賴(lài)的共享庫(kù)文件。動(dòng)態(tài)鏈接器會(huì)在共享庫(kù)文件中查找程序所引用的符號(hào)(函數(shù)、變量等),并將這些符號(hào)的地址解析出來(lái),填充到 ELF 文件的相應(yīng)位置(通過(guò)重定位操作),使得程序能夠正確地調(diào)用共享庫(kù)中的函數(shù)。例如,在前面提到的使用共享庫(kù)libfunc.so的例子中,動(dòng)態(tài)鏈接器會(huì)在運(yùn)行時(shí)找到libfunc.so,解析其中add函數(shù)的地址,并將這個(gè)地址填充到main程序中調(diào)用add函數(shù)的地方,這樣main程序就能夠正確地調(diào)用libfunc.so中的add函數(shù)了。
  4. 啟動(dòng)進(jìn)程:當(dāng) ELF 文件和所有依賴(lài)的共享庫(kù)都加載完成,并且動(dòng)態(tài)鏈接也完成后,系統(tǒng)會(huì)將程序的入口點(diǎn)地址(在 ELF 文件頭中指定)設(shè)置為進(jìn)程的執(zhí)行起始地址,然后開(kāi)始執(zhí)行程序的第一條指令,至此,ELF 文件成功地轉(zhuǎn)變?yōu)橐粋€(gè)正在運(yùn)行的 Linux 進(jìn)程,它可以與系統(tǒng)和用戶(hù)進(jìn)行交互,完成各種任務(wù)。

代碼示例如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

// 簡(jiǎn)單的ELF程序: 被加載執(zhí)行的程序
void create_target_elf() {
    FILE *f = fopen("target_elf.c", "w");
    if (!f) {
        perror("無(wú)法創(chuàng)建目標(biāo)文件");
        return;
    }

    fprintf(f, "#include <stdio.h>\n");
    fprintf(f, "int main() {\n");
    fprintf(f, "    printf(\"這是被加載的ELF程序在運(yùn)行\(zhòng)\n\");\n");
    fprintf(f, "    printf(\"進(jìn)程ID: %%d\\n\", getpid());\n");
    fprintf(f, "    return 0;\n");
    fprintf(f, "}\n");
    fclose(f);

    // 編譯生成ELF可執(zhí)行文件
    system("gcc target_elf.c -o target_elf");
}

int main() {
    // 創(chuàng)建一個(gè)簡(jiǎn)單的ELF可執(zhí)行文件
    create_target_elf();

    printf("父進(jìn)程開(kāi)始運(yùn)行, PID: %d\n", getpid());

    // 步驟1: 創(chuàng)建新進(jìn)程(fork)
    pid_t pid = fork();

    if (pid < 0) {
        // fork失敗
        perror("fork失敗");
        return 1;
    } 
    else if (pid == 0) {
        // 步驟2: 子進(jìn)程 - 加載并執(zhí)行ELF文件(exec)
        printf("子進(jìn)程創(chuàng)建成功, PID: %d\n", getpid());
        printf("子進(jìn)程開(kāi)始加載ELF文件...\n");

        // 執(zhí)行ELF文件, 這會(huì)替換當(dāng)前進(jìn)程的代碼段、數(shù)據(jù)段等
        char *args[] = {"./target_elf", NULL};
        if (execvp(args[0], args) == -1) {
            perror("execvp執(zhí)行失敗");
            exit(EXIT_FAILURE);
        }

        // 注意: execvp成功的話(huà), 下面的代碼不會(huì)執(zhí)行
        printf("這行代碼不會(huì)被執(zhí)行\(zhòng)n");
    } 
    else {
        // 父進(jìn)程
        printf("等待子進(jìn)程完成...\n");
        int status;
        waitpid(pid, &status, 0); // 等待子進(jìn)程結(jié)束
        printf("子進(jìn)程已結(jié)束, 父進(jìn)程退出\n");
    }

    return 0;
}

編譯并運(yùn)行這個(gè)程序,可以看到整個(gè)流程的輸出,包括父進(jìn)程 ID、子進(jìn)程 ID,以及被加載的 ELF 程序的運(yùn)行情況。這直觀地展示了從命令輸入到 ELF 文件成為運(yùn)行中進(jìn)程的整個(gè)過(guò)程。

三、ELF 文件與 Linux 進(jìn)程的深度關(guān)聯(lián)

3.1ELF 文件對(duì)進(jìn)程的重要性

ELF 文件對(duì)于 Linux 進(jìn)程而言,就如同基石對(duì)于高樓大廈,是進(jìn)程得以存在和正常運(yùn)行的根本。它為進(jìn)程提供了運(yùn)行所需的代碼和數(shù)據(jù),其結(jié)構(gòu)信息則像精準(zhǔn)的導(dǎo)航圖,指導(dǎo)著進(jìn)程的創(chuàng)建和內(nèi)存布局。

從代碼層面來(lái)看,ELF 文件中的.text 節(jié)區(qū)存放著可執(zhí)行代碼,這些代碼是進(jìn)程執(zhí)行各種任務(wù)的核心指令集。當(dāng)進(jìn)程啟動(dòng)時(shí),CPU 會(huì)從 ELF 文件指定的入口點(diǎn)地址開(kāi)始,讀取并執(zhí)行.text 節(jié)區(qū)中的代碼。例如,一個(gè)簡(jiǎn)單的 C 程序經(jīng)過(guò)編譯鏈接生成 ELF 可執(zhí)行文件后,程序中的函數(shù)調(diào)用、變量操作等邏輯都以機(jī)器指令的形式存儲(chǔ)在.text 節(jié)區(qū)。當(dāng)這個(gè) ELF 文件被加載為進(jìn)程運(yùn)行時(shí),CPU 會(huì)按照指令順序依次執(zhí)行,實(shí)現(xiàn)程序的功能,如打印信息、計(jì)算數(shù)據(jù)等。

在數(shù)據(jù)方面,.data 節(jié)區(qū)保存了已初始化的全局變量和靜態(tài)變量,.bss 節(jié)區(qū)為未初始化的全局變量和靜態(tài)變量預(yù)留了內(nèi)存空間。這些數(shù)據(jù)對(duì)于進(jìn)程的運(yùn)行狀態(tài)和功能實(shí)現(xiàn)至關(guān)重要。比如一個(gè)記錄用戶(hù)登錄信息的全局變量,就可能存儲(chǔ)在.data 節(jié)區(qū),進(jìn)程在運(yùn)行過(guò)程中可以隨時(shí)讀取和修改這個(gè)變量,以實(shí)現(xiàn)用戶(hù)認(rèn)證、權(quán)限管理等功能。

ELF 文件的程序頭表和節(jié)區(qū)頭表在進(jìn)程創(chuàng)建和內(nèi)存布局中發(fā)揮著關(guān)鍵作用。程序頭表中的 LOAD 類(lèi)型段,詳細(xì)描述了如何將 ELF 文件中的段加載到內(nèi)存中,包括段的文件偏移、虛擬地址、大小、權(quán)限等信息。操作系統(tǒng)根據(jù)這些信息,將代碼段、數(shù)據(jù)段等加載到內(nèi)存的相應(yīng)位置,并為其分配合適的權(quán)限(如代碼段可讀可執(zhí)行,數(shù)據(jù)段可讀可寫(xiě)等),從而構(gòu)建起進(jìn)程的內(nèi)存映像。節(jié)區(qū)頭表則為鏈接器和調(diào)試器提供了重要信息,在進(jìn)程的創(chuàng)建和調(diào)試過(guò)程中,幫助相關(guān)工具準(zhǔn)確地定位和處理各個(gè)節(jié)區(qū)的內(nèi)容。

3.2進(jìn)程運(yùn)行時(shí)對(duì) ELF 文件的依賴(lài)

當(dāng)進(jìn)程處于運(yùn)行時(shí),它對(duì) ELF 文件的依賴(lài)依然緊密,尤其是在動(dòng)態(tài)鏈接共享庫(kù)方面。對(duì)于動(dòng)態(tài)鏈接的 ELF 文件,進(jìn)程在運(yùn)行過(guò)程中需要?jiǎng)討B(tài)加載和鏈接所需的共享庫(kù)文件。這一過(guò)程是由動(dòng)態(tài)鏈接器負(fù)責(zé)完成的。

以一個(gè)使用了libc.so庫(kù)的程序?yàn)槔?dāng)該程序?qū)?yīng)的 ELF 文件被加載為進(jìn)程后,動(dòng)態(tài)鏈接器會(huì)首先根據(jù) ELF 文件中記錄的共享庫(kù)依賴(lài)信息,在系統(tǒng)中查找并加載libc.so共享庫(kù)。在查找共享庫(kù)時(shí),動(dòng)態(tài)鏈接器會(huì)按照一定的路徑順序進(jìn)行搜索,通常會(huì)先在/lib和/usr/lib等系統(tǒng)默認(rèn)目錄中查找,如果沒(méi)有找到,還會(huì)根據(jù)環(huán)境變量LD_LIBRARY_PATH指定的路徑進(jìn)行查找。

在共享庫(kù)加載完成后,動(dòng)態(tài)鏈接器需要解析共享庫(kù)中程序所引用的符號(hào)(如函數(shù)、變量等),并將這些符號(hào)的地址解析出來(lái),填充到 ELF 文件的相應(yīng)位置,這個(gè)過(guò)程稱(chēng)為重定位。而在這個(gè)過(guò)程中,GOT(Global Offset Table,全局偏移表)起著關(guān)鍵作用。GOT 表是 ELF 文件中的一個(gè)重要數(shù)據(jù)結(jié)構(gòu),用于存儲(chǔ)外部符號(hào)的地址。當(dāng)程序調(diào)用一個(gè)共享庫(kù)中的函數(shù)時(shí),程序代碼中并不會(huì)直接包含該函數(shù)的實(shí)際地址,而是通過(guò) GOT 表來(lái)間接獲取。

具體來(lái)說(shuō),當(dāng)程序第一次調(diào)用共享庫(kù)中的某個(gè)函數(shù)時(shí),程序會(huì)跳轉(zhuǎn)到該函數(shù)對(duì)應(yīng)的 GOT 表項(xiàng)。如果這是第一次調(diào)用,GOT 表項(xiàng)中存儲(chǔ)的可能是一個(gè)指向動(dòng)態(tài)鏈接器中重定位函數(shù)的地址。動(dòng)態(tài)鏈接器會(huì)根據(jù)這個(gè)重定位函數(shù),找到共享庫(kù)中該函數(shù)的實(shí)際地址,并將其填充到 GOT 表項(xiàng)中。這樣,下次程序再調(diào)用該函數(shù)時(shí),就可以直接從 GOT 表中獲取函數(shù)的實(shí)際地址,從而實(shí)現(xiàn)快速調(diào)用。這種機(jī)制實(shí)現(xiàn)了函數(shù)的延遲綁定,只有在函數(shù)第一次被調(diào)用時(shí)才進(jìn)行地址解析和綁定,提高了程序的加載效率和靈活性 。例如,在一個(gè)圖形渲染程序中,可能會(huì)頻繁調(diào)用共享庫(kù)中的圖形繪制函數(shù),通過(guò) GOT 表的延遲綁定機(jī)制,可以在程序啟動(dòng)時(shí)快速加載,而不必在一開(kāi)始就解析所有圖形繪制函數(shù)的地址,只有在真正需要繪制圖形時(shí)才進(jìn)行地址綁定,大大提高了程序的啟動(dòng)速度和運(yùn)行效率。

3.3ELF 加載與進(jìn)程運(yùn)行

(1)進(jìn)程地址空間的構(gòu)建

ELF 文件的加載過(guò)程與進(jìn)程地址空間的構(gòu)建密切相關(guān)。當(dāng)內(nèi)核加載 ELF 文件時(shí),會(huì)根據(jù) ELF 文件頭和程序頭表的信息,在進(jìn)程的虛擬地址空間中為各個(gè)段分配相應(yīng)的內(nèi)存區(qū)域 。

進(jìn)程的虛擬地址空間通??梢苑譃槎鄠€(gè)部分,每個(gè)部分都有其特定的用途和權(quán)限,并且這些部分與 ELF 文件中的段存在著明確的對(duì)應(yīng)關(guān)系 :

  • 代碼段(Text Segment):對(duì)應(yīng) ELF 文件中的.text節(jié),它包含了程序的可執(zhí)行機(jī)器代碼 。在進(jìn)程地址空間中,代碼段通常被映射到一個(gè)只讀且可執(zhí)行的區(qū)域,這樣可以防止程序運(yùn)行時(shí)對(duì)代碼段的意外修改,確保代碼的完整性和穩(wěn)定性 。例如,當(dāng)我們運(yùn)行一個(gè) C 語(yǔ)言編寫(xiě)的程序時(shí),經(jīng)過(guò)編譯和鏈接生成的 ELF 文件中的.text節(jié)會(huì)被加載到進(jìn)程地址空間的代碼段區(qū)域,CPU 從這里讀取指令并執(zhí)行 。
  • 數(shù)據(jù)段(Data Segment):對(duì)應(yīng) ELF 文件中的.data節(jié),存放已初始化的全局變量和靜態(tài)變量 。數(shù)據(jù)段在進(jìn)程地址空間中是可讀可寫(xiě)的,因?yàn)槌绦蛟谶\(yùn)行過(guò)程中可能需要修改這些變量的值 。比如在一個(gè)程序中定義了一個(gè)全局變量int global_var = 10;,這個(gè)變量就會(huì)被存儲(chǔ)在數(shù)據(jù)段中,程序運(yùn)行時(shí)可以對(duì)global_var進(jìn)行讀寫(xiě)操作 。
  • BSS 段(Block Started by Symbol):對(duì)應(yīng) ELF 文件中沒(méi)有實(shí)際內(nèi)容的.bss節(jié),用于存放未初始化的全局變量和靜態(tài)變量 。BSS 段在程序運(yùn)行時(shí)會(huì)被自動(dòng)初始化為 0,并且它不占用磁盤(pán)空間,只在內(nèi)存中分配空間 。這是因?yàn)樵诰幾g時(shí),未初始化的變量并不需要實(shí)際的數(shù)據(jù)存儲(chǔ),只需要在運(yùn)行時(shí)為它們分配內(nèi)存并初始化為 0 即可 。例如,定義一個(gè)未初始化的全局變量int uninit_global_var;,它就會(huì)被分配到 BSS 段中 。
  • 堆(Heap):用于動(dòng)態(tài)內(nèi)存分配,比如通過(guò)malloc、new等函數(shù)分配的內(nèi)存都來(lái)自堆區(qū) 。堆區(qū)在進(jìn)程地址空間中是向上增長(zhǎng)的,它的大小在程序運(yùn)行過(guò)程中可以動(dòng)態(tài)變化 。例如,在一個(gè)程序中使用malloc函數(shù)分配內(nèi)存int *ptr = (int *)malloc(10 * sizeof(int));,這 10 個(gè)整數(shù)大小的內(nèi)存空間就是從堆區(qū)分配得到的 。
  • 棧(Stack):用于存放函數(shù)調(diào)用的上下文信息,包括局部變量、函數(shù)參數(shù)、返回地址等 。棧區(qū)在進(jìn)程地址空間中是向下增長(zhǎng)的,每當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),會(huì)在棧頂為其分配棧幀,函數(shù)返回時(shí),棧幀被釋放 。比如在一個(gè)函數(shù)中定義了局部變量int local_var = 5;,這個(gè)局部變量就會(huì)被存儲(chǔ)在棧區(qū)的當(dāng)前函數(shù)棧幀中 。
  • 共享庫(kù)映射區(qū)域:用于映射動(dòng)態(tài)鏈接庫(kù)(共享對(duì)象文件) 。當(dāng)程序依賴(lài)于動(dòng)態(tài)鏈接庫(kù)時(shí),動(dòng)態(tài)連接器會(huì)將這些共享庫(kù)加載到該區(qū)域 。共享庫(kù)映射區(qū)域的大小也是動(dòng)態(tài)變化的,并且多個(gè)進(jìn)程可以共享同一個(gè)共享庫(kù)的內(nèi)存映射,從而節(jié)省內(nèi)存資源 。例如,很多程序都會(huì)依賴(lài) C 標(biāo)準(zhǔn)庫(kù)(如libc.so),這個(gè)共享庫(kù)就會(huì)被映射到進(jìn)程地址空間的共享庫(kù)映射區(qū)域 。

在內(nèi)核中,mm_struct和vm_area_struct等數(shù)據(jù)結(jié)構(gòu)在進(jìn)程地址空間的構(gòu)建中起著關(guān)鍵作用 。mm_struct結(jié)構(gòu)體代表一個(gè)進(jìn)程的內(nèi)存管理信息,它包含了指向進(jìn)程地址空間各個(gè)區(qū)域的指針,以及與內(nèi)存管理相關(guān)的其他信息 。而vm_area_struct結(jié)構(gòu)體則用于描述進(jìn)程地址空間中的一個(gè)虛擬內(nèi)存區(qū)域(VMA),每個(gè) VMA 都有其起始地址、結(jié)束地址、權(quán)限標(biāo)志、所屬的mm_struct等信息 。這些數(shù)據(jù)結(jié)構(gòu)通過(guò)鏈表或紅黑樹(shù)等方式組織起來(lái),方便內(nèi)核進(jìn)行內(nèi)存管理和地址空間的操作 ,例如在加載 ELF 文件時(shí),內(nèi)核會(huì)根據(jù) ELF 文件的信息創(chuàng)建相應(yīng)的vm_area_struct結(jié)構(gòu)體,并將其插入到mm_struct的管理結(jié)構(gòu)中,從而完成進(jìn)程地址空間的構(gòu)建 。

(2)程序執(zhí)行的 “幕后英雄”:頁(yè)表與 MMU

當(dāng) CPU 執(zhí)行 ELF 程序時(shí),需要將程序中的虛擬地址轉(zhuǎn)換為物理地址,這一過(guò)程主要依賴(lài)于頁(yè)表和 MMU(內(nèi)存管理單元) 。

頁(yè)表是一種數(shù)據(jù)結(jié)構(gòu),用于實(shí)現(xiàn)虛擬地址到物理地址的映射 。它以頁(yè)(Page)為單位進(jìn)行管理,每個(gè)頁(yè)通常大小為 4KB(在一些系統(tǒng)中也可能是其他大小) 。頁(yè)表中存儲(chǔ)了虛擬頁(yè)號(hào)(VPN)與物理頁(yè)號(hào)(PPN)的對(duì)應(yīng)關(guān)系 。例如,在一個(gè) 32 位的系統(tǒng)中,虛擬地址空間為 4GB,若頁(yè)大小為 4KB,則虛擬地址空間被劃分為 1048576 個(gè)頁(yè) 。當(dāng)程序訪問(wèn)一個(gè)虛擬地址時(shí),CPU 會(huì)根據(jù)虛擬地址的頁(yè)號(hào)部分在頁(yè)表中查找對(duì)應(yīng)的物理頁(yè)號(hào) 。

MMU 是 CPU 中的一個(gè)硬件單元,它在地址轉(zhuǎn)換過(guò)程中起著至關(guān)重要的作用 。當(dāng) CPU 執(zhí)行指令時(shí),會(huì)將指令中的虛擬地址發(fā)送給 MMU 。MMU 首先會(huì)根據(jù) CPU 中的頁(yè)表基址寄存器(如 x86 架構(gòu)中的 CR3 寄存器)找到對(duì)應(yīng)的頁(yè)表 ,然后根據(jù)虛擬地址的頁(yè)號(hào)在頁(yè)表中查找對(duì)應(yīng)的物理頁(yè)號(hào) 。如果在頁(yè)表中找到了匹配的項(xiàng)(稱(chēng)為頁(yè)表項(xiàng),PTE),MMU 會(huì)將物理頁(yè)號(hào)與虛擬地址的頁(yè)內(nèi)偏移部分組合起來(lái),形成最終的物理地址,然后將該物理地址發(fā)送給內(nèi)存控制器,以訪問(wèn)實(shí)際的內(nèi)存數(shù)據(jù) 。例如,假設(shè)虛擬地址為 0x08048000,頁(yè)大小為 4KB,虛擬地址的高 20 位表示頁(yè)號(hào),低 12 位表示頁(yè)內(nèi)偏移 。MMU 根據(jù)頁(yè)號(hào)在頁(yè)表中查找對(duì)應(yīng)的物理頁(yè)號(hào),假設(shè)找到的物理頁(yè)號(hào)為 0x10000,那么最終的物理地址就是 0x10000 << 12 | 0x08048000 & 0xFFF = 0x10000000 + 0x8000 = 0x10008000 。

在程序執(zhí)行過(guò)程中,PC(程序計(jì)數(shù)器)指針不斷推進(jìn),指向下一條要執(zhí)行的指令的虛擬地址 。每當(dāng) CPU 執(zhí)行完一條指令后,PC 指針會(huì)自動(dòng)增加,指向下一條指令 。例如,在一個(gè)簡(jiǎn)單的匯編程序中,指令mov eax, 1執(zhí)行完畢后,PC 指針會(huì)指向下一條指令的地址,CPU 會(huì)根據(jù) PC 指針的值從內(nèi)存中讀取下一條指令的虛擬地址,并通過(guò) MMU 將其轉(zhuǎn)換為物理地址,從而繼續(xù)執(zhí)行程序 。如果程序中包含函數(shù)調(diào)用、跳轉(zhuǎn)等指令,PC 指針會(huì)根據(jù)指令的要求進(jìn)行相應(yīng)的修改,跳轉(zhuǎn)到指定的地址繼續(xù)執(zhí)行 ,以實(shí)現(xiàn)程序的邏輯控制和流程跳轉(zhuǎn) 。

四、內(nèi)核加載 ELF 文件全流程

4.1內(nèi)核初次 “邂逅” ELF

當(dāng)execve系統(tǒng)調(diào)用被觸發(fā)后,內(nèi)核就開(kāi)始了 ELF 文件的加載之旅。首先,內(nèi)核會(huì)讀取 ELF 文件的頭部信息,這一步就像是我們打開(kāi)一本書(shū)先看目錄一樣 。

內(nèi)核通過(guò)open系統(tǒng)調(diào)用打開(kāi)指定的 ELF 文件,并將文件描述符傳遞給后續(xù)的處理函數(shù) 。在load_elf_binary函數(shù)中(位于 Linux 內(nèi)核源碼的fs/binfmt_elf.c文件中 ),會(huì)讀取 ELF 文件的前 128 個(gè)字節(jié),這部分內(nèi)容包含了 ELF 文件頭的關(guān)鍵信息 。

struct linux_binprm *bprm;
// 讀取ELF文件頭前128字節(jié)到bprm->buf中
if (kernel_read(bprm->file, bprm->buf, BINPRM_BUF_SIZE, &pos) != BINPRM_BUF_SIZE) {
    return -EIO;
}

我們可以使用readelf -h命令來(lái)查看 ELF 文件頭的關(guān)鍵信息,例如對(duì)于/bin/ls文件,執(zhí)行readelf -h /bin/ls會(huì)得到類(lèi)似如下的輸出:

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x403940
  Start of program headers:          64 (bytes into file)
  Start of section headers:          56440 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 27

從這些信息中,內(nèi)核可以獲取到文件的類(lèi)型(如可執(zhí)行文件、共享庫(kù)等 )、目標(biāo)機(jī)器架構(gòu)(如 x86 - 64、ARM 等 )、程序入口地址、程序頭表和節(jié)區(qū)頭表的偏移量等關(guān)鍵內(nèi)容 。其中,程序入口地址指明了程序執(zhí)行的起始位置,內(nèi)核后續(xù)會(huì)根據(jù)這個(gè)地址跳轉(zhuǎn)到程序的起始處開(kāi)始執(zhí)行;程序頭表和節(jié)區(qū)頭表的偏移量則幫助內(nèi)核找到對(duì)應(yīng)的表,從而進(jìn)一步獲取程序的段和節(jié)區(qū)信息 ,這些信息對(duì)于內(nèi)核后續(xù)將 ELF 文件正確地加載到內(nèi)存并運(yùn)行起著至關(guān)重要的指引作用 。

4.2按圖索驥:加載程序段

內(nèi)核在讀取并解析了 ELF 文件頭后,就會(huì)根據(jù)程序頭表(Program Header Table)中的信息,將 ELF 文件中的各個(gè)段加載到內(nèi)存中 。程序頭表就像是一份詳細(xì)的 “裝載指南”,它描述了每個(gè)段在文件中的位置、大小以及被放進(jìn)內(nèi)存后所在的位置和大小等信息 。

在程序頭表中,類(lèi)型為PT_LOAD的段表示需要被加載到內(nèi)存中的段 。內(nèi)核會(huì)遍歷程序頭表,查找所有類(lèi)型為PT_LOAD的段,并使用do_mmap函數(shù)將這些段的內(nèi)容映射到進(jìn)程的虛擬地址空間中 。

struct elf_phdr *elf_ppnt;
elf_ppnt = elf32_getphdr(bprm);
for (i = 0; i < elf_ex.e_phnum; i++, elf_ppnt++) {
    if (elf_ppnt->p_type == PT_LOAD) {
        unsigned long vaddr = elf_ppnt->p_vaddr;
        unsigned long memsz = elf_ppnt->p_memsz;
        unsigned long filesz = elf_ppnt->p_filesz;
        unsigned long offset = elf_ppnt->p_offset;
        int prot = 0;
        if (elf_ppnt->p_flags & PF_R) prot |= PROT_READ;
        if (elf_ppnt->p_flags & PF_W) prot |= PROT_WRITE;
        if (elf_ppnt->p_flags & PF_X) prot |= PROT_EXEC;
        // 使用do_mmap將段映射到內(nèi)存
        mm->mmap = do_mmap(file, offset, filesz, prot, MAP_PRIVATE, vma->vm_pgoff); 
        if (IS_ERR(mm->mmap)) {
            return PTR_ERR(mm->mmap);
        }
        // 如果內(nèi)存大小大于文件大小,對(duì)多出的內(nèi)存進(jìn)行清零初始化
        if (memsz > filesz) {
            vm = vma_merge(mm, vma, vaddr, vaddr + memsz, prot, NULL, NULL, 0); 
            if (!vm) {
                return -ENOMEM;
            }
            vma = vm;
            memset((char *)vma->vm_start + filesz, 0, memsz - filesz);
        }
    }
}

在這個(gè)過(guò)程中,p_vaddr指定了段在虛擬內(nèi)存中的起始地址,p_memsz表示段在內(nèi)存中的大小,p_filesz是段在文件中的大小,p_offset是段在文件中的偏移量 。p_flags則定義了段的權(quán)限,如PF_R表示可讀,PF_W表示可寫(xiě),PF_X表示可執(zhí)行 。內(nèi)核根據(jù)這些信息,通過(guò)do_mmap函數(shù)在進(jìn)程的虛擬地址空間中為段分配內(nèi)存,并將段的內(nèi)容從文件中讀取到分配的內(nèi)存區(qū)域 。如果段在內(nèi)存中的大小大于在文件中的大小,內(nèi)核會(huì)將多出的內(nèi)存區(qū)域清零初始化 ,以確保內(nèi)存中的數(shù)據(jù)是符合程序預(yù)期的 。

4.3尋找動(dòng)態(tài)連接器

對(duì)于動(dòng)態(tài)鏈接的 ELF 文件,內(nèi)核在加載完程序段后,還需要找到并加載動(dòng)態(tài)連接器(Dynamic Linker) 。動(dòng)態(tài)連接器在程序運(yùn)行過(guò)程中起著至關(guān)重要的作用,它負(fù)責(zé)解析程序?qū)蚕韼?kù)的依賴(lài),并將所需的共享庫(kù)加載到內(nèi)存中,同時(shí)還處理程序中的符號(hào)重定位等工作 。

內(nèi)核通過(guò)分析 ELF 文件中的.interp段來(lái)獲取動(dòng)態(tài)連接器的名稱(chēng) 。.interp段是一個(gè)字符串類(lèi)型的段,它包含了動(dòng)態(tài)連接器的路徑名 。例如,在常見(jiàn)的 x86 - 64 架構(gòu)的 Linux 系統(tǒng)中,動(dòng)態(tài)連接器的路徑通常是/lib64/ld-linux-x86-64.so.2 。

struct elf_phdr *interp_elf_ppnt;
interp_elf_ppnt = elf32_getphdr(bprm);
for (i = 0; i < elf_ex.e_phnum; i++, interp_elf_ppnt++) {
    if (interp_elf_ppnt->p_type == PT_INTERP) {
        char *interp_path = kmalloc(interp_elf_ppnt->p_filesz, GFP_KERNEL);
        if (!interp_path) {
            return -ENOMEM;
        }
        // 讀取.interp段內(nèi)容,獲取動(dòng)態(tài)連接器路徑
        if (kernel_read(bprm->file, interp_path, interp_elf_ppnt->p_filesz, &interp_elf_ppnt->p_offset) != interp_elf_ppnt->p_filesz) {
            kfree(interp_path);
            return -EIO;
        }
        // 加載動(dòng)態(tài)連接器
        if (load_elf_interp(interp_path, bprm) < 0) {
            kfree(interp_path);
            return -EINVAL;
        }
        kfree(interp_path);
        break;
    }
}

內(nèi)核找到動(dòng)態(tài)連接器的路徑后,會(huì)使用load_elf_interp函數(shù)來(lái)加載動(dòng)態(tài)連接器 。這個(gè)過(guò)程類(lèi)似于加載普通的 ELF 文件,內(nèi)核會(huì)為動(dòng)態(tài)連接器分配內(nèi)存空間,并將其代碼和數(shù)據(jù)加載到內(nèi)存中 。加載完成后,動(dòng)態(tài)連接器就開(kāi)始接管程序的后續(xù)初始化和運(yùn)行工作 。

4.4動(dòng)態(tài)鏈接與重定位

動(dòng)態(tài)連接器加載到內(nèi)存后,會(huì)首先檢查程序?qū)蚕韼?kù)的依賴(lài)關(guān)系 。它通過(guò)解析 ELF 文件中的動(dòng)態(tài)段(Dynamic Section)來(lái)獲取程序所依賴(lài)的共享庫(kù)列表 。動(dòng)態(tài)段中包含了一系列的標(biāo)記(Tags),其中DT_NEEDED標(biāo)記用于指定程序所依賴(lài)的共享庫(kù)名稱(chēng) 。

動(dòng)態(tài)連接器會(huì)根據(jù)這些依賴(lài)信息,在系統(tǒng)中查找并加載相應(yīng)的共享庫(kù) 。它會(huì)按照一定的搜索路徑來(lái)查找共享庫(kù),首先會(huì)檢查環(huán)境變量LD_LIBRARY_PATH指定的目錄,如果沒(méi)有找到,則會(huì)查找/etc/ld.so.cache中的緩存路徑,最后會(huì)查找默認(rèn)的庫(kù)路徑(如/lib和/usr/lib) 。

在加載共享庫(kù)的過(guò)程中,動(dòng)態(tài)連接器還會(huì)進(jìn)行重定位(Relocation)操作 。由于共享庫(kù)在編譯時(shí)并不知道它最終會(huì)被加載到內(nèi)存的哪個(gè)位置,所以其中的代碼和數(shù)據(jù)中涉及到的地址都是相對(duì)地址 。當(dāng)共享庫(kù)被加載到內(nèi)存后,動(dòng)態(tài)連接器需要根據(jù)其實(shí)際的加載地址,對(duì)共享庫(kù)中的地址引用進(jìn)行修正,使其指向正確的內(nèi)存位置,這個(gè)過(guò)程就是重定位 。

動(dòng)態(tài)連接器通過(guò)解析共享庫(kù)中的重定位表(Relocation Table)來(lái)進(jìn)行重定位操作 。重定位表中記錄了需要重定位的符號(hào)以及對(duì)應(yīng)的重定位類(lèi)型和偏移量等信息 。例如,對(duì)于一個(gè)需要重定位的符號(hào),動(dòng)態(tài)連接器會(huì)根據(jù)重定位表中的信息,找到符號(hào)在共享庫(kù)中的引用位置,并根據(jù)共享庫(kù)的加載地址對(duì)引用的地址進(jìn)行修正 。

此外,動(dòng)態(tài)鏈接還具有延遲定位(Lazy Binding)的特性 。在程序開(kāi)始運(yùn)行時(shí),動(dòng)態(tài)連接器并不會(huì)立即對(duì)所有的符號(hào)引用進(jìn)行重定位,而是在第一次使用某個(gè)符號(hào)時(shí)才進(jìn)行重定位 。這樣可以減少程序啟動(dòng)時(shí)的開(kāi)銷(xiāo),提高程序的啟動(dòng)速度 。例如,一個(gè)程序中可能有很多函數(shù)調(diào)用,在程序啟動(dòng)時(shí),動(dòng)態(tài)連接器并不會(huì)對(duì)所有函數(shù)調(diào)用的符號(hào)進(jìn)行重定位,只有當(dāng)實(shí)際調(diào)用某個(gè)函數(shù)時(shí),才會(huì)對(duì)該函數(shù)的符號(hào)引用進(jìn)行重定位 ,這種延遲定位的機(jī)制對(duì)于那些包含大量函數(shù)調(diào)用但在程序啟動(dòng)時(shí)并不需要全部使用的程序來(lái)說(shuō),能夠顯著提升程序的運(yùn)行效率 。

4.5程序初始化與啟動(dòng)

當(dāng)動(dòng)態(tài)連接器完成共享庫(kù)的加載和重定位后,就會(huì)執(zhí)行程序的初始化操作 。它會(huì)執(zhí)行 ELF 文件中.init節(jié)的代碼,這個(gè)節(jié)中的代碼通常用于完成一些程序運(yùn)行前的初始化工作,比如初始化全局變量、設(shè)置信號(hào)處理函數(shù)等 。

typedef void (*initfn_t)(void);
initfn_t init = (initfn_t)elf_entry;
init();

在完成初始化后,動(dòng)態(tài)連接器會(huì)將控制傳遞給程序 。它會(huì)根據(jù)ELF文件頭中指定的程序入口地址,跳轉(zhuǎn)到程序的入口點(diǎn),開(kāi)始執(zhí)行程序的代碼 。此時(shí),程序就正式開(kāi)始運(yùn)行了,我們?cè)诮K端輸入的命令也終于得以按照程序的邏輯執(zhí)行,并返回相應(yīng)的結(jié)果 ,整個(gè) ELF 文件的加載過(guò)程也至此全部完成,從最初的磁盤(pán)文件,到在內(nèi)存中被正確加載、鏈接和初始化,最終成為一個(gè)能夠在系統(tǒng)中正常運(yùn)行的進(jìn)程 。

五、ELF文件案例分析

為了更直觀地理解從 ELF 文件到 Linux 進(jìn)程的轉(zhuǎn)化過(guò)程,讓我們以一個(gè)簡(jiǎn)單的 C 程序?yàn)槔鸩秸故具@個(gè)神奇的旅程。

5.1編寫(xiě)與編譯 C 程序

首先,我們編寫(xiě)一個(gè)簡(jiǎn)單的 C 程序sum.c,它的功能是計(jì)算兩個(gè)整數(shù)的和并輸出結(jié)果:

#include <stdio.h>

int main() {
    int a = 3;
    int b = 5;
    int sum = a + b;
    printf("The sum of %d and %d is %d\n", a, b, sum);
    return 0;
}

使用 GCC 編譯器將其編譯為 ELF 可執(zhí)行文件:

gcc -o sum sum.c

這條命令會(huì)生成一個(gè)名為sum的 ELF 可執(zhí)行文件,它包含了我們程序的代碼和數(shù)據(jù),以及 ELF 文件格式所要求的各種頭部和表結(jié)構(gòu)。

5.2分析 ELF 文件結(jié)構(gòu)

接下來(lái),我們使用readelf命令來(lái)分析生成的 ELF 文件sum的結(jié)構(gòu)。

(1)查看 ELF 文件頭:使用readelf -h sum命令查看 ELF 文件頭信息

readelf -h sum

輸出結(jié)果包含了文件類(lèi)型(可執(zhí)行文件)、目標(biāo)機(jī)器架構(gòu)(如 x86_64)、入口點(diǎn)地址、程序頭表和節(jié)區(qū)頭表的偏移等重要信息。例如,通過(guò)入口點(diǎn)地址,我們可以知道程序從何處開(kāi)始執(zhí)行;通過(guò)文件類(lèi)型,我們能明確這是一個(gè)可直接運(yùn)行的可執(zhí)行文件 。

(2)查看程序頭表:使用readelf -l sum命令查看程序頭表:

readelf -l sum

程序頭表列出了各個(gè)段的信息,如 LOAD 類(lèi)型的段,包含了代碼段和數(shù)據(jù)段的加載信息,包括它們?cè)谖募械钠?、加載到內(nèi)存的虛擬地址、大小以及權(quán)限等。我們可以看到代碼段具有可讀和可執(zhí)行權(quán)限,數(shù)據(jù)段具有可讀和可寫(xiě)權(quán)限,這與我們之前對(duì) ELF 文件結(jié)構(gòu)的理解是一致的。

(3)查看節(jié)區(qū)頭表:使用readelf -S sum命令查看節(jié)區(qū)頭表:

readelf -S sum

節(jié)區(qū)頭表展示了各個(gè)節(jié)區(qū)的詳細(xì)信息,如.text 節(jié)區(qū)存放代碼,.data 節(jié)區(qū)存放已初始化的數(shù)據(jù),.bss 節(jié)區(qū)為未初始化數(shù)據(jù)預(yù)留空間等。我們還可以看到符號(hào)表節(jié)區(qū).symtab 和字符串表節(jié)區(qū).strtab,它們?cè)诔绦虻逆溄雍瓦\(yùn)行過(guò)程中起著關(guān)鍵作用,符號(hào)表記錄了程序中定義和引用的符號(hào)信息,字符串表則保存了符號(hào)的名字等字符串信息。

5.3跟蹤進(jìn)程創(chuàng)建與 ELF 文件加載

為了跟蹤從 ELF 文件到 Linux 進(jìn)程的創(chuàng)建和加載過(guò)程,我們使用strace工具。strace是一個(gè)強(qiáng)大的系統(tǒng)調(diào)用跟蹤工具,可以監(jiān)視進(jìn)程執(zhí)行時(shí)與內(nèi)核的交互,包括文件操作、進(jìn)程管理、內(nèi)存分配等。

使用strace運(yùn)行我們的sum程序:

strace -f -o sum_trace.txt./sum

這里的-f選項(xiàng)表示跟蹤子進(jìn)程,-o sum_trace.txt表示將跟蹤結(jié)果輸出到sum_trace.txt文件中。

在生成的sum_trace.txt文件中,我們可以看到一系列系統(tǒng)調(diào)用,其中關(guān)鍵的系統(tǒng)調(diào)用有:

(1)fork系統(tǒng)調(diào)用:在進(jìn)程創(chuàng)建階段,fork系統(tǒng)調(diào)用用于創(chuàng)建一個(gè)新的子進(jìn)程。在strace的輸出中,我們可以看到類(lèi)似這樣的記錄:

fork() = 23456

這里的23456是新創(chuàng)建子進(jìn)程的 PID。

(2)execve系統(tǒng)調(diào)用:execve系統(tǒng)調(diào)用負(fù)責(zé)執(zhí)行 ELF 文件,它是進(jìn)程執(zhí)行的核心系統(tǒng)調(diào)用。在strace的輸出中,我們可以找到類(lèi)似這樣的記錄:

execve("./sum", ["./sum"], 0x7ffd12d4) = 0

這表示sum程序被執(zhí)行,execve的第一個(gè)參數(shù)是 ELF 文件的路徑,第二個(gè)參數(shù)是傳遞給程序的參數(shù)數(shù)組,第三個(gè)參數(shù)是環(huán)境變量。

(3)動(dòng)態(tài)鏈接相關(guān)的系統(tǒng)調(diào)用:如果我們的程序依賴(lài)于共享庫(kù)(在這個(gè)簡(jiǎn)單示例中依賴(lài)于libc.so庫(kù)),在strace的輸出中,我們可以看到與動(dòng)態(tài)鏈接相關(guān)的系統(tǒng)調(diào)用,如打開(kāi)共享庫(kù)文件、解析符號(hào)等操作。例如:

openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3

這表示系統(tǒng)嘗試打開(kāi)libc.so.6共享庫(kù)文件,文件描述符為 3。隨后還會(huì)有一系列與符號(hào)解析、重定位相關(guān)的系統(tǒng)調(diào)用,這些操作確保了程序能夠正確地調(diào)用共享庫(kù)中的函數(shù)。

責(zé)任編輯:武曉燕 來(lái)源: 深度Linux
相關(guān)推薦

2022-10-20 08:02:29

ELFRTOSSymbol

2023-01-27 18:08:35

eBPF云原生

2023-03-11 11:19:07

loopbackSocket

2023-05-08 07:41:07

Linux內(nèi)核ELF文件

2014-09-24 11:01:10

多路鏡像流量聚合鏡像流量

2025-07-14 00:10:01

2022-08-27 10:53:15

C語(yǔ)言Linux內(nèi)核

2021-06-26 07:04:24

Epoll服務(wù)器機(jī)制

2020-06-04 08:36:55

Linux內(nèi)核線程

2025-09-09 02:11:00

2025-06-04 02:35:00

2023-11-24 11:24:16

Linux系統(tǒng)

2018-10-10 14:02:30

Linux系統(tǒng)硬件內(nèi)核

2021-07-07 23:38:05

內(nèi)核IOLinux

2011-08-25 14:10:47

execve中文man

2021-06-18 06:02:24

內(nèi)核文件傳遞

2025-10-27 01:55:00

2011-12-02 10:58:06

數(shù)據(jù)結(jié)構(gòu)Java

2025-10-11 04:11:00

2011-07-22 16:11:12

java
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)