可执行程序的装载

简介: 可执行程序的装载

1.程序和进程


程序(或者狭义上讲的可执行文件)是一个静态的概念,它就是一些预先编好的指令和数据集合的一个文件。


进程则是一个动态的概念,它是程序运行时的一个过程,很多时候把动态库叫做运行时(Runtime)也是有一定的含义。


我们知道程序被运行起来后,它将拥有自己独立的虚拟地址空间(Virtual Address Space)


这个虚拟地址空间的大小由计算机的硬件平台决定。具体来说由 CPU 的位数决定的。硬件决定了地址空间的最大理论上限,即硬件寻址空间大小。


比如 32 位的硬件平台决定了虚拟空间地址为 0~到 2^32 - 1(0x00000000~0xFFFFFFFF),也就是常说的 4G 虚拟空间。


那么 32 位平台下的 4GB 虚拟空间,我们程序是否可以任意使用呢?答案是不行。不同的平台架构略有差异。


默认情况下,Linux 操作系统中进程的虚拟地址空间分配如下:


image.png


2.装载


程序执行时所需要的指令和数据必须在内存中才能够正常运行,最简单的方法就是讲程序运行所需要的指令和数据全都装入内存,这样程序就可以正常运行,这是最简单的静态装入的办法。


但是很多情况下,程序所需要的内存数量大于物理内存的数量,当内存的数量不够时,根本办法就是添加内存。相对磁盘来说,内存是昂贵切稀有的。


所以人们想尽各种办法,希望能在不添加内存的情况下,尽可能的让更多的程序运行起来,有效的利用内存。


后来研究发现,程序运行时是有局部性原理,所以我们讲程序最常用的部分驻留在内存中,而将一些不常用的数据存放在磁盘里。这就是动态装入的基本原理。


2.1装载方式


覆盖装入页映射是两种典型的动态装载方式。他们采用的思想基本上差不多,原则上都是利用的局部性原理。程序需要哪个模块,就将哪个模块装入内存,如果不用,就存放在磁盘中。


覆盖装入:现在几乎被淘汰。本文暂不做过多介绍。


页映射:将内存和所有磁盘中的数据和指令按照”页(page)“ 为单位划分若干个页,以后所有的装载和操作的单位就是页。目前硬件规定的页的大小有 4096 字节、8192 字节、2MB、4MB 等


以下演示页映射的基本机制,假设我们的 32 位机器有 16KB 的内存,每个页大小为 4096 字节,则共有 4 个页:


页编号
地址
F0
0x00000000~0x00000FFF
F1
0x00001000~0x00001FFF
F2
0x00002000~0x00002FFF
F3
0x00003000~0x00003FFF


假设所有的指令和数据总和为 32KB,那么程序总共被分为 8 个页。我们将其编号为 P0~P7。很明显 16KB 的内存无法同时将 32KB 的程序装入,那么将按照动态装入的原理。来进行整个过程。


如果程序刚开始执行时的入口地址在 P0,这是装载管理器发现程序 P0 不在内存中,于是将 F0 分配给 P0,并且将 P0 的内容装入 F0;运行一段时间后程序要用到 P5,于是将 P5 装载到 F1;就这样当程序用到 P3 和 P6,分别装入了 F2 和 F3.

很明显,如果程序只需要 P0、P3、P5 和 P6,那么程序就能一直运行下去。但是如果此时程序需要访问 P4,那么装载器必须做出决策,它必须放弃正在使用的 4 个内存页中的其中一个来装载 P4.


至于选择哪个页,我们有很多算法。比如可以选择 F0,它是第一个被分配掉的内存页(这个算法我们称之为FIFO,先进先出);假设装载管理器发现 F2 很少被访问,那么我们可以选择 F2(这种算法可以称之为LUR,最少使用算法)


image.png


2.2linux 内核装载 ELF 过程简介


当我们在 linux 系统的 bash 下输入一个命令执行某个程序时, linux 是怎样装载这个 ELF 文件并执行它呢?


首先在用户层,bash 进程会调用 fork()系统调用,创建一个新的进程,然后新的进程调用 execve()系统调用执行指定的 ELF 文件。原先的 bash 进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。


execve 函数原型如下,它的 3 个参数分别是被执行的可执行文件名、执行参数和环境变量。glibc 对 execve 系统调用进行了包装,提供了 execl()、execlp()、execlp()、execle()、execvp()等 5 种不同的形式


/* unistd.h */
int execve(const char * filename, char *const argv[], char *const envp[] );


下面是简单的使用 fork 和 execlp()实现的 minibash:


image.png


在进入 execve 系统调用之后,linux 内核开始真正的装载工作。在 linux 内核中,execve()系统调用的相应入口是 sys_execve().


sys_execve    
-> do_execve:查找被执行文件,读取前128字节(区分不同的ELF文件,#!/bin/sh,#!/usr/bin/python)       
-> search_binary_handle:搜索和匹配合适的可执行文件装载处理过程           
-> load_elf_binary:可执行文件的装载处理过程。这里只关心ELF文件的装载


load_elf_binary 函数完成的步骤主要如下:


  • 检查ELF文件格式的有效性,比如魔术、程序头表中的段(Segment)的数量。


  • 寻找动态链接的".interp"段,设置动态链接路径。


  • 根据ELF可执行文件的程序头描述,对ELF文件进行映射,比如代码、数据和只读数据。


  • 初始化ELF进程环境。


  • 系统调用的返回地址改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的ELF可执行文件,这个程序入口就是ELF文件头中e_entry所指的地址;对于动态链接,程序入口点是动态链接器。


当load_elf_binary()执行完毕,返回至do_execve(),在返回至sys_ececve。上面的第五步已把系统调用的返回地址改成了被装载程序的入口地址。所以当sys_execve()系统调用从内核态返回用户时,EIP寄存器之间跳转到了ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件转载完毕。


3.总结


本文简单介绍 linux 下ELF 文件的装载过程,真实过程远比这复杂。要理解装载,不得不提操作系统,其中涉及到系统调用、虚拟空间、MMU、以及动态静态链接。

延申阅读:《程序员的自我修养》、《操作系统导论》

相关文章
[06-03] 用MASM32写的文件目录监视程序FileDirMon
[06-03] 用MASM32写的文件目录监视程序FileDirMon
|
7月前
|
算法 网络协议 Linux
Linux模块文件编译到内核与独立编译成.ko文件的方法
Linux模块文件编译到内核与独立编译成.ko文件的方法
1893 0
|
C++
【c++】c++ 编译链接成的可执行程序 执行时却表示无法找到某个或几个库
问题描述:c++ 程序已经完成了编译链接,但是在执行时,提醒说某个 库 地址找不到,无法启动进程服务。 使用 ldd 命令 查看执行程序 可以看到 存在 某个库 显示 not find
118 0
|
Linux
Linux环境显式使用动态库
Linux环境显式使用动态库
145 0
|
编译器 Linux C语言
在C语言/C++中把资源编译进exe可执行文件,并运行时释放资源
在C语言/C++中把资源编译进exe可执行文件,并运行时释放资源
407 0
玩转Makefile | 编译有共用文件的多个程序
玩转Makefile | 编译有共用文件的多个程序
298 0
玩转Makefile | 编译有共用文件的多个程序