关键词:fork、wait、execve、elf、ld.so、stack、heap等等。
本文着重分析一个进程从shell中输入,到执行起来后如何一步步形成maps地址空间的。
下面以Busybox为例,简单分析一个进程地址空间形成过程:从shell输入命令,到shell调用execve()进入内核加载二进制可执行文件和ld.so,再到ld.so到用户空间查找并加载动态库。最终将执行权交给可执行文件运行。
00008000-00009000 r-xp 00000000 b3:03 15 /root/data/maps------------------------进程可执行文件首地址如何确定?ELF文件中规定。 00009000-0000a000 r--p 00000000 b3:03 15 /root/data/maps 0000a000-0000b000 rw-p 00001000 b3:03 15 /root/data/maps 0000b000-0002c000 rwxp 00000000 00:00 0 [heap]---------------------------------确定heap低地址是heap的起始地址,紧跟可执行文件的bss段。 10000000-1001d000 r-xp 00000000 00:01 5918 /lib/ld-2.28.9000.so-------------------动态加载库地址如何确定?内核mmap区域的起始地址加载ld.so。 1001d000-1001e000 r--p 0001c000 00:01 5918 /lib/ld-2.28.9000.so 1001e000-1001f000 rw-p 0001d000 00:01 5918 /lib/ld-2.28.9000.so 1001f000-10020000 r-xp 00000000 00:00 0 [vdso] 10020000-1014a000 r-xp 00000000 00:01 5910 /lib/libc-2.28.9000.so-----------------由ld.so加载的库文件,地址紧挨ld.so和[vdso]。 1014a000-1014b000 ---p 0012a000 00:01 5910 /lib/libc-2.28.9000.so 1014b000-1014d000 r--p 0012a000 00:01 5910 /lib/libc-2.28.9000.so 1014d000-1014e000 rw-p 0012c000 00:01 5910 /lib/libc-2.28.9000.so 1014e000-10153000 rw-p 00000000 00:00 0 7f9f2000-7fa13000 rwxp 00000000 00:00 0 [stack]--------------------------------确定栈高地址是栈的起始地址。
1. shell执行进程
Busybox的shell入口为ash_main()函数,最终进入cmdloop()循环读取解析命令,然后执行。
static int cmdloop(int top) { ...
for (;;) {
... n = parsecmd(inter);------------------------------------解析命令,解析结果放在全局变量中,返回命令类型。 #if DEBUG if (DEBUG > 2 && debug && (n != NODE_EOF)) showtree(n); #endif if (n == NODE_EOF) { ... } else if (nflag == 0) { int i; /* job_warning can only be 2,1,0. Here 2->1, 1/0->0 */ job_warning >>= 1; numeof = 0; i = evaltree(n, 0);---------------------------------评估解析的命令。 if (n) status = i; } ... } return status; } static int evaltree(union node *n, int flags) { int checkexit = 0; int (*evalfn)(union node *, int); int status = 0; if (n == NULL) { TRACE(("evaltree(NULL) called ")); goto out; } TRACE(("evaltree(%p: %d, %d) called ", n, n->type, flags)); dotrap(); switch (n->type) { default: ... case NCMD: evalfn = evalcommand;-------------------------如果是命令,则使用evalcommand()进行处理。 checkexit: if (eflag && !(flags & EV_TESTED)) checkexit = ~0; goto calleval; case NFOR: ... evalfn = evaltree; calleval: status = evalfn(n, flags); goto setstatus; } case NIF: ... setstatus: exitstatus = status; break; } out: ... return exitstatus; }
evalcommand()创建一个子进程用于执行命令,父进程进行wait等处理。
子进程调用shellexec()执行命令,最终调用execve()系统调用。
static int evalcommand(union node *cmd, int flags) { ... /* Execute the command. */ switch (cmdentry.cmdtype) { default: { ... if (!(flags & EV_EXIT) || may_have_traps) { /* No, forking off a child is necessary */ INT_OFF; get_tty_state(); jp = makejob(/*cmd,*/ 1); if (forkshell(jp, cmd, FORK_FG) != 0) {--------------fork()子进程。 /* parent */ status = waitforjob(jp);-------------------------如果是父进程则在此wait()。 INT_ON; TRACE(("forked child exited with %d ", status)); break; } /* child */ FORCE_INT_ON; /* fall through to exec'ing external program */ } listsetvar(varlist.list, VEXPORT|VSTACK); shellexec(argv[0], argv, path, cmdentry.u.index);-----------执行argv[0]命令。 /* NOTREACHED */ } /* default */ case CMDBUILTIN: ... } /* switch */ ... return status; } static void shellexec(char *prog, char **argv, const char *path, int idx) NORETURN; static void shellexec(char *prog, char **argv, const char *path, int idx) { ... envp = listvars(VEXPORT, VUNSET, /*end:*/ NULL); if (strchr(prog, '/') != NULL #if ENABLE_FEATURE_SH_STANDALONE || (applet_no = find_applet_by_name(prog)) >= 0 #endif ) { tryexec(IF_FEATURE_SH_STANDALONE(applet_no,) prog, argv, envp); if (applet_no >= 0) { goto try_PATH; } e = errno; } else { try_PATH: e = ENOENT; while ((cmdname = path_advance(&path, prog)) != NULL) { if (--idx < 0 && pathopt == NULL) { tryexec(IF_FEATURE_SH_STANDALONE(-1,) cmdname, argv, envp); if (errno != ENOENT && errno != ENOTDIR) e = errno; } stunalloc(cmdname); } } .. } static void tryexec(IF_FEATURE_SH_STANDALONE(int applet_no,) char *cmd, char **argv, char **envp) { ... repeat: #ifdef SYSV do { execve(cmd, argv, envp); } while (errno == EINTR); #else execve(cmd, argv, envp); #endif ... }
所以执行命令最终核心是系统调用execve()。
2. execve()内核中运行可执行程序
execve()系统调用在内核的入口是do_execve()。
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) { struct user_arg_ptr argv = { .ptr.native = __argv }; struct user_arg_ptr envp = { .ptr.native = __envp }; return do_execveat_common(AT_FDCWD, filename, argv, envp, 0); }
do_evecveat_common()在运行可执行二进制文件前,为即将创建的进程创建内存空间、寻找合适CPU、程序名、环境变量等等信息。
核心是exec_binprm(),它用于加载可执行文件、并跳转到ld.so进行动态库加载工作。
不同格式的可执行程序通过search_binary_handler()找到合适的load_binary()进行处理。
static int do_execveat_common(int fd, struct filename *filename, struct user_arg_ptr argv, struct user_arg_ptr envp, int flags) { char *pathbuf = NULL; struct linux_binprm *bprm; struct file *file; struct files_struct *displaced; int retval; if (IS_ERR(filename)) return PTR_ERR(filename); if ((current->flags & PF_NPROC_EXCEEDED) && atomic_read(¤t_user()->processes) > rlimit(RLIMIT_NPROC)) { retval = -EAGAIN; goto out_ret; } /* We're below the limit (still or again), so we don't want to make * further execve() calls fail. */ current->flags &= ~PF_NPROC_EXCEEDED; retval = unshare_files(&displaced);-------------------------------为进程复制一份文件表。 if (retval) goto out_ret; retval = -ENOMEM; bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);------------------------分配一个struct linux_binprm结构体。 if (!bprm) goto out_files; retval = prepare_bprm_creds(bprm); if (retval) goto out_free; check_unsafe_exec(bprm); current->in_execve = 1; file = do_open_execat(fd, filename, flags);-----------------------打开可执行文件。 retval = PTR_ERR(file); if (IS_ERR(file)) goto out_unmark; sched_exec();-----------------------------------------------------找到合适的CPU来执行任务。 bprm->file = file;------------------------------------------------填充struct linux_binprm结构体file、filename、interp成员。 if (fd == AT_FDCWD || filename->name[0] == '/') { bprm->filename = filename->name; } else { if (filename->name[0] == '