就去射 你写的代码是何如跑起来的
本文来自微信公众号:设备内功修都 (ID:kfngxl)就去射,作家:张彦飞 allen
全国好,我是飞哥!
今天咱们来想考一个简便的问题,一个法度是如安在 Linux 上践诺起来的?
咱们就拿全天地最简便的 Hello World 法度来例如。
#include <stdio.h> int main() { printf("Hello, World!\n"); return 0; }
咱们在写完代码后,进行简便的编译,然后在 shell 号召行下就不错把它启动起来。
# gcc main.c -o helloworld # ./helloworld Hello, World!
那么在编译启动运行的经过中都发生了哪些事情了呢?今天就让咱们来深远地了解一下。
一、交融可践诺文献格式源代码在编译后会生成一个可践诺程小序件,咱们先来了解一下编译后的二进制文献是什么神态的。
咱们领先使用 file 号召稽查一下这个文献的格式。
# file helloworld helloworld: ELF 64-bit LSB executable, x86-64, version 1 (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。
接下来咱们分几个末节挨个先容一下。
1.1 ELF 文献头ELF 文献头记载了整个文献的属性信息。原始二进制特地未便于不雅察。不外咱们有趁手的器具 - readelf,这个器具不错帮咱们稽查 ELF 文献中的各式信息。
咱们先来看一下编译出来的可践诺文献的 ELF 文献头,使用 --file-header (-h) 选项即可稽查。
# 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 文献头包含了现时可践诺文献的摘抄信息,我把其中要津的几个拿出来给全国讲解一下。
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,以便捷操作系统更快速地加载。
由于 Segment 和 Section 翻译成中语的话,理由太接近了,特地不利于交融。是以本文中我就平直使用 Segment 和 Section 原汁原味的见地,而不是将它们翻译成段无意是节,这样太容易让东谈主混浊了。
Program headers table 即是算作通盘 Segments 的头信息,用来面目通盘的 Segments 的。。
使用 readelf 器具的 --program-headers(-l)选项不错领会稽查到这块区域里存储的内容。
# 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
上头的阻隔显现统统有 11 个 program headers。
关于每一个段,输出了 Offset、VirtAddr 等面目现时段的信息。Offset 暗示现时段在二进制文献中的脱手位置,FileSiz 暗示现时段的大小。Flag 暗示现时的段的权限类型,R 暗示可读、E 暗示可践诺、W 暗示可写。
在最底下,还把每个段是由哪几个 Section 构成的给展示了出来,比如 03 号段是由“.init .plt .text .fini” 四个 Section 构成的。
1.3 Section Header Table和 Program Header Table 不相同的是,Section header table 平直面目每一个 Section。这二者面目的其实都是各式 Section ,只不外宗旨不同,一个针对加载,一个针对通顺。
使用 readelf 器具的 --section-headers (-S)选项不错领会稽查到这块区域里存储的内容就去射。
# 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)
阻隔显现,该文献统统有 30 个 Sections,每一个 Section 在二进制文献中的位置通过 Offset 列暗示了出来。Section 的大小通过 Size 列体现。
在这 30 个 Section 中,每一个都有独到的作用。咱们编写的代码在编译成二进制提醒后都会放到 .text 这个 Section 中。另外咱们看到 .text 段的 Address 列显现的地址是 0000000000401040。回忆前边咱们在 ELF 文献头中看到 Entry point address 显现的进口地址为 0x401040。这证据,法度的进口地址即是 .text 段的地址。
另外还有两个值得关心的 Section 是 .data 和 .bss。代码中的全局变量数据在编译后将在在这两个 Section 中占据一些位置。如下简便代码所示。
//未运动荡的内存区域位于 .bss 段 int data1 ; //还是运动荡的内存区域位于 .data 段 int data2 = 100 ; //代码位于 .text 段 int main(void) { }1.4 进口进一步稽查
接下来,咱们想再稽查一下咱们前边提到的法度进口 0x401040,望望它到底是啥。咱们此次再借助 nm 号召来进一步稽查一下可践诺文献中的标记过火地址信息。-n 选项的作用是显现的标记以地址排序,而不是称号排序。
# 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 函数的地址,在这个函数践诺一些运动荡的操作之后,咱们的进口函数 main 将会被调用到,它位于 0x401126 地址处。
二、用户程度的创建经过综合在咱们编写的代码编译完生成可践诺法度之后,下一步即是使用 shell 把它加载起来并运行之。一般来说 shell 程度是通过 fork+execve 来加载并运行新程度的。一个简便加载 helloworld 号召的 shell 中枢逻辑是如下这个经过。
// shell 代码示例 int main(int argc, char * argv[]) { pid = fork(); if (pid==0){ // 要是是在程度中 //使用 exec 系列函数加载并运行可践诺文献 execve("helloworld", argv, envp); } else { } }
shell 程度先通过 fork 系统调用创建一个程度出来。然后在子程度中调用 execve 将践诺的程小序件加载起来,然后就不错调到程小序件的运行进口处运行这个法度了。
这个 fork 系统调用在内核进口是在 kernel / fork.c 下。
//file:kernel/fork.c SYSCALL_DEFINE0(fork) { return do_fork(SIGCHLD, 0, 0, NULL, NULL); }
在 do_fork 的罢了中,中枢是一个 copy_process 函数,它以拷贝父程度(线程)的方式来生成一个新的 task_struct 出来。
//file:kernel/fork.c long do_fork() { //复制一个 task_struct 出来 struct task_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 static struct task_struct *copy_process() { //复制程度 task_struct 结构体 struct task_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 双向链表在内存中的结构如下图所示。
咱们就以 ELF 的加载器 elf_format 为例,来望望这个加载器是何如注册的。在 Linux 中每一个加载器都用一个 linux_binfmt 结构来暗示。其中规矩了加载二进制可践诺文献的 load_binary 函数指针,以及加载崩溃文献 的 core_dump 函数等。其完好意思界说如下
//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 中规矩了具体的加载函数,例如 load_binary 成员指向的即是具体的 load_elf_binary 函数。这即是 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 会在运动荡的时候通过 register_binfmt 进行注册。
//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 操作系统撑握的格式的加载法度。
# 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, 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 系统调用到了 do_execve_common 函数。咱们来看这个函数的罢了。
//file:fs/exec.c static int do_execve_common(const char *filename, ) { //linux_binprm 结构用于保存加载二进制文献时使用的参数 struct linux_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 对象的具体使命不错用下图来暗示。
在这个函数中,完成了一下三块使命。
第一、使用 kzalloc 恳求 linux_binprm 内查对象。该内查对象用于保存加载二进制文献时使用的参数。在恳求完后,对该参数对象进行各式运动荡。
第二、在 bprm_mm_init 中会恳求一个全新的 mm_struct 对象,准备留着给新程度使用。
第三、给新程度的栈恳求一页的诬捏内存空间,偷拍偷窥并将栈指针记载下来。
第四、读取二进制文献头 128 字节。
咱们来看下运动荡栈的联系代码。
//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 *); }
在上头这个函数中恳求了一个 vma 对象(暗示诬捏地址空间里的一段限度),vm_end 指向了 STACK_TOP_MAX(地址空间的顶部隔邻的位置),vm_start 和 vm_end 之间留了一个 Page 大小。也即是说默许给栈恳求了 4KB 的大小。临了把栈的指针记载到 bprm->p 中。
另外再看下 prepare_binprm,在这个函数中,从文献头部读取了 128 字节。之是以这样干,是为了读取二进制文献头为了便捷后头判断其文献类型。
//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); }
在恳求并运动荡 brm 对象值完后,临了使用 search_binary_handler 函数遍历系统中已注册的加载器,尝试对现时可践诺文献进行领会并加载。
在 3.1 节咱们先容了系统通盘的加载器都注册到了 formats 全局链内外了。函数 search_binary_handler 的使命经过即是遍历这个全局链表,凭证二进制文献头中佩戴的文献类型数据查找领会器。找到后调用领会器的函数对二进制文献进行加载。
//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; } //加载失败链接轮回以尝试加载 } } }
在上述代码中的 list_for_each_entry 是在遍历 formats 这个全局链表,遍历时判断每一个链表元素是否有 load_binary 函数。有的话就调用它尝试加载。
回忆一下 3.1 注册可践诺文献加载法度,关于 ELF 文献加载器 elf_format 来说,load_binary 函数指针指向的是 load_elf_binary。
//file:fs/binfmt_elf.c static struct linux_binfmt elf_format = { .module = THIS_MODULE, .load_binary = load_elf_binary, };
那么加载使命就会干预到 load_elf_binary 函数中来进行。这个函数很长,不错说通盘的法度加载逻辑都在这个函数中体现了。我凭证这个函数的主要使命,分红以下 5 个小部分来给全国先容。
在先容的经过中,为了抒发泄露,我会略微调一下源码的位置,可能和内核源码行数限定会有所不同。
4.1 ELF 文献头读取在 load_elf_binary 中领先会读取 ELF 文献头。
文献头中包含一些现时文献格式类型等数据,是以在读取完文献头后会进行一些正当性判断。要是分歧法,则退出复返。
//file:fs/binfmt_elf.c static int load_elf_binary(struct linux_binprm *bprm) { //4.1 ELF 文献头领会 //界说结构题并恳求内存用来保存 ELF 文献头 struct { struct elfhdr elf_ex; struct elfhdr interp_elf_ex; } *loc; loc = kmalloc(sizeof(*loc), GFP_KERNEL); //得回二进制头 loc-elf_ex = *((struct elfhdr *)bprm-buf); //寇仇部进行一系列的正当性判断,分歧法例平直退出 if (loc-elf_ex.e_type != ET_EXEC && ){ goto out; } }4.2 Program Header 读取
在 ELF 文献头中记载着 Program Header 的数目,并且在 ELF 头之后紧接着即是 Program Header Tables。是以内核接下来不错将通盘的 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 数目 // 再凭证 Program Header 大小 sizeof(struct elf_phdr) // 整个经营出通盘的 Program Header 大小,并读取进来 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 清空父程度接受来的资源
在 fork 系统调用创建出来的程度中,包含了不少原程度的信息,如老的地址空间,信号表等等。这些在新的法度运行时并莫得什么用,是以需要清空处置一下。
具体使命包括运动荡新程度的信号表,欺骗新的地址空间对象等。
//file:fs/binfmt_elf.c static int load_elf_binary(struct linux_binprm *bprm) { //4.1 ELF 文献头领会 //4.2 Program Header 读取 //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 等各个地址空间联系指针。
咱们来看下具体的代码:
//file:fs/binfmt_elf.c static int load_elf_binary(struct linux_binprm *bprm) { //4.1 ELF 文献头领会 //4.2 Program Header 读取 //4.3 清空父程度接受来的资源 //4.4 践诺 Segment 加载经过 //遍历可践诺文献的 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 建筑内存 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 static int load_elf_binary(struct linux_binprm *bprm) { //4.1 ELF 文献头领会 //4.2 Program Header 读取 //4.3 清空父程度接受来的资源 //4.4 践诺 Segment 加载经过 //4.5 数据内存恳求&运动荡 retval = set_brk(elf_bss, elf_brk); }
在 set_brk 函数中作念了两件事情:第一是为数据段恳求诬捏内存,第二是将程度堆的脱手指针和阻隔指针运动荡一下。
//file:fs/binfmt_elf.c static int set_brk(unsigned long start, unsigned long end) { //1为数据段恳求诬捏内存 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; }
因为法度运动荡的时候,堆上照旧空的。是以堆指针运动荡的时候,堆的脱手地址 start_brk 和阻隔地址 brk 都缔造成了吞并个值。
4.6 跳转到法度进口践诺在 ELF 文献头中记载了法度的进口地址。要是长短动态通顺加载的情况,进口地址即是这个。
但是要是是动态通顺,也即是说存在 INTERP 类型的 Segment,由这个动态通顺器先来加载运行,然后再召回到法度的代码进口地址。
# 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]
关于是动态加载器类型的,需要先将动态加载器(本文示例中是 ld-linux-x86-64.so.2 文献)加载到地址空间中来。
加载完成后再经营动态加载器的进口地址。这段代码我展示鄙人面了,莫得耐烦的同学不错跳过。归正独一知谈这里是经营了一个法度的进口地址就不错了。
//file:fs/binfmt_elf.c static int load_elf_binary(struct linux_binprm *bprm) { //4.1 ELF 文献头领会 //4.2 Program Header 读取 //4.3 清空父程度接受来的资源 //4.4 践诺 Segment 加载 //4.5 数据内存恳求&堆运动荡 //4.6 跳转到法度进口践诺 //第一次遍历 program header table //只针对 PT_INTERP 类型的 segment 作念个预处置 //这个 segment 中保存着动态加载器在文献系统中的旅途信息 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++){ ... } //要是法度中指定了动态通顺器,就把动态通顺器法度读出来 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 恳求内存,并将堆的肇始指针进走运动荡
临了经营并跳转到法度进口践诺
当用户程度启动起来以后,咱们不错通过 proc 伪文献来稽查程度中的各个 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] ......
天然本文特地的长,但仍然其实只把大体的加载启动经过串了一下。要是你日后在使命学习中遭受想搞了了的问题,不错顺着本文的想路去到源码中寻找具体的问题,进而匡助你找到使命中的问题的解。
临了提一下,夺宗旨读者可能发现了,本文的实例中加载新法度运行的经过中其实有一些滥用,fork 系统调用领先将父程度的许多信息拷贝了一遍,而 execve 加载可践诺法度的时候又是再行赋值的。是以在本色的 shell 法度中,一般使用的是 vfork。其使命旨趣基本和 fork 一致,但区别是会少拷贝一些在 execve 系统调用顶用不到的信息,进而擢升加载性能。
告白声明:文内含有的对外跳转通顺(包括不限于超通顺、二维码、口令等模式)就去射,用于传递更多信息,节俭甄选期间,阻隔仅供参考,IT之家通盘著作均包含本声明。