我们利用gcc编译生成可执行文件a.out,在Terminal中输入./a.out即可执行成功,就能看到想要的结果。
./a.out可执行程序运行过程可以按一下几个步骤来概括。
当我们在Terminal(Ubuntu中图形界面形式的命令行,另外还有字符界面命令行console—可用Ctrl+Alt+F1~6切换)输入字符串时,bash回去解析输入的字符串,是属于外部可执行文件还是属于bash内建命令。
我们常见一些命令,比如echo、cd等,一些内置命令(内建命令)可以通过外部文件替代来实现同样的功能,比如echo.判断一个命令是否属于bash内建命令,可以用type指令。
对于内部命令(内建命令),bash直接执行其自身内部代码即可,快速无负担。 但对于外部文件,就比较麻烦些。
如果外部可执行文件,bash采用默认的方式:当我们输入./a.out并摁下回车的时候,bash会调用系统调用fork一个子进程,在子进程中执行a.out,bash一直等待,直到子进程执行结束。
还有另外一种方式,我们不用fork一个新的子进程,直接在bash命令行中输入:exec ./a.out,摁下回车就可以了,不过当a.out程序执行结束之后,执行命令的teminal也会关闭掉,因为该命令将shell进程替换掉了。
(调用sys_execve)进行一些参数的检查复制后,调用do_execve。
在do_execve() 函数中会检查文件是否存在,然后调用prepare_binprm()函数完成对文件前128字节的读取,目的是判断可执行文件的格式,每种可执行文件的开头几个字节都是很重要的,尤其是前四个字节,常常被称为魔数字(Magic Number),通过对前四个字节的判断,可以确定文件的类型和格式, 例如, ELF可执行文件的头4个字节为(分别为0X7f、0X45、0X4c、0X46,第一个对应ASCII字符里面的DEL控制符,后面3个字节是ELF这3个字母的ASCII码),我们可以用命令 readelf -h a.out
看得到:
如果被执行的是shell脚本或者python等解释型语言的脚本,那么第一行通常是(十六进制内容。。。),”#!/bin/sh”或”#!/usr/bin/python”,这时候前两个字节”#”和”!”构成构成了魔数,系统一旦判断到这两个字节,就对后面的字符串解析,以确定具体的解释程序的路径。(相关图片)(为什么要读128个字节那么多?)
在search_binary_handler()函数中,根据do_execve() 读取到的128个字节,根据魔数(两个根据导致句子不通顺),判断可执行文件的格式,然后搜索与匹配与可执行文件格式相匹配的装载程序。Linux支持的可执行文件格式都有相应的装载处理程序,ELF可执行文件格式的装载处理程序是load_elf_binary(), shell可执行脚本的装载处理程序是load_script()。这儿我们重点关注load_elf_binary()。
load_elf_binary()函数是我们的重点分析对象,在该函数中完成了程序执行所需要的主要准备工作,函数的主要工作可以分为下面几个部分:
>
// fs/binfmt_elf.c
// 为ELF可执行文件的各个程序头表分配空间
size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr);
elf_phdata = (struct elf_phdr *) kmalloc(size, GFP_KERNEL);
if (!elf_phdata)
goto out;
//读取ELF可执行文件的各个程序头表
retval = kernel_read(bprm->file, loc->elf_ex.e_phoff, (char *) elf_phdata, size);
……
//如果当前程序头表是.interp,读去.interp表对应”segment”中的内容,即,动态连接器的地址
if (elf_ppnt->p_type == PT_INTERP){
….
elf_interpreter = (char *) kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
if (!elf_interpreter)
goto out_free_file;
//elf_interpreter存放的是动态链接器的地址
retval = kernel_read(bprm->file, elf_ppnt->p_offset, elf_interpreter, elf_ppnt->p_filesz);
….
//
struct file *interpreter = open_exec(elf_interpreter);
….
}
>
// 如果动态链接器地址不为空,说明是动态连接器
if (elf_interpreter){
if (interpreter_type == INTERPRETER_AOUT){
elf_entry = load_aout_interp(&loc>interp_ex, interpreter);
}else{
elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_load_addr);
if (BAD_ADDR(elf_entry)){
printk(KERN_ERR “Unable to load interpreter %.128s\n”, elf_interpreter);
force_sig(SIGSEGV, current);
retval = -ENOEXEC;
goto out_free_dentry;
}
reloc_func_desc = interp_load_addr;
allow_write_access(interpreter);
fput(interpreter);
kfree(elf_interpreter);
// 如果静态链接,入口点设置为可执行文件的头中的e_entry.
} else {
elf_entry = loc->elf_ex.e_entry;
}
….
// 设置入口点
start_thread(regs, elf_entry, bprm->p);
/*******include/asm-x86_64/processor.h******/
// 设置入口点的详细代码
//#define start_thread(regs,new_rip,new_rsp) do { \
asm volatile("movl %0,%%fs; movl %0,%%es; movl %0,%%ds": :"r" (0)); \
load_gs_index(0); \
// 将new_rip, 也就是elf_entry设置为系统调用返回后的地址
(regs)->rip = (new_rip); \
(regs)->rsp = (new_rsp); \
write_pda(oldrsp, (new_rsp)); \
(regs)->cs = __USER_CS; \
(regs)->ss = __USER_DS; \
(regs)->eflags = 0x200; \
set_fs(USER_DS); \
} while(0);
完成以上工作后,当sys_execve()从内核态返回用户态时,EIP寄存器直接跳转到ELF程序的入口地址了,于是新的程序开始执行了,ELF可执行文件的加载结束。
当程序执行结束后,fork系统调用返回,也就意味着bash进程fork的子进程结束,返回到进程bash, 等待下一个命令的输入。
[1]bash执行命令各种情况分析
[2]程序员的自我修养. 俞甲子,石凡,潘爱民著