-
什么是代碼段?
-
什么是數(shù)據(jù)段?
-
數(shù)據(jù)的類型和長(zhǎng)度
-
尋址范圍
-
棧
-
實(shí)模式和保護(hù)模式
-
Linux 中的分段策略
飯是一口一口的吃,計(jì)算機(jī)也是一步一步的發(fā)展,例如下面這張英特爾公司的CPU型號(hào)歷史:
為了利用性能越來(lái)越強(qiáng)悍的計(jì)算機(jī),
操作系統(tǒng)的也是在逐步變得膨脹和復(fù)雜。
為了從
最底層來(lái)學(xué)習(xí)操作系統(tǒng)的一些
基本原理,我們只有拋開(kāi)操作系統(tǒng)的外衣,從
最原始的硬件和編程方式來(lái)入手,才能了解到一些根本的知識(shí)。
這篇文章我們就來(lái)繼續(xù)挖掘一下,
8086 這個(gè)開(kāi)天辟地的處理器中,是如何利用
段機(jī)制來(lái)對(duì)
內(nèi)存進(jìn)行尋址的。
什么是代碼段?
在上一篇文章:Linux 從頭學(xué) 01:CPU 是如何執(zhí)行一條指令的? 中,已經(jīng)提到過(guò),在處理器的內(nèi)部,執(zhí)行每一條指令碼時(shí),CPU是非常機(jī)械、非常單純地從
CS:IP 這2個(gè)寄存器計(jì)算得到轉(zhuǎn)換后的
物理地址,從這個(gè)物理地址所指向的
內(nèi)存地址處,讀取一定長(zhǎng)度的指令,然后交給邏輯運(yùn)算單元(
Arithmetic Logic Unit, ALU)去執(zhí)行。
物理地址的計(jì)算方式是:CS * 16 IP。
當(dāng)CPU讀取一條指令后,根據(jù)指令
操作碼它能夠自動(dòng)知道這條指令一共需要讀取
多少個(gè)字節(jié)。
指令被讀取之后,IP寄存器中的內(nèi)容就會(huì)
自增,指向內(nèi)存中
下一條指令的地址。
例如,在內(nèi)存20000H開(kāi)始的地方,存在2條指令:
mov ax, 1122H
mov bx, 3344H
當(dāng)執(zhí)行第一條指令時(shí),
CS = 2000H,IP = 0000H,經(jīng)過(guò)地址轉(zhuǎn)換之后的物理地址是:
2000H * 16 0000 = 20000H(乘以 16 也就表示十六進(jìn)制的數(shù)
左移 1 位):
當(dāng)?shù)谝粭l指令碼B8 22 11這3個(gè)字節(jié)被讀取之后,IP 寄存器中的內(nèi)容自動(dòng)增加3`,從而指向下一條指令:
當(dāng)?shù)诙l指令碼BB 44 33這3個(gè)字節(jié)被讀取之后,IP寄存器中的內(nèi)容又增加3,變?yōu)?006H。
正如上篇文章所寫(xiě),CPU只是反復(fù)的從CS:IP指向的內(nèi)存地址中讀取指令碼、執(zhí)行指令,再讀取指令碼、再執(zhí)行指令。
可以看出,要完成一個(gè)有意義的工作,
所有的指令碼必須集中在一起,統(tǒng)一放在內(nèi)存中某個(gè)確定的地址空間中,才能被CPU
依次的讀取、執(zhí)行。
內(nèi)存中的這塊地址空間就叫做一個(gè)
段,又因?yàn)?span>這個(gè)段中存儲(chǔ)的是代碼編譯得到的
指令,因此又稱作
代碼段。
因此,用來(lái)對(duì)代碼段進(jìn)行尋址的這兩個(gè)寄存器
CS 和 IP,它們的含義就非常清楚了:
CS: 段寄存器,其中的值左移 1 位之后,得到的值就表示代碼段在內(nèi)存中的首地址,或者稱作基地址;
IP: 指令指針寄存器,表示一條指令的地址,距離基地址的偏移量,也就是說(shuō),IP 寄存器是用來(lái)幫助 CPU 記?。耗男┲噶钜呀?jīng)被處理過(guò)了,下一個(gè)要被處理的指令是哪一個(gè);
什么是數(shù)據(jù)段?
作為一個(gè)有意義的程序,僅僅
只有指令是不夠的,還必須操作數(shù)據(jù)。
這些
數(shù)據(jù)也應(yīng)該集中放在一起,位于內(nèi)存中的某個(gè)地址空間中,這塊地址空間,也是一個(gè)段,稱作
數(shù)據(jù)段。
也就是說(shuō):
代碼段和數(shù)據(jù)段,就是內(nèi)存中的兩個(gè)地址空間,其中分別存儲(chǔ)了指令和數(shù)據(jù)。
可以想象一下:假如指令和數(shù)據(jù)
不是分開(kāi)存放的,而是
夾雜放在一起,那么CPU在讀取一條指令時(shí),肯定就會(huì)把數(shù)據(jù)
當(dāng)做指令來(lái)讀取、執(zhí)行,就像下面這樣,不發(fā)生錯(cuò)誤才怪呢!
CPU對(duì)內(nèi)存中
數(shù)據(jù)段的訪問(wèn)方式,與訪問(wèn)
代碼段是類似的,也是通過(guò)一個(gè)
基地址,再加上一個(gè)
偏移量來(lái)得到數(shù)據(jù)段中的某個(gè)
物理地址。
在8086處理其中,數(shù)據(jù)段的段寄存器是DS,也就是說(shuō),當(dāng)CPU執(zhí)行一條指令,這條指令需要
訪問(wèn)數(shù)據(jù)段時(shí),就會(huì)把DS這個(gè)數(shù)據(jù)段寄存器中的值左移 1 位之后得到的地址,當(dāng)做
數(shù)據(jù)段的基地址。
遺憾的是,CPU中并沒(méi)有提供一個(gè)類似IP寄存器的其他寄存器,來(lái)表示數(shù)據(jù)段的
偏移地址寄存器。
這其實(shí)并不是壞事,因?yàn)橐粋€(gè)程序在處理數(shù)據(jù)時(shí),需要對(duì)數(shù)據(jù)進(jìn)行什么樣操作,程序的
開(kāi)發(fā)者是最清楚的,因此我們就可以用
更靈活的方式來(lái)告訴CPU應(yīng)該如何計(jì)算數(shù)據(jù)的偏移地址。
就像猴子掰苞米一樣,不需要按照順序來(lái)掰,想掰哪個(gè)就掰哪個(gè)。同樣的,程序在操作
數(shù)據(jù)時(shí),無(wú)論操作
哪一個(gè)數(shù)據(jù),直接給出該數(shù)據(jù)的
偏移地址的值就可以了。
數(shù)據(jù)的類型和長(zhǎng)度
但是,在操作數(shù)據(jù)段中每一個(gè)數(shù)據(jù),有一個(gè)比較重要的概念需要時(shí)刻銘記:
數(shù)據(jù)的類型是什么,這個(gè)數(shù)據(jù)在內(nèi)存中占據(jù)的字節(jié)數(shù)是多少。
我們?cè)?span>高級(jí)語(yǔ)言編程中(eg:C語(yǔ)言),在定義一個(gè)變量的時(shí)候,必須明確這個(gè)變量的
類型是什么。一旦類型確定了,那么它在被加載到內(nèi)存中之后,所
占據(jù)的空間大小也就確定了。
比如下面這張圖:
假設(shè)30000H是數(shù)據(jù)段的
基地址(也就意味著DS寄存器中的內(nèi)容是3000H),那么30000H地址處的數(shù)據(jù)大小是多少:11H?2211H?還是44332211H?
這幾個(gè)都有可能,因?yàn)?span>沒(méi)有確定數(shù)據(jù)的類型!
我們知道,在C語(yǔ)言中,假如有一個(gè)指針ptr最終指向了這里的30000H物理地址處(C代碼中的ptr是虛擬地址,經(jīng)過(guò)地址轉(zhuǎn)換之后執(zhí)行這里的30000H物理地址)。
如果ptr定義成:
char *ptr;
那么可以說(shuō)ptr指針指向的數(shù)值是11H。
如果ptr定義成:
int *ptrt;
就可以說(shuō)ptr指針指向的數(shù)值就是44332211H(假設(shè)是小端格式)。
也就是說(shuō),指針ptr指向的數(shù)據(jù),取決于定義指針變量時(shí)的
類型。
這是高級(jí)語(yǔ)言中的情況,那么在
匯編語(yǔ)言中呢?
PS: 之前我曾說(shuō)過(guò),文章的主要目的是學(xué)習(xí) Linux 操作系統(tǒng),但是為了學(xué)習(xí)一些相對(duì)底層的內(nèi)容,在開(kāi)始階段必須拋開(kāi)操作系統(tǒng)的外衣,進(jìn)入到硬件最近的地方去看。
但是該怎么看呢?還是要借助一些原始的手段和工具,那么匯編代碼無(wú)疑就是最好的、也是唯一的手段;
不過(guò),涉及到的匯編代碼都是最簡(jiǎn)單的,僅僅是為了說(shuō)明原理;
在
匯編語(yǔ)言中,CPU是通過(guò)指令碼中的相關(guān)
寄存器來(lái)判斷操作數(shù)據(jù)的長(zhǎng)度。
在上一篇文章中說(shuō)過(guò),相對(duì)于寄存器來(lái)說(shuō),CPU操作內(nèi)存的速度是很慢的。
因此,CPU在對(duì)數(shù)據(jù)段中的數(shù)據(jù)進(jìn)行處理的時(shí)候,一般都是先把原始數(shù)據(jù)讀取到通用寄存器中(比如:
ax, bx, cx dx),然后進(jìn)行計(jì)算。
得到計(jì)算結(jié)果之后,再把結(jié)果寫(xiě)回到內(nèi)存的數(shù)據(jù)段中(如果需要的話)。
那么CPU在讀寫(xiě)數(shù)據(jù)時(shí),就根據(jù)指令碼中使用的寄存器,來(lái)決定讀寫(xiě)數(shù)據(jù)的長(zhǎng)度。例如:
mov ax, [0]
其中的
[0] 表示內(nèi)存的數(shù)據(jù)段中偏移地址是0的位置。
CPU在執(zhí)行這條指令的時(shí)候,就會(huì)到30000H(假設(shè)此時(shí)數(shù)據(jù)段寄存器DS的值為3000H) 這個(gè)物理地址處,取出2個(gè)字節(jié)的數(shù)據(jù),放到通用寄存器ax中,此時(shí)ax寄存器中的值就是2211H。
為什么取出2個(gè)字節(jié)?因?yàn)閍x寄存器的長(zhǎng)度是16位,就是2個(gè)字節(jié)。
那如果只想取1個(gè)字節(jié),該怎么辦?
16位的通用寄存器ax可以拆成2個(gè)8位的寄存器里使用:ah和al。
mov al, [0]
因?yàn)橹噶畲a中的al寄存器是8位,因此CPU就只讀取30000H處的一個(gè)字節(jié)11,放到al寄存器中。(此時(shí)ax寄存器的高8位,也就是ah中的值保持不變)
那如果想取3個(gè)字節(jié)或4個(gè)字節(jié)怎么辦?
作為相當(dāng)古老的處理器,8086CPU 中是16位的,只能對(duì)8位或16位的數(shù)據(jù)進(jìn)行操作。
尋址范圍
從以上內(nèi)容可以總結(jié)得出:
-
代碼段和數(shù)據(jù)段都是通過(guò) 【基地址 偏移地址】的方式進(jìn)行尋址;
-
基地址都放在各自的段寄存器中,CPU 會(huì)自動(dòng)把段寄存器的值,左移 1 位之后,作為段的基地址;
-
偏移地址決定了段中的每一個(gè)具體的地址,最大偏移地址是 16 個(gè) bit1,也即是 64KB 的空間;
注意:這里的段寄存器左移1位,是指
十六進(jìn)制的左移,相當(dāng)于是
乘以 16,因此段的基地址都是16的倍數(shù)。
我們?cè)賮?lái)看一下這里的64 KB空間,與20根地址線有什么瓜葛。
上篇文章說(shuō)到:8086處理器有20根地址線,一共可以表示1MB的內(nèi)存空間,即使給它更大的空間,它也沒(méi)有福氣去享受,因?yàn)閷ぶ凡坏酱笥? MB的地址空間?。?
這1MB的內(nèi)存空間,就可以
分割為很多個(gè)段。
例如:第1個(gè)段的地址范圍是:
我們來(lái)計(jì)算
最后一個(gè)段的空間。
段寄存器和偏移地址都取最大值,就是
FFFF:FFFF,先偏移再相加:
FFFF0 FFFF = 10FFEF =1M 64K - 16Bytes。
超過(guò)了1 MB的空間大小,但是畢竟只有20根地址線,肯定是
無(wú)法尋址超過(guò)1 MB地址空間的,因此系統(tǒng)會(huì)采取
回繞的方式來(lái)定位到一個(gè)地址空間,類似與數(shù)學(xué)中的
取模操作。
此外還有一點(diǎn),在表示一個(gè)內(nèi)存地址的時(shí)候,一般
不會(huì)直接給出物理地址的值(比如:3000A),而是使用
段地址:偏移地址 這樣的形式來(lái)表示(比如:3000:000A)。
棧
棧也是
數(shù)據(jù)空間的一種,只不過(guò)它的
操作方式有些特殊而已。
棧的操作方式就是4個(gè)字:
后進(jìn)先出。
在上面介紹
數(shù)據(jù)段的時(shí)候,我們都是在指令碼中
手動(dòng)對(duì)數(shù)據(jù)的偏移地址進(jìn)行設(shè)置,指哪打哪,因?yàn)檫@些數(shù)據(jù)放在什么位置、表示什么意思、怎么來(lái)使用,開(kāi)發(fā)者自己心里
最門(mén)清。
但是
棧有些不一樣,雖然它的功能也是用來(lái)存儲(chǔ)數(shù)據(jù)的,但是操作棧的方式,是由處理器提供的一些
專門(mén)的指令來(lái)操作的:push和pop。
push(入棧): 往??臻g中放入一個(gè)數(shù)據(jù);
pop(出棧): 從棧空間中彈出一個(gè)數(shù)據(jù);
注意:這里的數(shù)據(jù)是
固定 2 個(gè)字節(jié),也就是一個(gè)
字。
寫(xiě)過(guò)C/C程序的小伙伴都知道:在函數(shù)
調(diào)用的時(shí)候,存在
入棧操作;在函數(shù)
返回的時(shí)候,存在
出棧操作。
既然棧也是指一塊內(nèi)存空間,那么也就是表現(xiàn)為內(nèi)存中的一個(gè)段。
既然是一個(gè)段,那肯定就存在一個(gè)
段寄存器,用來(lái)代表它的
基地址,這個(gè)棧的段寄存器就是SS。
此外,由于棧在入棧和出棧的時(shí)候,是按照
連續(xù)的地址順序操作的,因此處理器為棧也提供了一個(gè)
偏移地址寄存器:SP(稱作:棧頂指針),指向??臻g中
最頂上的那個(gè)元素的位置。
例如下面這張圖:
棧空間的基地址是1000:0000,SS:SP執(zhí)行的地址空間是棧頂,此時(shí)棧頂中的元素是44。
當(dāng)執(zhí)行下面這2條指令時(shí):
mov ax, 1234H
push as
棧頂指針寄存器SP中的值首先
減 2,變成000A:
然后,再把寄存器ax中的值1234H放入SS:SP指向的內(nèi)存單元處:
出棧的操作順序是
相反的:
pop bx
首先把SS:SP指向的內(nèi)存單元中的數(shù)據(jù)1234H放入寄存器bx中,然后把棧頂指針寄存器SP中的值
加 2,變成000C:
以上描述的是
8086 處理器中對(duì)
棧操作的執(zhí)行過(guò)程。
如果你看過(guò)其他一些棧相關(guān)的描述書(shū)籍,可以看出這里使用的是
“滿遞減” 的棧操作方式,另外還還有:滿遞增,空遞減,空遞增 這幾種操作方式。
滿:是指棧頂指針指向的那個(gè)空間中,是一個(gè)有效的數(shù)據(jù)。當(dāng)一個(gè)新數(shù)據(jù)入棧時(shí),棧頂指針先指向下一個(gè)空的位置,然后 把數(shù)據(jù)放入這個(gè)位置;
空:是指棧頂指針指向的那個(gè)空間中,是一個(gè)無(wú)效的數(shù)據(jù)。當(dāng)一個(gè)新數(shù)據(jù)入棧時(shí),先把數(shù)據(jù)放入這個(gè)位置,然后棧頂指針指向下一個(gè)空的位置;
遞增:是指在數(shù)據(jù)入棧時(shí),棧頂指針向高地址方向增長(zhǎng);
遞減:是指在數(shù)據(jù)入棧時(shí),棧頂指針向低地址方向遞減;
實(shí)模式和保護(hù)模式
從以上對(duì)內(nèi)存的尋址方式中可以看出:只要在可尋址的范圍內(nèi),我們寫(xiě)的程序是可以對(duì)內(nèi)存中
任意一個(gè)位置的數(shù)據(jù)進(jìn)行操作的。
這樣的尋址方式,稱之為
實(shí)模式。實(shí),就是實(shí)在、實(shí)際的意思,簡(jiǎn)潔、直接,沒(méi)有什么彎彎繞。
既然編寫(xiě)代碼的是人,就一定會(huì)犯一些低級(jí)的小錯(cuò)誤。或者一些惡意的家伙,故意去操作那些
不應(yīng)該、不可以被操作的內(nèi)存空間中的代碼或數(shù)據(jù)。
為了對(duì)內(nèi)存進(jìn)行有效的保護(hù),從80386開(kāi)始,引入了
保護(hù)模式 來(lái)對(duì)內(nèi)存進(jìn)行尋址。
有些書(shū)籍中會(huì)提到 IA-32A 這個(gè)概念,IA-32 是英特爾 Architecture 32-bit簡(jiǎn)稱,即英特爾32位體系架構(gòu),也是在386中首先采用。
雖然引進(jìn)了保護(hù)模式,但是也存在實(shí)模式,即向前兼容。電腦開(kāi)機(jī)后處于實(shí)模式,BIOS 加載主引導(dǎo)記錄以及進(jìn)行一些寄存器的設(shè)置之后就進(jìn)入保護(hù)模式。
從386以后引入的
保護(hù)模式下,地址線變成了32根,最大尋址空間可以達(dá)到4GB。
當(dāng)然,處理器中的寄存器也變成了32位。
我們還是用
段基址 偏移量 的方式來(lái)計(jì)算一個(gè)
物理地址,假設(shè)段寄存器中內(nèi)容為0,偏移地址最大長(zhǎng)度也是32位,那么一個(gè)段能表示的最大空間也就是4GB。
這也是為什么如今現(xiàn)代處理器中,每個(gè)進(jìn)程的最大可尋址空間是4GB(一般指的是
虛擬地址)。
一句話總結(jié):實(shí)模式和保護(hù)模式最根本的區(qū)別就是
內(nèi)存是否收到保護(hù)。
Linux 中的分段策略
上面描述的分段機(jī)制是
x86 處理器中所提供的一種內(nèi)存尋址
機(jī)制,這僅僅是一種機(jī)制而已。
在x86處理器之上,運(yùn)行著Windows、Linux獲取其它操作系統(tǒng)。
我們開(kāi)發(fā)者是
面對(duì)操作系統(tǒng)來(lái)編程的,寫(xiě)出來(lái)的程序是被操作系統(tǒng)接管,并不是直接被x86處理器來(lái)接管。
相當(dāng)于操作系統(tǒng)把應(yīng)用程序和x86處理器之間進(jìn)行了
一層隔離:
因此,如何利用x86提供的分段機(jī)制是
操作系統(tǒng)需要操心的問(wèn)題。
而操作系統(tǒng)提供什么樣的
策略給
應(yīng)用程序來(lái)使用,這就是另外一個(gè)問(wèn)題了。
那么,Linux操作系統(tǒng)是如何來(lái)包裝、使用
x86提供的
段尋址方式的呢?
是否還記得上一篇文章中的這張圖:
這是Linux2.6版本中四個(gè)主要的
段描述符,這里先不用管段描述符是什么,它們最終都是用來(lái)描述內(nèi)存中的一塊空間而已。
在現(xiàn)代操作系統(tǒng)中,
分段和分頁(yè)都是對(duì)內(nèi)存的劃分和管理方式,在功能上是有點(diǎn)重復(fù)的。
Linux以非常
有限的方式使用分段,
更喜歡使用分頁(yè)方式。
上面的這張圖,一共定義了4個(gè)段,每一個(gè)段的基地址都是0x00000000,每一個(gè)段的Limit都是0xFFFFF。
從Limit的值可以得到:最大值是
2 的 20 次方,只有1 MB的空間。
但是其中的G字段表示了段的
粒度,1表示粒度是4 K,因此
1 MB * 4K = 4 GB ,也就是說(shuō),段的最大空間是4 GB。
這4個(gè)段的
基地址和
尋址范圍都是一樣的!主要的區(qū)別就是Type和DPL字段不同。
DPL表示優(yōu)先級(jí),2個(gè)
用戶段(代碼段和數(shù)據(jù)段) 的優(yōu)先級(jí)值是3,優(yōu)先級(jí)
最低(值越大,優(yōu)先級(jí)越低);2個(gè)
內(nèi)核段(代碼段和數(shù)據(jù)段)的優(yōu)先級(jí)值是0,優(yōu)先級(jí)
最高。
因此,可以得出Linux系統(tǒng)中的一個(gè)重要結(jié)論:
邏輯地址與線性地址,在數(shù)值上是相等的,因?yàn)榛刂肥?0x00000000。
關(guān)于Linux中的
內(nèi)存分段和分頁(yè)尋址方式更詳細(xì)的內(nèi)容,我們以后再慢慢聊。
------ End ------