linux下的可以直接执行的文件很多,它继承了unix的对可执行文件格式很开放的优势。unix或者linux中,对于程序的执行实际上并没有由内核 负责,就是说内核并不管哪些文件格式可以执行哪些不可以执行,内核只是简单地将一个可执行文件的加载和执行的任务交给了一个叫做解释器的东西,由该解释器负责文件的执行,这样可执行文件的格式就简单地与内核解耦了,这样只要提供解释器,linux便可以执行任何格式的文件,从而使得linux下的脚本百花 齐放,而windows却与此相反,它非常
复杂,包括注册表验证,内核识别...现在,windows的可执行文件主要就是pe文件,这是内核直 接支持的(linux内核当然也有一种它直接支持的文件格式,就是elf文件,要不然就会出现先有鸡先有蛋的问题了)。linux内核允许用户注册可执行文件的识别代码,用户可以自己定义一个可执行文件格式,然后写出解释器并将该解释器注册进内核,有这么麻烦吗?其实不必,内核专门提供了一个misc文件 格式接口给用户,并且导出到proc文件系统。
在linux中,可执行文件的识别被组织成了一个链表,每一种文件格式被定义为一个结构linux_binprm:
struct linux_binprm{
char buf[BINPRM_BUF_SIZE];
#ifdef CONFIG_MMU
struct vm_area_struct *vma;
#else
struct mm_struct *mm;
unsigned long p; /* current top of mem */
unsigned int sh_bang:1,
misc_bang:1;
struct file * file;
int e_uid, e_gid;
kernel_cap_t cap_post_exec_permitted;
bool cap_effective;
void *security;
int argc, envc;
char * filename; /* Name of binary as seen by procps */
char * interp; /* Name of the binary really executed. Most
of the time same as filename, but could be
different for binfmt_{misc,script} */
unsigned interp_flags;
unsigned interp_data;
unsigned long loader, exec;
};
其中interp就是解释器的名字,当执行exec的时候,内核执行do_execve,后者在最后执行search_binary_handler:
int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)
{
int try,retval;
struct linux_binfmt *fmt;
retval = security_bprm_check(bprm);
if (retval)
return retval;
set_fs(USER_DS);
retval = audit_bprm(bprm);
if (retval)
return retval;
retval = -ENOENT;
for (try=0; try<2; try++) {
read_lock(&binfmt_lock);
list_for_each_entry(fmt, &formats, lh) {//formats链表记录着内核目前支持的文件格式
int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
if (!fn)
continue;
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
retval = fn(bprm, regs);//调用该fmt的加载函数尝试加载可执行文件
if (retval >= 0) {
tracehook_report_exec(fmt, bprm, regs);
put_binfmt(fmt);
allow_write_access(bprm->file);
if (bprm->file)
fput(bprm->file);
bprm->file = NULL;
current->did_exec = 1;
proc_exec_connector(current);
return retval;
}
read_lock(&binfmt_lock);
put_binfmt(fmt);
if (retval != -ENOEXEC || bprm->mm == NULL)
break;
if (!bprm->file) {
read_unlock(&binfmt_lock);
return retval;
}
}
read_unlock(&binfmt_lock);
if (retval != -ENOEXEC || bprm->mm == NULL) {
break;
}
}
return retval;
}
formats链表在内核的src/fs/Makefile中定义,如下:
obj-$(CONFIG_BINFMT_AOUT) += binfmt_aout.o
obj-$(CONFIG_BINFMT_EM86) += binfmt_em86.o
obj-$(CONFIG_BINFMT_MISC) += binfmt_misc.o
# binfmt_script is always there
obj-y += binfmt_script.o
obj-$(CONFIG_BINFMT_ELF) += binfmt_elf.o
obj-$(CONFIG_COMPAT_BINFMT_ELF) += compat_binfmt_elf.o
obj-$(CONFIG_BINFMT_ELF_FDPIC) += binfmt_elf_fdpic.o
obj-$(CONFIG_BINFMT_SOM) += binfmt_som.o
obj-$(CONFIG_BINFMT_FLAT) += binfmt_flat.o
看到偏爱谁了吧,自上而下就是formats链表在search_binary_handler中被遍历的顺序,elf格式的几乎被排到了最后,而且是可 配置的,也就是说内核编译的时候可以不编译elf格式的可执行文件,不编译elf的情况下内核也是可以起来的,但是用户进程要想起来的话就要看用户自己 了,实际上我们完全可以写一个pe文件格式的加载模块,并且注册进内核,然后使得linux的init程序是个pe文件,所有文件都是pe文件,没有一个 elf文件,在这种情况下,所有的/bin目录和/sbin目录还有/lib目录下的文件都要是pe文件,整个用户空间要被重构,这种可能说明了 linux内核是完全提供机制而不管策略的。再看上面makefile的内容,无论如何脚本文件格式都是要被编译进内核的,这就使得用户空间的脚本扩展更 加容易,无论如何内核要求用户编译内核的时候必须提供一个类似于elf文件加载器的文件格式加载器,因为可执行文件的执行前必须映射到用户空间,因此用户 必须提供这样的接口然后编译进内核作为内核与用户空间的加载接口,这种意义上脚本文件格式和二进制文件格式是不对称的,从下面的代码我们可以看出来,
这个load函数就是script的load函数:
static int load_script(struct linux_binprm *bprm,struct pt_regs *regs)
{
char *cp, *i_name, *i_arg;
struct file *file;
char interp[BINPRM_BUF_SIZE];
int retval;
if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!') || (bprm->sh_bang))
return -ENOEXEC;
bprm->sh_bang = 1;
allow_write_access(bprm->file);
fput(bprm->file);
bprm->file = NULL;
bprm->buf[BINPRM_BUF_SIZE - 1] = '/0';
if ((cp = strchr(bprm->buf, '/n')) == NULL)
cp = bprm->buf+BINPRM_BUF_SIZE-1;
*cp = '/0';
while (cp > bprm->buf) {
cp--;
if ((*cp == ' ') || (*cp == '/t'))
*cp = '/0';
else
break;
}
for (cp = bprm->buf+2; (*cp == ' ') || (*cp == '/t'); cp++);
if (*cp == '/0')
return -ENOEXEC; /* No interpreter name found */
i_name = cp;
i_arg = NULL;
for ( ; *cp && (*cp != ' ') && (*cp != '/t'); cp++);
while ((*cp == ' ') || (*cp == '/t'))
*cp++ = '/0';
if (*cp)
i_arg = cp;
strcpy (interp, i_name);
retval = remove_arg_zero(bprm);
if (retval)
return retval;
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);
if (retval) return retval;
bprm->argc++;
bprm->interp = interp;//脚本的解释器,大多数情况是elf文件,比如#!/bin/sh和#!/bin/perl一样,但是还可以是疯狂的PE文件,我就想玩一把
file = open_exec(interp);//打开文件,必须可执行,否则出错
if (IS_ERR(file))
return PTR_ERR(file);
bprm->file = file;
retval = prepare_binprm(bprm);
if (retval < 0)
return retval;
return search_binary_handler(bprm,regs);//最终还是要加载脚本的解释器然后使得该脚本作为解释器的参数
}
从 最后的search_binary_handler可以看出脚本的load函数并不是最终的,而必须载入脚本的解释器,而解释器多是二进制文件,其 load函数可以最终载入,并映射进进程地址空间。在linux的早期版本中,不但提供了上述makefile中的文件格式,还提供了java文件格式的 binfmt结构可是后来取消了,取消是合理的,毕竟java没有什么特殊之处,不值得为它提供一个binfmt结构,如果提供的话,别的语言就有意见 了,所以干脆把这个功能提供给用户,那里热闹去耍吧,于是misc_format出现了,用户可以mount一个特殊文件系统,然后自己设置解释器和文件识别标示,这样用不着内核的参与用户就可以实现各式各样的可执行文件格式了,前提是只要有解释器,关于misc_format的细节就不多说了,没啥意思,无非就是用户一旦设置好了就触发一个系统调用,该系统调用注册一个结构,而这个结构在misc_format的加载函数里面用来查找解释器,就这么多东西,猜也猜到了
事实上也就是这样。
由上可以看出,linux提供了灵活的可执行文件支持机制,用户可以很不受约束的选择和定制,这就是各种脚本,解释型语言在linux平台茁壮成长的原因吧。