linux进程空间布局

https://www.cnblogs.com/smile267/archive/2012/10/21/2732099.html


本文主要是对于linux程序执行时建立的虚拟地址空间做一定程度的描述,以及个人对于代码到进程空间之间转换的理解。

从操作系统的角度来看,进程最关键的特征就是它拥有独立的虚拟地址空间,进程之间由此得以隔离区分。一个程序的执行主要做了三件事:

  • 创建一个独立的虚拟地址空间。
  • 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。
  • 将CPU的指令寄存器设置成为可执行文件的入口地址,启动运行。

这三件事是《程序员的自我修养》中6.3.1 进程的建立章节中所描述,具体内容本文不会重复描述,而是讲述个人对于程序执行过程中所建立的虚拟地址空间的一些浅显的理解。

在32位的操作系统中,虚拟地址空间的地址范围为0x00000000 ~ 0xFFFFFFFF,以下为大致的进程虚拟空间图。

    linux进程空间布局

此处我们暂且先不去理会虚拟地址空间如何映射到物理空间,也不关心如何将可执行文件装载到进程虚拟地址空间的过程,而是把重点放在代码与虚拟地址空间的映射关系上。我们知道在代码在经过预处理、编译、汇编与链接之后会生成一个可执行文件,用术语来说就是ELF格式文件(Executable Linkable Format)。

ELF文件由ELF文件头与许多段(section)组成,段中我们比较熟悉的有数据段、代码段等。一般来说,C语言编译之后的可执行语句变成了可执行机器代码,保存在.text段;已经初始化的全局变量和局部静态变量都保存在.data段;未初始化的全局变量和局部静态变量都保存在.bss段。其中未初始化的全局变量和局部静态变量默认值都是0,也就是说.bss段中的值都是0。空口无凭,下面用代码证明一下。

linux进程空间布局
#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", &param, &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进程空间布局

从上面的代码我们可以看出,有两个全局变量,一个已经初始化,一个未初始化;两个局部静态变量,也是一个已经初始化,一个未初始化。我们通过linux自带工具objdump可以查看经过编译、汇编之后的文件内容,如下。

linux进程空间布局
# 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]:
// 省略...
linux进程空间布局

从符号表(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可执行文件中的内容。

linux进程空间布局
# 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              [email protected]@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              [email protected]@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
linux进程空间布局

从执行结果与objdump查看的结果来看,main与func函数都位于.text段,之前在test.o中以未知的"COMMON符号"存在的未初始化的全局变量g_uninit_var也已经放到了.bss段。从初始化值来看,未初始化静态局部变量与全局变量值都是为0,未初始化的局部变量结果未知。根据test可执行文件的内容,我们可以画出以下的虚拟进程空间图。

  linux进程空间布局

在这个名为test的ELF可执行文件中,ELF文件头占据了54字节大小;程序的入口点为0x08048310,也就是.text段的起始地址,这个就是glibc的程序入口_start。在本文的最开始说过,进程的建立做的第三件事将CPU的指令寄存器设置成为可执行文件的入口地址,然后启动运行,而此test程序中所指的入口地址也就是_start。我们常说main函数是一个程序的入口,实际上linux程序实际的入口往往指的是_start,main函数的调用需要经历_start -> __libc_start_main,在__libc_start_main中传入环境变量,指定栈底的地址等操作之后,开始执行main函数。

写到这里,基本对于代码中的实现如何对应进程虚拟空间位置做了一定程序的叙述,顺便也提及了程序启动入口执行的位置,至于如何创建虚拟地址空间,系统将执行权交还给程序之后,程序如何递归调用函数,就不在本文中继续叙述了,有兴趣的童鞋可以研究或是查看一下资料。

本人随笔主要目的是为记录并分享一些个人心得,错误的地方欢迎大家指出,有兴趣的童鞋欢迎交流,切勿人身攻击。