linux進程中的內存分佈

很多小夥伴在調試C代碼的時候非常痛苦,C語言不像java那樣可以給你指出具體的錯誤地方和錯誤原因,C語音因爲指針的特殊性和C語言版本的兼容性的需要,很難直接定位到錯誤的地方。特別是各種段錯誤、溢出等。要想提高調試效率,瞭解和掌握進程內存佈局還是很有必要的。瞭解了內存空間分配,有時候就可以通過指針或者地址的位置來確定是否是程序本身寫錯了等等。

 進程空間分佈概述

對於一個進程,其空間分佈如下圖所示:

程序段(Text):程序代碼在內存中的映射,存放函數體的二進制代碼。

初始化過的數據(Data):在程序運行初已經對變量進行初始化的數據。

未初始化過的數據(BSS):在程序運行初未對變量進行初始化的數據。

棧 (Stack):存儲局部、臨時變量,函數調用時,存儲函數的返回指針,用於控制函數的調用和返回。在程序塊開始時自動分配內存,結束時自動釋放內存,其操作方式類似於數據結構中的棧。

堆 (Heap):存儲動態內存分配,需要程序員手工分配,手工釋放.注意它與數據結構中的堆是兩回事,分配方式類似於鏈表。

 

注:1.Text, BSS, Data段在編譯時已經決定了進程將佔用多少VM
可以通過size,知道這些信息。

2. 正常情況下,Linux進程不能對用來存放程序代碼的內存區域執行寫操作,即程序代碼是以只讀的方式加載到內存中,但它可以被多個進程安全的共享。

內核空間和用戶空間

Linux的虛擬地址空間範圍爲0~4G(intel x86架構32位),Linux內核將這4G字節的空間分爲兩部分,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF)供內核使用,稱爲“內核空間”。而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF)供各個進程使用,稱爲“用戶空間”。

因爲每個進程可以通過系統調用進入內核,因此,Linux內核由系統內的所有進程共享。於是,從具體進程的角度來看,每個進程可以擁有4G字節的虛擬空間。

Linux使用兩級保護機制:0級供內核使用,3級供用戶程序使用,每個進程有各自的私有用戶空間(0~3G),這個空間對系統中的其他進程是不可見的,最高的1GB字節虛擬內核空間則爲所有進程以及內核所共享。

內核空間中存放的是內核代碼和數據,而進程的用戶空間中存放的是用戶程序的代碼和數據。不管是內核空間還是用戶空間,它們都處於虛擬空間中。 雖然內核空間佔據了每個虛擬空間中的最高1GB字節,但映射到物理內存卻總是從最低地址(0x00000000),另外,使用虛擬地址可以很好的保護內核空間被用戶空間破壞,虛擬地址到物理地址轉換過程有操作系統和CPU共同完成(操作系統爲CPU設置好頁表,CPU通過MMU單元進行地址轉換)。

:多任務操作系統中的每一個進程都運行在一個屬於它自己的內存沙盒中,這個沙盒就是虛擬地址空間(virtual address space),在32位模式下,它總是一個4GB的內存地址塊。這些虛擬地址通過頁表(page table)映射到物理內存,頁表由操作系統維護並被處理器引用。每個進程都擁有一套屬於它自己的頁表。

進程內存空間分佈如下圖所示:

 



通常32位Linux內核地址空間劃分0~3G爲用戶空間,3~4G爲內核空間

: 1.這裏是32位內核地址空間劃分,64位內核地址空間劃分是不同的
2.現代的操作系統都處於32位保護模式下。每個進程一般都能尋址4G的物理空間。但是我們的物理內存一般都是幾百M,進程怎麼能獲得4G 的物理空間呢?這就是使用了虛擬地址的好處,通常我們使用一種叫做虛擬內存的技術來實現,因爲可以使用硬盤中的一部分來當作內存使用 。

 


Linux系統對自身進行了劃分,一部分核心軟件獨立於普通應用程序,運行在較高的特權級別上,它們駐留在被保護的內存空間上,擁有訪問硬件設備的所有權限,Linux將此稱爲內核空間。

相對地,應用程序則是在“用戶空間”中運行。運行在用戶空間的應用程序只能看到允許它們使用的部分系統資源,並且不能使用某些特定的系統功能,也不能直接訪問內核空間和硬件設備,以及其他一些具體的使用限制。

將用戶空間和內核空間置於這種非對稱訪問機制下有很好的安全性,能有效抵禦惡意用戶的窺探,也能防止質量低劣的用戶程序的侵害,從而使系統運行得更穩定可靠。

內核空間在頁表中擁有較高的特權級(ring2或以下),因此只要用戶態的程序試圖訪問這些頁,就會導致一個頁錯誤(page fault)。在Linux中,內核空間是持續存在的,並且在所有進程中都映射到同樣的物理內存,內核代碼和數據總是可尋址的,隨時準備處理中斷和系統調用。與之相反,用戶模式地址空間的映射隨着進程切換的發生而不斷的變化,如下圖所示:

 

上圖中藍色區域表示映射到物理內存的虛擬地址,而白色區域表示未映射的部分。可以看出,Firefox使用了相當多的虛擬地址空間,因爲它佔用內存較多。

 進程內存佈局

Linux進程標準的內存段佈局,如下圖所示,地址空間中的各個條帶對應於不同的內存段(memory segment),如:堆、棧之類的。
 

 


:這些段只是簡單的虛擬內存地址空間範圍,與Intel處理器的段沒有任何關係。

幾乎每個進程的虛擬地址空間中各段的分佈都與上圖完全一致,這就給遠程發掘程序漏洞的人打開了方便之門。一個發掘過程往往需要引用絕對內存地址:棧地址,庫函數地址等。遠程攻擊者必須依賴地址空間分佈的一致性,來探索出這些地址。如果讓他們猜個正着,那麼有人就會被整了。因此,地址空間的隨機排布方式便逐漸流行起來,Linux通過對棧、內存映射段、堆的起始地址加上隨機的偏移量來打亂佈局。但不幸的是,32位地址空間相當緊湊,這給隨機化所留下的空間不大,削弱了這種技巧的效果。

進程地址空間中最頂部的段是棧,大多數編程語言將之用於存儲函數參數和局部變量。調用一個方法或函數會將一個新的棧幀(stack frame)壓入到棧中,這個棧幀會在函數返回時被清理掉。由於棧中數據嚴格的遵守FIFO的順序,這個簡單的設計意味着不必使用複雜的數據結構來追蹤棧中的內容,只需要一個簡單的指針指向棧的頂端即可,因此壓棧(pushing)和退棧(popping)過程非常迅速、準確。進程中的每一個線程都有屬於自己的棧。

通過不斷向棧中壓入數據,超出其容量就會耗盡棧所對應的內存區域,這將觸發一個頁故障(page fault),而被Linux的expand_stack()處理,它會調用acct_stack_growth()來檢查是否還有合適的地方用於棧的增長。如果棧的大小低於RLIMIT_STACK(通常爲8MB),那麼一般情況下棧會被加長,程序繼續執行,感覺不到發生了什麼事情。這是一種將棧擴展到所需大小的常規機制。然而,如果達到了最大棧空間的大小,就會棧溢出(stack overflow),程序收到一個段錯誤(segmentation fault)。


:動態棧增長是唯一一種訪問未映射內存區域而被允許的情形,其他任何對未映射內存區域的訪問都會觸發頁錯誤,從而導致段錯誤。一些被映射的區域是隻讀的,因此企圖寫這些區域也會導致段錯誤。

內存映射段
在棧的下方是內存映射段,內核將文件的內容直接映射到內存。任何應用程序都可以通過Linux的mmap()系統調用或者Windows的CreateFileMapping()/MapViewOfFile()請求這種映射。內存映射是一種方便高效的文件I/O方式,所以它被用來加載動態庫。創建一個不對應於任何文件的匿名內存映射也是可能的,此方法用於存放程序的數據。在Linux中,如果你通過malloc()請求一大塊內存,C運行庫將會創建這樣一個匿名映射而不是使用堆內存。“大塊”意味着比MMAP_THRESHOLD還大,缺省128KB,可以通過mallocp()調整。


與棧一樣,堆用於運行時內存分配;但不同的是,堆用於存儲那些生存期與函數調用無關的數據。大部分語言都提供了堆管理功能。在C語言中,堆分配的接口是malloc()函數。如果堆中有足夠的空間來滿足內存請求,它就可以被語言運行時庫處理而不需要內核參與,否則,堆會被擴大,通過brk()系統調用來分配請求所需的內存塊。堆管理是很複雜的,需要精細的算法來應付我們程序中雜亂的分配模式,優化速度和內存使用效率。處理一個堆請求所需的時間會大幅度的變動。實時系統通過特殊目的分配器來解決這個問題。堆在分配過程中可能會變得零零碎碎,如下圖所示:
 



一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式類似於鏈表。


BBS和數據段
在C語言中,BSS和數據段保存的都是靜態(全局)變量的內容。區別在於BSS保存的是未被初始化的靜態變量內容,他們的值不是直接在程序的源碼中設定的。BSS內存區域是匿名的,它不映射到任何文件。如果你寫static intcntActiveUsers,則cntActiveUsers的內容就會保存到BSS中去。
數據段保存在源代碼中已經初始化的靜態變量的內容。數據段不是匿名的,它映射了一部分的程序二進制鏡像,也就是源代碼中指定了初始值的靜態變量。所以,如果你寫static int cntActiveUsers=10,則cntActiveUsers的內容就保存在了數據段中,而且初始值是10。儘管數據段映射了一個文件,但它是一個私有內存映射,這意味着更改此處的內存不會影響被映射的文件。

你可以通過閱讀文件/proc/pid_of_process/maps來檢驗一個Linux進程中的內存區域。記住:一個段可能包含許多區域。比如,每個內存映射文件在mmap段中都有屬於自己的區域,動態庫擁有類似BSS和數據段的額外區域。有時人們提到“數據段”,指的是全部的數據段+BSS+堆。

你還可以通過nm和objdump命令來察看二進制鏡像,打印其中的符號,它們的地址,段等信息。最後需要指出的是,前文描述的虛擬地址佈局在linux中是一種“靈活佈局”,而且作爲默認方式已經有些年頭了,它假設我們有值RLIMT_STACK。但是,當沒有該值得限制時,Linux退回到“經典佈局”,如下圖所示:

 

全局變量(初始化和未初始化)位於數據段

局部變量位於棧段 

const 常量位於棧段 

常字符串位於文本段 

動態申請內存位於堆段 

查看進程的內存佈局

C語言程序實例分析如下所示:

#include <stdio.h>
#include <malloc.h>

void print(char *, int);
int g1=12;
long g2;
void main(){
  char *s1 = "abcde";
  char *s2 = "abcde";
  char s3[] = "abcd";
  long int *s4[100];
  char *s5 = "abcde";//常量字符串"abcde"在常量區,但是s1,s2,s5本身在stack上,但它們用有相同的地址
  int a = 5;
  int b = 6; //a和b在stack上,所以&a>&b
  const int c =10;
  sleep(60);
  printf("變量地址\n&s1=%p\n&s2=%p\n&s3=%p\n&s4=%p\n&s5=%p\ns1=%p\ns2=%p\ns3=%p\ns4=%p\ns5=%p\na=%p\nb=%p\n",&s1,&s2,&s3,&s4,&s5,s1,s2,s3,s4,s5,&a,&b);
  printf("&g1=%p\n &g2=%p\n&c=%p\n",&g1,&g2,&c);
  printf("變量地址在進程調用中");
  print("ddddddd",5);
  printf("main=%p, print=%p\n",main,print);
  while(1){}
}
void print(char *str, int p)
{
  char *s1 = "abcde";
  char *s2 = "abcde";
  char s3[] = "abcd";
  long int *s4[100];
  char *s5 = "abcde";//常量字符串"abcde"在常量區,但是s1,s2,s5本身在stack上,但它們用有相同的地址
  int a = 5;
  int b = 6; //a和b在stack上,所以&a>&b
  int c;
  int d;
  char *q = str;
  int m =p;
  char *r=(char*)malloc(1);
  char *w = (char*)malloc(1);
   printf("變量地址\n&s1=%p\n&s2=%p\n&s3=%p\n&s4=%p\n&s5=%p\ns1=%p\ns2=%p\ns3=%p\ns4=%p\ns5=%p\na=%p\nb=%p\n",&s1,&s2,&s3,&s4,&s5,s1,s2,s3,s4,s5,&a,&b);
   printf("str=%p\nq=%p\n&q=%p\n&p=%p\n&m=%p\nr=%p\nw=%p\n&r=%p\n&w=%p\n",&str,q,&q,&p,&m,r,w,&r,&w);
}

 

變量地址
&s1=0x7ffe949e0308
&s2=0x7ffe949e0310
&s3=0x7ffe949e0640
&s4=0x7ffe949e0320
&s5=0x7ffe949e0318
s1=0x400998
s2=0x400998//s1,s2,s5都指向同一個地址,該地址就是字符串常量保存的位置,位於text段
s3=0x7ffe949e0640
s4=0x7ffe949e0320
s5=0x400998
a=0x7ffe949e02fc
b=0x7ffe949e0300
&g1=0x601050 //位於bss段
 &g2=0x601060
&c=0x7ffe949e0304
變量地址在進程調用中變量地址
&s1=0x7ffe949dff80
&s2=0x7ffe949dff88
&s3=0x7ffe949e02d0
&s4=0x7ffe949dffb0
&s5=0x7ffe949dff90
s1=0x400998
s2=0x400998
s3=0x7ffe949e02d0
s4=0x7ffe949dffb0
s5=0x400998
a=0x7ffe949dff74
b=0x7ffe949dff78
str=0x7ffe949dff68
q=0x400a2f
&q=0x7ffe949dff98
&p=0x7ffe949dff64
&m=0x7ffe949dff7c
r=0x1d3c420
w=0x1d3c440
&r=0x7ffe949dffa0
&w=0x7ffe949dffa8
main=0x400626, print=0x40076f

使用/proc/進程id/maps文件查看進程的內存空間分佈

查看進程的內存佈局: 

00400000-00401000這一段可以讀可以執行,是text段,也就是程序段;

00600000-00601000這一段可讀,應該是data段

00601000-0060200這一段可讀可寫,應該是bss段

01d3c000-01d5d000這一段可讀可寫,屬於heap區

7f44bbfd0000-7f44bc5c2000這一段應該是內存映射區(會增長)

7ffe949c0000-7ffe949e2000這一段屬於stack區,長度爲0x22000字節,

這裏寫圖片描述

其中未分配的堆棧內存中一部分用於內存映射也就是mmap。

從上圖可以看出,進程的內存空間從低地址到高地址內存佈局依次是: 保留區 –> 文本段–>數據段—>堆—>共享庫或mmap—>棧–>環境變量—>內核空間

查看該進程對應的內存佈局,結果如下:

每一行依次對應的是: 地址範圍、權限、偏移量、設備、文件inode、映射對象

64位地址時將0x0000,0000,0000,0000 – 0x0000,7fff,ffff,f000這128T地址用於用戶空間。參見定義:

#define TASK_SIZE_MAX   ((1UL << 47) - PAGE_SIZE),注意這裏還減去了一個頁面的大小做爲保護。

而0xffff,8000,0000,0000以上爲系統空間地址。注意:該地址前4個都是f,這是因爲目前實際上只用了64位地址中的48位(高16位是沒有用的),而從地址0x0000,7fff,ffff,ffff到0xffff,8000,0000,0000中間是一個巨大的空洞,是爲以後的擴展預留的。

從上述地址值來看,64位系統中應該有48根地址總線,低位:0~47位纔是有效的可變地址,高位:48~63位全補0或全補1。一般高位全補0對應的地址空間是用戶態。高位全補1對應的是內核態,如上面的第19行(vsyscall段)。這64位的地址空間並不能全部被使用(太多了),所以用戶態和內核態之間會有未使用的空間(據說叫AMD64空洞)。

而真正的系統空間的起始地址,是從0xffff,8800,0000,0000開始的,參見:

#define __PAGE_OFFSET     _AC(0xffff,8800,0000,0000, UL)

而32位地址時系統空間的起始地址爲0xC000,0000。

另外0xffff,8800,0000,0000 – 0xffff,c7ff,ffff,ffff這64T直接和物理內存進行映射,0xffff,c900,0000,0000 – 0xffff,e8ff,ffff,ffff這32T用於vmalloc/ioremap的地址空間。

至於用戶空間的幾個段的劃分,不同的架構和編譯選項貌似還不一樣。暫時沒有找到合適的解釋。

查看程序各段的大小,使用size 命令:

    text       data        bss        dec        hex    filename
   2379        576          8       2963        b93    mmtest

使用 pmap 命令查看進程內存分佈

pmap [參數] [進程pid]

參數:

  • -d 顯示詳細設備信息
  • -q 不顯示首尾行信息

運行測試程序,用top或ps 命令查詢進程pid:

屬性 含義
Address 地址空間:起始地址~
Kbytes 大小
Mode 權限:r可讀、w可寫、x可執行、s共享內存、p私有內存
Offset 虛擬內存偏移量
Device 所在設備(主:次): 008:00008表示sda8
mapped 虛擬內存分配大小
shared 共享內存大小

 mmtest 是運行程序的名字 
.so 是使用的動態鏈接庫 
stack 使用的棧空間 
anon 預分配的虛擬內存,還未有數據佔用

附錄:

棧與堆的區別:

參考:https://zhuanlan.zhihu.com/p/26857760

https://blog.csdn.net/chenyijun/article/details/79441166

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