本文來自微信公眾號(hào):開發(fā)內(nèi)功修煉 (ID:kfngxl),作者:張彥飛 allen
大家好,我是飛哥!
今天我們來思考一個(gè)簡(jiǎn)單的問題,一個(gè)程序是如何在 Linux 上執(zhí)行起來的?
我們就拿全宇宙最簡(jiǎn)單的 Hello World 程序來舉例。
#include <stdio.h>
int main()
{
printf("Hello, World!\n");
return 0;
}我們?cè)趯懲甏a后,進(jìn)行簡(jiǎn)單的編譯,然后在 shell 命令行下就可以把它啟動(dòng)起來。
# gcc main.c -o helloworld # ./helloworld Hello, World!
那么在編譯啟動(dòng)運(yùn)行的過程中都發(fā)生了哪些事情了呢?今天就讓我們來深入地了解一下。
一、理解可執(zhí)行文件格式
源代碼在編譯后會(huì)生成一個(gè)可執(zhí)行程序文件,我們先來了解一下編譯后的二進(jìn)制文件是什么樣子的。
我們首先使用 file 命令查看一下這個(gè)文件的格式。
# file helloworld helloworld: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), ...
file 命令給出了這個(gè)二進(jìn)制文件的概要信息,其中 ELF 64-bit LSB executable 表示這個(gè)文件是一個(gè) ELF 格式的 64 位的可執(zhí)行文件。x86-64 表示該可執(zhí)行文件支持的 cpu 架構(gòu)。
LSB 的全稱是 Linux Standard Base,是 Linux 標(biāo)準(zhǔn)規(guī)范。其目的是制定一系列標(biāo)準(zhǔn)來增強(qiáng) Linux 發(fā)行版的兼容性。
ELF 的全稱是 Executable Linkable Format,是一種二進(jìn)制文件格式。Linux 下的目標(biāo)文件、可執(zhí)行文件和 CoreDump 都按照該格式進(jìn)行存儲(chǔ)。
ELF 文件由四部分組成,分別是 ELF 文件頭 (ELF header)、Program header table、Section 和 Section header table。

接下來我們分幾個(gè)小節(jié)挨個(gè)介紹一下。
1.1 ELF 文件頭
ELF 文件頭記錄了整個(gè)文件的屬性信息。原始二進(jìn)制非常不便于觀察。不過我們有趁手的工具 - readelf,這個(gè)工具可以幫我們查看 ELF 文件中的各種信息。
我們先來看一下編譯出來的可執(zhí)行文件的 ELF 文件頭,使用 --file-header (-h) 選項(xiàng)即可查看。
# readelf --file-header helloworld 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: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x401040 Start of program headers: 64 (bytes into file) Start of section headers: 23264 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 11 Size of section headers: 64 (bytes) Number of section headers: 30 Section header string table index: 29
ELF 文件頭包含了當(dāng)前可執(zhí)行文件的概要信息,我把其中關(guān)鍵的幾個(gè)拿出來給大家解釋一下。
Magic:一串特殊的識(shí)別碼,主要用于外部程序快速地對(duì)這個(gè)文件進(jìn)行識(shí)別,快速地判斷文件類型是不是 ELF
Class:表示這是 ELF64 文件
Type:為 EXEC 表示是可執(zhí)行文件,其它文件類型還有 REL(可重定位的目標(biāo)文件)、DYN(動(dòng)態(tài)鏈接庫)、CORE(系統(tǒng)調(diào)試 coredump 文件)
Entry point address:程序入口地址,這里顯示入口在 0x401040 位置處
Size of this header:ELF 文件頭的大小,這里顯示是占用了 64 字節(jié)
以上幾個(gè)字段是 ELF 頭中對(duì) ELF 的整體描述。另外 ELF 頭中還有關(guān)于 program headers 和 section headers 的描述信息。
Start of program headers:表示 Program header 的位置
Size of program headers:每一個(gè) Program header 大小
Number of program headers:總共有多少個(gè) Program header
Start of section headers: 表示 Section header 的開始位置。
Size of section headers:每一個(gè) Section header 的大小
Number of section headers: 總共有多少個(gè) Section header
1.2 Program Header Table
在介紹 Program Header Table 之前我們展開介紹一下 ELF 文件中一對(duì)兒相近的概念 - Segment 和 Section。
ELF 文件內(nèi)部最重要的組成單位是一個(gè)一個(gè)的 Section。每一個(gè) Section 都是由編譯鏈接器生成的,都有不同的用途。例如編譯器會(huì)將我們寫的代碼編譯后放到 .text Section 中,將全局變量放到 .data 或者是 .bss Section 中。
但是對(duì)于操作系統(tǒng)來說,它不關(guān)注具體的 Section 是啥,它只關(guān)注這塊內(nèi)容應(yīng)該以何種權(quán)限加載到內(nèi)存中,例如讀,寫,執(zhí)行等權(quán)限屬性。因此相同權(quán)限的 Section 可以放在一起組成 Segment,以方便操作系統(tǒng)更快速地加載。

由于 Segment 和 Section 翻譯成中文的話,意思太接近了,非常不利于理解。所以本文中我就直接使用 Segment 和 Section 原汁原味的概念,而不是將它們翻譯成段或者是節(jié),這樣太容易讓人混淆了。
Program headers table 就是作為所有 Segments 的頭信息,用來描述所有的 Segments 的。。
使用 readelf 工具的 --program-headers(-l)選項(xiàng)可以解析查看到這塊區(qū)域里存儲(chǔ)的內(nèi)容。
# readelf --program-headers helloworld Elf file type is EXEC (Executable file) Entry point 0x401040 There are 11 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x0000000000000268 0x0000000000000268 R 0x8 INTERP 0x00000000000002a8 0x00000000004002a8 0x00000000004002a8 0x000000000000001c 0x000000000000001c R 0x1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000438 0x0000000000000438 R 0x1000 LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000 0x00000000000001c5 0x00000000000001c5 R E 0x1000 LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000 0x0000000000000138 0x0000000000000138 R 0x1000 LOAD 0x0000000000002e10 0x0000000000403e10 0x0000000000403e10 0x0000000000000220 0x0000000000000228 RW 0x1000 DYNAMIC 0x0000000000002e20 0x0000000000403e20 0x0000000000403e20 0x00000000000001d0 0x00000000000001d0 RW 0x8 NOTE 0x00000000000002c4 0x00000000004002c4 0x00000000004002c4 0x0000000000000044 0x0000000000000044 R 0x4 GNU_EH_FRAME 0x0000000000002014 0x0000000000402014 0x0000000000402014 0x000000000000003c 0x000000000000003c R 0x4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10 GNU_RELRO 0x0000000000002e10 0x0000000000403e10 0x0000000000403e10 0x00000000000001f0 0x00000000000001f0 R 0x1 Section to Segment ming: Segment Sections... 00 01 .interp 02 .interp .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 03 .init .plt .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .dynamic .got .got.plt .data .bss 06 .dynamic 07 .note.gnu.build-id .note.ABI-tag 08 .eh_frame_hdr 09 10 .init_array .fini_array .dynamic .got
上面的結(jié)果顯示總共有 11 個(gè) program headers。
對(duì)于每一個(gè)段,輸出了 Offset、VirtAddr 等描述當(dāng)前段的信息。Offset 表示當(dāng)前段在二進(jìn)制文件中的開始位置,F(xiàn)ileSiz 表示當(dāng)前段的大小。Flag 表示當(dāng)前的段的權(quán)限類型,R 表示可讀、E 表示可執(zhí)行、W 表示可寫。
在最下面,還把每個(gè)段是由哪幾個(gè) Section 組成的給展示了出來,比如 03 號(hào)段是由“.init .plt .text .fini” 四個(gè) Section 組成的。

1.3 Section Header Table
和 Program Header Table 不一樣的是,Section header table 直接描述每一個(gè) Section。這二者描述的其實(shí)都是各種 Section ,只不過目的不同,一個(gè)針對(duì)加載,一個(gè)針對(duì)鏈接。
使用 readelf 工具的 --section-headers (-S)選項(xiàng)可以解析查看到這塊區(qū)域里存儲(chǔ)的內(nèi)容。
# readelf --section-headers helloworld There are 30 section headers, starting at offset 0x5b10: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align ...... [13] .text PROGBITS 0000000000401040 00001040 0000000000000175 0000000000000000 AX 0 0 16 ...... [23] .data PROGBITS 0000000000404020 00003020 0000000000000010 0000000000000000 WA 0 0 8 [24] .bss NOBITS 0000000000404030 00003030 0000000000000008 0000000000000000 WA 0 0 1 ...... Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), l (large), p (processor specific)
結(jié)果顯示,該文件總共有 30 個(gè) Sections,每一個(gè) Section 在二進(jìn)制文件中的位置通過 Offset 列表示了出來。Section 的大小通過 Size 列體現(xiàn)。
在這 30 個(gè) Section 中,每一個(gè)都有獨(dú)特的作用。我們編寫的代碼在編譯成二進(jìn)制指令后都會(huì)放到 .text 這個(gè) Section 中。另外我們看到 .text 段的 Address 列顯示的地址是 0000000000401040?;貞浨懊嫖覀?cè)?ELF 文件頭中看到 Entry point address 顯示的入口地址為 0x401040。這說明,程序的入口地址就是 .text 段的地址。
另外還有兩個(gè)值得關(guān)注的 Section 是 .data 和 .bss。代碼中的全局變量數(shù)據(jù)在編譯后將在在這兩個(gè) Section 中占據(jù)一些位置。如下簡(jiǎn)單代碼所示。
//未初始化的內(nèi)存區(qū)域位于 .bss 段
int data1 ;
//已經(jīng)初始化的內(nèi)存區(qū)域位于 .data 段
int data2 = 100 ;
//代碼位于 .text 段
int main(void)
{
}1.4 入口進(jìn)一步查看
接下來,我們想再查看一下我們前面提到的程序入口 0x401040,看看它到底是啥。我們這次再借助 nm 命令來進(jìn)一步查看一下可執(zhí)行文件中的符號(hào)及其地址信息。-n 選項(xiàng)的作用是顯示的符號(hào)以地址排序,而不是名稱排序。
# nm -n helloworld w __gmon_start__ U __libc_start_main@@GLIBC_2.2.5 U printf@@GLIBC_2.2.5 0000000000401040 T _start 0000000000401126 T main
通過以上輸出可以看到,程序入口 0x401040 指向的是 _start 函數(shù)的地址,在這個(gè)函數(shù)執(zhí)行一些初始化的操作之后,我們的入口函數(shù) main 將會(huì)被調(diào)用到,它位于 0x401126 地址處。
二、用戶進(jìn)程的創(chuàng)建過程概述
在我們編寫的代碼編譯完生成可執(zhí)行程序之后,下一步就是使用 shell 把它加載起來并運(yùn)行之。一般來說 shell 進(jìn)程是通過 fork+execve 來加載并運(yùn)行新進(jìn)程的。一個(gè)簡(jiǎn)單加載 helloworld 命令的 shell 核心邏輯是如下這個(gè)過程。
// shell 代碼示例
int main(int argc, char * argv[])
{
pid = fork();
if (pid==0){ // 如果是在進(jìn)程中
//使用 exec 系列函數(shù)加載并運(yùn)行可執(zhí)行文件
execve("helloworld", argv, envp);
} else {
}
}shell 進(jìn)程先通過 fork 系統(tǒng)調(diào)用創(chuàng)建一個(gè)進(jìn)程出來。然后在子進(jìn)程中調(diào)用 execve 將執(zhí)行的程序文件加載起來,然后就可以調(diào)到程序文件的運(yùn)行入口處運(yùn)行這個(gè)程序了。
這個(gè) fork 系統(tǒng)調(diào)用在內(nèi)核入口是在 kernel / fork.c 下。
//file:kernel/fork.c
SYSCALL_DEFINE0(fork)
{
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}在 do_fork 的實(shí)現(xiàn)中,核心是一個(gè) copy_process 函數(shù),它以拷貝父進(jìn)程(線程)的方式來生成一個(gè)新的 task_struct 出來。
//file:kernel/fork.c
long do_fork()
{
//復(fù)制一個(gè) task_struct 出來
struct task_struct *p;
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
//子任務(wù)加入到就緒隊(duì)列中去,等待調(diào)度器調(diào)度
wake_up_new_task(p);
}在 copy_process 函數(shù)中為新進(jìn)程申請(qǐng) task_struct,并用當(dāng)前進(jìn)程自己的地址空間、命名空間等對(duì)新進(jìn)程進(jìn)行初始化,并為其申請(qǐng)進(jìn)程 pid。
//file:kernel/fork.c
static struct task_struct *copy_process()
{
//復(fù)制進(jìn)程 task_struct 結(jié)構(gòu)體
struct task_struct *p;
p = dup_task_struct(current);
//進(jìn)程核心元素初始化
retval = copy_files(clone_flags, p);
retval = copy_fs(clone_flags, p);
retval = copy_mm(clone_flags, p);
retval = copy_namespaces(clone_flags, p);
//申請(qǐng) pid && 設(shè)置進(jìn)程號(hào)
pid = alloc_pid(p-nsproxy-pid_ns);
p-pid = pid_nr(pid);
p-tgid = p-pid;
}執(zhí)行完后,進(jìn)入 wake_up_new_task 讓新進(jìn)程等待調(diào)度器調(diào)度。
不過 fork 系統(tǒng)調(diào)用只能是根據(jù)當(dāng)?shù)?shell 進(jìn)程再復(fù)制一個(gè)新的進(jìn)程出來。這個(gè)新進(jìn)程里的代碼、數(shù)據(jù)都還是和原來的 shell 進(jìn)程的內(nèi)容一模一樣。
要想實(shí)現(xiàn)加載并運(yùn)行另外一個(gè)程序,比如我們編譯出來的 helloworld 程序,那還需要使用到 execve 系統(tǒng)調(diào)用。
三. Linux 可執(zhí)行文件加載器
其實(shí) Linux 不是寫死只能加載 ELF 一種可執(zhí)行文件格式的。它在啟動(dòng)的時(shí)候,會(huì)把自己支持的所有可執(zhí)行文件的解析器都加載上。并使用一個(gè) formats 雙向鏈表來保存所有的解析器。其中 formats 雙向鏈表在內(nèi)存中的結(jié)構(gòu)如下圖所示。

我們就以 ELF 的加載器 elf_format 為例,來看看這個(gè)加載器是如何注冊(cè)的。在 Linux 中每一個(gè)加載器都用一個(gè) linux_binfmt 結(jié)構(gòu)來表示。其中規(guī)定了加載二進(jìn)制可執(zhí)行文件的 load_binary 函數(shù)指針,以及加載崩潰文件 的 core_dump 函數(shù)等。其完整定義如下
//file:include/linux/binfmts.h
struct linux_binfmt {
int (*load_binary)(struct linux_binprm *);
int (*load_shlib)(struct file *);
int (*core_dump)(struct coredump_params *cprm);
};其中 ELF 的加載器 elf_format 中規(guī)定了具體的加載函數(shù),例如 load_binary 成員指向的就是具體的 load_elf_binary 函數(shù)。這就是 ELF 加載的入口。
//file:fs/binfmt_elf.c
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
.load_shlib = load_elf_library,
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE,
};加載器 elf_format 會(huì)在初始化的時(shí)候通過 register_binfmt 進(jìn)行注冊(cè)。
//file:fs/binfmt_elf.c
static int __init init_elf_binfmt(void)
{
register_binfmt(&elf_format);
return 0;
}而 register_binfmt 就是將加載器掛到全局加載器列表 - formats 全局鏈表中。
//file:fs/exec.c
static LIST_HEAD(formats);
void __register_binfmt(struct linux_binfmt * fmt, int insert)
{
insert ? list_add(&fmt-lh, &formats) :
list_add_tail(&fmt-lh, &formats);
}Linux 中除了 elf 文件格式以外還支持其它格式,在源碼目錄中搜索 register_binfmt,可以搜索到所有 Linux 操作系統(tǒng)支持的格式的加載程序。
# grep -r "register_binfmt" * fs/binfmt_flat.c: register_binfmt(&flat_format); fs/binfmt_elf_fdpic.c: register_binfmt(&elf_fdpic_format); fs/binfmt_som.c: register_binfmt(&som_format); fs/binfmt_elf.c: register_binfmt(&elf_format); fs/binfmt_aout.c: register_binfmt(&aout_format); fs/binfmt_script.c: register_binfmt(&script_format); fs/binfmt_em86.c: register_binfmt(&em86_format);
將來在 Linux 在加載二進(jìn)制文件時(shí)會(huì)遍歷 formats 鏈表,根據(jù)要加載的文件格式來查詢合適的加載器。
四、execve 加載用戶程序
具體加載可執(zhí)行文件的工作是由 execve 系統(tǒng)調(diào)用來完成的。
該系統(tǒng)調(diào)用會(huì)讀取用戶輸入的可執(zhí)行文件名,參數(shù)列表以及環(huán)境變量等開始加載并運(yùn)行用戶指定的可執(zhí)行文件。該系統(tǒng)調(diào)用的位置在 fs / exec.c 文件中。
//file:fs/exec.c
SYSCALL_DEFINE3(execve, const char __user *, filename, )
{
struct filename *path = getname(filename);
do_execve(path-name, argv, envp)
}
int do_execve()
{
return do_execve_common(filename, argv, envp);
}execve 系統(tǒng)調(diào)用到了 do_execve_common 函數(shù)。我們來看這個(gè)函數(shù)的實(shí)現(xiàn)。
//file:fs/exec.c
static int do_execve_common(const char *filename, )
{
//linux_binprm 結(jié)構(gòu)用于保存加載二進(jìn)制文件時(shí)使用的參數(shù)
struct linux_binprm *bprm;
//1申請(qǐng)并初始化 brm 對(duì)象值
bprm = kzalloc(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_binprm(bprm);
//2遍歷查找合適的二進(jìn)制加載器
search_binary_handler(bprm);
}這個(gè)函數(shù)中申請(qǐng)并初始化 brm 對(duì)象的具體工作可以用下圖來表示。

在這個(gè)函數(shù)中,完成了一下三塊工作。
第一、使用 kzalloc 申請(qǐng) linux_binprm 內(nèi)核對(duì)象。該內(nèi)核對(duì)象用于保存加載二進(jìn)制文件時(shí)使用的參數(shù)。在申請(qǐng)完后,對(duì)該參數(shù)對(duì)象進(jìn)行各種初始化。
第二、在 bprm_mm_init 中會(huì)申請(qǐng)一個(gè)全新的 mm_struct 對(duì)象,準(zhǔn)備留著給新進(jìn)程使用。
第三、給新進(jìn)程的棧申請(qǐng)一頁的虛擬內(nèi)存空間,并將棧指針記錄下來。
第四、讀取二進(jìn)制文件頭 128 字節(jié)。
我們來看下初始化棧的相關(guān)代碼。
//file:fs/exec.c
static int __bprm_mm_init(struct linux_binprm *bprm)
{
bprm-vma = vma = kmem_cache_zalloc(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 *);
}在上面這個(gè)函數(shù)中申請(qǐng)了一個(gè) vma 對(duì)象(表示虛擬地址空間里的一段范圍),vm_end 指向了 STACK_TOP_MAX(地址空間的頂部附近的位置),vm_start 和 vm_end 之間留了一個(gè) Page 大小。也就是說默認(rèn)給棧申請(qǐng)了 4KB 的大小。最后把棧的指針記錄到 bprm->p 中。
另外再看下 prepare_binprm,在這個(gè)函數(shù)中,從文件頭部讀取了 128 字節(jié)。之所以這么干,是為了讀取二進(jìn)制文件頭為了方便后面判斷其文件類型。
//file:include/uapi/linux/binfmts.h
#define BINPRM_BUF_SIZE 128
//file:fs/exec.c
int prepare_binprm(struct linux_binprm *bprm)
{
memset(bprm-buf, 0, BINPRM_BUF_SIZE);
return kernel_read(bprm-file, 0, bprm-buf, BINPRM_BUF_SIZE);
}在申請(qǐng)并初始化 brm 對(duì)象值完后,最后使用 search_binary_handler 函數(shù)遍歷系統(tǒng)中已注冊(cè)的加載器,嘗試對(duì)當(dāng)前可執(zhí)行文件進(jìn)行解析并加載。

在 3.1 節(jié)我們介紹了系統(tǒng)所有的加載器都注冊(cè)到了 formats 全局鏈表里了。函數(shù) search_binary_handler 的工作過程就是遍歷這個(gè)全局鏈表,根據(jù)二進(jìn)制文件頭中攜帶的文件類型數(shù)據(jù)查找解析器。找到后調(diào)用解析器的函數(shù)對(duì)二進(jìn)制文件進(jìn)行加載。
//file:fs/exec.c
int search_binary_handler(struct linux_binprm *bprm)
{
for try=0; try2; try++ {
list_for_each_entry(fmt, &formats, lh) {
int (*fn)(struct linux_binprm *) = fmt-load_binary;
retval = fn(bprm);
//加載成功的話就返回了
if (retval = 0) {
return retval;
}
//加載失敗繼續(xù)循環(huán)以嘗試加載
}
}
}在上述代碼中的 list_for_each_entry 是在遍歷 formats 這個(gè)全局鏈表,遍歷時(shí)判斷每一個(gè)鏈表元素是否有 load_binary 函數(shù)。有的話就調(diào)用它嘗試加載。
回憶一下 3.1 注冊(cè)可執(zhí)行文件加載程序,對(duì)于 ELF 文件加載器 elf_format 來說,load_binary 函數(shù)指針指向的是 load_elf_binary。
//file:fs/binfmt_elf.c
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
};那么加載工作就會(huì)進(jìn)入到 load_elf_binary 函數(shù)中來進(jìn)行。這個(gè)函數(shù)很長(zhǎng),可以說所有的程序加載邏輯都在這個(gè)函數(shù)中體現(xiàn)了。我根據(jù)這個(gè)函數(shù)的主要工作,分成以下 5 個(gè)小部分來給大家介紹。
在介紹的過程中,為了表達(dá)清晰,我會(huì)稍微調(diào)一下源碼的位置,可能和內(nèi)核源碼行數(shù)順序會(huì)有所不同。
4.1 ELF 文件頭讀取
在 load_elf_binary 中首先會(huì)讀取 ELF 文件頭。

文件頭中包含一些當(dāng)前文件格式類型等數(shù)據(jù),所以在讀取完文件頭后會(huì)進(jìn)行一些合法性判斷。如果不合法,則退出返回。
//file:fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
//4.1 ELF 文件頭解析
//定義結(jié)構(gòu)題并申請(qǐng)內(nèi)存用來保存 ELF 文件頭
struct {
struct elfhdr elf_ex;
struct elfhdr interp_elf_ex;
} *loc;
loc = kmalloc(sizeof(*loc), GFP_KERNEL);
//獲取二進(jìn)制頭
loc-elf_ex = *((struct elfhdr *)bprm-buf);
//對(duì)頭部進(jìn)行一系列的合法性判斷,不合法則直接退出
if (loc-elf_ex.e_type != ET_EXEC && ){
goto out;
}
}4.2 Program Header 讀取
在 ELF 文件頭中記錄著 Program Header 的數(shù)量,而且在 ELF 頭之后緊接著就是 Program Header Tables。所以內(nèi)核接下來可以將所有的 Program Header 都讀取出來。

//file:fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
//4.1 ELF 文件頭解析
//4.2 Program Header 讀取
// elf_ex.e_phnum 中保存的是 Programe Header 數(shù)量
// 再根據(jù) Program Header 大小 sizeof(struct elf_phdr)
// 一起計(jì)算出所有的 Program Header 大小,并讀取進(jìn)來
size = loc-elf_ex.e_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 清空父進(jìn)程繼承來的資源
在 fork 系統(tǒng)調(diào)用創(chuàng)建出來的進(jìn)程中,包含了不少原進(jìn)程的信息,如老的地址空間,信號(hào)表等等。這些在新的程序運(yùn)行時(shí)并沒有什么用,所以需要清空處理一下。

具體工作包括初始化新進(jìn)程的信號(hào)表,應(yīng)用新的地址空間對(duì)象等。
//file:fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
//4.1 ELF 文件頭解析
//4.2 Program Header 讀取
//4.3 清空父進(jìn)程繼承來的資源
retval = flush_old_exec(bprm);
current-mm-start_stack = bprm-p;
}在清空完父進(jìn)程繼承來的資源后(當(dāng)然也就使用上了新的 mm_struct 對(duì)象),這之后,直接將前面準(zhǔn)備的進(jìn)程棧的地址空間指針設(shè)置到了 mm 對(duì)象上。這樣將來?xiàng)>涂梢员皇褂昧恕?/p>
4.4 執(zhí)行 Segment 加載
接下來,加載器會(huì)將 ELF 文件中的 LOAD 類型的 Segment 都加載到內(nèi)存里來。使用 elf_map 在虛擬地址空間中為其分配虛擬內(nèi)存。最后合適地設(shè)置虛擬地址空間 mm_struct 中的 start_code、end_code、start_data、end_data 等各個(gè)地址空間相關(guān)指針。

我們來看下具體的代碼:
//file:fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
//4.1 ELF 文件頭解析
//4.2 Program Header 讀取
//4.3 清空父進(jìn)程繼承來的資源
//4.4 執(zhí)行 Segment 加載過程
//遍歷可執(zhí)行文件的 Program Header
for(i = 0, elf_ppnt = elf_phdata;
i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
//只加載類型為 LOAD 的 Segment,否則跳過
if (elf_ppnt-p_type != PT_LOAD)
continue;
//為 Segment 建立內(nèi)存 mmap, 將程序文件中的內(nèi)容映射到虛擬內(nèi)存空間中
//這樣將來程序中的代碼、數(shù)據(jù)就都可以被訪問了
error = elf_map(bprm-file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, 0);
//計(jì)算 mm_struct 所需要的各個(gè)成員地址
start_code = ;
start_data =
end_code = ;
end_data = ;
}
current-mm-end_code = end_code;
current-mm-start_code = start_code;
current-mm-start_data = start_data;
current-mm-end_data = end_data;
}其中 load_bias 是 Segment 要加載到內(nèi)存里的基地址。這個(gè)參數(shù)有這么幾種可能
值為 0,就是直接按照 ELF 文件中的地址在內(nèi)存中進(jìn)行映射
值為對(duì)齊到整數(shù)頁的開始,物理文件中可能為了可執(zhí)行文件的大小足夠緊湊,而不考慮對(duì)齊的問題。但是操作系統(tǒng)在加載的時(shí)候?yàn)榱诉\(yùn)行效率,需要將 Segment 加載到整數(shù)頁的開始位置處。
4.5 數(shù)據(jù)內(nèi)存申請(qǐng) & 堆初始化
因?yàn)檫M(jìn)程的數(shù)據(jù)段需要寫權(quán)限,所以需要使用 set_brk 系統(tǒng)調(diào)用專門為數(shù)據(jù)段申請(qǐng)?zhí)摂M內(nèi)存。
//file:fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
//4.1 ELF 文件頭解析
//4.2 Program Header 讀取
//4.3 清空父進(jìn)程繼承來的資源
//4.4 執(zhí)行 Segment 加載過程
//4.5 數(shù)據(jù)內(nèi)存申請(qǐng)&初始化
retval = set_brk(elf_bss, elf_brk);
}在 set_brk 函數(shù)中做了兩件事情:第一是為數(shù)據(jù)段申請(qǐng)?zhí)摂M內(nèi)存,第二是將進(jìn)程堆的開始指針和結(jié)束指針初始化一下。

//file:fs/binfmt_elf.c
static int set_brk(unsigned long start, unsigned long end)
{
//1為數(shù)據(jù)段申請(qǐng)?zhí)摂M內(nèi)存
start = ELF_PAGEALIGN(start);
end = ELF_PAGEALIGN(end);
if (end start) {
unsigned long addr;
addr = vm_brk(start, end - start);
}
//2初始化堆的指針
current-mm-start_brk = current-mm-brk = end;
return 0;
}因?yàn)槌绦虺跏蓟臅r(shí)候,堆上還是空的。所以堆指針初始化的時(shí)候,堆的開始地址 start_brk 和結(jié)束地址 brk 都設(shè)置成了同一個(gè)值。
4.6 跳轉(zhuǎn)到程序入口執(zhí)行
在 ELF 文件頭中記錄了程序的入口地址。如果是非動(dòng)態(tài)鏈接加載的情況,入口地址就是這個(gè)。
但是如果是動(dòng)態(tài)鏈接,也就是說存在 INTERP 類型的 Segment,由這個(gè)動(dòng)態(tài)鏈接器先來加載運(yùn)行,然后再調(diào)回到程序的代碼入口地址。
# readelf --program-headers helloworld Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align INTERP 0x00000000000002a8 0x00000000004002a8 0x00000000004002a8 0x000000000000001c 0x000000000000001c R 0x1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
對(duì)于是動(dòng)態(tài)加載器類型的,需要先將動(dòng)態(tài)加載器(本文示例中是 ld-linux-x86-64.so.2 文件)加載到地址空間中來。

加載完成后再計(jì)算動(dòng)態(tài)加載器的入口地址。這段代碼我展示在下面了,沒有耐心的同學(xué)可以跳過。反正只要知道這里是計(jì)算了一個(gè)程序的入口地址就可以了。
//file:fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
//4.1 ELF 文件頭解析
//4.2 Program Header 讀取
//4.3 清空父進(jìn)程繼承來的資源
//4.4 執(zhí)行 Segment 加載
//4.5 數(shù)據(jù)內(nèi)存申請(qǐng)&堆初始化
//4.6 跳轉(zhuǎn)到程序入口執(zhí)行
//第一次遍歷 program header table
//只針對(duì) PT_INTERP 類型的 segment 做個(gè)預(yù)處理
//這個(gè) segment 中保存著動(dòng)態(tài)加載器在文件系統(tǒng)中的路徑信息
for (i = 0; i < loc->elf_ex.e_phnum; i++) {
...
}
//第二次遍歷 program header table, 做些特殊處理
elf_ppnt = elf_phdata;
for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++){
...
}
//如果程序中指定了動(dòng)態(tài)鏈接器,就把動(dòng)態(tài)鏈接器程序讀出來
if (elf_interpreter) {
//加載并返回動(dòng)態(tài)鏈接器代碼段地址
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias);
//計(jì)算動(dòng)態(tài)鏈接器入口地址
elf_entry += loc->interp_elf_ex.e_entry;
} else {
elf_entry = loc->elf_ex.e_entry;
}
//跳轉(zhuǎn)到入口開始執(zhí)行
start_thread(regs, elf_entry, bprm->p);
...
}五、總結(jié)
看起來簡(jiǎn)簡(jiǎn)單單的一行 helloworld 代碼,但是要想把它運(yùn)行過程理解清楚可卻需要非常深厚的內(nèi)功的。
本文首先帶領(lǐng)大家認(rèn)識(shí)和理解了二進(jìn)制可運(yùn)行 ELF 文件格式。在 ELF 文件中是由四部分組成,分別是 ELF 文件頭 (ELF header)、Program header table、Section 和 Section header table。
Linux 在初始化的時(shí)候,會(huì)將所有支持的加載器都注冊(cè)到一個(gè)全局鏈表中。對(duì)于 ELF 文件來說,它的加載器在內(nèi)核中的定義為 elf_format,其二進(jìn)制加載入口是 load_elf_binary 函數(shù)。
一般來說 shell 進(jìn)程是通過 fork + execve 來加載并運(yùn)行新進(jìn)程的。執(zhí)行 fork 系統(tǒng)調(diào)用的作用是創(chuàng)建一個(gè)新進(jìn)程出來。不過 fork 創(chuàng)建出來的新進(jìn)程的代碼、數(shù)據(jù)都還是和原來的 shell 進(jìn)程的內(nèi)容一模一樣。要想實(shí)現(xiàn)加載并運(yùn)行另外一個(gè)程序,那還需要使用到 execve 系統(tǒng)調(diào)用。
在 execve 系統(tǒng)調(diào)用中,首先會(huì)申請(qǐng)一個(gè) linux_binprm 對(duì)象。在初始化 linux_binprm 的過程中,會(huì)申請(qǐng)一個(gè)全新的 mm_struct 對(duì)象,準(zhǔn)備留著給新進(jìn)程使用。還會(huì)給新進(jìn)程的棧準(zhǔn)備一頁(4KB)的虛擬內(nèi)存。還會(huì)讀取可執(zhí)行文件的前 128 字節(jié)。
接下來就是調(diào)用 ELF 加載器的 load_elf_binary 函數(shù)進(jìn)行實(shí)際的加載。大致會(huì)執(zhí)行如下幾個(gè)步驟:
ELF 文件頭解析
Program Header 讀取
清空父進(jìn)程繼承來的資源,使用新的 mm_struct 以及新的棧
執(zhí)行 Segment 加載,將 ELF 文件中的 LOAD 類型的 Segment 都加載到虛擬內(nèi)存中
為數(shù)據(jù) Segment 申請(qǐng)內(nèi)存,并將堆的起始指針進(jìn)行初始化
最后計(jì)算并跳轉(zhuǎn)到程序入口執(zhí)行

當(dāng)用戶進(jìn)程啟動(dòng)起來以后,我們可以通過 proc 偽文件來查看進(jìn)程中的各個(gè) Segment。
# cat /proc/46276/maps 00400000-00401000 r--p 00000000 fd:01 396999 /root/work_temp/helloworld 00401000-00402000 r-xp 00001000 fd:01 396999 /root/work_temp/helloworld 00402000-00403000 r--p 00002000 fd:01 396999 /root/work_temp/helloworld 00403000-00404000 r--p 00002000 fd:01 396999 /root/work_temp/helloworld 00404000-00405000 rw-p 00003000 fd:01 396999 /root/work_temp/helloworld 01dc9000-01dea000 rw-p 00000000 00:00 0 [heap] 7f0122fbf000-7f0122fc1000 rw-p 00000000 00:00 0 7f0122fc1000-7f0122fe7000 r--p 00000000 fd:01 1182071 /usr/lib64/libc-2.32.so 7f0122fe7000-7f0123136000 r-xp 00026000 fd:01 1182071 /usr/lib64/libc-2.32.so ...... 7f01231c0000-7f01231c1000 r--p 0002a000 fd:01 1182554 /usr/lib64/ld-2.32.so 7f01231c1000-7f01231c3000 rw-p 0002b000 fd:01 1182554 /usr/lib64/ld-2.32.so 7ffdf0590000-7ffdf05b1000 rw-p 00000000 00:00 0 [stack] ......
雖然本文非常的長(zhǎng),但仍然其實(shí)只把大體的加載啟動(dòng)過程串了一下。如果你日后在工作學(xué)習(xí)中遇到想搞清楚的問題,可以順著本文的思路去到源碼中尋找具體的問題,進(jìn)而幫助你找到工作中的問題的解。
最后提一下,細(xì)心的讀者可能發(fā)現(xiàn)了,本文的實(shí)例中加載新程序運(yùn)行的過程中其實(shí)有一些浪費(fèi),fork 系統(tǒng)調(diào)用首先將父進(jìn)程的很多信息拷貝了一遍,而 execve 加載可執(zhí)行程序的時(shí)候又是重新賦值的。所以在實(shí)際的 shell 程序中,一般使用的是 vfork。其工作原理基本和 fork 一致,但區(qū)別是會(huì)少拷貝一些在 execve 系統(tǒng)調(diào)用中用不到的信息,進(jìn)而提高加載性能。
廣告聲明:文內(nèi)含有的對(duì)外跳轉(zhuǎn)鏈接(包括不限于超鏈接、二維碼、口令等形式),用于傳遞更多信息,節(jié)省甄選時(shí)間,結(jié)果僅供參考,IT之家包含外鏈的文章均包含本聲明。