保護模式篇——段暫存器
- 2021 年 9 月 19 日
- 筆記
- 羽夏看Win系統內核
寫在前面
此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎回饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並聲明我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統內核——簡述 ,方便學習本教程。
看此教程之前,問幾個問題,基礎知識儲備好了嗎?搭建好環境了嗎?沒有的話就不要繼續了。
🔒 華麗的分割線 🔒
什麼是段暫存器
當我們用彙編讀寫某一個地址時,比如用下面的程式碼:
mov dword ptr ds:[0x123456], eax
其實我們真正讀寫的地址是:ds.base
+ 0x123456
。並不是0x123456
,不過正好的是ds
段暫存器的基址是0
而已。
一些段暫存器
段暫存器有這幾個:ES、CS、SS、DS、FS、GS、LDTR、TR,它們各有自己特殊的用途。
段暫存器的結構
段暫存器的結構可用下圖表示:
段暫存器具有96位,但我們可見的只有16位。我們可以用OD隨意載入一個程式,如下圖所示:
段暫存器的讀寫
既然是暫存器了,那就可以進行讀寫操作,如下將介紹讀寫段暫存器的操作:
- Mov指令:
MOV AX,ES
,但只能讀16位的可見部分;MOV DS,AX
寫段暫存器,寫的是96位。 - 讀寫
LDTR
的指令為:SLDT
/LLDT
- 讀寫
TR
的指令為:STR
/LTR
段暫存器屬性探測
我介紹過段暫存器有96位
,但我們只能看見16位
,那如果證明Attribute
、Base
、Limit
的存在呢?我們將在下面進行初步探測。
段暫存器成員簡介
既然討論段暫存器屬性,首先要知道它們存著啥,下面表格的內容是我從虛擬機里查詢到的值,可能和我的不一樣,但無所謂。它們的屬性我已查詢並把它們的許可權寫到表格中,之所以為什麼我之後將會介紹。
Windows作業系統並不會使用
GS
暫存器,故用-
表示。
段暫存器 | Selector | Attribute | Base | Limit |
---|---|---|---|---|
ES | 0023 | 可讀、可寫 | 0 | 0xFFFFFFFF |
CS | 001B | 可讀、可執行 | 0 | 0xFFFFFFFF |
SS | 0023 | 可讀、可寫 | 0 | 0xFFFFFFFF |
DS | 0023 | 可讀、可寫 | 0 | 0xFFFFFFFF |
FS | 003B | 可讀、可寫 | 0x7FFDE000 | 0xFFF |
GS | – | – | – | – |
探測Attribute
如果你沒有使用Visual C++6.0
的話,我先將如何建立工程並寫程式碼簡單介紹一下,但只介紹一遍。打開Visual C++6.0
,通過File
->New
打開新建項目,選擇Win32 Console Application
,輸入你的Project name
,如下圖所示:
選中帶有 Hello World
的工程,這樣基本上IDE就幫我們建好要做實驗的工程了。
然後IDE會展示幫我們新建的內容資訊,如下圖所示,直接點確定即可,工程新建完畢。
按照下圖指示打開源程式碼文件,刪掉用不到的printf("Hello World!\n");
。就能開始做實驗了。
怎麼建工程我已詳細說明,那就正式進入正題,我們將用以下程式碼進行驗證Attribute
:
#include "stdafx.h"
int a=0;
int main(int argc, char* argv[])
{
_asm
{
mov ax,cs;
mov dword ptr ds:[a],10;
mov ds,ax;
mov dword ptr ds:[a],20;
}
return 0;
}
然後在main
函數的第一句程式碼下斷點,然後單步運行,運行過第一句給變數a
賦值的彙編程式碼時,成功通過,如下圖所示:
運行到第二個給變數a
賦值的彙編程式碼時,彈出一個錯誤資訊框,如下圖所示
翻譯過來就是地址訪問衝突,這是什麼原因呢?這就是由於cs段暫存器是可讀的,而不是可寫的。原來的ds是可讀可寫的,但將cs
通過ax
賦值給ds
時候,ds
不再是原來的ds
,而是cs
,故會引發此錯誤。
探測Base
老生常談程式的零地址無法訪問。但零地址一定是無法訪問嗎?我們將用以下程式碼進行驗證Base
:
#include "stdafx.h"
int a=0;
int main(int argc, char* argv[])
{
_asm
{
mov ax,fs;
mov es,ax;
mov eax,es:[0]; //不要用DS,否則編譯不過去
mov dword ptr ds:[a],eax;
}
return 0;
}
編譯運行通過,變數a
被正常賦值,如下圖所示:
探測Limit
我們將用以下程式碼進行驗證Limit
:
#include "stdafx.h"
int a=0;
int main(int argc, char* argv[])
{
_asm
{
mov eax,fs:[0x1000];
mov dword ptr ds:[a],eax;
}
return 0;
}
執行第一句彙編就發生記憶體訪問衝突錯誤,就是因為0x1000
超出了它的Limit
的值0xFFF
,如下圖所示:
踩坑問題
裡面有一些坑我還沒讓你踩,你踩一踩看看。
- 在驗證屬性的時候,用下面的程式碼,結果運行
mov dword ptr ds:[a],10
正常通過,放開程式跑後記憶體訪問衝突。
#include "stdafx.h"
int main(int argc, char* argv[])
{
int a=0;
_asm
{
mov ax,cs;
mov ds,ax;
mov dword ptr ds:[a],10;
}
return 0;
}
🎉 踩坑解決方案 🎉
為什麼會出現這個問題呢?我明明程式碼很正常但就不行呢,難道只是因為局部變數的問題呢。但全局變數和局部變數在記憶體上根本沒有區別。全局變數只是一個死地址,局部變數是該變數所在函數臨時的地址,但訪問上根本沒有區別。我們看一看編譯器到底把咱們的內聯彙編到底翻譯成了什麼?為什麼會出現這個問題呢?我明明程式碼很正常但就不行呢,難道只是因為局部變數的問題呢。但全局變數和局部變數在記憶體上根本沒有區別。全局變數只是一個死地址,局部變數是該變數所在函數臨時的地址,但訪問上根本沒有區別。我們看一看編譯器到底把咱們的內聯彙編到底翻譯成了什麼?
前面的內斂彙編編譯器給我一五一十的直接翻譯了,但這個mov dword ptr ds:[a],10;
內聯彙編給我翻譯成了啥,過分了!結果壓根沒有用ds
的許可權來訪問,而是默認的ss
來訪問,這和預期結果一樣才怪。
2.在探測Base屬性的時候,使用gs作為試驗暫存器,單步執行到mov eax,gs:[0]
,出現記憶體訪問衝突錯誤。
#include "stdafx.h"
int a=0;
int main(int argc, char* argv[])
{
_asm
{
mov ax,fs;
mov gs,ax;
mov eax,gs:[0];
mov dword ptr ds:[a],eax;
}
return 0;
}
🎉 踩坑解決方案 🎉
是因為每次單步調試,就會觸發單步調試異常,進入內核,內核會把gs
清零了,故導致實驗無法成功。
下一篇
保護模式篇——段描述符與段選擇子