进程中的地址是从何而来

  1. 1.可执行文件的生成过程
    1. 1.1.目标文件生成
    2. 1.2.可执行文件的生成
    3. 1.3.虚拟地址空间是独立的
  2. 2.可执行文件的执行过程

写了这么多年代码,地址这个东西每天都会使用,那么今天总结一下地址这个东西的由来。
本文参考了参考了《程序员的自我修养》一书.

先看看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <cstdint>

void fun()
{
std::cout<<"this a fun() "<<std::endl;
}

uint32_t a = 1;

int main()
{
uint32_t b = 2;
std::cout<<a<<std::endl;
std::cout<<b<<std::endl;
std::cout<<(void*)fun<<std::endl;
std::cout<<&b<<std::endl;

return 0;
}

执行结果如下:

1
2
3
4
1
2
0x4008ed
0x7fff391dd54c

1.可执行文件的生成过程

学过编译原理(没学过也行)都知道,可执行文件的生成过程包括:预编译,编译,汇编,链接;最终生成操作系统可直接装载执行的文件。

现在我们分析一下其中的两个重要环节:编译和链接。

1.1.目标文件生成

目标文件生成最终生成是通过Assembler完成的,这里我们不过多的深究汇编器的工作原理,我们只需要知道源代码通过预编译,编译和汇编生成了机器码,也就是目标文件。
目标文件在我们的工作过程中是很常见的一种编译中间过程(对于大型工程项目),我们首先看一下上面示例代码生成的’test.o’文件的内容。
在这之前我们需要知道linux下的目标文件的结构。
Linux下的可执行文件格式简称ELF,包括目标文件(‘.o’, 又叫可重定位文件), 可执行文件,共享目标文件(‘.so’)和核心转储文件(P57)

ELF文件本身由很多段组成,当然还包括:ELF文件头,段表(section header table), 符号表,重定位表,字符串表等结构。看过《C专家编程》的人应该都知道ELF文件的各个段,我应该也是在上学时通过这本书了解到ELF文件结构的,后来通过《程序员的自我修养》得到了深入了解。
关于elf文件中各个section的含义(P67)如下,:

段表是ELF中非常重要的一个结构,ELF中各个段都是段表来决定的,编译器,链接器和装载器都是通过Section Header table来定位和访问各个段的属性的。
说了那么多我们看一下编译生成的目标文件’test.o’的段表吧,可以通过readelf和objdump查看ELF文件结构和内容信息。

Objdump列出了目标文件的段表信息,可以看到程序的代码段size为:0xFF,文件偏移为:ox0040, 在text段后面的是data段,size为ox04(全局变量a),文件偏移为ox0140。
这里需要注意的是VMA和LMA两个字段, 分别代表:Virtual Memory Address和Load Memory Address,虚拟地址和装载地址(服务器开发中可以认为一致)的值全部都为0.我们知道:**程序是运行在虚拟地址空间中
说了那么多,就是要说一点:**编译生成的目标文件中,并没有分配虚拟空间地址**。
其实objdump没有列出目标文件的所有section的信息,可以通过readelf工具列出目标文件的段表的信息。

1.2.可执行文件的生成

编译生成目标文件后,就需要调用系统的链接器将目标文件进行链接生成可执行文件。处于简单考虑,这里以**静态链接**进行解释。
现在链接器对于多个目标文件进行链接时,都是采用相似段合并来进行操作,如下图:

现在的链接器一般采用**两步链接**方法进行链接:

  • **空间与地址分配**;
    这里空间与地址的分配有两层含义:
  • 输出的可执行文件的空间,即生成可执行文件;
  • 分配程序加载后的虚拟地址空间,即分配进行运行时使用的虚拟地址;
    这里需要强调的是:**虚拟地址的分配只是对段表中的各个段的起始虚拟地址进行初始化,不会对代码段中的指令使用的地址进行修改(这个修改在两步链接的第二步才会进行)**。
    可以看到链接后生成的可执行文件的各个段的VMA已经分配了虚拟地址:

这里需要知道:64位操作系统程序分配(加载)的**虚拟起始地址为0x400000,32位系统程序分配(加载)的虚拟起始地址为0x8048000**。

  • **符号解析与重定位**;
    链接器为各个段分配好起始虚拟地址后,接下来要做的就是对代码指令进行修正,使可执行文件中所有使用的地址都为虚拟地址,而非目标文件中的相对偏移地址。指令修正的过程使用到了目标文件中的段表,符号表,重定位表等等,这里目前没有深入了解,这些也都是静态链接的东西,本文旨在解释虚拟地址的由来,所以不过多深入。

图1是目标文件的反汇编后的代码,可以看到里面指令全是相对偏移,指令参数都是假地址。图2链接生成完整的可执行文件中,指令偏移全部都是虚拟地址,指令参数中的变量和函数地址都替换成了真正的虚拟地址。
可以运行调试源代码生成的ELF文件,如下:

1
2
3
4
5
6
7
8
9
10
Breakpoint 1, main () at test.cpp:37
37 return 0;
(gdb) p a
$1 = 1
(gdb) p &a
$2 = (uint32_t *) 0x601078 <a>
(gdb) p &b
$6 = (uint32_t *) 0x7fffffffe40c
(gdb) p fun
$7 = {void (void)} 0x4008ed <fun()>

输出a的地址和链接生成的可执行文件中的a的地址相吻合,fun函数的地址也吻合。

1.3.虚拟地址空间是独立的

可执行文件生成后,我们需要知道一点:**进程的虚拟地址空间是独立的,进程能够使用除了OS内核区域外的所有地址空间**。这里的独立是指进程内部使用的虚拟地址和其他进程没有关系,一个进程可以使用4GB(32位OS)中除去OS内核使用的其他所有虚拟地址空间,每个进程都是这样。那么进程间是怎么来进行隔离的呢,这个可以在下一节,进程的执行过程中,简单的介绍。

下面是进程地址空间的简易结构图:每个可执行文件生成的时候,虚拟地址空间都会从**0x400000开始分配(64位),32位为0x8048000**。

2.可执行文件的执行过程

由上面的阐述我们知道了,**编译链接生成可执行文件中的已经生成了虚拟地址,就是进程执行过程中的加载和使用的地址。那么进程的执行最终还是需要加载到物理内存上,所以运行过程中最重要的逻辑就是虚拟地址到物理地址的映射了**。
下面简单阐述一下,elf文件的执行过程:

  • 创建进程的虚拟地址空间
    这个过程只是创建一个最重要的**页表的数据结构,用于将虚拟地址空间映射到物理地址空间。
    页表,
    每个进程都有一个页表项**,进程页表用于逻辑页对应的物理页的映射。这里忽略操作系统如何分页,已经如何加载文件的过程。进程加载时会根据条件选择加载一些页到内存,当访问的页通过页表发现该页不在内存中,则发生缺页终端,进行页的加载。

  • 读取ELF文件头,建立虚拟空间和可执行文件的映射关系

  • 将寄存器设置为程序启动的入口地址,启动执行

【1】http://mqzhuang.iteye.com/blog/901602
【2】http://www.cnblogs.com/zy691357966/p/5525684.html