程序装载与进程结构

本文主要介绍在linux系统上,可执行文件如何变为内存中进程的整个流程。

0x00 程序装载

首先,一个终端始终处于等待执行的状态,它其实就是一个进程,当我们输入ls、mv和cp等命令时,它会解析命令并执行相应的命令,这些命令都是可执行文件,它们被存储在不同的文件目录中,一般这些文件都具有全局变量的属性,这是因为我们设置好了它们的环境变量,如下所示:

 echo $PATH

image-20240414214040335

查看全局环境变量时,输入env命令即可。接下来证明一下ls、cp和mv等命令都属于可执行文件,当我们使用这些命令时,操作系统会优先从上面的环境变量中去找,我们可以查看ls命令对应的可执行文件在哪个位置:

 which ls

image-20240414221754512

/usr/bin/这个目录是存在于上面的环境变量里面的,接下来我们就进入/usr/bin/目录下查看ls文件

image-20240414222059484

image-20240414222135566

同样的mv和cp命令对应的可执行文件也可以在这个目录下面查找到

image-20240414222303201

ok,既然我们可以将环境变量中的可执行文件作为bash终端的一个命令,那么我们同样也可以将我们自己编写的程序作为终端的命令,刚开始我的想法是将我desktop上的一个可执行文件移入到/usr/bin/目录中,但是得到了下面的结果:

image-20240414223759560

具体的原因我也不是很清楚,可能是与mv这个程序本身设置有关吧。后面我又换了一种想法,那就是将我的桌面环境添加到环境变量"$PATH"中去,试试这样可不可行,如下所示:

image-20240414233423697

环境变量已经添加完毕,接下来我创建了一个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;
 }

将其编译并运行:

image-20240414234517201

ok,理论上来说,我们可以在任意的目录下输入"aaaa"这个命令,最后终端会输出"3",测试如下:

image-20240414234722041

测试成功,解释完毕。既然这样的话,那么是否会有这么一个问题:为什么我们需要在当前目录下的可执行文件前面加上"./"符号才能运行?其实在前面加"./"表示的意思是当前目录,我们在终端输入命令其实是给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。 先放几张进程结构的图:

image-20240415205844966

image-20240415211640615

(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不在主存而在闪存或者磁盘中,对应项为辅存中地址。

image-20240415214305651

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

image-20240415205905158

参考文章:

存储器层次结构(五)虚拟机与虚拟存储--页表、TLB(详细图解) - 知乎 (zhihu.com)

Linux进程结构_进程几个重要的结构体-CSDN博客

是时候了解下 mmap 了 (qq.com)

进程的创建与可执行程序的加载 - Firewalls - 博客园 (cnblogs.com)

【UNIX】从一个可执行文件的生成到进程在内存中分布 (中)/文件到进程的转变_一个可执行文件怎么变成进程-CSDN博客

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值