-
實(shí)模式:bootloader 為程序計(jì)算段的基地址
-
保護(hù)模式:bootloader 為自己創(chuàng)建段描述符
-
確定 GDT 的地址
-
創(chuàng)建代碼段的描述符
-
創(chuàng)建數(shù)據(jù)段的描述符
-
創(chuàng)建棧段的描述符
-
段描述符是如何確保段的安全的?
-
段寄存器高速緩存
-
對(duì)段寄存器本身的保護(hù)
-
對(duì)段界限的檢查
在上一篇文章中,我們已經(jīng)順利的從
實(shí)模式,過渡到了
保護(hù)模式。
保護(hù)模式與實(shí)模式
最本質(zhì)的區(qū)別就是:保護(hù)模式使用了全局描述符表,用來保存每一個(gè)程序(bootloader,操作系統(tǒng),應(yīng)用程序)使用到的每個(gè)段信息:開始地址,長(zhǎng)度,以及其他一些保護(hù)參數(shù)。
這篇文章,我們來看一下bootloader是如何來進(jìn)行自我
進(jìn)化到保護(hù)模式的,然后深入看一下保護(hù)模式是如何對(duì)
內(nèi)存進(jìn)行安全保護(hù)的。
作為背景知識(shí),我們先來看一下
x86中的地址變換過程:
x86
處理器中的分頁機(jī)制是可以
被關(guān)閉的,此時(shí)線性地址就
等于物理地址,這也是我們一直討論的情況。
下一篇文章,我們就把 x86 中的分頁機(jī)制打開,并與 Linux 中的分段和分頁機(jī)制進(jìn)行對(duì)比。
實(shí)模式:bootloader 為程序計(jì)算段的基地址
在之前的文章:Linux從頭學(xué)06:16張結(jié)構(gòu)圖,徹底理解【代碼重定位】的底層原理中,我們討論了bootloader是如何把應(yīng)用程序讀取到內(nèi)存中,最后跳入到程序的入口地址的。
這里所說的程序,可以是操作系統(tǒng),也可以是應(yīng)用程序。
下面這張圖,是程序被加載到內(nèi)存中之后,header中的信息:
因?yàn)槌绦蚴潜籦ootloader動(dòng)態(tài)讀取到內(nèi)存中的,它是
不知道自己被放在內(nèi)存中的什么位置,因此它也
不知道自己代碼段、數(shù)據(jù)段、棧的開始地址。
但是,程序要想能夠正常執(zhí)行,就
必須要知道這些信息,那怎么辦?
只有bootloader才能解決問題,因?yàn)槭撬鼇戆殉绦驈挠脖P加載到內(nèi)存中的。
因此,bootloader在跳入程序的入口地址之前,必須把其中的代碼段、數(shù)據(jù)段、棧段的
基地址計(jì)算出來,然后寫入到程序的header中,如下圖所示:
這樣的話,程序開始執(zhí)行時(shí),就可以從自己的header中獲取到這3個(gè)段基地址,并且賦值給相應(yīng)的寄存器,從而順利的執(zhí)行程序。
也就是說:程序的header空間,充當(dāng)了bootloader與它進(jìn)行信息交互的媒介,用來傳遞3個(gè)段寄存器的基地址。
以上的這個(gè)過程,一直工作在
實(shí)模式,因此就沒有段描述符什么事情。
在以后文章中,我們還會(huì)看到在
保護(hù)模式下,bootloader仍然會(huì)利用OS的header空間,來傳遞段的索引號(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就必須能夠訪問到這個(gè)位置,并且是以
數(shù)據(jù)段的讀寫方式。
為了利用全部的4G內(nèi)存空間,bootloader可以把這4G空間,作為一個(gè)數(shù)據(jù)段來定義它的描述符,如下:
創(chuàng)建棧段描述符
理論上,bootloader可以使用內(nèi)存中的
任意一塊空閑空間,來作為自己的棧。
因?yàn)闂T趐ush操作的時(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ù)模式來執(zhí)行bootloader中后面的代碼了。
段描述符是如何確保段的安全訪問的?
段寄存器高速緩存
進(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來說:當(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)
沒有變化,處理器就直接從
高速緩存中讀取段描述,從而避免了查表操作,提升了系統(tǒng)效率。
對(duì)段寄存器本身的保護(hù)
當(dāng)邏輯地址中段寄存器的索引號(hào)
改變時(shí),就會(huì)根據(jù)新的索引號(hào),到GDT中去查表。
當(dāng)然了,這個(gè)索引號(hào)不能超過 GDT 的界限。
當(dāng)定位到某一個(gè)描述符表項(xiàng)之后,就開始進(jìn)行一系列檢查。
再來看一下每一個(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)做
代碼段來使用,禁止,殺無赦!”
因此,
處理器就會(huì)
拒絕把這個(gè)段描述符復(fù)制到
代碼段的高速緩存中,從而對(duì)
代碼段寄存器進(jìn)行了保護(hù)。
對(duì)段界限的檢查
在通過了第一層的
段類型保護(hù)之后,還會(huì)繼續(xù)對(duì)段的
界限進(jìn)行檢查,這就要使用到邏輯地址中的
偏移地址(EIP)了。
如果偏移地址
超過了描述符中規(guī)定的界限,那么就說明發(fā)生錯(cuò)誤了。
例如:在bootloader的代碼段描述符中,最大的界限是512B,如果把EIP設(shè)置為0x0000_1000,那就肯定錯(cuò)誤了。
因?yàn)檫@個(gè)地址壓根就
不屬于代碼段的空間范圍。
對(duì)于
數(shù)據(jù)段來說比較有意思,因?yàn)槲覀儼褦?shù)據(jù)段描述符的基地址設(shè)置為0x0000_0000,段的界限是整個(gè)4G的空間,所以它可以對(duì)整個(gè)
內(nèi)存進(jìn)行操作。
多想一步:
代碼段也是屬于這4G空間,因此可以通過
數(shù)據(jù)段,來改寫
代碼段空間中的指令內(nèi)容。
也就是說:如果你想修改代碼段的指令,直接通過代碼段來操作是
不可以的。
因?yàn)榇a段描述符中規(guī)定了:代碼段的內(nèi)容只能被
讀取、執(zhí)行,但是
不能被寫入。
此時(shí),就可以另辟蹊徑:代碼段也放在4G的空間,那么就可以通過數(shù)據(jù)段的可寫特性,來改寫代碼段中的指令。
想一想gdb的調(diào)試過程,是不是就利用了這個(gè)道理?
在文末的