链接装载(一)虚拟地址与物理地址
文章目录
- 一、基本概念
- 二、一个基本问题
- 三、程序的执行
- 四、从堆中分配的数据的逻辑地址
一、基本概念
当我们写出一个程序,即便是最基本的 Hello World,都需要经过 预处理、编译、汇编、链接才能生成最终的可执行文件。
预处理:
预处理过程主要处理源代码中以 #
开始的预编译指令,主要处理如下:
- 将所有的
#define
删除,并展开所有宏定义 - 处理所有的预编译指令,比如
#if、#ifdef、#endif
等等 - 处理
#include
,将所包含的文件的内容插入到该指令位置,该过程递归进行 - 删除所有注释
- 添加行号和文件名标识
编译:
将预处理好的文件经过一系列的词法分析、语法分析、语义分析产生汇编代码
汇编:
将汇编代码转为机器指令
链接:
将不同的机器指令文件链接形成一个可执行文件
二、一个基本问题
我们在学习过OS之后,都知道进程拥有自己的虚拟地址空间,需要经过页表映射到物理地址空间。那么,计算机在真正执行代码的时候,是如何将一个变量与一个逻辑地址相关联起来的呢?
初步的理解是,程序在经过编译、链接之后,就已经以虚拟地址的形式存在了,因此操作某个变量的代码就转换成了操作该变量虚拟地址的机器指令。
那链接到底是什么呢?
随着程序的日益增大,程序被划分为很多个独立的模块,将不同的模块组合为一个完整的程序,实际上是模块之间的组合与通信。而模块间的通信方式一种是模块间的函数访问,一种是变量访问,而这两种都需要知道目标的地址,其实就是对某个符号的虚拟地址的寻找,即某个模块把对引用符号相关的逻辑转换为对其虚拟地址操作的过程
。通俗的理解,这种寻找某个符号虚拟地址的过程就是链接。
链接的主要作用:地址和空间分配、符号绑定(地址绑定)、重定位
链接的目标是目标文件,目标文件经过编译、汇编产生。某个源文件编译时,已经将自身定义好的各种符号转换为了针对寄存器和地址偏移量的操作的序列(即汇编代码中并不存在我们定义的那些变量,而是转换为了各种寄存器和地址的读取存放操作,可以理解为对该符号虚拟地址的操作),但如果目标文件中有的符号来自其他目标文件,那么编译时无法为其生成偏移,因此会特殊标记,等链接的时候再将这个标记转换为对其虚拟地址的操作。(偏移量其实就相当于是虚拟地址)
因此产生另外一个问题,编译、链接的过程就已经完成虚拟地址的分配了吗?
初步理解是的,除了堆上malloc的(可能还有别的)一些需要运行时动态申请的内存的地址未定之外,其他的应该都已经在编译、汇编的过程中分配好了。malloc动态申请的也是一个固定区间内的,因为堆的大小和起始虚拟地址是确定的。
但是,这感觉还是有些别扭,不应该是程序在执行时,由OS分配虚拟地址空间,在加载程序的过程中为各种变量确定具体的虚拟地址吗? 如果提前在编译时虚拟地址就已经分配完毕,那如使用 mmap 预留一部分虚拟地址的操作是如何实现的呢?
我们从程序在OS上的具体执行过程中来寻找答案。
三、程序的执行
在OS上,我们运行一个程序,会创建一个对应的进程,然后加载可执行文件,执行其逻辑。此时OS要进行:
- 创建独立的虚拟地址空间
- 读取可执行文件头,建立虚拟地址空间与可执行文件的映射
- 将CPU的指令寄存器设置为可执行文件的入口地址。
创建虚拟地址空间
创建虚拟空间其实只是创建一个页表,此时页表为空。整个页表表示进程的虚拟地址空间,因此可执行文件中的确定好的虚拟地址会放到对应的虚拟地址的页表项中。即进程的虚拟地址空间的地址和可执行文件的虚拟地址是等价的。
与可执行文件进行映射
可执行文件中,某个段的虚拟地址为 0x12345678,那么其对应进程虚拟地址空间的地址也是0x12345678。
因此可执行文件头部中包含main
函数的虚拟地址,将程序加载后,将cpu的指令寄存器设置为 main
函数的虚拟地址,此时去查找页表,发生缺页中断,从磁盘中读取可执行文件的对应内容,然后开始执行main函数。
因此,编译好的可执行文件可以理解为从0x0虚拟地址开始编排,每个变量相对文件头的偏移量就是其虚拟地址。
OS创建虚拟地址空间,也是创建一个4G(假设32位)大小的空间,从0x0 开始和可执行文件一一对应,因此变量或者函数在可执行文件中的偏移量就是其在进程中的虚拟地址。所以,OS的创建的进程虚拟地址空间和可执行文件的虚拟地址是映像
的关系。(其实也不是完全的等价,因为可执行程序通常很小,用不掉3G的空间,因此进程的虚拟地址空间只有一部分用来存储可执行文件,其余部分的地址要么没有使用,要么被当作堆等等)
因此程序的执行,可以看作将磁盘中的可执行文件拷贝到内存中,然后对可执行文件进行修修改改。
只是为了提高内存利用率,引入了页表。实现逻辑上将可执行文件全部拷贝到了内存中,但实际上只是拷贝了需要的部分到内存。
那此时mmap的疑问还是没有得到解答,mmap如果映射一段虚拟地址,但是这段虚拟地址是已经被使用的怎么办呢? 或者使用mmap预留一段虚拟地址,但这块虚拟地址对应数据段或者代码段怎么办?
其实这个问题应该是不存在的,每个进程会专门预留一个段空间,即一部分虚拟地址空间专门用来进行映射,因此mmap函数所使用的虚拟地址都在该区域内部。
从这里的我们也可以得知。mmap的原理就是创建新的页表项,并与某个文件关联。
那mmap函数调用的时候,传入一个addr,这个addr会在mmap调用内部进行判断,如果小于内存映射区域的最小地址,会将其改为当前可用的最小地址。
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
if (!(file && path_noexec(&file->f_path)))
prot |= PROT_EXEC;
/* 假如没有设置MAP_FIXED标志,且addr小于mmap_min_addr, 因为可以修改addr,
所以就需要将addr设为mmap_min_addr的页对齐后的地址 */
if (!(flags & MAP_FIXED))
addr = round_hint_to_min(addr);
四、从堆中分配的数据的逻辑地址
在程序中,堆从某个地址开始,我们通过malloc等系统调用可以从堆中申请一个虚拟地址,那么这个虚拟地址我们是在程序运行时才会得知的,且该块虚拟地址并不会和可执行文件中的内容进行映射,因此需要通过read系统调用,从磁盘中读到内核,再由内核拷贝到该虚拟地址对应的物理地址中。所以,为了减少拷贝,才有了mmap。
而我们虽然只有在运行时才可以得到从堆中分配的内存的具体的虚拟地址,但是malloc返回的指针的虚拟地址我们是在编译时就可以确定的,只是其一开始的值为0,只有malloc调用之后,才会将分配内存的虚拟地址存储到该指针中,我们使用该指针,就间接的使用了这段内存。