1. 动态链接和静态链接

1.1. 编译命令

1aarch64-linux-gnu-gcc -o dummy_arm64 dummy.c
2aarch64-linux-gnu-gcc -static -o dummy_arm64_static dummy.c

1.2. 查看文件格式信息

1$ file dummy_arm64*
2dummy_arm64:        ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=3fa0f1369e1b3181f059174b078dd893e813abb2, for GNU/Linux 3.7.0, with debug_info, not stripped
3dummy_arm64_static: ELF 64-bit LSB executable, ARM aarch64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=5475ac20b174d94d96e666e170fb2d6ce69cdf76, for GNU/Linux 3.7.0, with debug_info, not stripped

1.3. 读取elf文件信息

可以看到动态链接的程序需要有解释器/lib64/ld-linux-x86-64.so.2,与file目录看到的是一样的。静态链接的程序没有解释器段。

 1$ readelf -l dummy_arm64
 2
 3Elf file type is EXEC (Executable file)
 4Entry point 0x4004c0
 5There are 9 program headers, starting at offset 64
 6
 7Program Headers:
 8  Type           Offset             VirtAddr           PhysAddr
 9                 FileSiz            MemSiz              Flags  Align
10  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
11                 0x00000000000001f8 0x00000000000001f8  R      0x8
12  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
13                 0x000000000000001b 0x000000000000001b  R      0x1
14      [Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
15  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
16                 0x00000000000006c8 0x00000000000006c8  R E    0x10000
17
18$ readelf -l dummy_arm64_static
19
20Elf file type is EXEC (Executable file)
21Entry point 0x400540
22There are 6 program headers, starting at offset 64
23
24Program Headers:
25  Type           Offset             VirtAddr           PhysAddr
26                 FileSiz            MemSiz              Flags  Align
27  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
28                 0x000000000007aa71 0x000000000007aa71  R E    0x10000
29  LOAD           0x000000000007b3b8 0x000000000048b3b8 0x000000000048b3b8
30                 0x0000000000002560 0x0000000000007ba8  RW     0x10000

1.4. 符号信息

aarch64-linux-gnu-nm -u dummy_arm64可以看到动态链接的程序由未定义的符号(用U标示),而静态链接的程序没有未定义的符号。

1$ aarch64-linux-gnu-nm -u dummy_arm64
2                 w __gmon_start__
3                 U __libc_start_main@GLIBC_2.34
4                 U abort@GLIBC_2.17

aarch64-linux-gnu-nm -n dummy_arm64看到的符号要比静态链接的程序少很多。

1$ aarch64-linux-gnu-nm -n dummy_arm64  | wc
2     32      93    1069
3
4$ aarch64-linux-gnu-nm -n dummy_arm64_static  | wc
5   1664    4992   59232

2. exec的内核实现

exec系统调用、1号进程的演化和内核调用用户态程序的umh最终都会调用到exec_binprmexec是一系列系统调用,对应的内核代码在fs/exec.c,其核心函数是exec_binprm

2.1. exec_binprm

exec过程中,主要涉及两个结构体:struct linux_binprmstruct linux_binfmt

struct linux_binprm包含了exec过程中需要用到的配置信息,其中也包括一些从用户态传递的信息。

struct linux_binfmtexec过程中用来表示不同可执行文件格式的结构体,它包含了如何解析和加载不同可执行文件格式的函数指针。

注意在alloc_bprm时,就已经通过bprm_mm_init为进程申请并初始化了struct mm_struct

2.2. search_binary_handler

search_binary_handler会通过prepare_binprm读取可执行文件的头部的BINPRM_BUF_SIZE(256)字节记录到struct linux_bprmbuf

search_binary_handler通过遍历formats链表中的struct linux_binfmt并尝试执行load_binaryload_binary会对buf进行检查,如果不匹配,则直接返回失败。search_binary_handler继续遍历,直到load_binary执行成功。

 1/// fs/exec.c
 2/* binfmt handlers will call back into begin_new_exec() on success. */
 3static int exec_binprm(struct linux_binprm *bprm)
 4{
 5	pid_t old_pid, old_vpid;
 6	int ret, depth;
 7
 8	/* Need to fetch pid before load_binary changes it */
 9	old_pid = current->pid;
10	rcu_read_lock();
11	old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
12	rcu_read_unlock();
13
14	/* This allows 4 levels of binfmt rewrites before failing hard. */
15	for (depth = 0;; depth++) {
16		struct file *exec;
17		if (depth > 5)
18			return -ELOOP;
19
20		ret = search_binary_handler(bprm);
21		if (ret < 0)
22			return ret;
23		if (!bprm->interpreter)
24			break;
25
26		exec = bprm->file;
27		bprm->file = bprm->interpreter;
28		bprm->interpreter = NULL;
29
30		allow_write_access(exec);
31		if (unlikely(bprm->have_execfd)) {
32			if (bprm->executable) {
33				fput(exec);
34				return -ENOEXEC;
35			}
36			bprm->executable = exec;
37		} else
38			fput(exec);
39	}
40
41	audit_bprm(bprm);
42	trace_sched_process_exec(current, old_pid, bprm);
43	ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
44	proc_exec_connector(current);
45	return 0;
46}

对于elf文件来说,exec_binprm会调用load_elf_binary来加载可执行文件。elf文件格式解析代码在fs/binfmt_elf.c

 1/// fs/binfmt_elf.c
 2static struct linux_binfmt elf_format = {
 3	.module		= THIS_MODULE,
 4	.load_binary	= load_elf_binary,
 5	.load_shlib	= load_elf_library,
 6#ifdef CONFIG_COREDUMP
 7	.core_dump	= elf_core_dump,
 8	.min_coredump	= ELF_EXEC_PAGESIZE,
 9#endif
10};

2.3. load_elf_binary

load_elf_binary比较长,其主要完成的工作如下:

  1. 对文件做一些检查,如magic、arch等。
  2. 解析elf文件头信息,查看是否有解释器段,如果有,打开解释器并记录到struct file *interpreter,并加载解释器的elf信息。
  3. 用户态可能是在多线程的一个子线程中调用了exec,需要清理其他线程,同时也要清理进程的一些其他资源,如文件、信号处理程序等
  4. 设置进程名
  5. 映射可执行文件一些必要的段。
  6. 设置进程的页表,包括加载程序段,加载解释器段等。
  7. 设置进程的栈,包括设置栈顶,设置栈保护等。
  8. 清空bss段。
  9. 修改当前进程的pt_regs
    1. 设置pstate为EL0。这样函数返回后,会切换至用户态。
    2. 配置进程的入口点,如果是动态链接程序,入口点是解释器elf文件的入口,如果是静态链接程序,入口点是可执行文件的入口。
 1+-- load_elf_binary
 2|   +-- load_elf_phdrs
 3|   +-- for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++)
 4|   |   +-- elf_read
 5|   |   +-- interpreter = open_exec(elf_interpreter)
 6|   +-- load_elf_phdrs      /// if (interpreter)
 7|   +-- parse_elf_properties(interpreter ?: bprm->file, ...)
 8|   +-- begin_new_exec
 9|   +-- SET_PERSONALITY2
10|   +-- setup_new_exec
11|   +-- setup_arg_pages
12|   +-- randomize_stack_top
13|   +-- set_brk
14|   +-- elf_map
15|   +-- load_elf_interp
16|   +-- set_binfmt
17|   +-- create_elf_tables
18|   +-- arch_randomize_brk
19|   +-- vm_mmap				/// if (current->personality & MMAP_PAGE_ZERO)
20|   +-- current_pt_regs
21|   +-- finalize_exec
22|   +-- START_THREAD(elf_ex, regs, elf_entry, bprm->p)