a.out程序执行的开始与结束

来源:互联网 发布:绣花机打版软件 编辑:程序博客网 时间:2024/06/08 04:05

简述

我们利用gcc编译生成可执行文件a.out,在Terminal中输入./a.out即可执行成功,就能看到想要的结果。
./a.out可执行程序运行过程可以按一下几个步骤来概括。

  • bash分析指令
  • bash调用fork()生成子进程,并等待子进程结束
  • sys_execve()
  • do_execve()
  • search_binary_handler()
  • load_elf_binary()
  • exit()子进程结束
    本文中涉及的内核源码来自linux-2.6.11

bash的工作

当我们在Terminal(Ubuntu中图形界面形式的命令行,另外还有字符界面命令行console—可用Ctrl+Alt+F1~6切换)输入字符串时,bash回去解析输入的字符串,是属于外部可执行文件还是属于bash内建命令。

bash内建命令

我们常见一些命令,比如echo、cd等,一些内置命令(内建命令)可以通过外部文件替代来实现同样的功能,比如echo.判断一个命令是否属于bash内建命令,可以用type指令。


对于内部命令(内建命令),bash直接执行其自身内部代码即可,快速无负担。 但对于外部文件,就比较麻烦些。

bash外部可执行文件

如果外部可执行文件,bash采用默认的方式:当我们输入./a.out并摁下回车的时候,bash会调用系统调用fork一个子进程,在子进程中执行a.out,bash一直等待,直到子进程执行结束。


还有另外一种方式,我们不用fork一个新的子进程,直接在bash命令行中输入:exec ./a.out,摁下回车就可以了,不过当a.out程序执行结束之后,执行命令的teminal也会关闭掉,因为该命令将shell进程替换掉了。

fork -> sys_execve

(调用sys_execve)进行一些参数的检查复制后,调用do_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()

在search_binary_handler()函数中,根据do_execve() 读取到的128个字节,根据魔数(两个根据导致句子不通顺),判断可执行文件的格式,然后搜索与匹配与可执行文件格式相匹配的装载程序。Linux支持的可执行文件格式都有相应的装载处理程序,ELF可执行文件格式的装载处理程序是load_elf_binary(), shell可执行脚本的装载处理程序是load_script()。这儿我们重点关注load_elf_binary()。

load_elf_binary

load_elf_binary()函数是我们的重点分析对象,在该函数中完成了程序执行所需要的主要准备工作,函数的主要工作可以分为下面几个部分:

  • 检查ELF可执行文件格式的有效性;
  • 寻找动态链接的”.interp”段,获取动态链器的路径。(下方代码块内容是。。。)

>
// 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);
….
}

  • 根据ELF可执行文件的程序头表描述,对ELF文件进行映射,比如代码段,数据断等;
  • 初始化ELF进程环境,比如进程启动时EDX的寄存器的地址是DT_FIN的地址;
  • 将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,如果是静态
    链接,这个入口点就是ELF文件中e_entry所指的地址;如果是动态链接,程序的入口点是动态链接器。

>
// 如果动态链接器地址不为空,说明是动态连接器
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]程序员的自我修养. 俞甲子,石凡,潘爱民著

0 0