linux进程空间布局
本文主要是对于linux程序执行时建立的虚拟地址空间做一定程度的描述,以及个人对于代码到进程空间之间转换的理解。
从操作系统的角度来看,进程最关键的特征就是它拥有独立的虚拟地址空间,进程之间由此得以隔离区分。一个程序的执行主要做了三件事:
- 创建一个独立的虚拟地址空间。
- 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。
- 将CPU的指令寄存器设置成为可执行文件的入口地址,启动运行。
这三件事是《程序员的自我修养》中6.3.1 进程的建立章节中所描述,具体内容本文不会重复描述,而是讲述个人对于程序执行过程中所建立的虚拟地址空间的一些浅显的理解。
在32位的操作系统中,虚拟地址空间的地址范围为0x00000000 ~ 0xFFFFFFFF,以下为大致的进程虚拟空间图。
此处我们暂且先不去理会虚拟地址空间如何映射到物理空间,也不关心如何将可执行文件装载到进程虚拟地址空间的过程,而是把重点放在代码与虚拟地址空间的映射关系上。我们知道在代码在经过预处理、编译、汇编与链接之后会生成一个可执行文件,用术语来说就是ELF格式文件(Executable Linkable Format)。
ELF文件由ELF文件头与许多段(section)组成,段中我们比较熟悉的有数据段、代码段等。一般来说,C语言编译之后的可执行语句变成了可执行机器代码,保存在.text段;已经初始化的全局变量和局部静态变量都保存在.data段;未初始化的全局变量和局部静态变量都保存在.bss段。其中未初始化的全局变量和局部静态变量默认值都是0,也就是说.bss段中的值都是0。空口无凭,下面用代码证明一下。
#include<stdio.h> int g_init_var = 123; int g_uninit_var; void func(int param) { static int s_init_var = 456; static int s_uninit_var; printf("param address:%p, s_init_var address:%p, s_uninit_var address:%p\n", ¶m, &s_init_var, &s_uninit_var); printf("g_init_var address:%p, g_uninit_var address:%p\n", &g_init_var, &g_uninit_var); printf("param value:%d, s_init_var value:%d, s_uninit_var value:%d\n", param, s_init_var, s_uninit_var); printf("g_init_var value:%d, g_uninit_var value:%d\n", g_init_var, g_uninit_var); } int main(void) { int a = 4; int b; func(b); printf("func address:%p\n", func); printf("main address:%p\n", main); return 0; }
从上面的代码我们可以看出,有两个全局变量,一个已经初始化,一个未初始化;两个局部静态变量,也是一个已经初始化,一个未初始化。我们通过linux自带工具objdump可以查看经过编译、汇编之后的文件内容,如下。
# gcc -c test.c # objdump -x test.o test.o: file format elf32-i386 test.o architecture: i386, flags 0x00000011: HAS_RELOC, HAS_SYMS start address 0x00000000 Sections: Idx Name Size VMA LMA File off Algn 0 .text 000000e5 00000000 00000000 00000034 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1 .data 00000008 00000000 00000000 0000011c 2**2 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000004 00000000 00000000 00000124 2**2 ALLOC 3 .rodata 000000fe 00000000 00000000 00000124 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .comment 0000002c 00000000 00000000 00000222 2**0 CONTENTS, READONLY 5 .note.GNU-stack 00000000 00000000 00000000 0000024e 2**0 CONTENTS, READONLY SYMBOL TABLE: 00000000 l df *ABS* 00000000 test.c 00000000 l d .text 00000000 .text 00000000 l d .data 00000000 .data 00000000 l d .bss 00000000 .bss 00000000 l d .rodata 00000000 .rodata 00000000 l O .bss 00000004 s_uninit_var.1708 00000004 l O .data 00000004 s_init_var.1707 00000000 l d .note.GNU-stack 00000000 .note.GNU-stack 00000000 l d .comment 00000000 .comment 00000000 g O .data 00000004 g_init_var 00000004 O *COM* 00000004 g_uninit_var 00000000 g F .text 00000097 func 00000000 *UND* 00000000 printf 00000097 g F .text 0000004e main RELOCATION RECORDS FOR [.text]: // 省略...
从符号表(SYMBOL TABLE)中,我们可以清楚的看到初始化了的全局变量g_init_var与局部静态变量s_init_var是放到了.data段中的,而未初始化的局部静态变量s_uninit_var放到了.bss段,至于未初始化的全局变量g_uninit_var放到了一个未定义的“COMMON符号”中。前面提过,.bss段中存放的是未初始化的全局变量和局部静态变量,而这里未初始化的全局变量g_uninit_var并未放到.bss段中,这其实是跟不同的语言与不同的编译器实现有关,有些编译器会将全局的的未初始化变量放到目标文件.bss段,有些则不存放,只是预留一个未定义的全局变量符号,等到最终链接成为可执行文件的时候再在.bss段分配空间。
从下面执行size查看test.o文件大小的结果中,我们可以清楚看到data段大小为8字节,正好等于两个int全局变量(全局变量g_init_var与局部静态变量s_init_var的大小)的大小,而.bss段中只有未初始化的局部静态变量s_uninit_var,所以大小为4字节,所以也是相符的。
# size test.o text data bss dec hex filename 330 8 4 342 156 test.o
接下来我们进一步把程序链接成为可执行程序,并查看一下ELF可执行文件中的内容。
# gcc -o test test.o # ./test param address:0xbff88320, s_init_var address:0x804a018, s_uninit_var address:0x804a024 g_init_var address:0x804a014, g_uninit_var address:0x804a028 param value:134513867, s_init_var value:456, s_uninit_var value:0 g_init_var value:123, g_uninit_var value:0 func address:0x80483c4 main address:0x804845b # objdump -x test test: file format elf32-i386 test architecture: i386, flags 0x00000112: EXEC_P, HAS_SYMS, D_PAGED start address 0x08048310 Program Header: // 省略... Dynamic Section: // 省略... Version References: // 省略... Sections: // 省略... SYMBOL TABLE: 08048134 l d .interp 00000000 .interp 08048148 l d .note.ABI-tag 00000000 .note.ABI-tag 08048168 l d .note.gnu.build-id 00000000 .note.gnu.build-id 0804818c l d .gnu.hash 00000000 .gnu.hash 080481ac l d .dynsym 00000000 .dynsym 080481fc l d .dynstr 00000000 .dynstr 08048248 l d .gnu.version 00000000 .gnu.version 08048254 l d .gnu.version_r 00000000 .gnu.version_r 08048274 l d .rel.dyn 00000000 .rel.dyn 0804827c l d .rel.plt 00000000 .rel.plt 08048294 l d .init 00000000 .init 080482c4 l d .plt 00000000 .plt 08048310 l d .text 00000000 .text 0804854c l d .fini 00000000 .fini 08048568 l d .rodata 00000000 .rodata 08048670 l d .eh_frame 00000000 .eh_frame 08049f14 l d .ctors 00000000 .ctors 08049f1c l d .dtors 00000000 .dtors 08049f24 l d .jcr 00000000 .jcr 08049f28 l d .dynamic 00000000 .dynamic 08049ff0 l d .got 00000000 .got 08049ff4 l d .got.plt 00000000 .got.plt 0804a00c l d .data 00000000 .data 0804a01c l d .bss 00000000 .bss 00000000 l d .comment 00000000 .comment 00000000 l df *ABS* 00000000 crtstuff.c 08049f14 l O .ctors 00000000 __CTOR_LIST__ 08049f1c l O .dtors 00000000 __DTOR_LIST__ 08049f24 l O .jcr 00000000 __JCR_LIST__ 08048340 l F .text 00000000 __do_global_dtors_aux 0804a01c l O .bss 00000001 completed.7065 0804a020 l O .bss 00000004 dtor_idx.7067 080483a0 l F .text 00000000 frame_dummy 00000000 l df *ABS* 00000000 crtstuff.c 08049f18 l O .ctors 00000000 __CTOR_END__ 08048670 l O .eh_frame 00000000 __FRAME_END__ 08049f24 l O .jcr 00000000 __JCR_END__ 08048520 l F .text 00000000 __do_global_ctors_aux 00000000 l df *ABS* 00000000 test.c 0804a024 l O .bss 00000004 s_uninit_var.1708 0804a018 l O .data 00000004 s_init_var.1707 08049ff4 l O .got.plt 00000000 _GLOBAL_OFFSET_TABLE_ 08049f14 l .ctors 00000000 __init_array_end 08049f14 l .ctors 00000000 __init_array_start 08049f28 l O .dynamic 00000000 _DYNAMIC 0804a00c w .data 00000000 data_start 080484b0 g F .text 00000005 __libc_csu_fini 08048310 g F .text 00000000 _start 00000000 w *UND* 00000000 __gmon_start__ 00000000 w *UND* 00000000 _Jv_RegisterClasses 08048568 g O .rodata 00000004 _fp_hw 0804854c g F .fini 00000000 _fini 00000000 F *UND* 00000000 __libc_start_main@@GLIBC_2.0 0804a028 g O .bss 00000004 g_uninit_var 0804a014 g O .data 00000004 g_init_var 0804856c g O .rodata 00000004 _IO_stdin_used 0804a00c g .data 00000000 __data_start 080483c4 g F .text 00000097 func 0804a010 g O .data 00000000 .hidden __dso_handle 08049f20 g O .dtors 00000000 .hidden __DTOR_END__ 080484c0 g F .text 0000005a __libc_csu_init 00000000 F *UND* 00000000 printf@@GLIBC_2.0 0804a01c g *ABS* 00000000 __bss_start 0804a02c g *ABS* 00000000 _end 0804a01c g *ABS* 00000000 _edata 0804851a g F .text 00000000 .hidden __i686.get_pc_thunk.bx 0804845b g F .text 0000004e main 08048294 g F .init 00000000 _init
从执行结果与objdump查看的结果来看,main与func函数都位于.text段,之前在test.o中以未知的"COMMON符号"存在的未初始化的全局变量g_uninit_var也已经放到了.bss段。从初始化值来看,未初始化静态局部变量与全局变量值都是为0,未初始化的局部变量结果未知。根据test可执行文件的内容,我们可以画出以下的虚拟进程空间图。
在这个名为test的ELF可执行文件中,ELF文件头占据了54字节大小;程序的入口点为0x08048310,也就是.text段的起始地址,这个就是glibc的程序入口_start。在本文的最开始说过,进程的建立做的第三件事将CPU的指令寄存器设置成为可执行文件的入口地址,然后启动运行,而此test程序中所指的入口地址也就是_start。我们常说main函数是一个程序的入口,实际上linux程序实际的入口往往指的是_start,main函数的调用需要经历_start -> __libc_start_main,在__libc_start_main中传入环境变量,指定栈底的地址等操作之后,开始执行main函数。
写到这里,基本对于代码中的实现如何对应进程虚拟空间位置做了一定程序的叙述,顺便也提及了程序启动入口执行的位置,至于如何创建虚拟地址空间,系统将执行权交还给程序之后,程序如何递归调用函数,就不在本文中继续叙述了,有兴趣的童鞋可以研究或是查看一下资料。