翼度科技»论坛 云主机 LINUX 查看内容

[kernel] 带着问题看源码 —— 脚本是如何被 execve 调用的

8

主题

8

帖子

24

积分

新手上路

Rank: 1

积分
24
前言

在《[apue] 进程控制那些事儿》一文的"进程创建-> exec -> 解释器文件"一节中,曾提到脚本文件的识别是由内核作为 exec 系统调用处理的一部分来完成的,并且有以下特性:

  • 指定解释器的以 #!  (shebang) 开头的第一行长度不得超过 128
  • shebang 最多只能指定一个参数
  • shebang 指定的命令与参数会成为新进程的前 2 个参数,用户提供的其它参数依次往后排
这些特性是如何实现的?带着这个疑问,找出系统对应的内核源码看个究竟。
源码定位

和《[kernel] 带着问题看源码 —— 进程 ID 是如何分配的》一样,这里使用 bootlin 查看内核 3.10.0 版本源码,脚本文件是在 execve 时解析的,所以先搜索 sys_ execve:

整个调用链如下:
sys_execve -> do_execve -> do_execve_common -> search_binary_handler-> load_binary -> load_script (binfmt_script.c)
为了快速进入主题,前面咱们就不一一细看了,主要解释一下 search_binary_handler。
Linux 中加载不同文件格式的方式是可扩展的,这主要是通过内核模块来实现的,每个模块实现一个格式,新的格式可通过编写内核模块实现快速支持而无需修改内核源码。刚才浏览代码的时候已经初窥门径:

这是目前内核内置的几个模块

  • binfmt_elf:最常用的 Linux 二进制可执行文件
  • binfmt_elf_fdpic:缺失 MMU 架构的二进制可执行文件
  • binfmt_em86:在 Aplha 机器上运行 Intel 的 Linux 二进制文件
  • binfmt_aout:Linux 老的可执行文件
  • binfmt_script:脚本文件
  • binfmt_misc:一种新机制,支持运行期文件格式与应用对应关系的绑定
基本可以归纳为三大类:

  • 可执行文件
  • 脚本文件 (script)
  • 机制拓展 (misc)
其中 misc 机制类似 Windows 上文件通过后缀与应用进行绑定的机制,它除了通过后缀,还可以通过检测文件中的 Magic 字段,作为判断文件类型的依据。目前的主要应用方向是跨架构运行,例如在 x86 机器上运行 arm64、甚至 Windows 程序 (wine),相比编写内核模块,便利性又降低了一个等级。
本文主要关注脚本文件的处理过程。
binfmt

内核模块本身并不难实现,以 script 为例:
  1. static struct linux_binfmt script_format = {
  2.         .module                = THIS_MODULE,
  3.         .load_binary        = load_script,
  4. };
  5. static int __init init_script_binfmt(void)
  6. {
  7.         register_binfmt(&script_format);
  8.         return 0;
  9. }
  10. static void __exit exit_script_binfmt(void)
  11. {
  12.         unregister_binfmt(&script_format);
  13. }
  14. core_initcall(init_script_binfmt);
  15. module_exit(exit_script_binfmt);
复制代码
主要是通过 register_binfmt / unregister_binfmt 来插入、删除 linux_binfmt 信息节点。
  1. /*
  2. * This structure defines the functions that are used to load the binary formats that
  3. * linux accepts.
  4. */
  5. struct linux_binfmt {
  6.         struct list_head lh;
  7.         struct module *module;
  8.         int (*load_binary)(struct linux_binprm *);
  9.         int (*load_shlib)(struct file *);
  10.         int (*core_dump)(struct coredump_params *cprm);
  11.         unsigned long min_coredump;        /* minimal dump size */
  12. };
复制代码
linux_binfmt 的内容不多,而且回调函数不用全部实现,没有用到的留空就完事了。下面看下插入节点过程:
  1. static LIST_HEAD(formats);
  2. static DEFINE_RWLOCK(binfmt_lock);
  3. void __register_binfmt(struct linux_binfmt * fmt, int insert)
  4. {
  5.         BUG_ON(!fmt);
  6.         write_lock(&binfmt_lock);
  7.         insert ? list_add(&fmt->lh, &formats) :
  8.                  list_add_tail(&fmt->lh, &formats);
  9.         write_unlock(&binfmt_lock);
  10. }
  11. /* Registration of default binfmt handlers */
  12. static inline void register_binfmt(struct linux_binfmt *fmt)
  13. {
  14.         __register_binfmt(fmt, 0);
  15. }
复制代码
利用 linux_binfmt.lh 字段 (list_head) 实现链表插入,链表头为全局变量 formats。
search_binary_handler

再看 search_binary_handler 利用 formats 遍历链表的过程:
  1. retval = -ENOENT;
  2. for (try=0; try<2; try++) {
复制代码
检查内核模块是否 alive;执行 load_binary 前解锁 formats 链表以便嵌套;更新嵌套深度
  1.     read_lock(&binfmt_lock);
  2.         list_for_each_entry(fmt, &formats, lh) {
复制代码
恢复嵌套尝试;执行成功,提前退出
  1.         int (*fn)(struct linux_binprm *) = fmt->load_binary;
  2.                 if (!fn)
  3.                         continue;
  4.                 if (!try_module_get(fmt->module))
  5.                         continue;
  6.                 read_unlock(&binfmt_lock);
  7.                 bprm->recursion_depth = depth + 1;
复制代码
执行失败,重新加锁;如果非 ENOEXEC 错误,继续尝试下个 fmt
  1.         retval = fn(bprm);
  2.                 bprm->recursion_depth = depth;
  3.                 if (retval >= 0) {
  4.                         if (depth == 0) {
  5.                                 trace_sched_process_exec(current, old_pid, bprm);
  6.                                 ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
  7.                         }
  8.                         put_binfmt(fmt);
  9.                         allow_write_access(bprm->file);
  10.                         if (bprm->file)
  11.                                 fput(bprm->file);
  12.                         bprm->file = NULL;
  13.                         current->did_exec = 1;
  14.                         proc_exec_connector(current);
  15.                         return retval;
  16.                 }
复制代码
遍历完毕,退出
其中 list_for_each_entry 就是 Linux 对 list 遍历的封装宏:
  1.         read_lock(&binfmt_lock);
  2.                 put_binfmt(fmt);
  3.                 if (retval != -ENOEXEC || bprm->mm == NULL)
  4.                         break;
  5.                 if (!bprm->file) {
  6.                         read_unlock(&binfmt_lock);
  7.                         return retval;
  8.                 }
  9.         }
复制代码
本质是个 for 循环。另外,之前的 for (try < 2) 其实并不生效,因为总会被末尾的 break 打断。
不过这里提示了一点 load_binary 的写法:当该接口返回 -ENOEXEC 时,表示这个文件“不合胃口”,请继续遍历 formats 列表尝试,下面在解读 load_script 时可以多加留意。
另外 binfmt 是可以嵌套的,假设在调用一个脚本,它使用 awk 作为解释器,那么整个执行过程看起来像下面这样:
execve (xxx.awk) -> load_script (binfmt_script) -> load_elf_binary (binfmt_elf)
这是因为 awk 作为可执行文件,本身也需要 binfmt 的处理,稍等就可以在 load_script 中看到这一点。
目前 Linux 没有对嵌套深度施加限制。
源码分析

经过一番背景知识铺垫,终于可以进入 binfmt_script 好好看看啦:
  1.     read_unlock(&binfmt_lock);
  2.         break;
  3. }
复制代码
脚本不以 #! 开头的,忽略;注意 interp 数组的长度:> pwd
/ext/code/apue/07.chapter/test black
> ls -lh
total 52K
-rwxr-xr-x 1 yunhai01 DOORGOD 48K Aug 23 19:17 echo
-rwxr--r-- 1 yunhai01 DOORGOD  47 Aug 23 19:17 echo.sh
> cat echo.sh
#! /ext/code/apue/07.chapter/test black/demo

> ./echo a b c
argv[0] = ./echo
argv[1] = a
argv[2] = b
argv[3] = c
> ./echo.sh a b c
bash: ./echo.sh: /ext/code/apue/07.chapter/test: bad interpreter: No such file or directory,这是 shebang 不能超过 128 的来源
  1. /**
  2. * list_for_each_entry        -        iterate over list of given type
  3. * @pos:        the type * to use as a loop cursor.
  4. * @head:        the head for your list.
  5. * @member:        the name of the list_struct within the struct.
  6. */
  7. #define list_for_each_entry(pos, head, member)                                \
  8.         for (pos = list_entry((head)->next, typeof(*pos), member);        \
  9.              &pos->member != (head);         \
  10.              pos = list_entry(pos->member.next, typeof(*pos), member))
复制代码
系统已读取文件头部的一部分字节到内存,脚本文件用完了,释放
  1. static int load_script(struct linux_binprm *bprm)
  2. {
  3.         const char *i_arg, *i_name;
  4.         char *cp;
  5.         struct file *file;
  6.         char interp[BINPRM_BUF_SIZE];
  7.         int retval;
  8.         if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!'))
  9.                 return -ENOEXEC;
复制代码
最多截取前 127 个字符,并向前搜索 shebang 结尾 (\n),若有,则设置新的结尾到那里
  1.     /*
  2.          * This section does the #! interpretation.
  3.          * Sorta complicated, but hopefully it will work.  -TYT
  4.          */
  5.         allow_write_access(bprm->file);
  6.         fput(bprm->file);
  7.         bprm->file = NULL;
复制代码
前后 trim 空白字符,如果没有任何内容,忽略;注意初始时 cp 指向字符串尾部,结束时,cp 指向有效信息头部
  1.     bprm->buf[BINPRM_BUF_SIZE - 1] = '\0';
  2.         if ((cp = strchr(bprm->buf, '\n')) == NULL)
  3.                 cp = bprm->buf+BINPRM_BUF_SIZE-1;
  4.         *cp = '\0';
复制代码
跳过命令名;忽略空白字符;剩下的若有内容全部作为一个参数;命令名复制到 interp 数组中备用
  1.     while (cp > bprm->buf) {
  2.                 cp--;
  3.                 if ((*cp == ' ') || (*cp == '\t'))
  4.                         *cp = '\0';
  5.                 else
  6.                         break;
  7.         }
  8.         for (cp = bprm->buf+2; (*cp == ' ') || (*cp == '\t'); cp++);
  9.         if (*cp == '\0')
  10.                 return -ENOEXEC; /* No interpreter name found */
复制代码
删除 argv 的第一个参数,分别将命令名 (i_name)、参数 (i_arg 如果有的话)、脚本文件名 (bprm->interp) 放置到 argv 前三位。
注意这里调用的顺序恰好相反:bprm->interp、i_arg、i_name,这是由于 argv 在进程中特殊的存放方式导致,参考后面的解说;最后更新 bprm 中的命令名
  1.     i_name = cp;
  2.         i_arg = NULL;
  3.         for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++)
  4.                 /* nothing */ ;
  5.         while ((*cp == ' ') || (*cp == '\t'))
  6.                 *cp++ = '\0';
  7.         if (*cp)
  8.                 i_arg = cp;
  9.         strcpy (interp, i_name);
复制代码
通过命令名指定的路径打开文件,并设置到当前进程,准备加载前的各种信息,包括预读文件的头部的一些内容
  1.     /*
  2.          * OK, we've parsed out the interpreter name and
  3.          * (optional) argument.
  4.          * Splice in (1) the interpreter's name for argv[0]
  5.          *           (2) (optional) argument to interpreter
  6.          *           (3) filename of shell script (replace argv[0])
  7.          *
  8.          * This is done in reverse order, because of how the
  9.          * user environment and arguments are stored.
  10.          */
  11.         retval = remove_arg_zero(bprm);
  12.         if (retval)
  13.                 return retval;
  14.         retval = copy_strings_kernel(1, &bprm->interp, bprm);
  15.         if (retval < 0) return retval;
  16.         bprm->argc++;
  17.         if (i_arg) {
  18.                 retval = copy_strings_kernel(1, &i_arg, bprm);
  19.                 if (retval < 0) return retval;
  20.                 bprm->argc++;
  21.         }
  22.         retval = copy_strings_kernel(1, &i_name, bprm);
  23.         if (retval) return retval;
  24.         bprm->argc++;
  25.         retval = bprm_change_interp(interp, bprm);
  26.         if (retval < 0)
  27.                 return retval;
复制代码
使用新命令的信息继续搜索 binfmt 模块并加载之
这里主要补充一点,对于 shebang 中的命令名字段,中间不能包含空格,否则会被提前截断,即使使用引号包围也不行 (解析代码根本未对引号做处理),下面是个例子:
  1.     /*
  2.          * OK, now restart the process with the interpreter's dentry.
  3.          */
  4.         file = open_exec(interp);
  5.         if (IS_ERR(file))
  6.                 return PTR_ERR(file);
  7.         bprm->file = file;
  8.         retval = prepare_binprm(bprm);
  9.         if (retval < 0)
  10.                 return retval;
复制代码
文件头预读

这里主要解释两点,一是 prepare_binprm 会预读文件头部的一些数据,供后面 binfmt 判断使用:
  1.     return search_binary_handler(bprm);
  2. }
复制代码
目前这个 BINPRM_BUF_SIZE 的长度也是 128:
  1. > pwd
  2. /ext/code/apue/07.chapter/test black
  3. > ls -lh
  4. total 52K
  5. -rwxr-xr-x 1 yunhai01 DOORGOD 48K Aug 23 19:17 echo
  6. -rwxr--r-- 1 yunhai01 DOORGOD  47 Aug 23 19:17 echo.sh
  7. > cat echo.sh
  8. #! /ext/code/apue/07.chapter/test black/demo
  9. > ./echo a b c
  10. argv[0] = ./echo
  11. argv[1] = a
  12. argv[2] = b
  13. argv[3] = c
  14. > ./echo.sh a b c
  15. bash: ./echo.sh: /ext/code/apue/07.chapter/test: bad interpreter: No such file or directory
复制代码
在 do_execve_common 中也会调用这个接口来为第一次 binfmt 识别做准备:
  1. /*
  2. * Fill the binprm structure from the inode.
  3. * Check permissions, then read the first 128 (BINPRM_BUF_SIZE) bytes
  4. *
  5. * This may be called multiple times for binary chains (scripts for example).
  6. */
  7. int prepare_binprm(struct linux_binprm *bprm)
  8. {
  9.         umode_t mode;
  10.         struct inode * inode = file_inode(bprm->file);
  11.         int retval;
  12.         mode = inode->i_mode;
  13.         if (bprm->file->f_op == NULL)
  14.                 return -EACCES;
  15.     ...
  16.         /* fill in binprm security blob */
  17.         retval = security_bprm_set_creds(bprm);
  18.         if (retval)
  19.                 return retval;
  20.         bprm->cred_prepared = 1;
  21.         memset(bprm->buf, 0, BINPRM_BUF_SIZE);
  22.         return kernel_read(bprm->file, 0, bprm->buf, BINPRM_BUF_SIZE);
  23. }
复制代码
没错,就是这里了
  1. #define BINPRM_BUF_SIZE 128
复制代码
argv 调整

另外一点是 argv 在内存中的布局,参考之前写的《[apue] 进程环境那些事儿》,这里直接贴图:

命令行参数与环境变量是放在进程高地址空间的末尾,以 \0 为间隔的字符串。由于有高地址“天花板”在存在,这里必需先根据字符串长度定位到起始位置,再复制整个字符串,此外为了保证 argv[0] 地址小于 argv[1],整个数组也需要从后向前遍历。这里借用之前写的一个例子证明这一点:
  1. /*
  2. * sys_execve() executes a new program.
  3. */
  4. static int do_execve_common(const char *filename,
  5.                                 struct user_arg_ptr argv,
  6.                                 struct user_arg_ptr envp)
  7. {
  8.         struct linux_binprm *bprm;
  9.         struct file *file;
  10.         struct files_struct *displaced;
  11.         bool clear_in_exec;
  12.         int retval;
  13.         const struct cred *cred = current_cred();
  14.     ...
  15.         file = open_exec(filename);
  16.         retval = PTR_ERR(file);
  17.         if (IS_ERR(file))
  18.                 goto out_unmark;
  19.         sched_exec();
  20.         bprm->file = file;
  21.         bprm->filename = filename;
  22.         bprm->interp = filename;
  23.         retval = bprm_mm_init(bprm);
  24.         if (retval)
  25.                 goto out_file;
  26.         bprm->argc = count(argv, MAX_ARG_STRINGS);
  27.         if ((retval = bprm->argc) < 0)
  28.                 goto out;
  29.         bprm->envc = count(envp, MAX_ARG_STRINGS);
  30.         if ((retval = bprm->envc) < 0)
  31.                 goto out;
  32.         retval = prepare_binprm(bprm);
  33.         if (retval < 0)
  34.                 goto out;
复制代码
随便给一些参数让它跑个输出:
  1.     retval = copy_strings_kernel(1, &bprm->filename, bprm);
  2.         if (retval < 0)
  3.                 goto out;
  4.         bprm->exec = bprm->p;
  5.         retval = copy_strings(bprm->envc, envp, bprm);
  6.         if (retval < 0)
  7.                 goto out;
  8.         retval = copy_strings(bprm->argc, argv, bprm);
  9.         if (retval < 0)
  10.                 goto out;
  11.         retval = search_binary_handler(bprm);
  12.         if (retval < 0)
  13.                 goto out;
  14.     ...
  15. }
复制代码
重点看下 argv 与 envp 的地址,envp 高于 argv;再看各个数组内部的情况,索引低的地址也低。结合之前的内存布局图,就需要这样排布各个参数:

  • 先排布 envp,envp 内部从后向前遍历
  • 后排布 argv,argv 内部从后向前遍历
代码也确实是这样写的:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int data1 = 2;
  4. int data2 = 3;
  5. int data3;
  6. int data4;
  7. int main (int argc, char *argv[])
  8. {
  9.   char buf1[1024] = { 0 };
  10.   char buf2[1024] = { 0 };
  11.   char *buf3 = malloc(1024);
  12.   char *buf4 = malloc(1024);
  13.   printf ("onstack %p, %p\n",
  14.     buf1,
  15.     buf2);
  16.   extern char ** environ;
  17.   printf ("env %p\n", environ);
  18.   for (int i=0; environ[i] != 0; ++ i)
  19.     printf ("env[%d] %p\n", i, environ[i]);
  20.   printf ("arg %p\n", argv);
  21.   for (int i=0; i < argc; ++ i)
  22.     printf ("arg[%d] %p\n", i, argv[i]);
  23.   printf ("onheap %p, %p\n",
  24.     buf3,
  25.     buf4);
  26.   free (buf3);
  27.   free (buf4);
  28.   printf ("on bss %p, %p\n",
  29.     &data3,
  30.     &data4);
  31.   printf ("on init %p, %p\n",
  32.     &data1,
  33.     &data2);
  34.   printf ("on code %p\n", main);
  35.   return 0;
  36. }
复制代码
上面这段之前在 do_execve_common 中展示过,先排布 envp 后排布 argv,再看数组内部的处理:
  1. > ./layout a b c d
  2. onstack 0x7fff2757a970, 0x7fff2757a570
  3. env 0x7fff2757aea8
  4. env[0] 0x7fff2757b4fb
  5. env[1] 0x7fff2757b511
  6. env[2] 0x7fff2757b534
  7. env[3] 0x7fff2757b544
  8. env[4] 0x7fff2757b558
  9. env[5] 0x7fff2757b566
  10. env[6] 0x7fff2757b587
  11. env[7] 0x7fff2757b5af
  12. env[8] 0x7fff2757b5c7
  13. env[9] 0x7fff2757b5e7
  14. env[10] 0x7fff2757b5fa
  15. env[11] 0x7fff2757b608
  16. env[12] 0x7fff2757bcc0
  17. env[13] 0x7fff2757bcc8
  18. env[14] 0x7fff2757be1d
  19. env[15] 0x7fff2757be3b
  20. env[16] 0x7fff2757be59
  21. env[17] 0x7fff2757be6a
  22. env[18] 0x7fff2757be81
  23. env[19] 0x7fff2757be9b
  24. env[20] 0x7fff2757bea3
  25. env[21] 0x7fff2757beb3
  26. env[22] 0x7fff2757bec4
  27. env[23] 0x7fff2757bee0
  28. env[24] 0x7fff2757bf13
  29. env[25] 0x7fff2757bf36
  30. env[26] 0x7fff2757bf62
  31. env[27] 0x7fff2757bf83
  32. env[28] 0x7fff2757bfa1
  33. env[29] 0x7fff2757bfc3
  34. env[30] 0x7fff2757bfce
  35. arg 0x7fff2757ae78
  36. arg[0] 0x7fff2757b4ea
  37. arg[1] 0x7fff2757b4f3
  38. arg[2] 0x7fff2757b4f5
  39. arg[3] 0x7fff2757b4f7
  40. arg[4] 0x7fff2757b4f9
  41. onheap 0x1056010, 0x1056420
  42. on bss 0x6066b8, 0x6066bc
  43. on init 0x606224, 0x606228
  44. on code 0x40179d
复制代码
倒序遍历数组
  1.     retval = copy_strings(bprm->envc, envp, bprm);
  2.         if (retval < 0)
  3.                 goto out;
  4.         retval = copy_strings(bprm->argc, argv, bprm);
  5.         if (retval < 0)
  6.                 goto out;
复制代码
计算当前字符串长度并预留位置,注意复制时可能存在跨页情况,字符串也是从尾向头分割为一块块复制的
  1. /*
  2. * 'copy_strings()' copies argument/environment strings from the old
  3. * processes's memory to the new process's stack.  The call to get_user_pages()
  4. * ensures the destination page is created and not swapped out.
  5. */
  6. static int copy_strings(int argc, struct user_arg_ptr argv,
  7.                         struct linux_binprm *bprm)
  8. {
  9.         struct page *kmapped_page = NULL;
  10.         char *kaddr = NULL;
  11.         unsigned long kpos = 0;
  12.         int ret;
  13.         while (argc-- > 0) {
复制代码
复制单个字符串,字符串可能非常大,一个就好几页,干活的主要是 copy_from_user
  1.         const char __user *str;
  2.                 int len;
  3.                 unsigned long pos;
  4.         ret = -EFAULT;
  5.                 str = get_user_arg_ptr(argv, argc);
  6.                 if (IS_ERR(str))
  7.                         goto out;
  8.                 len = strnlen_user(str, MAX_ARG_STRLEN);
  9.                 if (!len)
  10.                         goto out;
  11.                 ret = -E2BIG;
  12.                 if (!valid_arg_len(bprm, len))
  13.                         goto out;
  14.                 /* We're going to work our way backwords. */
  15.                 pos = bprm->p;
  16.                 str += len;
  17.                 bprm->p -= len;
复制代码
出错处理
了解了 argv 与 envp 的布局后,突然发现在数组前插入元素反而简单了,不过需要先将第一个元素删除,这里 Linux 使用了一个 trick:直接移动 argv 指针 (bprm->p) 略过第一个参数:
  1.         while (len > 0) {
  2.                         int offset, bytes_to_copy;
  3.                         if (fatal_signal_pending(current)) {
  4.                                 ret = -ERESTARTNOHAND;
  5.                                 goto out;
  6.                         }
  7.                         cond_resched();
  8.                         offset = pos % PAGE_SIZE;
  9.                         if (offset == 0)
  10.                                 offset = PAGE_SIZE;
  11.                         bytes_to_copy = offset;
  12.                         if (bytes_to_copy > len)
  13.                                 bytes_to_copy = len;
  14.                         offset -= bytes_to_copy;
  15.                         pos -= bytes_to_copy;
  16.                         str -= bytes_to_copy;
  17.                         len -= bytes_to_copy;
  18.                         if (!kmapped_page || kpos != (pos & PAGE_MASK)) {
  19.                                 struct page *page;
  20.                                 page = get_arg_page(bprm, pos, 1);
  21.                                 if (!page) {
  22.                                         ret = -E2BIG;
  23.                                         goto out;
  24.                                 }
  25.                                 if (kmapped_page) {
  26.                                         flush_kernel_dcache_page(kmapped_page);
  27.                                         kunmap(kmapped_page);
  28.                                         put_arg_page(kmapped_page);
  29.                                 }
  30.                                 kmapped_page = page;
  31.                                 kaddr = kmap(kmapped_page);
  32.                                 kpos = pos & PAGE_MASK;
  33.                                 flush_arg_page(bprm, kpos, kmapped_page);
  34.                         }
  35.                         if (copy_from_user(kaddr+offset, str, bytes_to_copy)) {
  36.                                 ret = -EFAULT;
  37.                                 goto out;
  38.                         }
  39.                 }
  40.         }
  41.         ret = 0;
复制代码
经过更新后,bprm->p 指向了第二个参数,argc 减少了 1,后面新参数插入时,会自动覆盖它:
  1. out:
  2.         if (kmapped_page) {
  3.                 flush_kernel_dcache_page(kmapped_page);
  4.                 kunmap(kmapped_page);
  5.                 put_arg_page(kmapped_page);
  6.         }
  7.         return ret;
  8. }
复制代码
copy_string_kernel 将基于 kernel 获取的源字符串调用 copy_strings,因此一切又回到了前面的逻辑,这里只要保持参数倒序处理即可,这段代码之前在 load_script 时展示过,这回大家看明白了吗?
总结

开头提出的三个问题:

  • 指定解释器的以 shebang 开头的第一行长度不得超过 128
  • shebang 最多只能指定一个参数
  • shebang 指定的命令与参数会成为新进程的前 2 个参数,用户提供参数依次后排
都一一得到了解答,其中 shebang 长度 128 这个限制,和整个 execve 预读长度 ()BINPRM_BUF_SIZE) 息息相关,也与 binfmt_misc 规定的格式相关,看起来不好随便突破。
另外通过通读源码,得到了以下额外的知识:

  • 解释器文件可嵌套,且没有深度限制
  • 解释器第一行中的命令名不能包含空白字符
  • 命令行参数在内存空间是倒排的,这一点貌似主要是为了在数组头部插入元素更便利,如果有需求要在数组尾部插入元素,可能就得改为正排了
最后对于 shebang 支持多个 arguments 这一点,目前看只要修改 binfmt_scrpts,应该是可以实现的,这个课题就留给感兴趣的读者作为作业吧,哈哈~
参考

[1]. linux下使用binfmt_misc设定不同二进制的打开程序
[2]. Linux中的binfmt-misc原理分析
[3]. binfmt.d 中文手册
[4]. Linux 的 binfmt_misc (binfmt) module 介紹
[5]. Linux系统的可执行文件格式详细解析
[6]. Kernel Support for miscellaneous Binary Formats (binfmt_misc)
 
   
来源:https://www.cnblogs.com/goodcitizen/p/18375902/how_linux_execve_script_file
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具