学过编程的人都知道,在所有高级编程语言中,C 是运行效率较高的语言之一。一个重要原因是 C 语言属于静态编译型语言,会直接被编译成机器码,然后由计算机直接执行。和大部分人一样,我对 C 语言编译之后的运行方式充满了好奇,主要包括以下几个方面:
- 可执行程序是什么,里面只是包含一大堆机器码吗?
- C 语言中定义的变量、常量、函数等,在内存中是如何分布的,它们之间又是如何相互访问的?
- 计算机中同时运行着很多进程,每个进程都占用内存空间。程序在编译时,如何知道自己运行时将被加载到哪里,又是如何被 CPU 执行的?
下面是一段简单的 C 程序:
#include <stdio.h>
int a = 10;
int main(){
printf("The a is %d\n", a);
return 0;
}
用 gcc 编译后,运行会打印出:
The a is 10
这就是一个可执行文件。查资料得知,Linux 下的可执行文件采用 ELF 格式,它主要有三种类型:可重定位的目标文件、可执行文件和共享库。
用 readelf 工具查看该文件的头信息:
[root@hitoy ~]# readelf -h a.out
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048310
Start of program headers: 52 (bytes into file)
Start of section headers: 1980 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 8
Size of section headers: 40 (bytes)
Number of section headers: 30
Section header string table index: 27
所以可以得出结论:可执行文件中存放的不只是可直接执行的机器码,还包括其他一些描述和帮助程序运行的信息。Linux 下一个 ELF 可执行文件的结构如下:

ELF Header 描述了体系结构和操作系统等基本信息,并指出 Section Header Table 和 Program Header Table 在文件中的位置。
Program Header Table 记录文件中各个 Segment 的分布,Segment 中存放的是各个数据段(Section)。Section Header Table 中保存了所有 Section 的描述信息,通过 Section Header Table 可以找到每个 Section 在文件中的位置。
在上面的 ELF Header 中,还有一个入口地址(Entry point address)。当程序运行时,CPU 会从这个地址开始执行。程序中不仅保存了入口地址,其他数据在编译之后也都有相应的地址,这样 CPU 才能够访问它们。
那么问题又来了:程序在编译的时候,并不能确定自己最终会在哪里运行,运行环境也可能各不相同。例如,有些计算机只有 512MB 内存,有些则可能达到 4GB 甚至更多,那么程序中这些地址还能直接使用吗?
而且我们注意到,程序中的地址是线性连续的,实际内存刚好能够分配这样一段连续空间吗?
把上面 C 程序中全局变量 a 的值改成 11,重新编译之后,会发现程序的入口地址和原来仍然一样。
实际上,操作系统的主要功能之一就是进行内存管理。现代操作系统普遍采用**虚拟内存管理(Virtual Memory Management)**机制,这需要处理器中的 **MMU(Memory Management Unit,内存管理单元)**提供支持。
有了 MMU 的支持,内存地址可以划分为虚拟地址和物理地址。当存在 MMU 支持时,CPU 的寻址操作会由 MMU 翻译成实际的物理地址,然后再操作真实的内存。
有了 MMU,就可以保证程序看到的是一段连续的地址空间,而不用关心实际数据在内存中的存放位置。这也解释了为什么不同的程序可以使用相同的虚拟地址,而不需要知道实际被加载到哪一块物理内存中。

那么对于不同配置的计算机,以及多个程序同时执行的情况,这些地址空间够用吗?通常,操作系统会把虚拟地址空间划分为用户空间和内核空间。对于相同的平台,它们的大小通常是确定的。
例如,在传统的 32 位 x86 Linux 系统中,虚拟地址空间范围是:
0x00000000 ~ 0xffffffff
其中前 3GB:
0x00000000 ~ 0xbfffffff
属于用户空间,后 1GB:
0xc0000000 ~ 0xffffffff
属于内核空间。
也就是说,不论你的 x86 机器实际安装了多少内存,对于一个进程来说,它通常都拥有一套独立的虚拟地址空间。操作系统会根据实际内存的使用情况进行动态管理,再通过 MMU 将虚拟地址映射到实际的物理内存。
评论0
欢迎分享你的看法,也欢迎补充不同的实践经验。