如何將網(wǎng)站指向404微信5000人接推廣費用
文章目錄
- 一、 程序地址空間回顧
- 1.程序地址空間各區(qū)域分布驗證
- 2.引入虛擬地址概念
- 二、進程地址空間(虛擬地址空間)的管理
- 三、虛擬地址空間的作用
一、 程序地址空間回顧
1.程序地址空間各區(qū)域分布驗證
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int g_unval;
int g_val = 100;int main(int argc, char *argv[], char *env[])
{const char *str = "helloworld";printf("code addr: %p\n", main);printf("read only string addr: %p\n", str);static int test = 10; printf("init global addr: %p\n", &g_val);printf("test static addr: %p\n", &test); printf("uninit global addr: %p\n", &g_unval);char *heap_mem = (char*)malloc(10);char *heap_mem1 = (char*)malloc(10);char *heap_mem2 = (char*)malloc(10);printf("heap addr: %p\n", heap_mem); printf("heap addr: %p\n", heap_mem1); printf("heap addr: %p\n", heap_mem2); printf("stack addr: %p\n", &heap_mem); printf("stack addr: %p\n", &heap_mem1); printf("stack addr: %p\n", &heap_mem2); printf("argv[0]: %p\n", argv[0]);printf("env[0]: %p\n", env[0]); return 0;
}
這是在linux系統(tǒng)gcc編譯器中運行的結(jié)果,在windows系統(tǒng)下的vs等編譯器運行可能會有不同結(jié)果(因為windows系統(tǒng)下編譯器會優(yōu)化代碼執(zhí)行過程,從而影響結(jié)果):
code addr: 0x40057d
read only string addr: 0x400780
init global addr: 0x60103c
test static addr: 0x601040
uninit global addr: 0x601048
heap addr: 0x63e010
heap addr: 0x63e030
heap addr: 0x63e050
stack addr: 0x7ffd840398c0
stack addr: 0x7ffd840398b8
stack addr: 0x7ffd840398b0
argv[0]: 0x7ffd8403b803
env[0]: 0x7ffd8403b80f
各區(qū)域地址劃分是符合規(guī)則的:
- 代碼區(qū)(正文代碼):存放可執(zhí)行的代碼(如:函數(shù)體的二進制代碼)和 只讀常量。它們的地址是最小的
- 靜態(tài)區(qū)(初始化數(shù)據(jù) 和 未初始化數(shù)據(jù)):存放全局變量 和 靜態(tài)數(shù)據(jù)。它們的地址只比代碼區(qū)大(未初始化數(shù)據(jù) 地址高于 初始化數(shù)據(jù))
- 堆區(qū):使用malloc、calloc、realloc等函數(shù)動態(tài)開辟的空間。地址高于靜態(tài)區(qū),開辟空間時地址向上增長
- 棧區(qū):棧區(qū)主要存放運行函數(shù)而分配的局部變量、函數(shù)參數(shù)、返回數(shù)據(jù)、返回地址等。地址高于堆區(qū),申請空間時地址向下增長
- 命令行參數(shù) 和 環(huán)境變量區(qū)域:地址高于棧區(qū)
最后,以一個問題收尾,以上打印出來的所有地址都是真實對應(yīng)的物理地址嘛?
答:這些地址全都不是真實的物理地址,而是虛擬地址(解釋見下節(jié))
2.引入虛擬地址概念
從示例出發(fā),引出虛擬地址概念:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h> int g_val = 0; int main()
{ pid_t id = fork(); if(id == 0) { //child while(1) { printf("child[%d], g_val: %d, &g_val: %p\n", getpid(), g_val, &g_val); g_val++; sleep(1); } } else { //parent while(1) { printf("parent[%d], g_val: %d, &g_val: %p\n", getpid(), g_val, &g_val); sleep(1); } } return 0;
}
同一個地址,怎么可能會查出來不同的值?
這側(cè)面證明了 用戶在語言層面中使用的地址根本就不是物理地址,而是虛擬地址。
虛擬地址具體是指什么?
進程的pcb(task_struct)中存放著虛擬地址空間(詳見 “進程地址空間的管理” )的起始地址,虛擬地址空間上的地址就是虛擬地址。
進程的代碼和數(shù)據(jù)保存在內(nèi)存中。進程在運行時,先找到虛擬地址,再通過頁表把虛擬地址轉(zhuǎn)換成物理地址,然后通過物理地址訪問內(nèi)存中存放的代碼和數(shù)據(jù)。
解釋以上代碼運行結(jié)果中的現(xiàn)象:同一個地址,查出來不同的值
我們自己寫的可執(zhí)行程序(父進程)運行時,使用fork函數(shù)創(chuàng)建子進程, 創(chuàng)建出的子進程是以父進程為模板的,它的虛擬地址空間 和 頁表是父進程的拷貝,它的pcb中內(nèi)容與父進程大體相同,只修改了pid 和 ppid等少量屬性。 因為子進程的虛擬地址空間 和 頁表是父進程的拷貝,所以頁表轉(zhuǎn)換的物理地址 指向 父進程的代碼和數(shù)據(jù),剛創(chuàng)建出的子進程共享父進程的代碼 和 數(shù)據(jù)。
子進程和父進程共享數(shù)據(jù),直到發(fā)生數(shù)據(jù)寫入,一方要修改數(shù)據(jù)時,為了維護進程間數(shù)據(jù)的獨立性,一方修改數(shù)據(jù),不能影響另一方,該數(shù)據(jù)會進行寫實拷貝,對子進程和父進程的該數(shù)據(jù)進行分離,這樣一方修改數(shù)據(jù)就不會影響另一方了。
比如:上述代碼中,子進程剛創(chuàng)建出來的時候共享父進程的數(shù)據(jù),子進程共享父進程的g_val變量,當子進程對g_val變量進行修改的時候,為了維護進程間數(shù)據(jù)的獨立性,要對子進程和父進程的該數(shù)據(jù)進行分離,也就是對該數(shù)據(jù)進行寫實拷貝,具體步驟就是 在內(nèi)存開辟一個新空間,然后把該內(nèi)容拷貝到新空間,最后將子進程頁表中對應(yīng)的物理地址修改成新空間的地址。 子進程的g_val變量 和 父進程的g_val變量分離,子進程對拷貝到新空間的g_val變量修改不會影響父進程的g_val變量。
需注意的是,寫實拷貝的整個過程中,都不會對虛擬地址進行修改,修改的是虛擬地址對應(yīng)的物理地址。 這就是同一個地址(虛擬地址),能查出來不同的值的原因:
子進程的頁表是父進程的拷貝,子進程 和 父進程的g_val變量的虛擬地址是一樣的。子進程修改g_val時,發(fā)生寫實拷貝(修改的是虛擬地址對應(yīng)的物理地址),這時子進程和父進程的g_val變量的 虛擬地址仍然是一樣的,但它們的虛擬地址對應(yīng)的物理地址是不同的,所以同一個地址(虛擬地址),能查出來不同的值(數(shù)據(jù)存在物理地址對應(yīng)的空間中)
二、進程地址空間(虛擬地址空間)的管理
理解虛擬地址空間:
大富翁有很多的私生子,這些私生子彼此都不知道對方的存在。大富翁有十個億的資產(chǎn),他和每一個私生子都說:“我有10個億的資產(chǎn),等我去世之后,就由你繼承這些資產(chǎn)”。大富翁給每一個私生子畫大餅,于是每一個私生子都認為自己以后能獨自擁有這10個億的資產(chǎn)。
大富翁平時對每一個私生子的要錢申請基本有求必應(yīng),當然這些請求得在合理范圍,比如一次要個幾千、幾萬、 甚至幾十萬之類。如果私生子的要錢請求過于高,比如一次要幾千萬、一億、十億之類,大富翁會直接數(shù)落他一頓:“我還沒死呢,你就想掏空我的財產(chǎn)”,然后駁回他的請求。
在linux系統(tǒng)下,其實大富翁就相當于操作系統(tǒng)(進行內(nèi)存管理),私生子就相當于進程,操作系統(tǒng)給每一個進程畫的大餅就叫虛擬地址空間(虛擬地址空間的地址編號 是和 內(nèi)存的物理地址編號一樣多的),相當于操作系統(tǒng)跟每一個進程都說:“你獨自享有整個內(nèi)存空間(其實是給每一個進程配一個虛擬地址空間來哄騙它們)”。實際上,進程運行過程中,每一次申請內(nèi)存空間都不能過多,否則空間申請不會成功;每一個進程的代碼和數(shù)據(jù)其實都只占據(jù)了內(nèi)存空間中的一小部分。
虛擬地址空間的實質(zhì):
虛擬地址空間本質(zhì)其實就是一個結(jié)構(gòu)體(不夠全面,后面補充虛擬地址空間本質(zhì)):struct mm_struct
進程pcb(task_struct)中存放了指向 struct mm_struct 的指針
struct task_struct
{ ...struct mm_struct *mm; //對于普通的??進程來說該字段指向他的虛擬地址空間的??空間部分...
}
struct mm_struct
{...struct vm_area_struct *mmap; // 指向虛擬區(qū)間(VMA)鏈的開頭...// 代碼段、數(shù)據(jù)段、堆棧段、參數(shù)段及環(huán)境段的起始和結(jié)束地址。 unsigned long start_code, end_code, start_data, end_data;unsigned long start_brk, brk, start_stack, end_stack;unsigned long arg_start, arg_end, env_start, env_end;
}
虛擬地址空間中劃分了很多分區(qū),要想描述虛擬地址空間,就得描述出其中的每一個分區(qū),struct mm_struct 就采取了記錄每一個分區(qū)的起始和結(jié)束地址的方式 來劃分出虛擬地址空間中的各個分區(qū), 如下:
但實際上,只使用 struct mm_struct 是無法全面描述虛擬地址空間的。
注意到 struct mm_struct 中的struct vm_area_struct *mmap 指針變量還未被使用,要想全面描述虛擬地址空間,還得把這個指針變量使用起來。
先介紹一下 struct vm_area_struct :
struct vm_area_struct
{unsigned long vm_start; // 虛擬內(nèi)存區(qū)域的起始unsigned long vm_end; // 虛擬內(nèi)存區(qū)域的結(jié)束struct vm_area_struct *vm_next, *vm_prev; // 前后指針struct mm_struct *vm_mm; // 回指所屬的 mm_structpgprot_t vm_page_prot; // 所屬分區(qū)的權(quán)限...
}
linux內(nèi)核使用 vm_area_struct 結(jié)構(gòu)來表示?個獨立的虛擬內(nèi)存區(qū)域(VMA)。由于虛擬地址空間不同分區(qū)的虛擬內(nèi)存區(qū)域功能和內(nèi)部機制都不同,因此?個進程要使用多個vm_area_struct結(jié)構(gòu)來分別表示不同類型的虛擬內(nèi)存區(qū)域。使用雙鏈表管理多個vm_area_struct結(jié)構(gòu)(描述一個進程不同類型的虛擬內(nèi)存區(qū)域),struct mm_struct中的mmap指針指向這個雙鏈表結(jié)構(gòu):
所以最終結(jié)論是:虛擬地址空間 = struct mm_struct + 內(nèi)核數(shù)據(jù)結(jié)構(gòu)(由 struct vm_area_struct 組成的雙鏈表)
struct mm_struct 描述了虛擬地址空間的整理情況(虛擬地址空間各個區(qū)域的劃分),由 struct vm_area_struct 組成的雙鏈表 描述了 虛擬地址空間各個區(qū)域的詳細信息(功能和內(nèi)部機制),它們共同構(gòu)建了虛擬地址空間。
三、虛擬地址空間的作用
- 增加了虛擬地址空間,虛擬地址就必須通過頁表轉(zhuǎn)換成物理地址才能去訪存,在地址轉(zhuǎn)換過程中進行安全審核(比如:地址、權(quán)限檢查),變相保護了物理內(nèi)存的安全
假如代碼中使用的地址都是物理地址,那么如果進程A的代碼中錯誤的使用了一個野指針,而這個野指針又指向另一個進程的數(shù)據(jù),對野指針指向的數(shù)據(jù)進行修改。這樣會出大問題,本來只是你一個進程出問題,又影響到其它進程,這不符合進程間的獨立性原則。
所以用戶在代碼中使用的地址絕對不可以是物理地址,而是要使用虛擬地址,在頁表進行虛擬地址向物理地址的轉(zhuǎn)換過程中對這些不在頁表中的野指針地址進行警告報錯處理。
實際上,頁表還有一列權(quán)限欄:
(1)char * str = “hello world”;
// "hello world"是字符串常量,保存在代碼區(qū)(代碼區(qū)的權(quán)限是只讀),str指向該字符串常量起始地址
*str = ‘c’;
// 執(zhí)行這句代碼會報錯,而且報的是運行時出錯。解釋:運行到這句代碼時,要進行訪存修改數(shù)據(jù),虛擬地址 向 物理地址轉(zhuǎn)換的過程中發(fā)現(xiàn)該地址對應(yīng)的權(quán)限是只讀,而此時要進行的操作是寫操作,沒有寫權(quán)限,頁表阻止這次地址轉(zhuǎn)換,并進行報錯處理。
(2)const char * str = “hello world”;
// 如果定義字符串常量時加了const修飾;再進行 *str = ‘c’ 操作時,編譯器就會報編譯時出錯。解釋:用const修飾,是告訴編譯器這是不能修改的數(shù)據(jù),編譯器知道這個信息后,就會對這個數(shù)據(jù)的寫入操作進行語法檢查,編譯器就能發(fā)現(xiàn) *str = ‘c’ 操作的語法錯誤,語法檢查是編譯過程做的事,所以報的是編譯時出錯。
- 進程看待自己的代碼和數(shù)據(jù),全部都是"有序看待",這得益于虛擬地址空間的有序性
可執(zhí)行程序執(zhí)行時,它的代碼和數(shù)據(jù)理論上可以加載到內(nèi)存上的任意位置,這也就意味著實際上可執(zhí)行程序的代碼和數(shù)據(jù)在內(nèi)存上的排布是隨機的、無序的。
但其實有了虛擬地址空間之后,進程運行時,根本就不需要關(guān)心它的代碼和數(shù)據(jù)到底存放在內(nèi)存中的哪一個位置,進程只需要和虛擬地址空間打交道就行,而虛擬地址空間的排版是非常有規(guī)律的,代碼地址存在代碼區(qū),局部變量地址存在棧區(qū),動態(tài)開辟空間的地址存在堆區(qū),得到這些虛擬地址后,后續(xù)操作由操作系統(tǒng)自動完成:頁表將虛擬地址轉(zhuǎn)換成物理地址,再進行訪存。