Linux内核如何装载和启动一个可执行程序
陈琛+ 原创作品转载请注明出处《Linux内核分析》MOOC课
1.理解编译链接的过程和ELF可执行文件格式
1)程序编译链接过程
编译->汇编->链接
1)预处理
gcc -E -o hello.cpp hello.c -m32
2)编译为汇编代码
gcc -x cpp-output -S -o hello.s hello.cpp -m32
3)汇编代码编译为目标代码
gcc -x assembler -c hello.s -o hello.o -m32
4)链接
gcc -o hello hello.o -m32
使用共享库的编译,libc, printf
5)
静态编译(所依赖的都放在hello.static内部)
gcc -o hello.static hello.o -m32 -static
2)ELF文件格式
a.out
COFF
PE
ELF(EXECUTABLE AND LINKABLE FORMAT)
三种目标文件:
- 可重定位文件 .o文件
- 可执行文件
- 共享目标文件 .so文件
用来被两个链接器链接:链接编辑器,可以和其他可重定位和共享文件object来创建其他object(静态链接); 动态链接器,联合一个可执行文件和其他共享object文件来创建一个进程映像 .
ELF文件加载内存(静态链接,所有代码放在一个段),形成进程,默认是加载到以0x8048000开始处.
gcc -shared shlibexample.c -o libshlibexample.so -m32
gcc -shared dllibexample.c -o libdllibexample.so -m32
gcc main.c -o main -L$PWD -lshlibexample -ldl -m32 //-ldl动态加载库
export LD_LIBRARY_PATH=$PWD //当前目录加入到库搜索路径
2.编程使用exec*库函数加载一个可执行文件
可执行文件的装载
- 执行一个程序的shell环境,直接使用execve系统调用
- shell不限制命令行个数,取决于命令本身
- shell调用execve将命令行参数和环境参数传递给可执行程序的main函数
- int execve(const char *filename, char * const argv[], char * const envp[])
- 库函数exec*是系统调用execve的封装例程
- sys_execve会解析可执行文件格式
- do_execve -> do_execve_common -> exec_binprm
- search_binary_handler符合寻找文件格式对应的解析模块(根据文件头部信息寻找对应的文件格式处理模块)
list_for_each_entry(fmt, &formats, lh) { if (!try_module_get(fmt->module)) continue; read_unlock(&binfmt_lock); bprm->recursion_depth++; retval = fmt->load_binary(bprm);//解析elf文件格式的执行位置 read_lock(&binfmt_lock);
- 对于ELF格式的可执行文件fmt->load_binary(bprm);执行的应该是load_elf_binary其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读
- Linux内核是如何支持多种不同的可执行文件格式的?
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, }; static int __init init_elf_binfmt(void) { register_binfmt(&elf_format); return 0; }
- 可执行文件开始执行的起点在哪里?如何才能让execve系统调用返回到用户态时执行新程序?
- 庄生梦蝶 —— 醒来迷惑是庄周梦见了蝴蝶还是蝴蝶梦见了庄周?
- 庄周(调用execve的可执行程序)入睡(调用execve陷入内核),醒来(系统调用execve返回用户态)发现自己是蝴蝶(被execve加载的可执行程序)
- 修改int 0x80压入内核堆栈的EIP
- load_elf_binary -> start_thread//通过修改内核堆栈中EIP的值作为新程序的起点
- 动态链接的过程内核做了什么?可执行文件依赖的动态链接库(共享库)是由谁负责加载以及如何递归加载的?
execve 处理过程
linux/fs/exec.c
SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
return do_execve(getname(filename), argv, envp);
}
int do_execve(struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp) //__user 用户态指针
{
struct user_arg_ptr argv = { .ptr.native = __argv };//命令行参数变成结构
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execve_common(filename, argv, envp);
}
static int do_execve_common(struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp)
{
struct linux_binprm *bprm; //保存要执行的文件相关的信息(include/linux/binfmts.h)
...
file = do_open_exec(filename);//打开执行的可执行文件
//填充bprm结构
bprm->file = file;
bprm->filename = bprm->interp = filename->name;
...
retval = copy_strings(bprm->argc, argv, bprm);//命令行参数和环境变量copy到结构体里
retval = exec_binprm(bprm);//
}
1405 static int exec_binprm(struct linux_binprm *bprm)
1406 {
1407 pid_t old_pid, old_vpid;
1408 int ret;
1409
1410 /* Need to fetch pid before load_binary changes it */
1411 old_pid = current->pid;
1412 rcu_read_lock();
1413 old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
1414 rcu_read_unlock();
1415
1416 ret = search_binary_handler(bprm);//寻找可执行文件的处理函数
1417 if (ret >= 0) {
1418 audit_bprm(bprm);
1419 trace_sched_process_exec(current, old_pid, bprm);
1420 ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
1421 proc_exec_connector(current);
1422 }
1423
1424 return ret;
1425 }
/*
1350 * cycle the list of binary formats handler, until one recognizes the image
1351 */
1352 int search_binary_handler(struct linux_binprm *bprm)
1353 {
struct linux_binfmt *fmt;
...
//循环寻找能够解析当前可执行文件的代码
1369 list_for_each_entry(fmt, &formats, lh) {
1370 if (!try_module_get(fmt->module))
1371 continue;
1372 read_unlock(&binfmt_lock);
1373 bprm->recursion_depth++;
1374 retval = fmt->load_binary(bprm);//加载可执行文件的处理函数,函数指针,实际调用load_elf_binary(linux/fs/binfmt_elf.c)
1375 read_lock(&binfmt_lock);
1376 put_binfmt(fmt);
1377 bprm->recursion_depth--;
1378 if (retval < 0 && !bprm->mm) {
1379 /* we got to flush_old_exec() and failed after it */
1380 read_unlock(&binfmt_lock);
1381 force_sigsegv(SIGSEGV, current);
1382 return retval;
1383 }
1384 if (retval != -ENOEXEC || !bprm->file) {
1385 read_unlock(&binfmt_lock);
1386 return retval;
1387 }
1388 }
1389 read_unlock(&binfmt_lock);
...
}
// include/linux/binfmts.h
14 struct linux_binprm {
15 char buf[BINPRM_BUF_SIZE];
16 #ifdef CONFIG_MMU
17 struct vm_area_struct *vma;
18 unsigned long vma_pages;
19 #else
20 # define MAX_ARG_PAGES 32
21 struct page *page[MAX_ARG_PAGES];
22 #endif
23 struct mm_struct *mm;
24 unsigned long p; /* current top of mem */
25 unsigned int
26 cred_prepared:1,/* true if creds already prepared (multiple
27 * preps happen for interpreters) */
28 cap_effective:1;/* true if has elevated effective capabilities,
29 * false if not; except for init which inherits
30 * its parent's caps anyway */
31 #ifdef __alpha__
32 unsigned int taso:1;
33 #endif
34 unsigned int recursion_depth; /* only for search_binary_handler() */
35 struct file * file;
36 struct cred *cred; /* new credentials */
37 int unsafe; /* how unsafe this exec is (mask of LSM_UNSAFE_*) */
38 unsigned int per_clear; /* bits to clear in current->personality */
39 int argc, envc;
40 const char * filename; /* Name of binary as seen by procps */
41 const char * interp; /* Name of the binary really executed. Most
42 of the time same as filename, but could be
43 different for binfmt_{misc,script} */
44 unsigned interp_flags;
45 unsigned interp_data;
46 unsigned long loader, exec;
47 };
//include/linux/binfmts.h
//linux_binfmt
66 /*
67 * This structure defines the functions that are used to load the binary formats that
68 * linux accepts.
69 */
70 struct linux_binfmt {
71 struct list_head lh;
72 struct module *module;
73 int (*load_binary)(struct linux_binprm *);
74 int (*load_shlib)(struct file *);
75 int (*core_dump)(struct coredump_params *cprm);
76 unsigned long min_coredump; /* minimal dump size */
77 };
//linux/fs/binfmt_elf.c
//结构体变量, 赋值(加载到一个链表里面,观察者模式)
82 static struct linux_binfmt elf_format = {
83 .module = THIS_MODULE,
84 .load_binary = load_elf_binary,
85 .load_shlib = load_elf_library,
86 .core_dump = elf_core_dump,
87 .min_coredump = ELF_EXEC_PAGESIZE,
88 };
2198 static int __init init_elf_binfmt(void)
2199 {
2200 register_binfmt(&elf_format); //efl_format注册到一个结构体链表里
2201 return 0;
2202 }
//按照elf格式严格解析elf文件.将文件映射到进程空间
//对于elf格式文件总被映射到0x8048000地址
//需要动态链接的可执行文件先加载连接器ld
571 static int load_elf_binary(struct linux_binprm *bprm)
572 {
...
626 retval = kernel_read(bprm->file, loc->elf_ex.e_phoff,
627 (char *)elf_phdata, size);
975 start_thread(regs, elf_entry, bprm->p);
...
}
3 使用gdb跟踪sys_execve内核函数的处理过程
git clone https://github.com/mengning/menu.git
mv test_exec.c test.c
make rootfs
qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img -S -s
// new terminal
gdb
file ../linux-3.18.6/vmlinux
target remote:1234
//断点
b sys_execve
b load_elf_binary
b start_thread
// 执行
c
//列出断点代码
list
//单步
s
//next
n
主要代码:shell(庄子)执行过程中加载了hello(蝴蝶)程序:
new_ip是返回到用户态的第一条指令的地址
po new_ip
readelf -h hello
可执行文件hello的elf信息显示入口地址为0x8040d0a,而new_ip的值正好为这个地址
动态链接库文件的加载呢?
依赖动态连接器ld解析这个elf文件
// linux/fs/binfmt_elf.c
// static int load_elf_binary(struct linux_binprm *bprm)
...
if(elf_interpreter){
unsigned long inter_map_addr = 0;
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias);
}
...
elf_entry返回的 不再是elf文件的入口地址,而是动态链接器的起点.
动态链接库的装载过程是一个图的广度遍历,装载和链接之后ld将CPU的控制权交给可执行程序。
可执行程序加载的两种情况:
-
静态链接库
直接进入可执行程序入口;
-
动态链接
由ld动态链接,加载完成,控制权移交给可执行程序入口。所以动态加载不是由内核完成的,而是由ld(libc,用户态)完成