本篇部落格來認識一下linux下程式位址空間的概念
示範所用系統:CentOS 7.6
文章目錄
- 1.引入程式位址空間
- 1.1 驗證不同區域
- 1.2 fork感覺位址空間的存在
- 2.簡述程式位址空間
- 2.1 程式位址空間和代碼編譯
- 2.2 寫時拷貝
- fork兩個傳回值的解釋
- 3.程式位址空間的作用
- 結語
1.引入程式位址空間
之前學習
C/C++
的時候,多少應該都聽過棧區/堆區/靜态區/全局區的概念,還有一張很經典的示範圖,大部分講解這幾個記憶體區域的圖檔都和下圖類似
但是有一個問題,這裡的程式位址空間,是我們的實體記憶體上的東西嗎?
并不是!
- 程式/程序位址空間是作業系統上的概念,它和我們實體記憶體本身不是一個東西
1.1 驗證不同區域
用下面這個代碼來簡單驗證一下不同區域上的差別
#include<stdio.h>
#include<stdlib.h>
int un_global_val;//未初始化全局變量
int global_val=100;//已初始化全局變量
//main函數的參數
int main(int argc, char *argv[], char *env[])
{
printf("code addr : %p\n", main);
printf("init global addr : %p\n", &global_val);
printf("uninit global addr: %p\n", &un_global_val);
char *m1 = (char*)malloc(100);
char *m2 = (char*)malloc(100);
char *m3 = (char*)malloc(100);
char *m4 = (char*)malloc(100);
int a = 100;
static int s = 100;
printf("heap addr : %p\n", m1);
printf("heap addr : %p\n", m2);
printf("heap addr : %p\n", m3);
printf("heap addr : %p\n", m4);
printf("stack addr : %p\n", &m1);
printf("stack addr : %p\n", &m2);
printf("stack addr : %p\n", &m3);
printf("stack addr : %p\n", &m4);
printf("stack addr a : %p\n", &a);
printf("stack addr s : %p\n", &s);
printf("\n");
for(int i = 0; i < argc; i++)
{
printf("argv addr : %p\n", argv[i]);
}
printf("\n");
for(int i =0 ; env[i];i++)
{
printf("env addr : %p\n", env[i]);
}
return 0;
}
通過上面的測試,可以看到其結果和文章最開始的那張圖相同。這裡解釋一下
向上/向下
的含義
- 向上增長:向位址增大的方向增長
- 向下增長:向位址減小的方向增長
不過那個圖檔内部還少了一些東西,比如指令行參數和環境變量其實是存放在棧區之上的。補全之後的圖檔如下
其中我們還可以發現,棧區和堆區之間有非常大的記憶體空隙
heap addr : 0x1a140f0
heap addr : 0x1a14160
stack addr : 0x7ffe6671ec60
stack addr : 0x7ffe6671ec58
因為在C/C++中定義的變量都是在棧上儲存的,棧向下增長,先定義的變量位址較高!
int a = 100;
static int s = 100;
關于函數中
static
修飾的變量,可以看到其位址空間屬于全局靜态區。雖然在函數中用
static
修飾是限制其隻能在該函數内通路,但是該變量的聲明周期是跟随整個程式的!
stack addr a : 0x7ffe6671ec44
stack addr s : 0x601048
說了這麼多,我們也沒看看出來程式位址空間在哪兒啊?
1.2 fork感覺位址空間的存在
下面可以用一個簡單的fork代碼來确認程式位址空間的存在!
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main()
{
int test = 10;
int ret = fork();
if(ret == 0)
{
while(1)
{
printf("我是子程序%d,ppid:%d,test:%d,&test: %p\n\n",getpid(),getppid(),test,&test);
sleep(1);
}
}
else
{
while(1)
{
printf("我是父程序%d,ppid:%d,test:%d,&test: %p\n\n",getpid(),getppid(),test,&test);
sleep(1);
}
}
return 0;
}
依舊是最簡單的一個fork代碼,正常情況下,二者列印的結果應該是一樣的!
可如果我們在子程序中修改一下test呢?
這時候就會發現一個離譜的現象:子程序和父程序列印的test值不一樣,但是其位址卻完全相同!
如果我們在
C/C++
中使用的位址就是實體位址,是不可能出現這種情況的!怎麼可能在實體記憶體的同一個位址通路出兩個不同的結果呢?
就好比張三和李四在同一天的同一時間去了 AA路30号
這個位址,不可能會出現張三去了發現是超市,而李四去了發現是醫院的情況
這便告訴我們了程式位址空間的存在,亦或者說,我們在程式設計中使用的位址都是虛拟位址
2.簡述程式位址空間
每一個程序在啟動的時候,都會讓作業系統給其配置設定一個位址空間,這就是程序位址空間
- 以
的理念,程序位址空間其實是作業系統核心的一個資料結構先描述再組織
struct mm_struct
- 之前提到過程序具有獨立性,在多程序運作的時候,需要獨享各種資源。而程序位址空間的作用,就是讓程序認為自己是獨占作業系統中的所有資源!
這個操作,其實就是作業系統給該進程序畫了一個假的記憶體(虛拟位址)程序需要記憶體的時候,作業系統就會在頁表裡面畫一個位址給他,再将該位址映射到實體記憶體上面
在Linux源碼中可以看到這玩意的存在,其中的
struct vm_area_struct * mmap;
就是一個我們的頁表
這裡就能看到虛拟位址空間的start和end了!
2.1 程式位址空間和代碼編譯
我們直到,C語言代碼需要經過
預處理-編譯-連結-彙編
這幾個步驟
- 程式編譯出來,沒有被加載的時候,程式内部有位址(如果沒有位址,無法進行連結)
- 程式編譯出來,沒有被加載的時候,程式内部有區域(
可以檢視區域)readelf -s 可執行檔案
[muxue@bt-7274:~/git/raspi/code/22-10-07_程式位址空間]$ readelf -S test
There are 30 section headers, starting at offset 0x19f8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
00000000000000c0 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400378 00000378
0000000000000059 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 00000000004003d2 000003d2
0000000000000010 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 00000000004003e8 000003e8
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400408 00000408
0000000000000018 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400420 00000420
00000000000000a8 0000000000000018 AI 5 23 8
[11] .init PROGBITS 00000000004004c8 000004c8
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 00000000004004f0 000004f0
0000000000000080 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400570 00000570
00000000000001e2 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 0000000000400754 00000754
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 0000000000400760 00000760
000000000000005e 0000000000000000 A 0 0 8
[16] .eh_frame_hdr PROGBITS 00000000004007c0 000007c0
0000000000000034 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 00000000004007f8 000007f8
00000000000000f4 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000600e10 00000e10
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600e18 00000e18
0000000000000008 0000000000000008 WA 0 0 8
[20] .jcr PROGBITS 0000000000600e20 00000e20
0000000000000008 0000000000000000 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000600e28 00000e28
00000000000001d0 0000000000000010 WA 6 0 8
[22] .got PROGBITS 0000000000600ff8 00000ff8
0000000000000008 0000000000000008 WA 0 0 8
[23] .got.plt PROGBITS 0000000000601000 00001000
0000000000000050 0000000000000008 WA 0 0 8
[24] .data PROGBITS 0000000000601050 00001050
0000000000000004 0000000000000000 WA 0 0 1
[25] .bss NOBITS 0000000000601054 00001054
0000000000000004 0000000000000000 WA 0 0 1
[26] .comment PROGBITS 0000000000000000 00001054
000000000000002d 0000000000000001 MS 0 0 1
[27] .symtab SYMTAB 0000000000000000 00001088
0000000000000648 0000000000000018 28 46 8
[28] .strtab STRTAB 0000000000000000 000016d0
000000000000021e 0000000000000000 0 0 1
[29] .shstrtab STRTAB 0000000000000000 000018ee
0000000000000108 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
需要注意的是,程式内部的位址,和記憶體的位址沒有關系
可以了解為,我們程式内部都存放的是一個相對位址。編譯程式的時候,認為程式是按照
0000~FFFF
進行編址的。
當程式被加載到記憶體當中時,假設系統将該程式的代碼從記憶體
0x100
開始加載,就可以依照程式編址的資料加上這個偏移量,進而存放在記憶體中。
比如程式中有一個代碼段的位置是
0x1F
,這時候在加載程式的時候,就會把這個代碼段加上偏移量來加載
代碼位址 | 虛拟位址 |
0x1f | 0x11f |
0x20 | 0x120 |
大概就是這樣,吧哩吧啦……
2.2 寫時拷貝
現在就可以來解答一下
1.2
中出現的問題了
當子程序嘗試修改test變量的時候,作業系統就會開始一個寫時拷貝,開辟一個新的空間,将對應的值考入該空間,再重新映射頁表。
這時候,雖然頁表左側的虛拟位址沒有變化,但是映射的實體位址已經不一樣了!
這樣就能保證父子程序的獨立性,誰修改變量都互不影響!
類似C++中實作的深拷貝!
fork兩個傳回值的解釋
pid_t id
這個變量屬于父程序棧空間中定義的變量,但是fork内部,return會被執行兩次(return的本質是通過寄存器将傳回值寫入到接收傳回值的變量中)
當
id = fork()
的時候,誰先傳回,誰就會發生一次寫時拷貝。是以同一個變量有不同的内容值,本質上也是同一個虛拟位址,對應了不同實體位址的展現!
- 列印
的傳回值,即可觀察到和fork
中一樣的情況,虛拟位址相同,但是ret的值不同1.2
3.程式位址空間的作用
需要注意的是,記憶體作為一個硬體,沒有辦法拒絕你的讀寫!記憶體是不帶控制功能的!
直接讓使用者修改實體記憶體風險極大:
- 野指針問題
- 使用者可能直接修改作業系統需要用到的記憶體位址,導緻系統boom
程式位址空間讓通路記憶體時添加了一層軟硬體層,可以對轉化過程進行稽核,攔截非法的通路
- 保護記憶體
- 可以使用程序管理更好的對功能子產品進行解耦(linux記憶體管理)
- 讓程式/程序可以用統一的方式/視角來看待記憶體,以統一的方式編譯加載所有可執行程式,簡化程式本身的設計和實作
同時,程式位址空間還可以延遲使用者的記憶體使用。比如我們現在
malloc
了100個位元組的空間,實際上作業系統并不會立馬給你申請空間,而是操作你的
mm_struct
讓程序以為自己已經申請成功了。當程式真正使用這個空間的時候,作業系統才會去實體記憶體中進行映射!
申請的時候,是通過linux的記憶體管理子產品進行操作的。該子產品隻負責開辟記憶體,而不管開辟記憶體的用途