写了这么多年代码,地址这个东西每天都会使用,那么今天总结一下地址这个东西的由来。
本文参考了参考了《程序员的自我修养》一书.
先看看下面代码:
#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
0x4008ed
0x7fff391dd54c
1.可执行文件的生成过程
学过编译原理(没学过也行)都知道,可执行文件的生成过程包括:预编译,编译,汇编,链接;最终生成操作系统可直接装载执行的文件。
[img1]
现在我们分析一下其中的两个重要环节:编译和链接。
1.1.目标文件生成
目标文件生成最终生成是通过Assembler完成的,这里我们不过多的深究汇编器的工作原理,我们只需要知道源代码通过预编译,编译和汇编生成了机器码,也就是目标文件。
目标文件在我们的工作过程中是很常见的一种编译中间过程(对于大型工程项目),我们首先看一下上面示例代码生成的’test.o’文件的内容。
在这之前我们需要知道linux下的目标文件的结构。
Linux下的可执行文件格式简称ELF,包括目标文件(‘.o’, 又叫可重定位文件), 可执行文件,共享目标文件(‘.so’)和核心转储文件(P57)。
[img2]
ELF文件本身由很多段组成,当然还包括:ELF文件头,段表(section header table), 符号表,重定位表,字符串表等结构。看过《C专家编程》的人应该都知道ELF文件的各个段,我应该也是在上学时通过这本书了解到ELF文件结构的,后来通过《程序员的自我修养》得到了深入了解。
关于elf文件中各个section的含义(P67)如下,:
[img3]
段表是ELF中非常重要的一个结构,ELF中各个段都是段表来决定的,编译器,链接器和装载器都是通过Section Header table来定位和访问各个段的属性的。
说了那么多我们看一下编译生成的目标文件’test.o’的段表吧,可以通过readelf和objdump查看ELF文件结构和内容信息。
[img4]
Objdump列出了目标文件的段表信息,可以看到程序的代码段size为:0xFF,文件偏移为:ox0040, 在text段后面的是data段,size为ox04(全局变量a),文件偏移为ox0140。
这里需要注意的是VMA和LMA两个字段, 分别代表:Virtual Memory Address和Load Memory Address,虚拟地址和装载地址(服务器开发中可以认为一致)的值全部都为0.我们知道:**程序是运行在虚拟地址空间中。
说了那么多,就是要说一点:编译生成的目标文件中,并没有分配虚拟空间地址。
其实objdump没有列出目标文件的所有section的信息,可以通过readelf工具列出目标文件的段表的信息。
[img5]
1.2.可执行文件的生成
编译生成目标文件后,就需要调用系统的链接器将目标文件进行链接生成可执行文件。处于简单考虑,这里以静态链接进行解释。
现在链接器对于多个目标文件进行链接时,都是采用相似段合并来进行操作,如下图:
[img6]
现在的链接器一般采用两步链接方法进行链接:
- 空间与地址分配;
这里空间与地址的分配有两层含义:
- 输出的可执行文件的空间,即生成可执行文件;
- 分配程序加载后的虚拟地址空间,即分配进行运行时使用的虚拟地址;
这里需要强调的是:虚拟地址的分配只是对段表中的各个段的起始虚拟地址进行初始化,不会对代码段中的指令使用的地址进行修改(这个修改在两步链接的第二步才会进行)。
可以看到链接后生成的可执行文件的各个段的VMA已经分配了虚拟地址:
[img7]
这里需要知道:64位操作系统程序分配(加载)的虚拟起始地址为0x400000,32位系统程序分配(加载)的虚拟起始地址为0x8048000。
[img8]
-
符号解析与重定位;
链接器为各个段分配好起始虚拟地址后,接下来要做的就是对代码指令进行修正,使可执行文件中所有使用的地址都为虚拟地址,而非目标文件中的相对偏移地址。指令修正的过程使用到了目标文件中的段表,符号表,重定位表等等,这里目前没有深入了解,这些也都是静态链接的东西,本文旨在解释虚拟地址的由来,所以不过多深入。
[img9]
[img10]
图1是目标文件的反汇编后的代码,可以看到里面指令全是相对偏移,指令参数都是假地址。图2链接生成完整的可执行文件中,指令偏移全部都是虚拟地址,指令参数中的变量和函数地址都替换成了真正的虚拟地址。
可以运行调试源代码生成的ELF文件,如下:
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。
[img11]
2.可执行文件的执行过程
由上面的阐述我们知道了,编译链接生成可执行文件中的已经生成了虚拟地址,就是进程执行过程中的加载和使用的地址。那么进程的执行最终还是需要加载到物理内存上,所以运行过程中最重要的逻辑就是虚拟地址到物理地址的映射了。
下面简单阐述一下,elf文件的执行过程:
- 创建进程的虚拟地址空间
这个过程只是创建一个最重要的页表的数据结构,用于将虚拟地址空间映射到物理地址空间。
页表,每个进程都有一个页表项,进程页表用于逻辑页对应的物理页的映射。这里忽略操作系统如何分页,已经如何加载文件的过程。进程加载时会根据条件选择加载一些页到内存,当访问的页通过页表发现该页不在内存中,则发生缺页终端,进行页的加载。
[img12]
读取ELF文件头,建立虚拟空间和可执行文件的映射关系
[img13]将寄存器设置为程序启动的入口地址,启动执行
【1】http://mqzhuang.iteye.com/blog/901602
【2】http://www.cnblogs.com/zy691357966/p/5525684.html