本文主要介绍在linux系统上,可执行文件如何变为内存中进程的整个流程。
0x00 程序装载
首先,一个终端始终处于等待执行的状态,它其实就是一个进程,当我们输入ls、mv和cp等命令时,它会解析命令并执行相应的命令,这些命令都是可执行文件,它们被存储在不同的文件目录中,一般这些文件都具有全局变量的属性,这是因为我们设置好了它们的环境变量,如下所示:
echo $PATH
查看全局环境变量时,输入env命令即可。接下来证明一下ls、cp和mv等命令都属于可执行文件,当我们使用这些命令时,操作系统会优先从上面的环境变量中去找,我们可以查看ls命令对应的可执行文件在哪个位置:
which ls
/usr/bin/这个目录是存在于上面的环境变量里面的,接下来我们就进入/usr/bin/目录下查看ls文件
同样的mv和cp命令对应的可执行文件也可以在这个目录下面查找到
ok,既然我们可以将环境变量中的可执行文件作为bash终端的一个命令,那么我们同样也可以将我们自己编写的程序作为终端的命令,刚开始我的想法是将我desktop上的一个可执行文件移入到/usr/bin/目录中,但是得到了下面的结果:
具体的原因我也不是很清楚,可能是与mv这个程序本身设置有关吧。后面我又换了一种想法,那就是将我的桌面环境添加到环境变量"$PATH"中去,试试这样可不可行,如下所示:
环境变量已经添加完毕,接下来我创建了一个test.c文件,并将其编译成名为"aaaa"的可执行文件,代码如下:
#include<stdio.h> int add(int x,int y){ return (x+y); } int main(){ int a=1,b=2; printf("%d\n",add(a,b)); return 0; }
将其编译并运行:
ok,理论上来说,我们可以在任意的目录下输入"aaaa"这个命令,最后终端会输出"3",测试如下:
测试成功,解释完毕。既然这样的话,那么是否会有这么一个问题:为什么我们需要在当前目录下的可执行文件前面加上"./"符号才能运行?其实在前面加"./"表示的意思是当前目录,我们在终端输入命令其实是给execve函数传递参数(可以在终端输入可执行文件的完整路径,同样可以运行)。execve函数是什么?它是一个系统调用函数,在linux系统中,一个进程通过fork函数来创建另外一个进程,创建的子进程完全复制父进程的资源,在子进程中通过使用execve系统调用函数来创建一个自己的进程,即将指定的文件名或者路径名找到可执行文件,并用来取代从父进程fork过来的数据段、代码段和堆栈段,在执行完之后,原调用的进程内容除了进程号之外,其他全部被替换了,最终可执行文件也就变为了进程。
上面介绍了可执行文件变为进程的整体过程,接下来介绍可执行文件装载的过程。 装载可执行文件一般采用覆盖装入(overlay)和页映射(paging)两种方法,目前主要使用页映射。最开始我们创建一个进程,然后装载相应的可执行文件并执行,最开始只需要做三件事情: ①创建一个独立的虚拟地址空间,主要是分配一个页目录; ②读取可执行文件的头,并且建立虚拟地址空间和可执行文件的映射关系。将可执行文件映射到虚拟地址空间,即磁盘中文件的物理页和进程虚拟地址空间的映射,这样当我们将cpu执行虚拟页中的地址时,会从磁盘中找到并copy至内存中; ③将cpu的指令寄存器设置为可执行文件的入口地址,启动运行。从elf文件中的入口地址开始执行程序。 bash进程调用fork之后创建一个新的进程,然后新的进程调用execve系统调用函数执行指定的elf文件,bash进程继续返回等待新进程执行结束,然后重新等待用户输入命令,execve系统调用函数定义在unistd.h文件中,其原型如下:
int execve(const char* filename, char *const argv[], char *const envp[]);
其中这三个参数分别为可执行文件名,执行参数,环境变量,glibc对execve系统调用进行了包装,提供了execl(),execlp(),execle(),execv().execvp()等五个不同形式的exec系列API,它们只是在调用的参数形式上有所区别,但最终都会调用到execve()这个系统调用函数上。
调用execve系统调用之后,再调用内核的入口sys_execve,sys_execve进行一些参数的检查复制之后,调用do_execve(),因为可执行文件不止elf一种,还有java程序和以"#!"开始的脚本程序等,所以do_execve()会首先检查被执行文件,读取前128个字节,特别是开头4个字节的魔数(Magic),用来判断可执行文件的格式,如果是解释型语言的脚本,前两个字节"#!"就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定程序解释器的路径。当do_execve()读取了这128个字节的文件头部之后,然后调用search_binary_handle()去搜索和匹配合适的可执行文件装载处理的过程。Linux中所有被支持的可执行文件格式都有相应的装载处理过程,search_binary_handle()会通过判断文件头部的魔数确定文件的格式,并调用相应的装载处理过程。如ELF用load_elf_binary()、a.out用load_aout_binary(),脚本用load_script()。其中ELF装载过程的主要步骤是: ①检查ELF可执行文件格式的有效性,比如魔数、程序头表中段(Segment)的数量; ②寻找动态链接的".interp"段(该段保存可执行文件所需要的动态链接器的路径),设置动态链接器的路径; ③根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码、数据、只读数据; ④初始化ELF进程环境,比如进程启动时edx寄存器的地址应该是DT_FINI的地址(结束代码地址); ⑤将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的ELF可执行文件,这个程序入口就是ELF文件的文件头中e_enEry所指的地址,对于动态链接的可执行文件,程序入口点是动态链接器。
当load_elf_binary()执行完毕,返回至do_execve()再返回到sys_execve(),上面的第⑤步中已经把系统调用的返回地址改为了被装载的ELF程序的入口地址。所以当sys_execve()系统调用从内核态返回到用户态时,eip寄存器直接跳转到了ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件装载完成。
0x01 进程结构
前面讲的内容全部都是有关可执行文件装载的过程,我们经常听到可执行文件在内存中以进程的形式存在,进程都有自己的虚拟地址空间。虚拟地址空间其实只是个抽象的概念,实际上虚拟地址空间是一个结构体(由多个vm_area_struct组成),这里我再仔细介绍一下。 Linux进程结构主要涉及三个结构体task_struct、mm_struct、vm_area_struct。 先放几张进程结构的图:
(1) task_struct task_struct是Linux系统中的进程控制块(PCB),内部包含了一个进程所需的各项信息。其中mm_struct *mm则表示进程所拥有的内存空间描述。
struct task_struct { //表示进程当前运行状态 //volatile避免了读取缓存在寄存器中的脏数据,而是直接从内存中取 //-1 不可运行 0 可运行的 >0 暂停状态 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ struct thread_info *thread_info; atomic_t usage; //进程标记符 flags反映进程的状态信息,用于内核标识当前进程的状态 unsigned long flags; /* per process flags, defined below */ unsigned long ptrace; int lock_depth; /* Lock depth */ ... //mm进程所拥有的内存空间描述符,对于内核线程的mm为NULL //activi_mm进程运行时所使用的进程描述符 struct mm_struct *mm, *active_mm; /* task state */ struct linux_binfmt *binfmt; int exit_code, exit_signal; int pdeath_signal; /* The signal sent when the parent dies */ /* ??? */ unsigned long personality; int did_exec:1; //进程的唯一标识 pid_t pid; pid_t __pgrp; /* Accessed via process_group() */ pid_t tty_old_pgrp; pid_t session; //线程组标识符 pid_t tgid; ... };
(2) mm_struct
mm_struct是进程的内存描述,其中说明了代码段、数据段的起始和结束位置,堆的起始结束位置,栈的起始位置,特别注意这里,pgd指针其指向进程的页目录,虚拟地址到物理地址的转换(包括虚拟地址到磁盘中文件的地址、虚拟地址到内存中物理地址) valid为1,表示page在主存中,对应项为物理地址; valid为0,表示page不在主存而在闪存或者磁盘中,对应项为辅存中地址。
struct mm_struct { //vm_area_struct的链表 struct vm_area_struct * mmap; /* list of VMAs */ struct rb_root mm_rb; struct vm_area_struct * mmap_cache; /* last find_vma result */ unsigned long free_area_cache; /* first hole */ //指向进程的页目录,虚拟地址到物理地址的转换 //Linux中每个进程有它自己的PGD(Page Global Directory),它是一个物理页,并包含一个pdg_t数组 pgd_t * pgd; atomic_t mm_users; /* How many users with user space? */ atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */ //vm_area_struct的数量 int map_count; /* number of VMAs */ struct rw_semaphore mmap_sem; spinlock_t page_table_lock; /* Protects task page tables and mm->rss */ struct list_head mmlist; /* List of all active mm's. These are globally strung * together off init_mm.mmlist, and are protected * by mmlist_lock */ //代码段起始、结束位置,数据段起始、结束位置 unsigned long start_code, end_code, start_data, end_data; //堆的起始、结束位置,栈的起始位置,栈因为其性质,只有起始位置 unsigned long start_brk, brk, start_stack; //参数段、环境段的起始结束位置 unsigned long arg_start, arg_end, env_start, env_end; //total_vm映射的page数量 unsigned long rss, total_vm, locked_vm; unsigned long def_flags; cpumask_t cpu_vm_mask; unsigned long swap_address; unsigned long saved_auxv[40]; /* for /proc/PID/auxv */ unsigned dumpable:1; #ifdef CONFIG_HUGETLB_PAGE int used_hugetlb; #endif /* Architecture-specific MM context */ mm_context_t context; /* coredumping support */ int core_waiters; struct completion *core_startup_done, core_done; /* aio bits */ rwlock_t ioctx_list_lock; struct kioctx *ioctx_list; struct kioctx default_kioctx; };
(3) vm_area_struct
在Linux中,每个segment用一个vm_area_struct结构体表示。每一个vm_area_struct的大小为4K的整数倍,即一页的整数倍。vm_area_struct使用链表和红黑树来组织。在进程地址空间中查找vm_area_struct是非常频繁的操作,比如发生page fault时,使用链表找到与特定地址关联的vm_area_struct时,其时间复杂度是O(N),使用红黑树的时间复杂度是O(log2N),尤其在vm_area_struct数量很多的时候,可以显著减少查找所需的时间,同时红黑树是一种非平衡二叉树,可以简化重新平衡树的过程。
struct vm_area_struct { //vm_area_struct所属的mm_struct struct mm_struct * vm_mm; /* The address space we belong to. */ //该虚拟内存空间的首地址 unsigned long vm_start; /* Our start address within vm_mm. */ //该虚拟内存空间的尾地址 unsigned long vm_end; /* The first byte after our end address within vm_mm. */ //vm_area_struct链的下一个成员 /* linked list of VM areas per task, sorted by address */ struct vm_area_struct *vm_next; pgprot_t vm_page_prot; /* Access permissions of this VMA. */ unsigned long vm_flags; /* Flags, listed below. */ //将本vm_area_struct作为一个节点加入到红黑树中 struct rb_node vm_rb; /* * For areas with an address space and backing store, * one of the address_space->i_mmap{,shared} lists, * for shm areas, the list of attaches, otherwise unused. */ struct list_head shared; /* Function pointers to deal with this struct. */ struct vm_operations_struct * vm_ops; /* Information about our backing store: */ unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */ struct file * vm_file; /* File we map to (can be NULL). */ void * vm_private_data; /* was vm_pte (shared mem) */ };
参考文章:
存储器层次结构(五)虚拟机与虚拟存储--页表、TLB(详细图解) - 知乎 (zhihu.com)
进程的创建与可执行程序的加载 - Firewalls - 博客园 (cnblogs.com)
【UNIX】从一个可执行文件的生成到进程在内存中分布 (中)/文件到进程的转变_一个可执行文件怎么变成进程-CSDN博客