重新認識Linux進程地址空間

程序和進程

程序存儲在永久性介質上,被加載到內存後變成進程,兩者最大區別是程序包含加載的信息。進程時Linux的說法。

程序存儲空間

程序鏡像包含五類信息:頭部,程序正文(包括代碼和數據),重定位信息 ,符號 ,調試信息。
只要讓程序可以正常運行,其鏡像格式不限,甚至其內容可以和內存中一模一樣。比如下面一段程序:

org	07c00h						; 告訴彙編程序加載到7c00處,彙編器計算符號地址需要從0x7c00處開始
times 	506-($-$$)	db	0		; 填充剩下的空間,使生成的二進制代碼恰好爲512字節
dw	0xeeee						; misc data
dw	0xffff						; misc data
dw 	0xaa55						; 結束標誌

編譯該程序並反彙編查看其指令,寫入floppy鏡像a.img作爲啓動盤

root@hy:/home/work/orange/source/chapter1/a# nasm -o empty.bin empty.asm 
root@hy:/home/work/orange/source/chapter1/a# ll empty.bin 
-rw-r--r-- 1 root root 512 3月  22 20:17 empty.bin
root@hy:/home/work/orange/source/chapter1/a# ndisasm -o 0x7c00 empty.bin | head -n 5
00007C00  0000              add [bx+si],al
00007C02  0000              add [bx+si],al
00007C04  0000              add [bx+si],al
00007C06  0000              add [bx+si],al
00007C08  0000              add [bx+si],al
root@hy:/home/work/orange/source/chapter1/a# ndisasm -o 0x7c00 empty.bin | tail -n 5
00007DF8  0000              add [bx+si],al
00007DFA  EE                out dx,al
00007DFB  EE                out dx,al
00007DFC  FF                db 0xff
00007DFD  FF55AA            call [di-0x56]
root@hy:/home/work/orange/source/chapter1/a# hexdump -C empty.bin 
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001f0  00 00 00 00 00 00 00 00  00 00 ee ee ff ff 55 aa  |..............U.|
00000200
root@hy:/home/work/orange/source/chapter1/a# dd if=empty.bin of=a.img conv=notrunc
1+0 records in
1+0 records out
512 bytes copied, 0.000567204 s, 903 kB/s

使用bochs從a.img啓動並在0x7c00處斷住,查看empty.bin的內存分佈如下:

root@hy:/home/work/orange/source/chapter1/a# cbochs 
========================================================================
                       Bochs x86 Emulator 2.6.8
                Built from SVN snapshot on May 3, 2015
                  Compiled on Mar 17 2019 at 23:26:49
========================================================================
<bochs:3> x/8hb 0x7DF8
[bochs]:
0x0000000000007df8 <bogus+       0>:	0x00	0x00	0xee	0xee	0xff	0xff	0x55	0xaa
<bochs:4> x/8hb 0x7c00
[bochs]:
0x0000000000007c00 <bogus+       0>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00

代碼生成的鏡像大小512字節,除了尾部數據,其餘全是0。被加載到內存後,它佔用的空間同樣512字節。並且和鏡像中的數據一模一樣。這是最原始的程序,沒有附加任何鏡像加載需要的信息。MS-DOS的.COM文件和這裏的empty.bin同屬這類文件。上面程序可以運行,是因爲這一條語句:

org	07c00h						; 告訴彙編程序加載到0x7c00處,彙編器計算符號地址需要從0x7c00處開始

作用相當於和加載它的程序約定好,自己需要在0x7c00處開始運行才能成功,間接告訴加載器自己的加載信息。
/*
TODO: 現代C程序中程序鏡像其餘組成字段的作用。
*/

進程地址空間

進程運行在內存上,其目標就是執行代碼段提供的指令,所有進程數據都爲代碼執行服務,包括初始化數據段、BSS段(block started by symbol)、堆、棧等,不同數據類型應用於程序的不同執行場景。
一個hello程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int adder(int a, int b, int c, int d,
          int e, int f, int g, int h);

int adder(int a, int b, int c, int d,
          int e, int f, int g, int h)
{
    return a+b+c+d+e+f+g+h;
}

void main(void)
{
    int sum;
    char *s = malloc(10);
    memcpy(s, "xxxxxxxxx", 9);
    s[9] = '\0';
    sum = adder(1,2,3,4,5,6,7,8);
    printf("s=%s,sum=%d\n",s,sum);
    while(1)
    {}
}

地址空間分佈如下:

root@hy:/home/work/mm_struct# cat /proc/`pidof hello`/maps
55fc37444000-55fc37445000 r--p 00000000 08:15 921133                     /home/work/mm_struct/hello
55fc37445000-55fc37446000 r-xp 00001000 08:15 921133                     /home/work/mm_struct/hello
55fc37446000-55fc37447000 r--p 00002000 08:15 921133                     /home/work/mm_struct/hello
55fc37447000-55fc37448000 r--p 00002000 08:15 921133                     /home/work/mm_struct/hello
55fc37448000-55fc37449000 rw-p 00003000 08:15 921133                     /home/work/mm_struct/hello
55fc375b3000-55fc375d4000 rw-p 00000000 00:00 0                          [heap]
7efd40e24000-7efd40e46000 r--p 00000000 08:13 267705                     /lib/x86_64-linux-gnu/libc-2.28.so
7efd40e46000-7efd40fb7000 r-xp 00022000 08:13 267705                     /lib/x86_64-linux-gnu/libc-2.28.so
7efd40fb7000-7efd41003000 r--p 00193000 08:13 267705                     /lib/x86_64-linux-gnu/libc-2.28.so
7efd41003000-7efd41004000 ---p 001df000 08:13 267705                     /lib/x86_64-linux-gnu/libc-2.28.so
7efd41004000-7efd41008000 r--p 001df000 08:13 267705                     /lib/x86_64-linux-gnu/libc-2.28.so
7efd41008000-7efd4100a000 rw-p 001e3000 08:13 267705                     /lib/x86_64-linux-gnu/libc-2.28.so
7efd4100a000-7efd41010000 rw-p 00000000 00:00 0 
7efd41030000-7efd41031000 r--p 00000000 08:13 267679                     /lib/x86_64-linux-gnu/ld-2.28.so
7efd41031000-7efd41051000 r-xp 00001000 08:13 267679                     /lib/x86_64-linux-gnu/ld-2.28.so
7efd41051000-7efd41059000 r--p 00021000 08:13 267679                     /lib/x86_64-linux-gnu/ld-2.28.so
7efd41059000-7efd4105a000 r--p 00028000 08:13 267679                     /lib/x86_64-linux-gnu/ld-2.28.so
7efd4105a000-7efd4105b000 rw-p 00029000 08:13 267679                     /lib/x86_64-linux-gnu/ld-2.28.so
7efd4105b000-7efd4105c000 rw-p 00000000 00:00 0 
7ffffb479000-7ffffb49a000 rw-p 00000000 00:00 0                          [stack]
7ffffb5a3000-7ffffb5a6000 r--p 00000000 00:00 0                          [vvar]
7ffffb5a6000-7ffffb5a8000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

進程的地址空間由低到高佈局如下:
C程序進程空間佈局

正文段:可執行的正文程序,可共享,只讀。進程其餘所有數據都爲此服務。

初始化數據段:正文程序中明確賦值的變量,運行時會用到,所有初始化的全局變量放在這裏。初始化常量也放到這裏。

BSS段:由符號開始的塊,正文程序中聲明但沒有初始化的變量。這段被初始化爲0。

堆:程序運行過程中可能通過調用malloc()這樣的函數申請內存空間,Linux內核爲這類行爲預留了堆供其使用。如果堆中的數據持續增加,Linux內核可能會擴展堆的空間。

棧:數據先入後出,其特點決定程序中函數的遞歸調用,函數參數,局部變量的空間都從此分配。如果棧中的數據持續增加,Linux內核可能會擴展棧空間。

分析hello.c程序
數據分佈情況:

char *s = malloc(10);	
malloc從堆中分配數據,所以應指向堆:
55fc375b3000-55fc375d4000 rw-p 00000000 00:00 0                          [heap]

memcpy(s, "xxxxxxxxx", 9);
字符常量"xxxxxxxxx"應放在初始化的數據段:
55fc37446000-55fc37447000 r--p 00002000 08:15 921133                     /home/work/mm_struct/hello
55fc37447000-55fc37448000 r--p 00002000 08:15 921133                     /home/work/mm_struct/hello
/*
TODO: 上面兩段pgoff一樣,是否都爲初始化數據段?
*/

sum = adder(1,2,3,4,5,6,7,8);
x64架構函數調用使用“寄存器+棧”的方式傳參,前6個參數用rdi,rsi,rdx,rcx,r8,r9傳遞,第7個及以上用棧。所以參數7和8在adder調用入棧:
7ffffb479000-7ffffb49a000 rw-p 00000000 00:00 0                          [stack]

正文段:

除了hello程序本身編譯產生的程序段,還包括bash加載時的ini程序段,Libc共享庫提供的可重定位程序段,都應放在正文段:
55fc37445000-55fc37446000 r-xp 00001000 08:15 921133                     /home/work/mm_struct/hello
正文段屬性r-xp,表示可讀,可執行的私有段(private/share)

驗證:

gdb attach `pidof hello`
(gdb) info registers rip
rip            0x55fc37445219      0x55fc37445219 <main+132>
(gdb) disas main+132
Dump of assembler code for function main:
   0x000055fc37445195 <+0>:		push   %rbp
   0x000055fc37445196 <+1>:		mov    %rsp,%rbp	// rbp指向棧頂
   0x000055fc37445199 <+4>:		sub    $0x10,%rsp	// rsp棧針下移16byte
   0x000055fc3744519d <+8>:		mov    $0xa,%edi	// 調用malloc做準備,參數10傳給edi 
   0x000055fc374451a2 <+13>:	callq  0x55fc37445050 <malloc@plt>	// 調用malloc,其地址在落在正文段中
   0x000055fc374451a7 <+18>:	mov    %rax,-0x8(%rbp)	// 返回參數s放入rax,保存到rbp-8的棧中,它指向的應該時heap中的地址
   0x000055fc374451ab <+22>:	mov    -0x8(%rbp),%rax	// 調用memcpy做準備,s->rax
   0x000055fc374451af <+26>:	mov    $0x9,%edx		// 9->edx
   0x000055fc374451b4 <+31>:	lea    0xe49(%rip),%rsi        # 0x55fc37446004	// 字符串常量的地址落在數據段中 “xxxxxxxxx”->rsi
   0x000055fc374451bb <+38>:	mov    %rax,%rdi		// s->rdi
   0x000055fc374451be <+41>:	callq  0x55fc37445040 <memcpy@plt>	// 同malloc
   0x000055fc374451c3 <+46>:	mov    -0x8(%rbp),%rax	
   0x000055fc374451c7 <+50>:	add    $0x9,%rax
   0x000055fc374451cb <+54>:	movb   $0x0,(%rax)
   0x000055fc374451ce <+57>:	pushq  $0x8	// 調用adder做準備,參數入棧,rsp下移8byte
   0x000055fc374451d0 <+59>:	pushq  $0x7	// rsp再下移8byte
   0x000055fc374451d2 <+61>:	mov    $0x6,%r9d
   0x000055fc374451d8 <+67>:	mov    $0x5,%r8d
   0x000055fc374451de <+73>:	mov    $0x4,%ecx
   0x000055fc374451e3 <+78>:	mov    $0x3,%edx
   0x000055fc374451e8 <+83>:	mov    $0x2,%esi
   0x000055fc374451ed <+88>:	mov    $0x1,%edi
   0x000055fc374451f2 <+93>:	callq  0x55fc37445155 <adder>
   0x000055fc374451f7 <+98>:	add    $0x10,%rsp	// 上移16byte
   0x000055fc374451fb <+102>:	mov    %eax,-0xc(%rbp)
   0x000055fc374451fe <+105>:	mov    -0xc(%rbp),%edx
   0x000055fc37445201 <+108>:	mov    -0x8(%rbp),%rax
   0x000055fc37445205 <+112>:	mov    %rax,%rsi
   0x000055fc37445208 <+115>:	lea    0xdff(%rip),%rdi        # 0x55fc3744600e
   0x000055fc3744520f <+122>:	mov    $0x0,%eax
   0x000055fc37445214 <+127>:	callq  0x55fc37445030 <printf@plt>
=> 0x000055fc37445219 <+132>:	jmp    0x55fc37445219 <main+132>

cpu當前一直在程序末尾處循環,從頭分析,rbp最開始指向棧頂,rsp下移了16byte,之後調用malloc申請內存,返回值保存到rax,rax將其壓棧,所以(rbp-8)/(rsp+8)地址處存放的時s的值。之後rbp一直沒變,rsp由於調用adder函數下移16byte,函數返回後仍然保持原位置。所以直到最後的循環,(rbp-8)/(rsp+8)仍然保存了局部變量s的值:

End of assembler dump.
(gdb) info registers rsp	// 查看rsp
rsp            0x7ffffb497240      0x7ffffb497240
(gdb) x/10xg 0x7ffffb497248	// 查看 0x8(%rsp)處的內存信息,這個值是個指向heap的指針。
0x7ffffb497248:	0x000055fc375b3260	0x000055fc37445220
0x7ffffb497258:	0x00007efd40e4809b	0x0000000000000000
0x7ffffb497268:	0x00007ffffb497338	0x0000000100040000
0x7ffffb497278:	0x000055fc37445195	0x0000000000000000
0x7ffffb497288:	0x665a784ccc3224d0	0x000055fc37445070
(gdb) x/1sb 0x000055fc375b3260	// 查看s指向的內容,印證了我們的分析
0x55fc375b3260:	"xxxxxxxxx"
(gdb) info registers rbp
rbp            0x7ffffb497250      0x7ffffb497250

內核數據結構

三個關鍵數據結構: task_struct,mm_struct,vm_area_struct
進程地址空間數據結構關係
/*
TODO

  • 各個數據結構關係介紹
  • 操作各數據結構的接口
  • 各數據結構的生命週期
  • 實驗驗證
    */

內核工作原理

/*
TODO:

  • 內核管理進程地址空間的工作流程
  • 實驗驗證
    */

Linux進程空間管理實現原理

/*
TODO

  • Intel段機制
  • Intel頁機制
    */
  • Intel段機制
    • GDT(Global Descriptor Table)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章