今天我們來(lái)思考一個(gè)簡(jiǎn)單的問(wèn)題,一個(gè)程序是如何在 Linux 上執(zhí)行起來(lái)的?
我們就拿全宇宙最簡(jiǎn)單的 Hello World 程序來(lái)舉例。
#include
int main()
{
printf("Hello, World!\\n");
return 0;
}
我們?cè)趯懲甏a后,進(jìn)行簡(jiǎn)單的編譯,然后在 shell 命令行下就可以把它啟動(dòng)起來(lái)。
# gcc main.c -o helloworld
# ./helloworld
Hello, World!
那么在編譯啟動(dòng)運(yùn)行的過(guò)程中都發(fā)生了哪些事情了呢?今天就讓我們來(lái)深入地了解一下。
一、理解可執(zhí)行文件格式
源代碼在編譯后會(huì)生成一個(gè)可執(zhí)行程序文件,我們先來(lái)了解一下編譯后的二進(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)來(lái)增強(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。
接下來(lái)我們分幾個(gè)小節(jié)挨個(gè)介紹一下。
1.1 ELF 文件頭
ELF 文件頭記錄了整個(gè)文件的屬性信息。原始二進(jìn)制非常不便于觀察。不過(guò)我們有趁手的工具 - readelf,這個(gè)工具可以幫我們查看 ELF 文件中的各種信息。
我們先來(lái)看一下編譯出來(lái)的可執(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è)拿出來(lái)給大家解釋一下。
- Magic:一串特殊的識(shí)別碼,主要用于外部程序快速地對(duì)這個(gè)文件進(jìn)行識(shí)別,快速地判斷文件類型是不是 ELF
- Class:表示這是 ELF64 文件
- Type:為 EXEC 表示是可執(zhí)行文件,其它文件類型還有 REL(可重定位的目標(biāo)文件)、DYN(動(dòng)態(tài)鏈接庫(kù))、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)來(lái)說(shuō),它不關(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 的頭信息,用來(lái)描述所有的 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 mapping:
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 組成的給展示了出來(lái),比如 03 號(hào)段是由“.init .plt .text .fini” 四個(gè) Section 組成的。
1.3 Section Header Table
和 Program Header Table 不一樣的是,Section header table 直接描述每一個(gè) Section。這二者描述的其實(shí)都是各種 Section ,只不過(guò)目的不同,一個(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)制文件中的位置通過(guò) Offset 列表示了出來(lái)。Section 的大小通過(guò) Size 列體現(xiàn)。
在這 30 個(gè)Section中,每一個(gè)都有獨(dú)特的作用。我們編寫的代碼在編譯成二進(jìn)制指令后都會(huì)放到 .text 這個(gè) Section 中。另外我們看到 .text 段的 Address 列顯示的地址是 0000000000401040。回憶前面我們?cè)?ELF 文件頭中看到 Entry point address 顯示的入口地址為 0x401040。這說(shuō)明,程序的入口地址就是 .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)一步查看
接下來(lái),我們想再查看一下我們前面提到的程序入口 0x401040,看看它到底是啥。我們這次再借助 nm 命令來(lái)進(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
通過(guò)以上輸出可以看到,程序入口 0x401040 指向的是 _start 函數(shù)的地址,在這個(gè)函數(shù)執(zhí)行一些初始化的操作之后,我們的入口函數(shù) main 將會(huì)被調(diào)用到,它位于 0x401126 地址處。
二、用戶進(jìn)程的創(chuàng)建過(guò)程概述
在我們編寫的代碼編譯完生成可執(zhí)行程序之后,下一步就是使用 shell 把它加載起來(lái)并運(yùn)行之。一般來(lái)說(shuō) shell 進(jìn)程是通過(guò)fork+execve來(lái)加載并運(yùn)行新進(jìn)程的。一個(gè)簡(jiǎn)單加載 helloworld 命令的 shell 核心邏輯是如下這個(gè)過(guò)程。
// 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)程先通過(guò) fork 系統(tǒng)調(diào)用創(chuàng)建一個(gè)進(jìn)程出來(lái)。然后在子進(jìn)程中調(diào)用 execve 將執(zhí)行的程序文件加載起來(lái),然后就可以調(diào)到程序文件的運(yùn)行入口處運(yùn)行這個(gè)程序了。
在上一篇文章[《Linux進(jìn)程是如何創(chuàng)建出來(lái)的?》]中,我們?cè)敿?xì)介紹過(guò)了 fork 的工作過(guò)程。這里我們?cè)俸?jiǎn)單過(guò)一下。
這個(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)程(線程)的方式來(lái)生成一個(gè)新的 task_struct 出來(lái)。
//file:kernel/fork.c
long do_fork(...)
{
//復(fù)制一個(gè) task_struct 出來(lái)
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)度。
不過(guò) fork 系統(tǒng)調(diào)用只能是根據(jù)當(dāng)?shù)?shell 進(jìn)程再?gòu)?fù)制一個(gè)新的進(jìn)程出來(lái)。這個(gè)新進(jìn)程里的代碼、數(shù)據(jù)都還是和原來(lái)的 shell 進(jìn)程的內(nèi)容一模一樣。
要想實(shí)現(xiàn)加載并運(yùn)行另外一個(gè)程序,比如我們編譯出來(lái)的 helloworld 程序,那還需要使用到 execve 系統(tǒng)調(diào)用。
三. Linux 可執(zhí)行文件加載器
其實(shí) Linux 不是寫死只能加載 ELF 一種可執(zhí)行文件格式的。它在啟動(dòng)的時(shí)候,會(huì)把自己支持的所有可執(zhí)行文件的解析器都加載上。并使用一個(gè) formats 雙向鏈表來(lái)保存所有的解析器。其中 formats 雙向鏈表在內(nèi)存中的結(jié)構(gòu)如下圖所示。
我們就以 ELF 的加載器 elf_format 為例,來(lái)看看這個(gè)加載器是如何注冊(cè)的。在 Linux 中每一個(gè)加載器都用一個(gè) linux_binfmt 結(jié)構(gòu)來(lái)表示。其中規(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í)候通過(guò) 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);
將來(lái)在 Linux 在加載二進(jìn)制文件時(shí)會(huì)遍歷 formats 鏈表,根據(jù)要加載的文件格式來(lái)查詢合適的加載器。
-
Linux
+關(guān)注
關(guān)注
87文章
11326瀏覽量
209961 -
代碼
+關(guān)注
關(guān)注
30文章
4808瀏覽量
68811 -
helloworld
+關(guān)注
關(guān)注
0文章
13瀏覽量
4372
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論