色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

你寫的代碼是如何跑起來的?

roborobo_0706 ? 來源:開發(fā)內(nèi)功修煉 ? 作者:張彥飛allen ? 2022-12-08 15:50 ? 次閱讀

大家好,我是飛哥!

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

我們就拿全宇宙最簡(jiǎn)單的 Hello World 程序來舉例。

#include
intmain()
{
printf("Hello,World!
");
return0;
}

我們?cè)趯懲甏a后,進(jìn)行簡(jiǎn)單的編譯,然后在 shell 命令行下就可以把它啟動(dòng)起來。

#gccmain.c-ohelloworld
#./helloworld
Hello,World!

那么在編譯啟動(dòng)運(yùn)行的過程中都發(fā)生了哪些事情了呢?今天就讓我們來深入地了解一下。

一、理解可執(zhí)行文件格式

源代碼在編譯后會(huì)生成一個(gè)可執(zhí)行程序文件,我們先來了解一下編譯后的二進(jìn)制文件是什么樣子的。

我們首先使用 file 命令查看一下這個(gè)文件的格式。

#filehelloworld
helloworld:ELF64-bitLSBexecutable,x86-64,version1(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。

b62b743c-76ad-11ed-8abf-dac502259ad0.png

接下來我們分幾個(gè)小節(jié)挨個(gè)介紹一下。

1.1 ELF 文件頭

ELF 文件頭記錄了整個(gè)文件的屬性信息。原始二進(jìn)制非常不便于觀察。不過我們有趁手的工具 - readelf,這個(gè)工具可以幫我們查看 ELF 文件中的各種信息。

我們先來看一下編譯出來的可執(zhí)行文件的 ELF 文件頭,使用 --file-header (-h) 選項(xiàng)即可查看。

#readelf--file-headerhelloworld
ELFHeader:
Magic:7f454c46020101000000000000000000
Class:ELF64
Data:2'scomplement,littleendian
Version:1(current)
OS/ABI:UNIX-SystemV
ABIVersion:0
Type:EXEC(Executablefile)
Machine:AdvancedMicroDevicesX86-64
Version:0x1
Entrypointaddress:0x401040
Startofprogramheaders:64(bytesintofile)
Startofsectionheaders:23264(bytesintofile)
Flags:0x0
Sizeofthisheader:64(bytes)
Sizeofprogramheaders:56(bytes)
Numberofprogramheaders:11
Sizeofsectionheaders:64(bytes)
Numberofsectionheaders:30
Sectionheaderstringtableindex: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)更快速地加載。

b63c5450-76ad-11ed-8abf-dac502259ad0.png

由于 Segment 和 Section 翻譯成中文的話,意思太接近了,非常不利于理解。所以本文中我就直接使用 Segment 和 Section 原汁原味的概念,而不是將它們翻譯成段或者是節(jié),這樣太容易讓人混淆了。

Program headers table 就是作為所有 Segments 的頭信息,用來描述所有的 Segments 的。。

使用 readelf 工具的 --program-headers(-l)選項(xiàng)可以解析查看到這塊區(qū)域里存儲(chǔ)的內(nèi)容。

#readelf--program-headershelloworld
ElffiletypeisEXEC(Executablefile)
Entrypoint0x401040
Thereare11programheaders,startingatoffset64

ProgramHeaders:
TypeOffsetVirtAddrPhysAddr
FileSizMemSizFlagsAlign
PHDR0x00000000000000400x00000000004000400x0000000000400040
0x00000000000002680x0000000000000268R0x8
INTERP0x00000000000002a80x00000000004002a80x00000000004002a8
0x000000000000001c0x000000000000001cR0x1
[Requestingprograminterpreter:/lib64/ld-linux-x86-64.so.2]
LOAD0x00000000000000000x00000000004000000x0000000000400000
0x00000000000004380x0000000000000438R0x1000
LOAD0x00000000000010000x00000000004010000x0000000000401000
0x00000000000001c50x00000000000001c5RE0x1000
LOAD0x00000000000020000x00000000004020000x0000000000402000
0x00000000000001380x0000000000000138R0x1000
LOAD0x0000000000002e100x0000000000403e100x0000000000403e10
0x00000000000002200x0000000000000228RW0x1000
DYNAMIC0x0000000000002e200x0000000000403e200x0000000000403e20
0x00000000000001d00x00000000000001d0RW0x8
NOTE0x00000000000002c40x00000000004002c40x00000000004002c4
0x00000000000000440x0000000000000044R0x4
GNU_EH_FRAME0x00000000000020140x00000000004020140x0000000000402014
0x000000000000003c0x000000000000003cR0x4
GNU_STACK0x00000000000000000x00000000000000000x0000000000000000
0x00000000000000000x0000000000000000RW0x10
GNU_RELRO0x0000000000002e100x0000000000403e100x0000000000403e10
0x00000000000001f00x00000000000001f0R0x1

SectiontoSegmentmapping:
SegmentSections...
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 組成的。

b649d5f8-76ad-11ed-8abf-dac502259ad0.png

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-headershelloworld
Thereare30sectionheaders,startingatoffset0x5b10:

SectionHeaders:
[Nr]NameTypeAddressOffset
SizeEntSizeFlagsLinkInfoAlign
......
[13].textPROGBITS000000000040104000001040
00000000000001750000000000000000AX0016
......
[23].dataPROGBITS000000000040402000003020
00000000000000100000000000000000WA008
[24].bssNOBITS000000000040403000003030
00000000000000080000000000000000WA001
......
KeytoFlags:
W(write),A(alloc),X(execute),M(merge),S(strings),I(info),
L(linkorder),O(extraOSprocessingrequired),G(group),T(TLS),
C(compressed),x(unknown),o(OSspecific),E(exclude),
l(large),p(processorspecific)

結(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段
intdata1;

//已經(jīng)初始化的內(nèi)存區(qū)域位于.data段
intdata2=100;

//代碼位于.text段
intmain(void)
{
...
}

1.4 入口進(jìn)一步查看

接下來,我們想再查看一下我們前面提到的程序入口 0x401040,看看它到底是啥。我們這次再借助 nm 命令來進(jìn)一步查看一下可執(zhí)行文件中的符號(hào)及其地址信息。-n 選項(xiàng)的作用是顯示的符號(hào)以地址排序,而不是名稱排序。

#nm-nhelloworld
w__gmon_start__
U__libc_start_main@@GLIBC_2.2.5
Uprintf@@GLIBC_2.2.5
......
0000000000401040T_start
......
0000000000401126Tmain

通過以上輸出可以看到,程序入口 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代碼示例
intmain(intargc,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è)俸?jiǎn)單過一下。

這個(gè) fork 系統(tǒng)調(diào)用在內(nèi)核入口是在 kernel/fork.c 下。

//file:kernel/fork.c
SYSCALL_DEFINE0(fork)
{
returndo_fork(SIGCHLD,0,0,NULL,NULL);
}

在 do_fork 的實(shí)現(xiàn)中,核心是一個(gè) copy_process 函數(shù),它以拷貝父進(jìn)程(線程)的方式來生成一個(gè)新的 task_struct 出來。

//file:kernel/fork.c
longdo_fork(...)
{
//復(fù)制一個(gè)task_struct出來
structtask_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
staticstructtask_struct*copy_process(...)
{
//復(fù)制進(jìn)程task_struct結(jié)構(gòu)體
structtask_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)如下圖所示。

b659aa00-76ad-11ed-8abf-dac502259ad0.png

我們就以 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
structlinux_binfmt{
...
int(*load_binary)(structlinux_binprm*);
int(*load_shlib)(structfile*);
int(*core_dump)(structcoredump_params*cprm);
};

其中 ELF 的加載器 elf_format 中規(guī)定了具體的加載函數(shù),例如 load_binary 成員指向的就是具體的 load_elf_binary 函數(shù)。這就是 ELF 加載的入口。

//file:fs/binfmt_elf.c
staticstructlinux_binfmtelf_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
staticint__initinit_elf_binfmt(void)
{
register_binfmt(&elf_format);
return0;
}

而 register_binfmt 就是將加載器掛到全局加載器列表 - formats 全局鏈表中。

//file:fs/exec.c
staticLIST_HEAD(formats);

void__register_binfmt(structlinux_binfmt*fmt,intinsert)
{
...
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,constchar__user*,filename,...)
{
structfilename*path=getname(filename);
do_execve(path->name,argv,envp)
...
}

intdo_execve(...)
{
...
returndo_execve_common(filename,argv,envp);
}

execve 系統(tǒng)調(diào)用到了 do_execve_common 函數(shù)。我們來看這個(gè)函數(shù)的實(shí)現(xiàn)。

//file:fs/exec.c
staticintdo_execve_common(constchar*filename,...)
{
//linux_binprm結(jié)構(gòu)用于保存加載二進(jìn)制文件時(shí)使用的參數(shù)
structlinux_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ì)象的具體工作可以用下圖來表示。

b66fc538-76ad-11ed-8abf-dac502259ad0.png

在這個(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
staticint__bprm_mm_init(structlinux_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
#defineBINPRM_BUF_SIZE128

//file:fs/exec.c
intprepare_binprm(structlinux_binprm*bprm)
{
......
memset(bprm->buf,0,BINPRM_BUF_SIZE);
returnkernel_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)行解析并加載。

b685eea8-76ad-11ed-8abf-dac502259ad0.png

在 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
intsearch_binary_handler(structlinux_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){
...
returnretval;
}
//加載失敗繼續(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
staticstructlinux_binfmtelf_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 文件頭。

b69bf59a-76ad-11ed-8abf-dac502259ad0.png

文件頭中包含一些當(dāng)前文件格式類型等數(shù)據(jù),所以在讀取完文件頭后會(huì)進(jìn)行一些合法性判斷。如果不合法,則退出返回。

//file:fs/binfmt_elf.c
staticintload_elf_binary(structlinux_binprm*bprm)
{
//4.1ELF文件頭解析
//定義結(jié)構(gòu)題并申請(qǐng)內(nèi)存用來保存ELF文件頭
struct{
structelfhdrelf_ex;
structelfhdrinterp_elf_ex;
}*loc;
loc=kmalloc(sizeof(*loc),GFP_KERNEL);

//獲取二進(jìn)制頭
loc->elf_ex=*((structelfhdr*)bprm->buf);

//對(duì)頭部進(jìn)行一系列的合法性判斷,不合法則直接退出
if(loc->elf_ex.e_type!=ET_EXEC&&...){
gotoout;
}
...
}

4.2 Program Header 讀取

在 ELF 文件頭中記錄著 Program Header 的數(shù)量,而且在 ELF 頭之后緊接著就是 Program Header Tables。所以內(nèi)核接下來可以將所有的 Program Header 都讀取出來。

b6b734cc-76ad-11ed-8abf-dac502259ad0.png

//file:fs/binfmt_elf.c
staticintload_elf_binary(structlinux_binprm*bprm)
{
//4.1ELF文件頭解析

//4.2ProgramHeader讀取
//elf_ex.e_phnum中保存的是ProgrameHeader數(shù)量
//再根據(jù)ProgramHeader大小sizeof(structelf_phdr)
//一起計(jì)算出所有的ProgramHeader大小,并讀取進(jìn)來
size=loc->elf_ex.e_phnum*sizeof(structelf_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í)并沒有什么用,所以需要清空處理一下。

b6d7ba30-76ad-11ed-8abf-dac502259ad0.png

具體工作包括初始化新進(jìn)程的信號(hào)表,應(yīng)用新的地址空間對(duì)象等。

//file:fs/binfmt_elf.c
staticintload_elf_binary(structlinux_binprm*bprm)
{
//4.1ELF文件頭解析
//4.2ProgramHeader讀取

//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)指針。

b6e96758-76ad-11ed-8abf-dac502259ad0.png

我們來看下具體的代碼:

//file:fs/binfmt_elf.c
staticintload_elf_binary(structlinux_binprm*bprm)
{
//4.1ELF文件頭解析
//4.2ProgramHeader讀取
//4.3清空父進(jìn)程繼承來的資源

//4.4執(zhí)行Segment加載過程
//遍歷可執(zhí)行文件的ProgramHeader
for(i=0,elf_ppnt=elf_phdata;
ielf_ex.e_phnum;i++,elf_ppnt++){

//只加載類型為L(zhǎng)OAD的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
staticintload_elf_binary(structlinux_binprm*bprm)
{
//4.1ELF文件頭解析
//4.2ProgramHeader讀取
//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é)束指針初始化一下。

b6f7f430-76ad-11ed-8abf-dac502259ad0.png

//file:fs/binfmt_elf.c
staticintset_brk(unsignedlongstart,unsignedlongend)
{
//1.為數(shù)據(jù)段申請(qǐng)?zhí)摂M內(nèi)存
start=ELF_PAGEALIGN(start);
end=ELF_PAGEALIGN(end);
if(end>start){
unsignedlongaddr;
addr=vm_brk(start,end-start);
}

//2.初始化堆的指針
current->mm->start_brk=current->mm->brk=end;
return0;
}

因?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-headershelloworld
......
ProgramHeaders:
TypeOffsetVirtAddrPhysAddr
FileSizMemSizFlagsAlign
INTERP0x00000000000002a80x00000000004002a80x00000000004002a8
0x000000000000001c0x000000000000001cR0x1
[Requestingprograminterpreter:/lib64/ld-linux-x86-64.so.2]

對(duì)于是動(dòng)態(tài)加載器類型的,需要先將動(dòng)態(tài)加載器(本文示例中是 ld-linux-x86-64.so.2 文件)加載到地址空間中來。

b7083ef8-76ad-11ed-8abf-dac502259ad0.png

加載完成后再計(jì)算動(dòng)態(tài)加載器的入口地址。這段代碼我展示在下面了,沒有耐心的同學(xué)可以跳過。反正只要知道這里是計(jì)算了一個(gè)程序的入口地址就可以了。

//file:fs/binfmt_elf.c
staticintload_elf_binary(structlinux_binprm*bprm)
{
//4.1ELF文件頭解析
//4.2ProgramHeader讀取
//4.3清空父進(jìn)程繼承來的資源
//4.4執(zhí)行Segment加載
//4.5數(shù)據(jù)內(nèi)存申請(qǐng)&堆初始化
//4.6跳轉(zhuǎn)到程序入口執(zhí)行

//第一次遍歷programheadertable
//只針對(duì)PT_INTERP類型的segment做個(gè)預(yù)處理
//這個(gè)segment中保存著動(dòng)態(tài)加載器在文件系統(tǒng)中的路徑信息
for(i=0;ielf_ex.e_phnum;i++){
...
}

//第二次遍歷programheadertable,做些特殊處理
elf_ppnt=elf_phdata;
for(i=0;ielf_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í)行

b7194522-76ad-11ed-8abf-dac502259ad0.png

當(dāng)用戶進(jìn)程啟動(dòng)起來以后,我們可以通過 proc 偽文件來查看進(jìn)程中的各個(gè) Segment。

#cat/proc/46276/maps
00400000-00401000r--p00000000fd:01396999/root/work_temp/helloworld
00401000-00402000r-xp00001000fd:01396999/root/work_temp/helloworld
00402000-00403000r--p00002000fd:01396999/root/work_temp/helloworld
00403000-00404000r--p00002000fd:01396999/root/work_temp/helloworld
00404000-00405000rw-p00003000fd:01396999/root/work_temp/helloworld
01dc9000-01dea000rw-p0000000000:000[heap]
7f0122fbf000-7f0122fc1000rw-p0000000000:000
7f0122fc1000-7f0122fe7000r--p00000000fd:011182071/usr/lib64/libc-2.32.so
7f0122fe7000-7f0123136000r-xp00026000fd:011182071/usr/lib64/libc-2.32.so
......
7f01231c0000-7f01231c1000r--p0002a000fd:011182554/usr/lib64/ld-2.32.so
7f01231c1000-7f01231c3000rw-p0002b000fd:011182554/usr/lib64/ld-2.32.so
7ffdf0590000-7ffdf05b1000rw-p0000000000:000[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)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11322

    瀏覽量

    209862
  • 程序
    +關(guān)注

    關(guān)注

    117

    文章

    3792

    瀏覽量

    81167
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4803

    瀏覽量

    68752

原文標(biāo)題:萬字圖文 | 你寫的代碼是如何跑起來的?

文章出處:【微信號(hào):ExASIC,微信公眾號(hào):ExASIC】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    MotorControl Workbench生成的代碼是開環(huán)的嗎,為什么電機(jī)跑起來很容易受到外力導(dǎo)致停機(jī)?

    請(qǐng)問各位高手 MotorControl Workbench 生成的代碼是開環(huán)的嗎?為什么我的電機(jī)跑起來很容易受到外力導(dǎo)致停機(jī),我想讓它不停機(jī),請(qǐng)問有什么好的辦法嗎 ?
    發(fā)表于 03-21 07:12

    [MsgOS]讓系統(tǒng)跑起來

    子技術(shù)論壇)鑒于大家水平高低不齊,對(duì)于一些人可能拿到源碼還不能順利跑起來,這里就一個(gè)詳細(xì)的教程。步驟1.下載安裝相關(guān)軟件(這個(gè)就不細(xì)講了吧)window操作系統(tǒng) win7旗艦版,keilMDK 版本
    發(fā)表于 06-15 21:09

    請(qǐng)問HVMotorCtrl+PfcKit_v1.7/HVPM_sensorless_2833x代碼能不能讓電機(jī)跑起來?需要修改哪些參數(shù)?

    ,現(xiàn)在想測(cè)試一下代碼能不能讓電機(jī)跑起來,從level1——level6,不知道從哪個(gè)level可以讓電機(jī)跑起來,聽說比較危險(xiǎn),不知道需要改什么參數(shù)不,母線電壓貌似程序里沒怎么提到,能不能指點(diǎn)下
    發(fā)表于 06-13 05:19

    請(qǐng)問stm32f103工程代碼如何在stm32f407芯片上跑起來

    如題:1、stm32f103工程代碼如何在stm32f407芯片上跑起來?2、要做哪些修改?
    發(fā)表于 09-04 09:27

    如何讓的ESP32跑起來

    ESP32是了國內(nèi)樂鑫科技推出的Wifi&藍(lán)牙物聯(lián)網(wǎng)MCU,而最近項(xiàng)目正好在用ESP32,所以我們今天就來分享下,如何讓的ESP32跑起來,并應(yīng)用于更多實(shí)際項(xiàng)目。1ESP32簡(jiǎn)...
    發(fā)表于 07-16 06:57

    怎樣讓自己編譯的uboot跑起來

    小目標(biāo):讓自己編譯的uboot跑起來參考:wiki.friendlyarm.com/wiki/index.php/NanoPi_NEO首先熟悉一下板子和開發(fā)流程。維基主要參考《使用全志原廠BSP
    發(fā)表于 11-08 06:37

    程序能跑起來就是很好的c代碼

    程序能跑起來并不見得代碼就是很好的c代碼了,衡量代碼的好壞應(yīng)該從以下幾個(gè)方面來添加鏈接描述看:海風(fēng)教育投訴1,
    發(fā)表于 11-23 08:00

    如何利用XR806開發(fā)板讓hello跑起來

    如何利用XR806開發(fā)板讓hello跑起來?
    發(fā)表于 12-29 06:16

    如何讓u-boot跑起來?

    如何讓u-boot跑起來?
    發(fā)表于 01-26 08:26

    如何讓的ESP32跑起來

    ESP32是了國內(nèi)樂鑫科技推出的Wifi&藍(lán)牙物聯(lián)網(wǎng)MCU,而最近項(xiàng)目正好在用ESP32,所以我們今天就來分享下,如何讓的ESP32跑起來,并應(yīng)用于更多實(shí)際項(xiàng)目。1ESP32簡(jiǎn)介ESP32
    發(fā)表于 02-10 06:25

    Zynq 7015 linux跑起來之導(dǎo)入之BOOT.bin生成詳解

    本文主要介紹Zynq 7015 linux跑起來之導(dǎo)入之BOOT.bin生成,具體的跟隨小編一起來了解一下。
    的頭像 發(fā)表于 06-27 10:01 ?7513次閱讀

    FreeRTOS_003 _讓系統(tǒng)在板子上跑起來

    FreeRTOS_003_讓系統(tǒng)在板子上跑起來
    的頭像 發(fā)表于 03-14 11:25 ?2785次閱讀
    FreeRTOS_003 _讓系統(tǒng)在板子上<b class='flag-5'>跑起來</b>

    windows安裝ubuntu并讓pioneer1應(yīng)用程序跑起來的過程

    本文介紹在windows下安裝ubuntu并且讓pioneer1的應(yīng)用程序跑起來的全過程。雖然安裝ubuntu不是本文重點(diǎn),但是還是啰嗦地一遍吧。
    的頭像 發(fā)表于 10-23 10:41 ?2381次閱讀
    windows安裝ubuntu并讓pioneer1應(yīng)用程序<b class='flag-5'>跑起來</b>的過程

    知道代碼是怎樣跑起來的嗎(上)

    今天我們來思考一個(gè)簡(jiǎn)單的問題,一個(gè)程序是如何在 Linux 上執(zhí)行起來的? 我們就拿全宇宙最簡(jiǎn)單的 Hello World 程序來舉例。
    的頭像 發(fā)表于 05-05 14:36 ?541次閱讀
    <b class='flag-5'>你</b>知道<b class='flag-5'>你</b><b class='flag-5'>寫</b>的<b class='flag-5'>代碼</b>是怎樣<b class='flag-5'>跑起來</b>的嗎(上)

    知道代碼是怎樣跑起來的嗎(下)

    今天我們來思考一個(gè)簡(jiǎn)單的問題,一個(gè)程序是如何在 Linux 上執(zhí)行起來的? 我們就拿全宇宙最簡(jiǎn)單的 Hello World 程序來舉例。
    的頭像 發(fā)表于 05-05 14:36 ?483次閱讀
    <b class='flag-5'>你</b>知道<b class='flag-5'>你</b><b class='flag-5'>寫</b>的<b class='flag-5'>代碼</b>是怎樣<b class='flag-5'>跑起來</b>的嗎(下)
    主站蜘蛛池模板: 55夜色66夜亚洲精品播放| 欧美最猛性xxxxx亚洲精品| 无码人妻精品一区二区蜜桃色| 51xx午夜影视福利| 久久亚洲AV成人无码国产漫画| 一个人看的www视频动漫版| 国产免国产免费| 无码任你躁久久久久久久| 国产成人在线视频| 午夜视频在线网站| 狠狠色狠狠色狠狠五月ady| 夜色福利院在线观看免费| 久久vs国产| 757一本到午夜宫| 暖暖免费 高清 日本社区中文| 99国产在线观看| 青青草原免费在线| 攻把受做哭边走边肉楼梯PLAY| 少妇内射视频播放舔大片| 国产亚洲AV无码成人网站| 亚洲欧美中文在线一区| 九九色精品国偷自产视频| 777黄色片| 色中色辩论区| 快播欧美大片| 第一福利视频网站在线| 午夜国产大片免费观看| 国内精品久久久久久西瓜色吧| 2023国产精品一卡2卡三卡4卡| 美女被黑人巨大进入| 草柳最新地址| 亚洲国产在线2o20| 久久视热频这里只精品| xxx日本hd| 亚洲欧美成人无码久久久| 免费乱理伦片在线观看八戒| 姑娘视频日本在线播放| 亚洲日韩成人| 欧美日本高清动作片www网站| 国产香蕉视频| 99久久久A片无码国产精|