本文主要是對於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函數。
寫到這里,基本對於代碼中的實現如何對應進程虛擬空間位置做了一定程序的敘述,順便也提及了程序啟動入口執行的位置,至於如何創建虛擬地址空間,系統將執行權交還給程序之后,程序如何遞歸調用函數,就不在本文中繼續敘述了,有興趣的童鞋可以研究或是查看一下資料。