Linux虛拟位址布局
x64 layout
在x86_64下面,其實虛拟位址隻使用了48位。是以C程式裡,列印的位址都是隻有12位16進制。48位位址長度也就是對應了256TB的位址空間。
而在Linux下有效的位址區間是從
0x00000000 00000000 ~ 0x00007FFF FFFFFFFF
還有
0xFFFF8000 00000000 ~ 0xFFFFFFFF FFFFFFFF
兩個位址區間。而每個位址區間都有128TB的位址空間可以使用,是以總共是256TB的可用空間。位址空間的劃分就如下所示
ffffffff`ffffffff _____________
| |
| 核心空間 |
ffff8000` |____________|
| |
| 未使用 |
| 的空間 |
| |
fff`ffffffff |____________|
| |
| 使用者空間 |
` |____________|
下面分析使用者空間的位址布局。一個程式的記憶體布局,可以通過
cat /proc/<pid>/maps
pmap <pid> -X
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiQ3chVEa0V3bT9CX5RXa2Fmcn9CXwczLcVmds92czlGZvwVP9EUTDZ0aRJkSwk0LcxGbpZ2LcBDM08CXlpXazRnbvZ2LcRlMMVDT2EWNvwFdu9mZvwla50WVmZ1MWZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39jNwEDM1IjMzITNxgDM3EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
這是一個簡單的helloworld程式的示例布局
- 前三行分别是
、Text Segment
和Data Segment
,BSS Segment
其實就是存放二進制可執行代碼的位置,是以它的權限是讀與可執行,Text Segment
存放的是靜态常量,是以該位址段權限是隻讀,Data Segment
存放未初始化的靜态變量,是以也就是可以随意讀寫。BSS Segment
- 接下來是
位址段,heap位址是往高位址增長的,是用來動态配置設定記憶體的區域。它跟棧相反,是往高位址增長的,對應的記憶體申請系統調用是brk()。Heap
- 接下來的區域是
。這塊位址也是用來配置設定記憶體區域的,一般是用來把檔案映射進記憶體用的,但是你也可以直接在這裡申請記憶體空間來使用,對應的記憶體申請系統調用是mmap()。Memory Mapping Segment
- 再下面就是
位址段了。這個棧已經用了136KB了。棧的最大範圍,我們可以通過Stack
指令看到,預設的情況下是8MB,和Linux-x86一樣。prlimit
- 再下面就是
,vvar
和vdso
了。這三個東西都為了加速通路核心資料,比如讀取時間vsyscall
,肯定不能頻繁地進行系統調用陷入核心,是以就映射到使用者空間了。所有程式都有這3個映射位址段。gettimeofday
關于vvar,vdso和vsyscall。先說vsyscall,這東西出現最早,比如讀取時間,核心會把時間資料和
gettimeofday
的實作映射到這塊區域,使用者空間可以直接調用。但是vsyscall區域太小了,而且映射區域固定,有安全問題。後來又造出了vdso,之是以保留是為了相容使用者空間程式。vdso相當于加載一個linux-vd.so庫檔案一樣(名字也由此而來),也就是把一些函數實作映射到這個區域,而vvar也就是存放資料的地方了,那麼使用者可以通過調用vdso裡的函數,使用vvar裡的資料,來獲得自己想要的資訊。而且位址是随機的,更安全。
gettimeofday
具體的x64的記憶體布局如下圖所示:
可以發現裡面有不少Random xx offset,這是Linux裡的ASLR政策。ASLR的話就是Address space layout randomization,是一種安全機制,主要防止緩沖區溢出攻擊。
brk嘗試
brk系統調用可以通過調整heap區域的brk指針,進而調整heap對的虛拟記憶體空間大小。實驗代碼如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
void *curr_brk, *tmp_brk = NULL;
printf("Welcome to sbrk example:%d\n", getpid());
tmp_brk = curr_brk = sbrk(); //獲得當然brk的位址
printf("Program Break Location1:%p\n", curr_brk);
getchar();
brk(curr_brk+); //增加Heap段4KB空間
curr_brk = sbrk();
printf("Program break Location2:%p\n", curr_brk);
int* a = (int *)tmp_brk+; //嘗試使用剛剛擴充的記憶體空間
*a = ;
printf("%d\n", *a); //沒有問題
getchar();
brk(tmp_brk); //恢複到原來的brk大小
curr_brk = sbrk();
printf("Program Break Location3:%p\n", curr_brk);
getchar();
printf("%d\n", *a); //産生SIGSEGV,Segmentation Fault
return ;
}
初始狀态
按回車,調用brk(curr_brk+4096)之後
看到heap大小從132KB變成了136KB,而且位址空間可以正常使用。再次回車,調用brk(tmp_brk)
發現heap堆大小變回去了。再回車發現會産生段錯誤,因為記憶體已經被回收,程序無法使用該記憶體位址。
mmap嘗試
mmap系統調用是把一個檔案映射到一段記憶體位址空間,如上面截圖裡的/lib/libc.so所做的一樣。也可以匿名直接申請一段記憶體空間使用。實驗代碼如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <errno.h>
int main()
{
void * addr;
printf("Welcome to sbrk example:%d\n", getpid());
getchar();
//申請4096*2=8KB的空間,位址空間可讀寫,私有且匿名。
addr = mmap(NULL, * , PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, , );
if(addr < ) {
perror("mmap");
}
printf("Program Allocate :%p\n", addr);
getchar();
if(munmap(addr, ) != ){
perror("munmap");
}
return ;
}
運作結果:在mmap之前,有這麼一條位址空間,大小是12KB
在mmap之後,發現大小變成了20KB,剛好是我們申請了8KB
注意mmap申請記憶體的時候,如果申請位址長度小于一個PAGE_SIZE=2KB=4096位元組=0x1000位元組,那麼會直接申請2KB。而PAGE_SIZE的話可以通過 getconf PAGE_SIZE
指令來檢視。
其實mmap不一定要在
Memory Mapping Segment
進行記憶體申請,你可以指定任意的記憶體位址,當然隻要不跟已有的沖突就好。這個位址也一定要是000結尾,才使得能頁對齊。比如你可以申請0x100000的位址段,他的位址比
Text Segment
的位址更低。這沒有任何問題。而mmap可申請的最低的位址由一個mmap_min_addr核心參數來控制,可以通過下面的指令讀取。
sysctl vm.mmap_min_addr
在debian下,它的預設值是4096。也就是你申請的首位址必須比0x1000大。