天天看點

程序眼中的線性位址空間

從文章的題目我們就知道今天是以一個程序的角度來看待自身的運作環境。我們先提出第一個問題,什麼是程序?對于這個問題,各種參考資料上給出的定義都顯得過于抽象而難以了解,下面是我自己的定義:

程序是一個動态的概念,它是靜态的可執行檔案執行過程的描述,其包含了一個靜态程式運作時的狀态和其所占據的系統資源的總和。

還是很抽象嗎?那麼,我們可以這樣比喻,如果說菜單是程式代碼,廚具是硬體的話,那麼炒菜的整個過程就是一個程序。這下了解了吧?那我們繼續。

每個程式在啟動之後都會擁有自己的虛拟位址空間(virtual address space),這個虛拟位址空間的大小由計算機平台決定,具體一點說由作業系統的位數和cpu的位址總線寬度所決定,其中cpu的位址總線寬度決定了位址空間的理論上限(先不考慮主機闆…)。

比如32位的硬體平台可編址範圍就是0x00000000~0xffffffff,即就是4gb。而64位的硬體平台達到了理論上0x0000000000000000~0xffffffffffffffff的尋址空間,即就是17179869184gb的大小(事實上我自己的64位intel core i3處理器也僅有36位位址總線,加上虛拟位址擴充功能也才48位,并非64位)。

為了行文的簡單,我就以32位硬體平台來描述吧(事實上我對64位所知甚少,不敢信口開河…),同時指定環境為32位的linux作業系統。

可能看到這裡你反而更迷惑了,我一直在說一個程序擁有4gb的線性位址空間(以下隻讨論32位),可是作業系統上同時在運作着n個程序,難不成每個都有4gb的線性位址空間不成?沒錯,每個都有。我們一直在使用術語“線性位址空間”而非“主存儲器(記憶體)”,因為線性位址空間并非和主存等價。我們平時隻要一提到“位址”這個概念,想必大家自然而然的就想到了主存儲器。但事實上并非線性位址就一定指向主存儲器的實體位址,如果你對“線性位址空間”不了解的話,我建議你先去看看我的另一篇博文《基于intel 80×86 cpu的ibm pc及其相容計算機的啟動流程》。

其實說到線性位址空間,就不得不提到intel cpu保護模式下的記憶體分段和分頁,但這偏離了文章的主旨。我們暫時隻需要知道,之是以程序擁有獨立的4gb的虛拟位址,是因為cpu和作業系統提供了一種虛拟位址到實際實體位址的映射機制,在頁映射模式下,cpu發出的是虛拟位址,即程序看到的虛拟的位址,經過mmu(memory management unit)部件轉換之後就成了實體位址。

好了,下文中我将假定讀者了解了線性位址空間的概念,并認可了每個程序擁有4gb線性位址空間這一事實(實體位址擴充(pae:physical address extension)技術後面再說)。那麼,這4gb的線性位址空間裡都有些什麼呢?我們畫一張圖來說明一下。

程式眼中的線性位址空間

記憶體高位址區域是被作業系統核心所占據的,linux作業系統占據了高位址區域的1gb記憶體(windows系統預設保留2gb給作業系統,但是可以配置為保留1gb)。如果我們想知道一個程序具體的記憶體空間布局的話,可以去/proc目錄找以程序的pid所命名的目錄下一個叫maps的檔案,使用cat指令檢視即可(需要root權限)。

我們從圖中可以看到,32位linux系統中,代碼段總是從位址0x08048000處開始的。資料段一般是在下一個4kb(分頁機制預設選擇4kb一個記憶體頁)對齊的位址處開始。運作時堆是在資料段之後又一個4kb對齊處開始的,并通過malloc()函數調用向上增長(linux下的malloc()一般依靠調用brk()或者mmap()系統調用實作)。再接着跳過動态連結庫的區域就是程序的運作時棧了,需要注意的是棧是由高位址向着低位址增長的。棧空間再往上就是作業系統保留區域了,用于駐留核心的代碼和資料。即就是在一個程序的眼裡,隻有它和作業系統在一起。

也許你會問,那麼一個程序如何修改另外一個程序的運作時資料呢?比如所謂的外挂程式。我們想想,一個程序不知道另一個程序,那誰知道所有的程序呢?作業系統呗,沒錯,作業系統提供了這種抽象,它也就擁有通路所有程序位址空間的能力。答案就是,一個程序倘若要修改不屬于自己的程序空間的資料,就需要作業系統提供相關的系統調用(或api函數)的支援來實作。

我們具體來看看代碼段,以c語言為例,程式代碼段的入口_start位址處的啟動代碼(startup code)是在目标檔案ctr1.o(屬于c運作時庫的部分)中定義的,對于特定平台上的c程式都一樣。其執行流程如下:

而我們平時寫的main函數隻是整個c程式運作過程中所調用的一環而已。

我們給出一段代碼來看看一個c語言程式編譯連結之後如何安排各個元素的記憶體位置吧,代碼和注釋如下:

注釋中我們看到了各個元素所在記憶體段的位置。而編譯好的main函數本身是存在于代碼區的(一般代碼段也是隻讀段)。我們這個程式運作後如果是動态連結的c語言運作時庫的話,動态庫會存在圖示的動态庫映射區。其實無論使用c語言運作時庫的程式無論有多少,運作時庫的代碼在記憶體裡隻會有一份。對于不同的程式,進行位址映射即可。

接下來我們簡單說說棧(stack),關于棧的基本概念到處都是,如果大家不明白可以自己去查查。其實這裡的棧就是把一段位于使用者線性位址空間最高處的一段連續記憶體以棧的思想來使用罷了。大家不要覺得線性空間有4gb,棧占據了很大。其實棧大小預設就幾mb罷了。linux可以在終端下執行 ulimit -a指令檢視限制。如圖所示:

程式眼中的線性位址空間

我這裡不過也就預設8192kb(8mb)大小,不過可以使用ulimit指令調整(調整隻在本次bash執行過程中有效,下次需要重新設定)。

棧也經常被叫做棧幀(stack frame)或者活動記錄(activate record)。棧裡通常存儲以下内容:

函數的臨時變量; 函數的傳回位址和參數; 函數調用過程中儲存的上下文。

在i386中,使用esp和ebp寄存器劃定範圍。esp寄存器始終指向棧頂,随着壓棧和出棧操作而改變值。ebp寄存器随着調用過程,暫時的指向一個固定的棧位置,便于尋址操作的進行。

我們畫一張圖來看看吧:

程式眼中的線性位址空間

這裡照抄網上的函數調用流程:

把所有的參數壓入棧(有時候是一部分參數,剩餘參數通過寄存器傳遞) 把目前指令的下一條指令的位址壓入棧 跳轉到函數體執行

我繼續續上後面的操作:

在棧裡繼續建立該函數的臨時變量和其他資料 函數代碼執行完之後棧後退到局部變量之上的位置 恢複之前儲存的所有寄存器 取出原先儲存的傳回位址,跳轉回去 eax寄存器儲存了函數的傳回值(浮點數是把傳回值放在第一個浮點寄存器上%st(0) )

為了不讓大家變的過于糾結,我就不貼出相關的彙編代碼了,有興趣的同學可以自己研究編譯器生成的彙編語言。具體方法在《編譯和連結那點事》和《淺談緩沖區溢出之棧溢出》中有詳細的描述。

好了,本篇暫時結束,下文以後再說。

繼續閱讀