4 意猶未盡
好了,現在來詳細講一下昨天遺留下來的問題。首先來說明一下naskfunc.nas的_load_gdtr。
_load_gdtr: ; void load_gdtr(int limit, int addr); MOV AX, [ESP+4] ; limit MOV [ESP+6], AX LGDT [ESP+6] RET
這個函數用來將指定的段上限(limit)和地址值賦值給名為GDTR的48位寄存器。這是一個很特別的48位寄存器,并不能用我們常用的MOV指令來賦值。給它賦值的時候,唯一的方法就是指定一個內存地址,從指定的地址讀取6個字節(也就是48位),然后賦值給GDTR寄存器。完成這一任務的指令,就是LGDT。
該寄存器的低16位(即內存的最初2個字節)是段上限,它等于“GDT的有效字節數 -1”。今后我們還會偶爾用到上限這個詞,意思都是表示量的大小,一般為“字節數 -1”。剩下的高32位(即剩余的4個字節),代表GDT的開始地址。
在最初執行這個函數的時候,DWORD[ESP+4]里存放的是段上限,DWORD[ESP+8]里存放的是地址。具體到實際的數值,就是0x0000ffff和0x00270000。把它們按字節寫出來的話,就成了[FF FF 00 00 00 00 27 00](要注意低位放在內存地址小的字節里)。為了執行LGDT,筆者希望把它們排列成[FF FF 00 00 27 00]的樣子,所以就先用“MOV AX, [ESP+4]”讀取最初的0xffff,然后再寫到[ESP+6]里。這樣,結果就成了[FF FF FF FF 00 00 27 00],如果從[ESP+6]開始讀6字節的話,正好是我們想要的結果。
■■■■■
naskfunc.nas的_load_idtr設置IDTR的值,因為IDTR與GDTR結構體基本上是一樣的,程序也非常相似。
最后再補充說明一下dsctbl.c里的set_segmdesc函數。這個有些難度,我們僅介紹一些與本書相關的內容。
本次的dsctbl.c節選
struct SEGMENT_DESCRIPTOR { short limit_low, base_low; char base_mid, access_right; char limit_high, base_high; }; void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar) { if (limit > 0xfffff) { ar |= 0x8000; /* G_bit = 1 */ limit /= 0x1000; } sd->limit_low = limit & 0xffff; sd->base_low = base & 0xffff; sd->base_mid = (base >> 16) & 0xff; sd->access_right = ar & 0xff; sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0); sd->base_high = (base >> 24) & 0xff; return; }
說到底,這個函數是按照CPU的規格要求,將段的信息歸結成8個字節寫入內存的。這8個字節里到底填入了什么內容呢?昨天已經講到,有以下3點:
? 段的大小
? 段的起始地址
? 段的管理屬性(禁止寫入,禁止執行,系統專用等)
為了寫入這些信息,我們準備了struct SEGMENT_DESCRIPTOR這樣一個結構體。下面我們就來說明這個結構體。
■■■■■
首先看一下段的地址。地址當然是用32位來表示。這個地址在CPU世界的語言里,被稱為段的基址。所以這里使用了base這樣一個變量名。在這個結構體里base又分為low(2字節), mid(1字節), high(1字節)3段,合起來剛好是32位。所以,這里只要按順序分別填入相應的數值就行了。雖然有點難懂,但原理很簡單。程序中使用了移位運算符和AND運算符往各個字節里填入相應的數值。
為什么要分為3段呢?主要是為了與80286時代的程序兼容。有了這樣的規格,80286用的操作系統,也可以不用修改就在386以后的CPU上運行了。
■■■■■
下面再說一下段上限。它表示一個段有多少個字節。可是這里有一個問題,段上限最大是4GB,也就是一個32位的數值,如果直接放進去,這個數值本身就要占用4個字節,再加上基址(base),一共就要8個字節,這就把整個結構體占滿了。這樣一來,就沒有地方保存段的管理屬性信息了,這可不行。
因此段上限只能使用20位。這樣一來,段上限最大也只能指定到1MB為止。明明有4GB,卻只能用其中的1MB,有種又回到了16位時代的錯覺,太可悲了。在這里英特爾的叔叔們又想了一個辦法,他們在段的屬性里設了一個標志位,叫做Gbit。這個標志位是1的時候,limit的單位不解釋成字節(byte),而解釋成頁(page)。頁是什么呢?在電腦的CPU里,1頁是指4KB。
這樣一來,4KB × 1M = 4GB,所以可以指定4GB的段。總算能放心了。順便說一句,G bit的“G”,是“granularity”的縮寫,是指單位的大小。
這20位的段上限分別寫到limit_low和limit_high里。看起來它們好像是總共有3字節,即24位,但實際上我們接著要把段屬性寫入limit_high的高4位里,所以最后段上限還是只有20,好復雜呀。
■■■■■
最后再來講一下12位的段屬性。段屬性又稱為“段的訪問權屬性”,在程序中用變量名access_right或ar來表示。因為12位段屬性中的高4位放在limit_high的高4位里,所以程序里有意把ar當作如下的16位構成來處理:
xxxx0000xxxxxxxx(其中x是0或1)
ar的高4位被稱為“擴展訪問權”。為什么這么說呢?因為這高4位的訪問屬性在80286的時代還不存在,到386以后才可以使用。這4位是由“GD00”構成的,其中G是指剛才所說的G bit, D是指段的模式,1是指32位模式,0是指16位模式。這里出現的16位模式主要只用于運行80286的程序,不能用于調用BIOS。所以,除了運行80286程序以外,通常都使用D=1的模式。
■■■■■
ar的低8位從80286時代就已經有了,如果要詳細說明的話,夠我們說一天的了,所以這里只是簡單地介紹一下。
00000000(0x00):未使用的記錄表(descriptor table)。 10010010(0x92):系統專用,可讀寫的段。不可執行。 10011010(0x9a):系統專用,可執行的段。可讀不可寫。 11110010(0xf2):應用程序用,可讀寫的段。不可執行。 11111010(0xfa):應用程序用,可執行的段。可讀不可寫。
“系統專用”,“應用程序用”什么的,聽著讓人摸不著頭腦。都是些什么東西呀?在32位模式下,CPU有系統模式(也稱為“ring0”)和應用模式(也稱為“ring3”)之分。操作系統等“管理用”的程序,和應用程序等“被管理”的程序,運行時的模式是不同的。
比如,如果在應用模式下試圖執行LGDT等指令的話,CPU則對該指令不予執行,并馬上告訴操作系統說“那個應用程序居然想要執行LGDT,有問題!”。另外,當應用程序想要使用系統專用的段時,CPU也會中斷執行,并馬上向操作系統報告“那個應用程序想要盜取系統信息。也有可能不僅要盜取信息,還要寫點東西來破壞系統呢。”
“想要盜取系統信息這一點我明白,但要阻止LGDT的執行這一點,我還是不懂。”可能有人會有這種疑問。當然要阻止啦。因為如果允許應用程序執行LGDT,那應用程序就會根據自己的需要,偷偷準備GDT,然后重新設定LGDT來讓它執行自己準備的GDT。這可就麻煩了。有了這個漏洞,操作系統再怎么防守還是會防不勝防。
CPU到底是處于系統模式還是應用模式,取決于執行中的應用程序是位于訪問權為0x9a的段,還是位于訪問權為0xfa的段。