在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

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

roborobo_0706 ? 來源:開發內功修煉 ? 作者:張彥飛allen ? 2022-12-08 15:50 ? 次閱讀

大家好,我是飛哥!

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

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

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

我們在寫完代碼后,進行簡單的編譯,然后在 shell 命令行下就可以把它啟動起來。

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

那么在編譯啟動運行的過程中都發生了哪些事情了呢?今天就讓我們來深入地了解一下。

一、理解可執行文件格式

源代碼在編譯后會生成一個可執行程序文件,我們先來了解一下編譯后的二進制文件是什么樣子的。

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

#filehelloworld
helloworld:ELF64-bitLSBexecutable,x86-64,version1(SYSV),...

file 命令給出了這個二進制文件的概要信息,其中 ELF 64-bit LSB executable 表示這個文件是一個 ELF 格式的 64 位的可執行文件。x86-64 表示該可執行文件支持的 cpu 架構。

LSB 的全稱是 Linux Standard Base,是 Linux 標準規范。其目的是制定一系列標準來增強 Linux 發行版的兼容性。

ELF 的全稱是 Executable Linkable Format,是一種二進制文件格式。Linux 下的目標文件、可執行文件和 CoreDump 都按照該格式進行存儲。

ELF 文件由四部分組成,分別是 ELF 文件頭 (ELF header)、Program header table、Section 和 Section header table。

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

接下來我們分幾個小節挨個介紹一下。

1.1 ELF 文件頭

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

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

#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 文件頭包含了當前可執行文件的概要信息,我把其中關鍵的幾個拿出來給大家解釋一下。

Magic:一串特殊的識別碼,主要用于外部程序快速地對這個文件進行識別,快速地判斷文件類型是不是 ELF

Class:表示這是 ELF64 文件

Type:為 EXEC 表示是可執行文件,其它文件類型還有 REL(可重定位的目標文件)、DYN(動態鏈接庫)、CORE(系統調試 coredump文件)

Entry point address:程序入口地址,這里顯示入口在 0x401040 位置處

Size of this header:ELF 文件頭的大小,這里顯示是占用了 64 字節

以上幾個字段是 ELF 頭中對 ELF 的整體描述。另外 ELF 頭中還有關于 program headers 和 section headers 的描述信息。

Start of program headers:表示 Program header 的位置

Size of program headers:每一個 Program header 大小

Number of program headers:總共有多少個 Program header

Start of section headers: 表示 Section header 的開始位置。

Size of section headers:每一個 Section header 的大小

Number of section headers: 總共有多少個 Section header

1.2 Program Header Table

在介紹 Program Header Table 之前我們展開介紹一下 ELF 文件中一對兒相近的概念 - Segment 和 Section。

ELF 文件內部最重要的組成單位是一個一個的 Section。每一個 Section 都是由編譯鏈接器生成的,都有不同的用途。例如編譯器會將我們寫的代碼編譯后放到 .text Section 中,將全局變量放到 .data 或者是 .bss Section中。

但是對于操作系統來說,它不關注具體的 Section 是啥,它只關注這塊內容應該以何種權限加載到內存中,例如讀,寫,執行等權限屬性。因此相同權限的 Section 可以放在一起組成 Segment,以方便操作系統更快速地加載。

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

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

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

使用 readelf 工具的 --program-headers(-l)選項可以解析查看到這塊區域里存儲的內容。

#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

上面的結果顯示總共有 11 個 program headers。

對于每一個段,輸出了 Offset、VirtAddr 等描述當前段的信息。Offset 表示當前段在二進制文件中的開始位置,FileSiz 表示當前段的大小。Flag 表示當前的段的權限類型, R 表示可都、E 表示可執行、W 表示可寫。

在最下面,還把每個段是由哪幾個 Section 組成的給展示了出來,比如 03 號段是由“.init .plt .text .fini” 四個 Section 組成的。

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

1.3 Section Header Table

和 Program Header Table 不一樣的是,Section header table 直接描述每一個 Section。這二者描述的其實都是各種 Section ,只不過目的不同,一個針對加載,一個針對鏈接。

使用 readelf 工具的 --section-headers (-S)選項可以解析查看到這塊區域里存儲的內容。

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

結果顯示,該文件總共有 30 個 Sections,每一個 Section 在二進制文件中的位置通過 Offset 列表示了出來。Section 的大小通過 Size 列體現。

在這 30 個Section中,每一個都有獨特的作用。我們編寫的代碼在編譯成二進制指令后都會放到 .text 這個 Section 中。另外我們看到 .text 段的 Address 列顯示的地址是 0000000000401040。回憶前面我們在 ELF 文件頭中看到 Entry point address 顯示的入口地址為 0x401040。這說明,程序的入口地址就是 .text 段的地址。

另外還有兩個值得關注的 Section 是 .data 和 .bss。代碼中的全局變量數據在編譯后將在在這兩個 Section 中占據一些位置。如下簡單代碼所示。

//未初始化的內存區域位于.bss段
intdata1;

//已經初始化的內存區域位于.data段
intdata2=100;

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

1.4 入口進一步查看

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

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

通過以上輸出可以看到,程序入口 0x401040 指向的是 _start 函數的地址,在這個函數執行一些初始化的操作之后,我們的入口函數 main 將會被調用到,它位于 0x401126 地址處。

二、用戶進程的創建過程概述

在我們編寫的代碼編譯完生成可執行程序之后,下一步就是使用 shell 把它加載起來并運行之。一般來說 shell 進程是通過fork+execve來加載并運行新進程的。一個簡單加載 helloworld命令的 shell 核心邏輯是如下這個過程。

//shell代碼示例
intmain(intargc,char*argv[])
{
...
pid=fork();
if(pid==0){//如果是在子進程中
//使用exec系列函數加載并運行可執行文件
execve("helloworld",argv,envp);
}else{
...
}
...
}

shell 進程先通過 fork 系統調用創建一個進程出來。然后在子進程中調用 execve 將執行的程序文件加載起來,然后就可以調到程序文件的運行入口處運行這個程序了。

在上一篇文章《Linux進程是如何創建出來的?》中,我們詳細介紹過了 fork 的工作過程。這里我們再簡單過一下。

這個 fork 系統調用在內核入口是在 kernel/fork.c 下。

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

在 do_fork 的實現中,核心是一個 copy_process 函數,它以拷貝父進程(線程)的方式來生成一個新的 task_struct 出來。

//file:kernel/fork.c
longdo_fork(...)
{
//復制一個task_struct出來
structtask_struct*p;
p=copy_process(clone_flags,stack_start,stack_size,
child_tidptr,NULL,trace);

//子任務加入到就緒隊列中去,等待調度器調度
wake_up_new_task(p);
...
}

在 copy_process 函數中為新進程申請 task_struct,并用當前進程自己的地址空間、命名空間等對新進程進行初始化,并為其申請進程 pid。

//file:kernel/fork.c
staticstructtask_struct*copy_process(...)
{
//復制進程task_struct結構體
structtask_struct*p;
p=dup_task_struct(current);
...

//進程核心元素初始化
retval=copy_files(clone_flags,p);
retval=copy_fs(clone_flags,p);
retval=copy_mm(clone_flags,p);
retval=copy_namespaces(clone_flags,p);
...

//申請pid&&設置進程號
pid=alloc_pid(p->nsproxy->pid_ns);
p->pid=pid_nr(pid);
p->tgid=p->pid;
......
}

執行完后,進入 wake_up_new_task 讓新進程等待調度器調度。

不過 fork 系統調用只能是根據當的 shell 進程再復制一個新的進程出來。這個新進程里的代碼、數據都還是和原來的 shell 進程的內容一模一樣。

要想實現加載并運行另外一個程序,比如我們編譯出來的 helloworld 程序,那還需要使用到 execve 系統調用。

三. Linux 可執行文件加載器

其實 Linux 不是寫死只能加載 ELF 一種可執行文件格式的。它在啟動的時候,會把自己支持的所有可執行文件的解析器都加載上。并使用一個 formats 雙向鏈表來保存所有的解析器。其中 formats 雙向鏈表在內存中的結構如下圖所示。

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

我們就以 ELF 的加載器 elf_format 為例,來看看這個加載器是如何注冊的。在 Linux 中每一個加載器都用一個 linux_binfmt 結構來表示。其中規定了加載二進制可執行文件的 load_binary 函數指針,以及加載崩潰文件 的 core_dump 函數等。其完整定義如下

//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 中規定了具體的加載函數,例如 load_binary 成員指向的就是具體的 load_elf_binary 函數。這就是 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 會在初始化的時候通過 register_binfmt 進行注冊。

//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 操作系統支持的格式的加載程序。

#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 在加載二進制文件時會遍歷 formats 鏈表,根據要加載的文件格式來查詢合適的加載器。

四、execve 加載用戶程序

具體加載可執行文件的工作是由 execve 系統調用來完成的。

該系統調用會讀取用戶輸入的可執行文件名,參數列表以及環境變量等開始加載并運行用戶指定的可執行文件。該系統調用的位置在 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 系統調用到了 do_execve_common 函數。我們來看這個函數的實現。

//file:fs/exec.c
staticintdo_execve_common(constchar*filename,...)
{
//linux_binprm結構用于保存加載二進制文件時使用的參數
structlinux_binprm*bprm;

//1.申請并初始化brm對象值
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.遍歷查找合適的二進制加載器
search_binary_handler(bprm);
}

這個函數中申請并初始化 brm 對象的具體工作可以用下圖來表示。

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

在這個函數中,完成了一下三塊工作。

第一、使用 kzalloc 申請 linux_binprm 內核對象。該內核對象用于保存加載二進制文件時使用的參數。在申請完后,對該參數對象進行各種初始化。
第二、在 bprm_mm_init 中會申請一個全新的 mm_struct 對象,準備留著給新進程使用。
第三、給新進程的棧申請一頁的虛擬內存空間,并將棧指針記錄下來。
第四、讀取二進制文件頭 128 字節。

我們來看下初始化棧的相關代碼。

//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*);
}

在上面這個函數中申請了一個 vma 對象(表示虛擬地址空間里的一段范圍),vm_end 指向了 STACK_TOP_MAX(地址空間的頂部附近的位置),vm_start 和 vm_end 之間留了一個 Page 大小。也就是說默認給棧申請了 4KB 的大小。最后把棧的指針記錄到 bprm->p 中。

另外再看下 prepare_binprm,在這個函數中,從文件頭部讀取了 128 字節。之所以這么干,是為了讀取二進制文件頭為了方便后面判斷其文件類型。

//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);
}

在申請并初始化 brm 對象值完后,最后使用 search_binary_handler 函數遍歷系統中已注冊的加載器,嘗試對當前可執行文件進行解析并加載。

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

在 3.1 節我們介紹了系統所有的加載器都注冊到了 formats 全局鏈表里了。函數 search_binary_handler 的工作過程就是遍歷這個全局鏈表,根據二進制文件頭中攜帶的文件類型數據查找解析器。找到后調用解析器的函數對二進制文件進行加載。

//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;
}
//加載失敗繼續循環以嘗試加載
...
}
}
}

在上述代碼中的 list_for_each_entry 是在遍歷 formats 這個全局鏈表,遍歷時判斷每一個鏈表元素是否有 load_binary 函數。有的話就調用它嘗試加載。

回憶一下 3.1 注冊可執行文件加載程序,對于 ELF 文件加載器 elf_format 來說, load_binary 函數指針指向的是 load_elf_binary。

//file:fs/binfmt_elf.c
staticstructlinux_binfmtelf_format={
.module=THIS_MODULE,
.load_binary=load_elf_binary,
......
};

那么加載工作就會進入到 load_elf_binary 函數中來進行。這個函數很長,可以說所有的程序加載邏輯都在這個函數中體現了。我根據這個函數的主要工作,分成以下 5 個小部分來給大家介紹。

在介紹的過程中,為了表達清晰,我會稍微調一下源碼的位置,可能和內核源碼行數順序會有所不同。

4.1 ELF 文件頭讀取

在 load_elf_binary 中首先會讀取 ELF 文件頭。

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

文件頭中包含一些當前文件格式類型等數據,所以在讀取完文件頭后會進行一些合法性判斷。如果不合法,則退出返回。

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

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

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

4.2 Program Header 讀取

在 ELF 文件頭中記錄著 Program Header 的數量,而且在 ELF 頭之后緊接著就是 Program Header Tables。所以內核接下來可以將所有的 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數量
//再根據ProgramHeader大小sizeof(structelf_phdr)
//一起計算出所有的ProgramHeader大小,并讀取進來
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 清空父進程繼承來的資源

在 fork系統調用創建出來的進程中,包含了不少原進程的信息,如老的地址空間,信號表等等。這些在新的程序運行時并沒有什么用,所以需要清空處理一下。

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

具體工作包括初始化新進程的信號表,應用新的地址空間對象等。

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

//4.3清空父進程繼承來的資源
retval=flush_old_exec(bprm);
...

current->mm->start_stack=bprm->p;
}

在清空完父進程繼承來的資源后(當然也就使用上了新的 mm_struct 對象),這之后,直接將前面準備的進程棧的地址空間指針設置到了 mm 對象上。這樣將來棧就可以被使用了。

4.4 執行 Segment 加載

接下來,加載器會將 ELF 文件中的 LOAD 類型的 Segment 都加載到內存里來。使用 elf_map 在虛擬地址空間中為其分配虛擬內存。最后合適地設置虛擬地址空間 mm_struct 中的 start_code、end_code、start_data、end_data 等各個地址空間相關指針。

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

我們來看下具體的代碼:

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

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

//只加載類型為LOAD的Segment,否則跳過
if(elf_ppnt->p_type!=PT_LOAD)
continue;
...

//為Segment建立內存mmap,將程序文件中的內容映射到虛擬內存空間中
//這樣將來程序中的代碼、數據就都可以被訪問了
error=elf_map(bprm->file,load_bias+vaddr,elf_ppnt,
elf_prot,elf_flags,0);

//計算mm_struct所需要的各個成員地址
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 要加載到內存里的基地址。這個參數有這么幾種可能

值為 0,就是直接按照 ELF 文件中的地址在內存中進行映射

值為對齊到整數頁的開始,物理文件中可能為了可執行文件的大小足夠緊湊,而不考慮對齊的問題。但是操作系統在加載的時候為了運行效率,需要將 Segment 加載到整數頁的開始位置處。

4.5 數據內存申請&堆初始化

因為進程的數據段需要寫權限,所以需要使用 set_brk 系統調用專門為數據段申請虛擬內存。

//file:fs/binfmt_elf.c
staticintload_elf_binary(structlinux_binprm*bprm)
{
//4.1ELF文件頭解析
//4.2ProgramHeader讀取
//4.3清空父進程繼承來的資源
//4.4執行Segment加載過程
//4.5數據內存申請&堆初始化
retval=set_brk(elf_bss,elf_brk);
......
}

在 set_brk 函數中做了兩件事情:第一是為數據段申請虛擬內存,第二是將進程堆的開始指針和結束指針初始化一下。

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

//file:fs/binfmt_elf.c
staticintset_brk(unsignedlongstart,unsignedlongend)
{
//1.為數據段申請虛擬內存
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;
}

因為程序初始化的時候,堆上還是空的。所以堆指針初始化的時候,堆的開始地址 start_brk 和結束地址 brk 都設置成了同一個值。

4.6 跳轉到程序入口執行

在 ELF 文件頭中記錄了程序的入口地址。如果是非動態鏈接加載的情況,入口地址就是這個。

但是如果是動態鏈接,也就是說存在 INTERP 類型的 Segment,由這個動態鏈接器先來加載運行,然后再調回到程序的代碼入口地址。

#readelf--program-headershelloworld
......
ProgramHeaders:
TypeOffsetVirtAddrPhysAddr
FileSizMemSizFlagsAlign
INTERP0x00000000000002a80x00000000004002a80x00000000004002a8
0x000000000000001c0x000000000000001cR0x1
[Requestingprograminterpreter:/lib64/ld-linux-x86-64.so.2]

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

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

加載完成后再計算動態加載器的入口地址。這段代碼我展示在下面了,沒有耐心的同學可以跳過。反正只要知道這里是計算了一個程序的入口地址就可以了。

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

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

//第二次遍歷programheadertable,做些特殊處理
elf_ppnt=elf_phdata;
for(i=0;ielf_ex.e_phnum;i++,elf_ppnt++){
...
}

//如果程序中指定了動態鏈接器,就把動態鏈接器程序讀出來
if(elf_interpreter){
//加載并返回動態鏈接器代碼段地址
elf_entry=load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias);
//計算動態鏈接器入口地址
elf_entry+=loc->interp_elf_ex.e_entry;
}else{
elf_entry=loc->elf_ex.e_entry;
}

//跳轉到入口開始執行
start_thread(regs,elf_entry,bprm->p);
...
}

五、總結

看起來簡簡單單的一行 helloworld 代碼,但是要想把它運行過程理解清楚可卻需要非常深厚的內功的。

本文首先帶領大家認識和理解了二進制可運行 ELF 文件格式。在 ELF 文件中是由四部分組成,分別是 ELF 文件頭 (ELF header)、Program header table、Section 和 Section header table。

Linux 在初始化的時候,會將所有支持的加載器都注冊到一個全局鏈表中。對于 ELF 文件來說,它的加載器在內核中的定義為 elf_format,其二進制加載入口是 load_elf_binary 函數。

一般來說 shell 進程是通過 fork + execve 來加載并運行新進程的。執行 fork 系統調用的作用是創建一個新進程出來。不過 fork 創建出來的新進程的代碼、數據都還是和原來的 shell 進程的內容一模一樣。要想實現加載并運行另外一個程序,那還需要使用到 execve 系統調用。

在 execve 系統調用中,首先會申請一個 linux_binprm 對象。在初始化 linux_binprm 的過程中,會申請一個全新的 mm_struct 對象,準備留著給新進程使用。還會給新進程的棧準備一頁(4KB)的虛擬內存。還會讀取可執行文件的前 128 字節。

接下來就是調用 ELF 加載器的 load_elf_binary 函數進行實際的加載。大致會執行如下幾個步驟:

ELF 文件頭解析

Program Header 讀取

清空父進程繼承來的資源,使用新的 mm_struct 以及新的棧

執行 Segment 加載,將 ELF 文件中的 LOAD 類型的 Segment 都加載到虛擬內存中

為數據 Segment 申請內存,并將堆的起始指針進行初始化

最后計算并跳轉到程序入口執行

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

當用戶進程啟動起來以后,我們可以通過 proc 偽文件來查看進程中的各個 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]
......

雖然本文非常的長,但仍然其實只把大體的加載啟動過程串了一下。如果你日后在工作學習中遇到想搞清楚的問題,可以順著本文的思路去到源碼中尋找具體的問題,進而幫助你找到工作中的問題的解。

最后提一下,細心的讀者可能發現了,本文的實例中加載新程序運行的過程中其實有一些浪費,fork 系統調用首先將父進程的很多信息拷貝了一遍,而 execve 加載可執行程序的時候又是重新賦值的。所以在實際的 shell 程序中,一般使用的是 vfork。其工作原理基本和 fork 一致,但區別是會少拷貝一些在 execve 系統調用中用不到的信息,進而提高加載性能。

審核編輯:湯梓紅

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • Linux
    +關注

    關注

    87

    文章

    11326

    瀏覽量

    209961
  • 程序
    +關注

    關注

    117

    文章

    3793

    瀏覽量

    81215
  • 代碼
    +關注

    關注

    30

    文章

    4808

    瀏覽量

    68808

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

文章出處:【微信號:ExASIC,微信公眾號:ExASIC】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    MotorControl Workbench生成的代碼是開環的嗎,為什么電機跑起來很容易受到外力導致停機?

    請問各位高手 MotorControl Workbench 生成的代碼是開環的嗎?為什么我的電機跑起來很容易受到外力導致停機,我想讓它不停機,請問有什么好的辦法嗎 ?
    發表于 03-21 07:12

    [MsgOS]讓系統跑起來

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

    請問HVMotorCtrl+PfcKit_v1.7/HVPM_sensorless_2833x代碼能不能讓電機跑起來?需要修改哪些參數?

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

    請問stm32f103工程代碼如何在stm32f407芯片上跑起來

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

    如何讓的ESP32跑起來

    ESP32是了國內樂鑫科技推出的Wifi&藍牙物聯網MCU,而最近項目正好在用ESP32,所以我們今天就來分享下,如何讓的ESP32跑起來,并應用于更多實際項目。1ESP32簡...
    發表于 07-16 06:57

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

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

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

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

    如何利用XR806開發板讓hello跑起來

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

    如何讓u-boot跑起來

    如何讓u-boot跑起來
    發表于 01-26 08:26

    如何讓的ESP32跑起來

    ESP32是了國內樂鑫科技推出的Wifi&藍牙物聯網MCU,而最近項目正好在用ESP32,所以我們今天就來分享下,如何讓的ESP32跑起來,并應用于更多實際項目。1ESP32簡介ESP32
    發表于 02-10 06:25

    Zynq 7015 linux跑起來之導入之BOOT.bin生成詳解

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

    FreeRTOS_003 _讓系統在板子上跑起來

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

    windows安裝ubuntu并讓pioneer1應用程序跑起來的過程

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

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

    今天我們來思考一個簡單的問題,一個程序是如何在 Linux 上執行起來的? 我們就拿全宇宙最簡單的 Hello World 程序來舉例。
    的頭像 發表于 05-05 14:36 ?548次閱讀
    <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>的嗎(上)

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

    今天我們來思考一個簡單的問題,一個程序是如何在 Linux 上執行起來的? 我們就拿全宇宙最簡單的 Hello World 程序來舉例。
    的頭像 發表于 05-05 14:36 ?487次閱讀
    <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>的嗎(下)
    主站蜘蛛池模板: 日本特黄特色| 欧美黑人性色黄在线视频| 最色成人网| 国产伦精品一区二区三区女| 磁力bt种子搜索在线| 欧美成人三级伦在线观看| 亚洲国产婷婷综合在线精品| 一级在线观看| 永久黄网站色视频免费观看99| 中文成人在线| 国产精品久久久香蕉| xxxxxxxx日本69| 1024你懂的在线观看| 日韩亚洲欧洲在线rrrr片| 久久天天躁狠狠躁夜夜2020一| 午夜影院日韩| 日日夜夜操操| 午夜一区二区免费视频| 日本一本高清| 天天操精品| 天天色天天操天天射| 伊人网址| 人人爽天天爽夜夜爽曰| 操日韩| 看屁屁www视频免费观看| 夜恋秀场欧美成人影院| 仓本c仔国产精品| jzzjlzz亚洲乱熟在线播放| 国产精品性| 日韩欧免费一区二区三区| 国产稀缺精品盗摄盗拍| 国产日本特黄特色大片免费视频| 精品一级毛片| 狠狠色丁香婷婷综合欧美| 加勒比视频网站| 在线视频 亚洲| 国产免费高清视频在线观看不卡| 在线观看视频一区| 天天噜噜日日噜噜久久综合网| 天堂日本| 韩国在线免费视频|