訂閱
糾錯(cuò)
加入自媒體

Linux之x86 處理器如何進(jìn)行層層的內(nèi)存保護(hù)?

實(shí)模式:bootloader 為程序計(jì)算段的基地址

保護(hù)模式:bootloader 為自己創(chuàng)建段描述符

確定 GDT 的地址

創(chuàng)建代碼段的描述符

創(chuàng)建數(shù)據(jù)段的描述符

創(chuàng)建棧段的描述符

段描述符是如何確保段的安全的?

段寄存器高速緩存

對(duì)段寄存器本身的保護(hù)

對(duì)段界限的檢查

在上一篇文章中,我們已經(jīng)順利的從實(shí)模式,過(guò)渡到了保護(hù)模式。

保護(hù)模式與實(shí)模式最本質(zhì)的區(qū)別就是:保護(hù)模式使用了全局描述符表,用來(lái)保存每一個(gè)程序(bootloader,操作系統(tǒng),應(yīng)用程序)使用到的每個(gè)段信息:開始地址,長(zhǎng)度,以及其他一些保護(hù)參數(shù)。

這篇文章,我們來(lái)看一下 bootloader 是如何來(lái)進(jìn)行自我進(jìn)化到保護(hù)模式的,然后深入看一下保護(hù)模式是如何對(duì)內(nèi)存進(jìn)行安全保護(hù)的。

作為背景知識(shí),我們先來(lái)看一下 x86 中的地址變換過(guò)程:

x86 處理器中的分頁(yè)機(jī)制是可以被關(guān)閉的,此時(shí)線性地址就等于物理地址,這也是我們一直討論的情況。

下一篇文章,我們就把 x86 中的分頁(yè)機(jī)制打開,并與 Linux 中的分段和分頁(yè)機(jī)制進(jìn)行對(duì)比。

實(shí)模式:bootloader 為程序計(jì)算段的基地址

在之前的文章:Linux從頭學(xué)06:16張結(jié)構(gòu)圖,徹底理解【代碼重定位】的底層原理中,我們討論了 bootloader 是如何把應(yīng)用程序讀取到內(nèi)存中,最后跳入到程序的入口地址的。

這里所說(shuō)的程序,可以是操作系統(tǒng),也可以是應(yīng)用程序。

下面這張圖,是程序被加載到內(nèi)存中之后,header 中的信息:

因?yàn)槌绦蚴潜?bootloader 動(dòng)態(tài)讀取到內(nèi)存中的,它是不知道自己被放在內(nèi)存中的什么位置,因此它也不知道自己代碼段、數(shù)據(jù)段、棧的開始地址。

但是,程序要想能夠正常執(zhí)行,就必須要知道這些信息,那怎么辦?

只有 bootloader 才能解決問(wèn)題,因?yàn)槭撬鼇?lái)把程序從硬盤加載到內(nèi)存中的。

因此,bootloader 在跳入程序的入口地址之前,必須把其中的代碼段、數(shù)據(jù)段、棧段的基地址計(jì)算出來(lái),然后寫入到程序的 header 中,如下圖所示:

這樣的話,程序開始執(zhí)行時(shí),就可以從自己的 header 中獲取到這 3 個(gè)段基地址,并且賦值給相應(yīng)的寄存器,從而順利的執(zhí)行程序。

也就是說(shuō):程序的 header 空間,充當(dāng)了 bootloader 與它進(jìn)行信息交互的媒介,用來(lái)傳遞 3 個(gè)段寄存器的基地址。

以上的這個(gè)過(guò)程,一直工作在實(shí)模式,因此就沒(méi)有段描述符什么事情。

在以后文章中,我們還會(huì)看到在保護(hù)模式下,bootloader 仍然會(huì)利用 OS 的 header 空間,來(lái)傳遞段的索引號(hào)。然后 OS 利用這個(gè)段索引號(hào),去查找 GDT 表,從而找到每一個(gè)段的基地址以及其他一些保護(hù)信息。

保護(hù)模式:bootloader 為自己創(chuàng)建段描述符

bootloader 從 BIOS 接管系統(tǒng)之后,剛開始是運(yùn)行在實(shí)模式下的。

當(dāng)它完成一些準(zhǔn)備工作之后,就可以進(jìn)入保護(hù)模式了,也就是把 CR0 寄存器的 bit0 設(shè)置為 1。

這個(gè)準(zhǔn)備工作中,最重要的就是:建立 GDT 這個(gè)表,并且把 GDT 的開始地址,存儲(chǔ)到寄存器 GDTR 中。

下面這張圖,是 bootloader 被加載到內(nèi)存中的布局圖:

bootloader 被加載到 0x0000_7C00 地址處。

它最少需要?jiǎng)?chuàng)建 3 個(gè)段描述符:代碼段、數(shù)據(jù)段和棧段。

確定 GDT 的地址

在創(chuàng)建段描述符之前,需要先確定: 把 GDT 表放在內(nèi)存中的什么位置?

暫且就把它放在 0x0001_0000 這個(gè)地址吧,距離零地址 64K 的位置。

按照處理器的要求,在第 1 個(gè)表項(xiàng)(稱之為 item 或者 entry,每本書上都不一樣)必須為空描述符(index = 0)。

創(chuàng)建代碼段描述符

bootloader 的代碼放在 0x0000_7C00 開始的地址,長(zhǎng)度是 512B。

根據(jù)這些信息,就可以構(gòu)造出代碼段的描述符了:

創(chuàng)建數(shù)據(jù)段描述符

bootloader 待會(huì)需要把操作系統(tǒng)或其他應(yīng)用程序,從硬盤讀取到內(nèi)存中,例如:讀取到 0x0002_0000 的位置。

那么 bootloader 就必須能夠訪問(wèn)到這個(gè)位置,并且是以數(shù)據(jù)段的讀寫方式。

為了利用全部的 4G 內(nèi)存空間,bootloader 可以把這 4G 空間,作為一個(gè)數(shù)據(jù)段來(lái)定義它的描述符,如下:

創(chuàng)建棧段描述符

理論上,bootloader 可以使用內(nèi)存中的任意一塊空閑空間,來(lái)作為自己的棧。

因?yàn)闂T?push 操作的時(shí)候,是向低地址方向增長(zhǎng)的。

因此很多書籍都會(huì)把棧頂基地址設(shè)置為 bootloader 的開始地址,也就是 0x0000_7C00 地址處,并且把棧的空間大小限制在 4K 的范圍。

根據(jù)以上這些信息,就可以創(chuàng)建出棧的段描述符,如下:

當(dāng)以上這幾個(gè)段的描述符都創(chuàng)建好之后,就可以把 GDT 的地址(0x0001_0000),設(shè)置到 GDTR 寄存器中了。

最后,再把 CR0 寄存器的 bit0 設(shè)置為 1,就正式的進(jìn)入保護(hù)模式來(lái)執(zhí)行 bootloader 中后面的代碼了。

段描述符是如何確保段的安全訪問(wèn)的? 段寄存器高速緩存

進(jìn)入保護(hù)模式之后,雖然對(duì)段寄存器中內(nèi)容的解釋改變了,但是執(zhí)行每一條指令,還是需要使用到這些段寄存器的: cs, ds, ss等等。

想象一下:每執(zhí)行一條指令,都會(huì)從邏輯地址中,獲取到段索引號(hào),然后去查找 GDT 表,從而定位到段的基地址。

大家都知道程序有個(gè)“局部性”原理,也就是連續(xù)執(zhí)行的代碼,都是集中在一段連續(xù)的程序空間中的。

這個(gè)連續(xù)的程序空間,它們都是在同一個(gè)代碼段中,因此段的基地址都是相同的,那么它們都屬于 GDT 中同一個(gè)代碼段描述符所代表的段空間。

如果每一條指令都去查表,就會(huì)影響到程序的執(zhí)行效率。

所以,處理器內(nèi)部就為每一個(gè)段寄存器,安排了一個(gè)高速緩存。

拿代碼段寄存器 cs 來(lái)說(shuō):當(dāng)執(zhí)行一條指令的時(shí)候,如果它與上一條指令中的段索引號(hào)不同,才會(huì)根據(jù)新的段索引號(hào)到 GDT 中查找相應(yīng)的段描述符表項(xiàng)。

查找到之后,就把這個(gè)表項(xiàng)的內(nèi)容復(fù)制到 cs 寄存器的高速緩存中。

當(dāng)繼續(xù)執(zhí)行后面的指令時(shí),如果邏輯地址中的段索引號(hào)沒(méi)有變化,處理器就直接從高速緩存中讀取段描述,從而避免了查表操作,提升了系統(tǒng)效率。

對(duì)段寄存器本身的保護(hù)

當(dāng)邏輯地址中段寄存器的索引號(hào)改變時(shí),就會(huì)根據(jù)新的索引號(hào),到 GDT 中去查表。

當(dāng)然了,這個(gè)索引號(hào)不能超過(guò) GDT 的界限。

當(dāng)定位到某一個(gè)描述符表項(xiàng)之后,就開始進(jìn)行一系列檢查。

再來(lái)看一下每一個(gè)段描述符中 8 個(gè)字節(jié)的內(nèi)容:

bit8 ~ bit11 定義了當(dāng)前這個(gè)段的類型。

假如: 我們?cè)谇袚Q代碼段空間的時(shí)候,不小心犯錯(cuò),定位到了 GDT 中的一個(gè)數(shù)據(jù)段描述符表項(xiàng),那么處理器就能夠及時(shí)發(fā)現(xiàn):

“當(dāng)前這個(gè)段描述符的類型是數(shù)據(jù)段,你卻把它當(dāng)做代碼段來(lái)使用,禁止,殺無(wú)赦!”

因此,處理器就會(huì)拒絕把這個(gè)段描述符復(fù)制到代碼段的高速緩存中,從而對(duì)代碼段寄存器進(jìn)行了保護(hù)。

對(duì)段界限的檢查

在通過(guò)了第一層的段類型保護(hù)之后,還會(huì)繼續(xù)對(duì)段的界限進(jìn)行檢查,這就要使用到邏輯地址中的偏移地址( EIP )了。

如果偏移地址超過(guò)了描述符中規(guī)定的界限,那么就說(shuō)明發(fā)生錯(cuò)誤了。

例如:在 bootloader 的代碼段描述符中,最大的界限是 512B,如果把 EIP 設(shè)置為 0x0000_1000,那就肯定錯(cuò)誤了。

因?yàn)檫@個(gè)地址壓根就不屬于代碼段的空間范圍。

對(duì)于數(shù)據(jù)段來(lái)說(shuō)比較有意思,因?yàn)槲覀儼褦?shù)據(jù)段描述符的基地址設(shè)置為 0x0000_0000,段的界限是整個(gè) 4G 的空間,所以它可以對(duì)整個(gè)內(nèi)存進(jìn)行操作。

多想一步:

代碼段也是屬于這 4G 空間,因此可以通過(guò)數(shù)據(jù)段,來(lái)改寫代碼段空間中的指令內(nèi)容。

也就是說(shuō):如果你想修改代碼段的指令,直接通過(guò)代碼段來(lái)操作是不可以的。

因?yàn)榇a段描述符中規(guī)定了:代碼段的內(nèi)容只能被讀取、執(zhí)行,但是不能被寫入。

此時(shí),就可以另辟蹊徑:代碼段也放在 4G 的空間,那么就可以通過(guò)數(shù)據(jù)段的可寫特性,來(lái)改寫代碼段中的指令。

想一想 gdb 的調(diào)試過(guò)程,是不是就利用了這個(gè)道理?

在文末的推薦閱讀中,就有一篇文章來(lái)介紹 gdb 的調(diào)試原理,有興趣的小伙伴可以看一下。

------ End ------

至此,我們對(duì)保護(hù)模式下,段描述符的相關(guān)內(nèi)容,就全部討論結(jié)束了,不知道對(duì)你是否有幫助。

在準(zhǔn)備這篇文章的時(shí)候,我特意看了一下 《深入理解 Linux 內(nèi)核》這部書的第二章:"內(nèi)存尋址"部分的內(nèi)容。

書中直接把 x86 處理器中實(shí)模式和保護(hù)模式的尋址方式作為結(jié)論告訴我們了,但是并沒(méi)有具體的講解其中的原理。

如果把之前的這幾篇文章都理解了,再去看 Linux 內(nèi)核的相關(guān)書籍,就不會(huì)那么吃力了。

Linux 雖然很復(fù)雜,但是它也是建立在處理器所提供的基本功能上的。

就像頂尖的乒乓球運(yùn)動(dòng)員許昕,打出那么多匪夷所思的神仙球,并不總是妙手偶得,而是建立在他們平時(shí)嚴(yán)格、機(jī)械、枯燥的日常訓(xùn)練,所練就的扎實(shí)的基本功。

如果沒(méi)有這些堅(jiān)實(shí)的基本功作為支撐,任何高級(jí)的花式技巧都只能是曇花一現(xiàn)。

學(xué)習(xí)也是一樣!


聲明: 本文由入駐維科號(hào)的作者撰寫,觀點(diǎn)僅代表作者本人,不代表OFweek立場(chǎng)。如有侵權(quán)或其他問(wèn)題,請(qǐng)聯(lián)系舉報(bào)。

發(fā)表評(píng)論

0條評(píng)論,0人參與

請(qǐng)輸入評(píng)論內(nèi)容...

請(qǐng)輸入評(píng)論/評(píng)論長(zhǎng)度6~500個(gè)字

您提交的評(píng)論過(guò)于頻繁,請(qǐng)輸入驗(yàn)證碼繼續(xù)

  • 看不清,點(diǎn)擊換一張  刷新

暫無(wú)評(píng)論

暫無(wú)評(píng)論

人工智能 獵頭職位 更多
掃碼關(guān)注公眾號(hào)
OFweek人工智能網(wǎng)
獲取更多精彩內(nèi)容
文章糾錯(cuò)
x
*文字標(biāo)題:
*糾錯(cuò)內(nèi)容:
聯(lián)系郵箱:
*驗(yàn) 證 碼:

粵公網(wǎng)安備 44030502002758號(hào)