2013年10月24日

Linux进程地址空间学习(一)


进程虚拟地址空间的分段

找工作期间,常被问起一个简单的问题,C程序中,变量的内存空间分配有哪几种形式?比这稍微复杂一点的是,谈谈对进程虚拟地址空间的认识,譬如存在哪些段,数据和代码都是如何存放的。更为复杂一点的,一个进程存在多个线程,这些线程共享此进程地址空间中的哪些段,能共享代码段,栈,堆吗?还有,共享库位于进程地址空间的哪些地方等等一系列围绕进程地址空间的基本问题。

最近阅读了下《系统虚拟化:原理与实现》,中间简单的介绍了进程的地址空间和从逻辑地址映射到线性地址,线性地址映射到物理地址的过程。突然发现大学期间学的内存分段分页的那些知识可以和进程地址空间完整的贯通起来,构成对虚拟内存的一个理解,也能更好的对面试过程遇到的问题给出全面的回答。

补充:最近认真阅读了程序员的自我修养一书,发现已经无法直视自己写的这两篇关于进程地址空间和ELF文件格式的博客,太肤浅了,完全的重复和堆砌,和网上一堆关于这方面的文章没有区别,更为糟糕的是缺乏对ELF和进程地址空间的正确认识和进一步理解。一定要把这两篇重写,不能容忍这么空的内容!

进程地址空间

我们一般都知道,每个程序都能看到一片完整连续的地址空间,这些空间并没有直接关联到物理内存,而是操作系统提供了内存的一种抽象概念,使得每个进程都有一个连续完整的地址空间,在程序的运行过程,再完成虚拟地址到物理地址的转换。我们同样知道,进程的地址空间是分段的,存在所谓的数据段,代码段,bbs段,堆,栈等等。每个段都有特定的作用,仔细看下面这张图,就对进程地址空间中的划分有了清楚的了解了。

对于32位的机器来说,虚拟的地址空间大小就是4G,可能实际的物理内存大小才1G到2G,意味着程序可以使用比物理内存更大的空间。

在下一篇中文章中,我们提到了ELF文件格式,可执行文件分为很多segment,目标文件包含很多section,还强调ELF提供了两种并行的视图,而在进程的地址空间中也提到了多个关于段的概念,实际上地址空间的段与我们在elf文件提到的segment和section还是存在一定差别的,对于他们的正确理解可以参考程序员的自我修养的第164面

例子

补充:在学习ELF格式的过程中,突然想到下面的这些内容与进程的地址空间并不是一一对应的概念,下面的内容应该顶多是对程序文件的组织格式进行的探索,而进程是运行的程序的数据和代码等等的集合,对它的地址空间的分析,应该是分析处在运行状态的程序的地址空间状态,更为准确的内容大家可以参考程序猿的自我修养一书。 下面的内容权当是对ELF文件格式理解的一个热身。

我们可以写一个简单的程序来进行观察,例如下面的代码:

#include<iostream>

using namespace std;

int global_a = 100;
int global_b;
static int global_static_a = 200;

int main(){
    static int main_static_b = 200;
    static int main_static_c;
    cout << "Hello World" << endl;
    return 0;
}

使用g++编译过之后,我们可以先使用size命令来查看下各个段的大小。

choudan@ubuntu:~/coding/cpp$ size a.out 
    text    data     bss     dec     hex filename
    1914      316     160    2390     956 a.out

该命令描述了上面程序三个段的大小,dec是这三个段的总和,hex是这个总和的十六进制表示。除此之外,我们还可以通过objdump命令来观察各个段中的内容,例如,程序中声明了两个赋值的全局变量和一个赋值的静态变量,他们应该处于data段,还有处于bbs段的两个未初始化的变量。

使用readelf命令可以查看二进制信息,但是没有可读性,大家可以试试,例如 readelf -x 13 a.out ,(其中13代表数据段,大家可以通过readelf -S a.out看该程序中有多少段,其实除了上面提到的重要的几个段之外还有很多其他作用的,下篇文章介绍。)

查看data段的结果如下:

choudan@ubuntu:~/coding/cpp$ readelf -x 25 a.out

Hex dump of section '.data':
  0x0804a020 00000000 00000000 64000000 c8000000 ........d.......
    0x0804a030 c8000000                            ....

我们读取出了data段的数据信息,0x0804a020表示data的起始地址,然后是该起始地址后四个字节的具体内容,我们使用objdump反汇编来看一些方便理解的内容。

choudan@ubuntu:~/coding/cpp$ objdump -d -j .data a.out 

a.out:     file format elf32-i386


Disassembly of section .data:

0804a020 <__data_start>:
 804a020:   00 00                   add    %al,(%eax)
    ...
    
0804a024 <__dso_handle>:
 804a024:   00 00 00 00                                         ....
     
0804a028 <global_a>:
 804a028:  64 00 00 00                                          d...
      
0804a02c <_ZL15global_static_a>:
 804a02c: c8 00 00 00                                           ....
       
0804a030 <_ZZ4mainE13main_static_b>:
 804a030:    c8 00 00 00                                        ....

从中我们可以看出,内容和上面读出的是一样的,还带了一些符号,和我们申明的变量名一样,只是在上面多做了一些处理,例如global_static_a这个变量名被处理为_ZL15global_static_a了,然后我们看它的值,int类型的变量,占4个字节,将C8000000小端表示转换成大端表示000000C8,转换成十进制,就是我们申明的static int global_static_a = 200中的200这个值了。同样我们可以查看text,bbs段的具体内容。

总结

有了上面这些基本的认识之后,我们在接下来的一篇博客中探索关于编译之后的文件,到底是什么样的格式,上面我们已经简单的演示通过size,readelf,objdump等命令来读取其中相关段的信息。

突然还想到巨人网络笔试时一道有趣的题,例如对于指向某个类的空指针,是否能够直接调用该类的某个函数;

Test *test = NULL;
test->print();

上面的代码是否可执行,在什么样的情况下能,在什么样的情况下又不能?

参考文章

  1. Linux进程地址空间的一步步探究
  2. Linux进程地址空间之初探:一
  3. Linux进程地址空间和虚拟内存
前一篇: Linux进程地址空间学习(二) 后一篇: Openstack Nfs挂载问题