久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

萬字圖文 | 你寫的代碼是如何跑起來的?

 天承辦公室 2022-12-06 發(fā)布于江蘇

今天我們來思考一個(gè)簡單的問題,,一個(gè)程序是如何在 Linux 上執(zhí)行起來的?

我們就拿全宇宙最簡單的 Hello World 程序來舉例,。

#include <stdio.h>
int main()
{
   printf("Hello, World!\n");
   return 0;
}

我們?cè)趯懲甏a后,,進(jì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 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 組成的給展示了出來,比如 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ù)一些位置,。如下簡單代碼所示,。

//未初始化的內(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è)簡單加載 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è)程序了,。

在上一篇文章《Linux進(jìn)程是如何創(chuàng)建出來的?》中,,我們?cè)敿?xì)介紹過了 fork 的工作過程,。這里我們?cè)俸唵芜^一下。

這個(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; try<2; 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ù)很長,可以說所有的程序加載邏輯都在這個(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)>涂梢员皇褂昧恕?/span>

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é)

看起來簡簡單單的一行 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]
......

雖然本文非常的長,但仍然其實(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)而提高加載性能,。

原文鏈接:
https://mp.weixin.qq.com/s/1bdktqYF7VyAMadRlcRrSg

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點(diǎn),。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,,謹(jǐn)防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào),。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多