有些bash脚本写的不规范,没有在文件开头写#!,但是却能直接执行,可是如果看内核代码,shell脚本的加载函数中的开头就会判断,如果没有#!的话就会返回错误:
static int load_script(struct linux_binprm *bprm,struct pt_regs *regs)
{
...
if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!') || (bprm->sh_bang))
return -ENOEXEC;
...
}
为何在linux上写bash脚本时文件开头不写#!也能成功调用?bash中执行一条键盘命令时调用的是shell_execve函数,该函数封装了很多逻辑:
int shell_execve (command, args, env)
{
execve (command, args, env);
if (errno != ENOEXEC) {
//非文件格式的错误,返回出错
} else {
int larray = array_len (args) + 1;
int i, should_exec = 0;
int fd = open (command, O_RDONLY);
if (fd != -1) {
unsigned char sample[80]; //读取文件头的80个字节,然后进行格式判断
int sample_len = read (fd, &sample[0], 80);
...//如果是空文件,则正确返回,bash容忍这种情况
if (sample_len > 0 && sample[0] == '#' && sample[1] == '!')
return (execute_shell_script(...));//执行shell脚本,其实现过程几乎和内核的script_format结构对象中的
load_binary回调函数一模一样,主要用于不识别#!的操作系统
else if (check_binary_file (sample, sample_len))
//如果是二进制文件,则bash是不会逐行帮助执行的,出错返回
}
...//bash会帮你执行command:
/*
args[0] = shell_name; //就是本shell的全路径
args[1] = command; //要执行的脚本
execve (shell_name, args, env); //重新执行
*/
}
以下是上述shell_execve函数依赖的帮助函数和一些宏定义:
由于bash是通过字符的可见性来判断是否是二进制文件的,由于空格/制表符等也是不可见的字符,然而它们确实是属于文本ascii码的,因此需要单独进行过滤:
#define isspace(c) ((c) == ' ' || (c) == '/t' || (c) == '/n' || (c) == '/f')
文本字符包括字母字符,数字字符以及除此之外的可以打印字符:
#define isprint(c) (isletter(c) || digit(c) || ispunct(c))
判断读取的80个字节是否全部是文本字符,只要有一个不是就将该文件判定为二进制文件,虽然前80字节的字符是文本不能保证后面的就一定是文本,可是bash毕竟需要做出一个权衡和一个假设:
int check_binary_file (sample, sample_len)
{
for (i = 0; i < sample_len; i++) {
if (sample[i] == '/n')
break;
if (!isspace (sample[i]) && !isprint (sample[i]))
return (1);
}
return (0);
}
static int load_script(struct linux_binprm *bprm,struct pt_regs *regs)
{
...
if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!') || (bprm->sh_bang))
return -ENOEXEC;
...
}
为何在linux上写bash脚本时文件开头不写#!也能成功调用?bash中执行一条键盘命令时调用的是shell_execve函数,该函数封装了很多逻辑:
int shell_execve (command, args, env)
{
execve (command, args, env);
if (errno != ENOEXEC) {
//非文件格式的错误,返回出错
} else {
int larray = array_len (args) + 1;
int i, should_exec = 0;
int fd = open (command, O_RDONLY);
if (fd != -1) {
unsigned char sample[80]; //读取文件头的80个字节,然后进行格式判断
int sample_len = read (fd, &sample[0], 80);
...//如果是空文件,则正确返回,bash容忍这种情况
if (sample_len > 0 && sample[0] == '#' && sample[1] == '!')
return (execute_shell_script(...));//执行shell脚本,其实现过程几乎和内核的script_format结构对象中的
load_binary回调函数一模一样,主要用于不识别#!的操作系统
else if (check_binary_file (sample, sample_len))
//如果是二进制文件,则bash是不会逐行帮助执行的,出错返回
}
...//bash会帮你执行command:
/*
args[0] = shell_name; //就是本shell的全路径
args[1] = command; //要执行的脚本
execve (shell_name, args, env); //重新执行
*/
}
以下是上述shell_execve函数依赖的帮助函数和一些宏定义:
由于bash是通过字符的可见性来判断是否是二进制文件的,由于空格/制表符等也是不可见的字符,然而它们确实是属于文本ascii码的,因此需要单独进行过滤:
#define isspace(c) ((c) == ' ' || (c) == '/t' || (c) == '/n' || (c) == '/f')
文本字符包括字母字符,数字字符以及除此之外的可以打印字符:
#define isprint(c) (isletter(c) || digit(c) || ispunct(c))
判断读取的80个字节是否全部是文本字符,只要有一个不是就将该文件判定为二进制文件,虽然前80字节的字符是文本不能保证后面的就一定是文本,可是bash毕竟需要做出一个权衡和一个假设:
int check_binary_file (sample, sample_len)
{
for (i = 0; i < sample_len; i++) {
if (sample[i] == '/n')
break;
if (!isspace (sample[i]) && !isprint (sample[i]))
return (1);
}
return (0);
}
因此如果执行一个空文件,将会看到什么也不输出,如果执行一个非linux可执行的二进制文件比如cer格式的二进制证书,那么内核将返回ENOEXEC,接下来bash会尝试执行之,然后最终返回一个EX_BINARY_FILE错误,打印“cannot execute binary file”,如果执行一个文件头不是#!的文本文件,同样内核会返回一个ENOEXEC错误,但是bash却可以在shell_execve的后续执行中成功直接执行,可是如果在一个二进制文件的头部增加80字节或者80字节以上的文本,那么根据shell_execve的逻辑,bash也会将之作为脚本来分行执行,然后就会大错特错,因此虽然有的时候脚本中不写#!也能直接执行,但是要知道那可是bash本身帮你执行的,而不是内核直接执行的,起码执行绪会先从内核错误返回,然后继续shell_execve的后面的部分,确实稍微影响了效率,因此最好还是写上标准的#!,况且有的shell并不会像bash这么智能地重新解释执行命令。
本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1271797