● 用戶程序編譯連接形成的地址空間在什麼範圍內?
● 內核編譯後地址空間在什麼範圍內?
● 要對外設進行訪問,I/O的地址空間又是什麼樣的?
於是就有了這篇文章,從大概上把內存相關知識點介紹一下,減少同學們在驅動課時對內存的困惑
先回答第一個問題。Linux最常見的可執行文件格式爲elf(Executable and Linkable Format)。在elf格式的可執行代碼中,ld總是從0x800 0000開始安排程序的“代碼段”,對每個程序都是這樣。至於程序執行時在物理內存中的實際地址,則由內核爲其建立內存映射時臨時分配,具體地址取決於當時所分配的物理內存頁面。
我們可以用Linux的實用程序obj對你的程序進行反彙編,從而知曉其地址範圍。
例如:假定我們有一個簡單的C程序Hello.c
# include <stdio.h>
greeting ( )
{
printf(“Hello,world!\n”);
}
main()
{
greeting();
}
之所以把這樣簡單的程序寫成兩個函數,是爲了說明指令的轉移過程。我們用gcc和ld對其進行編譯和連接,得到可執行代碼hello。然後,用Linux的實用程序obj對其進行反彙編:
$obj –d hello
得到的主要片段爲:
08048568 <greeting>:
8048568: pushl %ebp
8048569: movl %esp, %ebp
804856b: pushl $0x809404
8048570: call 8048474 <_init+0x84>
8048575: addl $0x4, %esp
8048578: leave
8048579: ret
804857a: movl %esi, %esi
0804857c <main>:
804857c: pushl %ebp
804857d: movl %esp, %ebp
804857f: call 8048568 <greeting>
8048584: leave
8048585: ret
8048586: nop
8048587: nop
其中,像08048568這樣的地址,就是我們常說的虛地址(這個地址實實在在的存在,只不過因爲物理地址的存在,顯得它是“虛”的罷了)。
虛擬內存、內核空間和用戶空間(部分內容參考《ULK》V3中文版)
Linux虛擬內存的大小爲2^32(在32位的x86機器上),內核將這4G字節的空間分爲兩部分。最高的1G字節(從虛地址0xC0000000到0xFFFFFFFF)供內核使用,稱爲“內核空間”。而較低的3G字節(從虛地址0x00000000到0xBFFFFFFF),供各個進程使用,稱爲“用戶空間”。因爲每個進程可以通過系統調用進入內核,因此,Linux內核空間由系統內的所有進程共享。於是,從具體進程的角度來看,每個進程可以擁有4G字節的虛擬地址空間(也叫虛擬內存)。
每個進程有各自的私有用戶空間(0~3G),這個空間對系統中的其他進程是不可見的。最高的1GB內核空間則爲所有進程以及內核所共享。另外,進程的“用戶空間”也叫“地址空間”,在後面的敘述中,我們對這兩個術語不再區分。
用戶空間不是進程共享的,而是進程隔離的。每個進程最大都可以有3GB的用戶空間。一個進程對其中一個地址的訪問,與其它進程對於同一地址的訪問絕不衝突。比如,一個進程從其用戶空間的地址0x1234ABCD處可以讀出整數8,而另外一個進程從其用戶空間的地址0x1234ABCD處可以讀出整數20,這取決於進程自身的邏輯。
任意一個時刻,在一個CPU上只有一個進程在運行。所以對於此CPU來講,在這一時刻,整個系統只存在一個4GB的虛擬地址空間,這個虛擬地址空間是面向此進程的。當進程發生切換的時候,虛擬地址空間也隨着切換。由此可以看出,每個進程都有自己的虛擬地址空間,只有此進程運行的時候,其虛擬地址空間才被運行它的CPU所知。在其它時刻,其虛擬地址空間對於CPU來說,是不可知的。所以儘管每個進程都可以有4 GB的虛擬地址空間,但在CPU眼中,只有一個虛擬地址空間存在。虛擬地址空間的變化,隨着進程切換而變化。
從上面我們知道,一個程序編譯連接後形成的地址空間是一個虛擬地址空間,但是程序最終還是要運行在物理內存中。因此,應用程序所給出的任何虛地址最終必須被轉化爲物理地址,所以,虛擬地址空間必須被映射到物理內存空間中,這個映射關係需要通過硬件體系結構所規定的數據結構來建立。這就是我們所說的段描述符表和頁表,Linux主要通過頁表來進行映射。
於是,我們得出一個結論,如果給出的頁表不同,那麼CPU將某一虛擬地址空間中的地址轉化成的物理地址就會不同。所以我們爲每一個進程都建立其頁表,將每個進程的虛擬地址空間根據自己的需要映射到物理地址空間上。既然某一時刻在某一CPU上只能有一個進程在運行,那麼當進程發生切換的時候,將頁表也更換爲相應進程的頁表,這就可以實現每個進程都有自己的虛擬地址空間而互不影響。所以,在任意時刻,對於一個CPU來說,只需要有當前進程的頁表,就可以實現其虛擬地址到物理地址的轉化。
內核空間到物理內存的映射
在驅動中我們提的比較多的就是內核空間與硬件內存地址,那麼我們下面來詳細介紹下內核空間和實際的硬件物理地址。
內核空間對所有的進程都是共享的,其中存放的是內核代碼和數據,而進程的用戶空間中存放的是用戶程序的代碼和數據,不管是內核程序還是用戶程序,它們被編譯和連接以後,所形成的指令和符號地址都是虛地址,而不是物理內存中的物理地址。
雖然內核空間佔據了每個虛擬空間中的最高1GB字節,但映射到物理內存卻總是從最低地址(0x00000000)開始的,之所以這麼規定,是爲了在內核空間與物理內存之間建立簡單的線性映射關係。其中,3GB(0xC0000000)就是物理地址與虛擬地址之間的位移量,在Linux代碼中就叫做PAGE_OFFSET。
我們來看一下在include/asm/i386/page.h頭文件中對內核空間中地址映射的說明及定義:
#define __PAGE_OFFSET (0xC0000000)
……
#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
對於內核空間而言,給定一個虛地址x,其物理地址爲“x- PAGE_OFFSET”,給定一個物理地址x,其虛地址爲“x+ PAGE_OFFSET”。
這裏再次說明,宏__pa()僅僅把一個內核空間的虛地址映射到物理地址,而決不適用於用戶空間,用戶空間的地址映射要複雜得多,它通過分頁機制完成。