exec系列函数主要实现装入新的可执行文件或脚本镜像,并执行;调用后不再返回,而是跳转到新镜像的入口去执行。
exec在linux上均是对execve系统调用的封装,除了下述内容外,进程其它内容均保持不变:
1.将设置了处理函数的信号handler,重置为默认SIG_DFL
2.内存映射,mmap
3.SysV共享内存,shmat
4.POSIX共享内存,shm_open
5.POSIX消息队列,mq_overview
6.POSIX信号量,sem_overview
7.打开的目录,open_dir
8.内存锁,mlock/mlockall
9.exit处理,atexit/on_exit
10.浮点数环境,fenv
等
execve的主要作用为:
1.分配进程新的地址空间,将环境变量、main参数等拷贝到新地址空间的推栈中;
2.解析可执行文件,将代码、数据装入/映射到内存
3.进程新环境的设置,如关闭设置FD_CLOEXEC的文件等
4.设置execve函数返回到用户态时的执行地址;解析器入口地址或程序的入口地址
I.do_execve
execve系统调用是通过do_execve实现的,其处理流程如下:
1.进程分配并使用自己的文件描述符表,不再使用共享的文件描述符表
2.分配进程新的地址空间mm_struct,并复制老mm_struct的context,并分配一页大小的堆栈内存区。
3.打开可执行文件;检查可执行文件权限,并预读可执行文件的前BINPRM_BUF_SIZE=128字节数据到buf中
4.计算环境变量、main参数个数;并将这些参数复制到新地址空间的堆栈中
5.根据可执行文件的格式,查找对应的handler作后续处理;
do_execve的实现fs/exec.c:
1342 /*
1343 * sys_execve() executes a new program.
1344 */
1345 int do_execve(char * filename,
1346 char __user *__user *argv,
1347 char __user *__user *envp,
1348 struct pt_regs * regs)
1349 {
1355
1356 retval = unshare_files(&displaced);
1357 if (retval)
1358 goto out_ret;
1359
1375 file = open_exec(filename);
1376 retval = PTR_ERR(file);
1377 if (IS_ERR(file))
1378 goto out_unmark;
1379
1385
1386 retval = bprm_mm_init(bprm);
1387 if (retval)
1388 goto out_file;
1389
1390 bprm->argc = count(argv, MAX_ARG_STRINGS);
1391 if ((retval = bprm->argc) < 0)
1392 goto out;
1393
1394 bprm->envc = count(envp, MAX_ARG_STRINGS);
1395 if ((retval = bprm->envc) < 0)
1396 goto out;
1397
1398 retval = prepare_binprm(bprm);
1399 if (retval < 0)
1400 goto out;
1401
1402 retval = copy_strings_kernel(1, &bprm->filename, bprm);
1403 if (retval < 0)
1404 goto out;
1405
1406 bprm->exec = bprm->p;
1407 retval = copy_strings(bprm->envc, envp, bprm);
1408 if (retval < 0)
1409 goto out;
1410
1411 retval = copy_strings(bprm->argc, argv, bprm);
1412 if (retval < 0)
1413 goto out;
1414
1415 current->flags &= ~PF_KTHREAD;
1416 retval = search_binary_handler(bprm,regs);
1417 if (retval < 0)
1418 goto out;
1419
1453 return retval;
1454 }
字符串复制copy_strings函数:
1、将字符串从当前地址空间复制到新地址空间的堆栈中;
2、复制字符串时,使用的仍是老地址空间页表,所以不能使用新地址空间的堆栈地址直接复制,必须映射堆栈页帧到内核空间后再做复制操作。
3、由于字符串可能跨页,一个字符串可能存在两次页帧映射与复制
II.ELF格式文件处理
ELF格式文件处理主要对ELF文件进行解析,将可执行程序的代码及数据装载到内存中;并根据ELF文件信息,设置进程的环境
ELF格式:
ELF的管理信息主要由三部分组成:ELF头、程序头(程序段)表、节区表;一个段的地址空间通常包括一个或多个节区。
如数据段包括数据节区、BSS节区等
linux对ELF格式文件处理代码为load_elf_binary,主要流程如下:
i.根据ELF文件头数据,检查ELF文件的合法性
ii.根据ELF文件头中程序头表偏移和程序头数目,读出程序头表数据。
iii.根据解析器程序头信息,读出解析器可执行文件头数据放入buf中,以便后续处理
iv.清除原exec环境
1.线程处理
a.发送SIGKILL到线程组其它线程,杀死除自己以外所有的线程
b.如果自己不是主线程,继承主线程信息并将自己设置成主线程
c.删除信号时钟,并清空时钟信号
d.如果信号处理handler表与其它进程共享,则创建并复制自己的handler表
2.将可执行文件句柄设置成新的文件句柄
3.切换地址空间,使用新的地址空间
v.设置新exec环境
1.设置进程名称
2.更新信号handler表,非忽略信号设置成行为,并清空mask
3.关闭有FD_CLOEXEC标识的文件
vi. 堆栈内存区最后处理,更新内存区标识、权限,内存区可能重定位和扩容
vii.将ELF文件中PT_LOAD类型的程序段映射到内存中
1.根据PT_LOAD类型的程序头信息,将可执行文件中代码段、数据段等通过文件映射方式映射到内存
2.PT_LOAD一般包括代码段和数据段,数据段虚拟内存大小通常大于文件大小,多出的部分包括bss内存空间等;采用文件映射且程序段虚拟内存大小大于文件大小时,将多出部分清0(最后一个程序段中,多出部分通常采用匿名映射)
3.设置start_code,end_code,start_data,end_data,elf_bss,elf_brk;一般情况,elf_bss=数据段虚拟地址+文件大小,elf_brk=数据段虚拟地址+虚拟内存大小
viii.对elf_bss与elf_brk之间的部分(BSS等)作匿名映射;
ix.设置exec返回用户空间后,执行代码的地址;如果有解析器(可执行程序通常是/lib/ld-linux.so.2,ld-linux.so.2作用是装载可执行文件所需的动态链接库),装载解析器,并将程序入口设置成解析器的入口;否则设置成镜像的入口点。
x.设置elf_table
1.将main参数个数,参数字符串地址,NULL写到堆栈中
2.将环境变量字符串地址,NULL写到堆栈中
3.设置elf表,表项为(id,val)对;如程序的入口地址等
xi.设置进程参数,如IP更改为新程序入口地址,堆栈切换;然后返回用户空间执行解析器代码或程序代码
ELF格式文件处理的实现fs/binfmt_elf.c:
563 static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
564 {
565 struct file *interpreter = NULL; /* to shut gcc up */
566 unsigned long load_addr = 0, load_bias = 0;
567 int load_addr_set = 0;
568 char * elf_interpreter = NULL;
569 unsigned long error;
570 struct elf_phdr *elf_ppnt, *elf_phdata;
571 unsigned long elf_bss, elf_brk;
572 int retval, i;
573 unsigned int size;
574 unsigned long elf_entry;
575 unsigned long interp_load_addr = 0;
576 unsigned long start_code, end_code, start_data, end_data;
577 unsigned long reloc_func_desc