注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

脚本执行及参数设置  

2012-11-04 22:07:04|  分类: Linux内核 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、脚本
脚本文件是内核直接支持的一种格式,所谓直接是因为它虽然格式简单,但是和复杂的ELF格式一样,它是一个内核直接支持的文件格式,所以在逻辑上脚本可执行程序和ELF格式都是相同的文件格式。
对于脚本文件来说,它的格式比较简单,内核中对于这个格式的解析也非常简单。在2.6.21内核中,binfmt_script.c文件总共只有115行。文件比较简单,用户态处理起来可能就会复杂一些,因为内核做的工作很少,其它的需要用户态的进程来完成额外操作。
二、脚本参数
在之前的一篇博客中,讨论了内核对于文件的加载是经过特殊处理的。脚本本身并不具备执行功能,它必须依赖于自己特定的解析器来完成功能的完成。为了统一,内核会修改命令行参数的格式。在load_script函数中,内核把脚本文件中的解析器作为命令行参数的第一个参数,脚本中的参数同样放在参数的后面,这样在用户态看到的行为和真正启动时的内容是不同的。说了这么多,可能很混乱,所以还是通过一个例子来解释一下
下面是测试脚本内容
[root@Harry recursiveScript]# cat scriptarg.sh
#!/bin/sh -x
sleep 11111
然后在系统中可以看到进程的命令行格式为
root     19090  0.0  0.1   4928  1064 pts/3    S+   21:39   0:00 /bin/sh -x ./scriptarg.sh
需要注意的是,这里的命令行中 /bin/sh -x 放在最前面,而脚本文件本身放在了最后,反而我们在命令行中执行的./scriptarg.sh并没有作为第一个参数来实现。看一下内核的实现代码来说,这个现象并不难解释,另外由于也不是这里讨论的重点,所以跳过。
现在的问题是在我们执行脚本的时候,执行的是 ./scriptarg.sh,在这个脚本中,和通常一样,我们希望通过arv[0]来得到脚本本身的名字,进而通过名字来获得脚本本身在文件系统中的位置,如果在操作系统把命令悄悄的替换成了/bin/sh 的格式,此时argv[0]显示出/bin/sh岂不是达不到我们希望的效果?
但是当尝试一下之后你会发现,在脚本中通过 $0 依然可以获得脚本的完整路径。
当然这个地方也没有神奇的地方,因为这个转换是shell悄悄的完成的。在bash的实现代码中,它对脚本中的 $0类型的参数并不是从自己的C语言的argv中获得,而是通过特殊处理保证了用户看到的$0是脚本而不是/bin/sh。
在shell.c文件的main函数里
 open_shell_script (argv[arg_index]);--->>>> dollar_vars[0] = savestring (script_name);
所以shell特殊处理了脚本中看到的数字参数的内容。
三、解析器脚本嵌套
在内核的load_script函数中,有一个对于脚本递归的判断,这里所说的脚本嵌套就是脚本中指定的解析器依然是一个脚本。由于这里比较绕,还是写个例子,这里需要注意的是第一个脚本的解析器second.sh同样还是还是一个脚本。
[root@Harry recursiveScript]# cat first.sh
#!/home/tsecer/CodeTest/recursiveScript/second.sh
echo in first
[root@Harry recursiveScript]# cat second.sh
#!/bin/sh
echo in second
sleep 1234
这种情况内核是有特殊限制的:
static int load_script(struct linux_binprm *bprm,struct pt_regs *regs)
    if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!') || (bprm->sh_bang))
        return -ENOEXEC;
    /*
     * This section does the #! interpretation.
     * Sorta complicated, but hopefully it will work.  -TYT
     */

    bprm->sh_bang++;
这意味着对于前面的例子执行的结果是会返回错误的。在我使用的fedoracore版本中测试了一下这个程序,输出的内容是
[root@Harry recursiveScript]# ./first.sh
in second
我们看一下系统参数内容
root     19225  0.0  0.1   4928  1080 pts/4    S+   21:54   0:00 /bin/sh /home/tsecer/CodeTest/recursiveScript/second.sh ./first.sh
也就是内核识别了这种格式。看到这种情况的时候我是有点震惊的,所以看了一下2.6.37内核中的实现。发现新的版本中对这里脚本嵌套的处理做了和之前版本不同的处理
if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!') ||
        (bprm->recursion_depth > BINPRM_MAX_RECURSION))
        return -ENOEXEC;
    /*
     * This section does the #! interpretation.
     * Sorta complicated, but hopefully it will work.  -TYT
     */

    bprm->recursion_depth++;
其中
#define BINPRM_MAX_RECURSION 4
这意味着脚本的嵌套是可以有最多4层。我在自己阅读的2.6.21内核变出的机器上运行,这个脚本输出的内容就是first。
四、脚本中返回ENOEXEC的处理
按照常规的理解,如果内核返回错误值,那意味着这个系统调用是失败了,但是为什么还可以打印出内容呢?这个同样是shell做的特殊处理(shell好忙啊)。在bash的shell_execve函数中,对于execve系统调用做了判断
  execve (command, args, env);
  if (i != ENOEXEC)
{}
  /* Is this supposed to be an executable script?
     If so, the format of the line is "#! interpreter [argument]".
     A single argument is allowed.  The BSD kernel restricts
     the length of the entire line to 32 characters (32 bytes
     being the size of the BSD exec header), but we allow 80
     characters. */
  if (sample_len > 0)
    {
#if !defined (HAVE_HASH_BANG_EXEC)
      if (sample_len > 2 && sample[0] == '#' && sample[1] == '!')
    return (execute_shell_script (sample, sample_len, command, args, env));执行失败之后把内容当做shell脚本来执行
      else
#endif
      if (check_binary_file (sample, sample_len))
    {
      internal_error (_("%s: cannot execute binary file"), command);
      return (EX_BINARY_FILE);
    }
    }
busybox中ash对于这种情况的处理
repeat:
#ifdef SYSV
    do {
        execve(cmd, argv, envp);
    } while (errno == EINTR);
#else
    execve(cmd, argv, envp);
#endif
    if (repeated) {
        free(argv);
        return;
    }
    if (errno == ENOEXEC) {
        char **ap;
        char **new;

        for (ap = argv; *ap; ap++)
            continue;
        ap = new = ckmalloc((ap - argv + 2) * sizeof(ap[0]));
        ap[1] = cmd;
        ap[0] = cmd = (char *)DEFAULT_SHELL;
        ap += 2;
        argv++;
        while ((*ap++ = *argv++) != NULL)
            continue;
        argv = new;
        repeated++;
        goto repeat;
    }
# define DEFAULT_SHELL "/proc/self/exe"
五、load_script中一个细节的尝试解释
remove_arg_zero(bprm); 这里删除第一个参数,按照我们的理解,这个参数应该就是我们传入的第一个参数,也就是脚本的名称,这里为什么要先删除,然后在把脚本名称写进去呢?我想可能是execve的时候,真正的可执行文件和给执行任务的argv没有必然联系的,脚本的文件名为test.sh,你可以在argv指定为hello world,这里这么处理是为了保证一定是一个没有捣蛋的参数
    retval = copy_strings_kernel(1, &bprm->interp, bprm);
    if (retval < 0) return retval;
    bprm->argc++;
    if (i_arg) {
        retval = copy_strings_kernel(1, &i_arg, bprm);
        if (retval < 0) return retval;
        bprm->argc++;
    }
    retval = copy_strings_kernel(1, &i_name, bprm);
  评论这张
 
阅读(806)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017