一、ELF文件
ELF(Executable and Linkable Format)文件是linux下的二进制可执行文件,它同时兼容可执行文件和可链接文件。
一个ELF文件包含两个部分:一个固定长度的文件头和多个可扩展的数据块。其中,文件头是整个可执行文件的总地图,描述了整个文件的组织结构。可扩展数据块分为两类,对应着不同的视图——在链接视图下,数据块的单位是节(Section),用多个节区头索引所有内容;而在执行视图下,数据块的单位是段(Segment),用程序头(Program Header)索引所有的段。如下图所示:
通过readelf -S
命令可以打印出文件的节区头部分,这里使用ln
命令作为源程序分析:
[ma@localhost:~]$ readelf -S /bin/ls
There are 29 section headers, starting at offset 0x1a358:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400200 00000200
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 000000000040021c 0000021c
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 000000000040023c 0000023c
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400260 00000260
0000000000000064 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002c8 000002c8
0000000000000be8 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400eb0 00000eb0
00000000000005c4 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000401474 00001474
00000000000000fe 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000401578 00001578
00000000000000a0 0000000000000000 A 6 3 8
[ 9] .rela.dyn RELA 0000000000401618 00001618
00000000000001b0 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000004017c8 000017c8
0000000000000990 0000000000000018 A 5 12 8
[11] .init PROGBITS 0000000000402158 00002158
0000000000000018 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000402170 00002170
0000000000000670 0000000000000010 AX 0 0 4
[13] .text PROGBITS 00000000004027e0 000027e0
000000000000fb68 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 0000000000412348 00012348
000000000000000e 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 0000000000412360 00012360
0000000000003b27 0000000000000000 A 0 0 32
[16] .eh_frame_hdr PROGBITS 0000000000415e88 00015e88
00000000000006b4 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000416540 00016540
0000000000001fdc 0000000000000000 A 0 0 8
[18] .ctors PROGBITS 0000000000619000 00019000
0000000000000010 0000000000000000 WA 0 0 8
[19] .dtors PROGBITS 0000000000619010 00019010
0000000000000010 0000000000000000 WA 0 0 8
[20] .jcr PROGBITS 0000000000619020 00019020
0000000000000008 0000000000000000 WA 0 0 8
[21] .data.rel.ro PROGBITS 0000000000619040 00019040
0000000000000a48 0000000000000000 WA 0 0 32
[22] .dynamic DYNAMIC 0000000000619a88 00019a88
00000000000001d0 0000000000000010 WA 6 0 8
[23] .got PROGBITS 0000000000619c58 00019c58
0000000000000098 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 0000000000619cf0 00019cf0
0000000000000348 0000000000000008 WA 0 0 8
[25] .data PROGBITS 000000000061a040 0001a040
0000000000000200 0000000000000000 WA 0 0 32
[26] .bss NOBITS 000000000061a240 0001a240
0000000000000d20 0000000000000000 WA 0 0 32
[27] .gnu_debuglink PROGBITS 0000000000000000 0001a240
0000000000000010 0000000000000000 0 0 4
[28] .shstrtab STRTAB 0000000000000000 0001a250
0000000000000101 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
节区头包含了我们经常见到的字段:.data/.text/.init
等,这里只描述一下这几个常见节区的作用。
1.1 .text 节区
该节区存储了源文件编译生成的机器指令。对开发者来说,这可能是最重要的一个节区,所有的程序逻辑都放在这里。但开发者对该节区能做的控制却很少,能影响它的因素只有开发者写的程序逻辑,以及编译时使用的选项。比如,使用 -O1 优化选项编译程序可以生成尽量紧凑的 .text 节区,而用 -O2 优化选项会使编译器倾向于生成执行速度更快的指令组合,但有可能让 .text 节区的体积轻微地增大。
1.2 .rodata 节区
从字面上就能看出,ro
表示read only
,即不可写,因此该节区存储了程序中的常量数据,例如:
const char *p = "helloworld";
1.3 .data 节区
所有的全局和静态的已初始化变量会存放在这个节区中,这个节区是可读可写的。
1.4 .bss 节区
该节区存储了所有未初始化或初始化为 0 的全局和静态变量,该节区的设计初衷就是为了节省目标文件的存储空间。变量未被初始化,或者虽被初始化了,但值为 0,就没必要浪费空间,再在目标文件中存储大量的 0 值。
1.5 .got 和 .plt 节区
这两个节区存储了动态链接用到的全局入口表和跳转表。当程序中用到动态链接库中的某个函数时,会在该节区内记录相应的数据。
二、进程内存分布
2.1 虚拟地址空间
linux为每个进程都分配了4G(32位平台)的虚拟地址空间,虚拟空间可以认为是操作系统给每个进程准备的沙盒。就像电影《黑客帝国》中 Matrix 给每个人准备的充满营养液的容器一样。实际上,每个进程只存活在自己的虚拟世界里,却感觉自己独占了所有的系统资源(内存)。
当一个进程要使用某块内存时,它会将自己世界里的一个内存地址告诉操作系统,剩下的事情就由操作系统接管了。操作系统中的内存管理策略将决定映射哪块真实的物理内存,供应用使用。操作系统会竭尽全力满足所有进程合法的内存访问请求。一旦发现应用试图访问非法内存,它将会把进程杀死,防止它做“坏事”影响到系统或其他进程。
这样做,一方面为了安全,防止进程操作其他进程或者系统内核的数据;另一方面为了保证系统可同时运行多个进程,且单个进程使用的内存空间可以超过实际的物理内存容量。
该做法的另一结果则是降低了每个进程内存管理的复杂度,进程只需关心如何使用自己线性排列的虚拟地址,而不需关心物理内存的实际容量,以及如何使用真实的物理内存。
2.2 虚拟地址空间分布
虚拟内存的排布规则如下所示:
从下往上依次是0-4G的内存空间,分别分给了不同的区段。可以用一段程序来验证这一个观点:
#include <stdlib.h>
static const char *p = "HelloWorld";
static int s_init_var = 0;
static int s_not_init;
int g_init_var = 0;
int g_not_init;
int main() {
const char *q = "abc";
int a, b;
int *pa = (int *)malloc(sizeof(int));
int *pb = (int *)malloc(sizeof(int));
printf("static variable address: %p %p\n",
&s_init_var, &s_not_init);
printf("global variable address: %p %p\n",
&g_init_var, &g_not_init);
printf("const variable:%p[static] %p\n",
p, q);
printf("a and b: %p %p\n", &a, &b);
printf("pa and pb: %p %p\n", pa, pb);
return 0;
}
代码分别创建了静态变量、全局变量以及临时变量等多种不同场景下的变量,打印出它们的地址。使用gcc
编译运行:
static variable address: 0x600a0c 0x600a10
global variable address: 0x600a08 0x600a14
const variable:0x4006c8 0x4006d3
a and b: 0x7ffda942df14 0x7ffda942df10
pa and pb: 0xa82010 0xa82030
可以看到已经初始化的静态变量s_init_var
和已经初始化的全局变量g_init_var
两者地址紧紧相邻,间距刚好隔了4
个字节,因为他们被初始化了,都放在了.data
区域。而未初始化的两个虽然也是相连在一起的,但是和初始化了的变量还是隔了一段距离,但距离不是很大,因为.bss
和.data
地址差别不大。
而被const
修饰的两个变量地址也是一样,处在同一个地址空间,和上面的静态变量以及全局变量相比,地址比它们小,这也就说明了.rodata
是在.data
和.bss
下面的。
最后的a/b
和pa/pb
则验证了栈区地址和堆区地址的位置,堆是处在靠下的地址,而栈从地址位数上来说就已经很高了。
这里还能看出一点:局部变量b在a后面申请,但是它的地址比a要小;而pb同样也是在pa后面申请,地址却比pa大。
这说明了栈的地址空间是从上往下申请的,而堆则正常由下往上。
三、参考文档
文章部分摘抄于:攻克 Linux 系统编程
原文写得很不错,有兴趣可自行参考。
此处评论已关闭