調試寄存器
在x86/x64CPU內部,還有一組用于支持軟件調試的寄存器。
調試,對于我們程序員是家常便飯,必備技能。但你想過你的程序能夠被調試背后的原理嗎?
程序能夠被調試,關鍵在于能夠被中斷執行和恢復執行,被中斷的地方就是我們設置的斷點。那程序是如何能在遇到斷點的時候停下來呢?
對于一些解釋執行(PHP、Python、JavaScript)或虛擬機執行(Java)的高級語言,這很容易辦到,因為它們的執行都在解釋器/虛擬機的掌控之中。
而對于像C、C++這樣的“底層”編程語言,程序代碼是直接編譯成CPU的機器指令來執行的,這就需要CPU來提供對于調試的支持了。
對于通常的斷點,也就是程序執行到某個位置下就停下來,這種斷點實現的方式,在x86/x64上,是利用了一條軟中斷指令:int 3來進行實現的。
注意,這里的int不是指高級語言里面的整數,而是表示interrupt中斷的意思,是一條匯編指令,int 3則表示中斷向量號為3的中斷。
在我們使用調試器下斷點時,調試器將會把對應位置的原來的指令替換為一個int 3指令,機器碼為0xCC。這個動作對我們是透明的,我們在調試器中看到的依然是原來的指令,但實際上內存中已經不是原來的指令了。
順便提一句,兩個0xCC是漢字【燙】的編碼,在一些編譯器里,會給線程的棧中填充大量的0xCC,如果程序出錯的時候,我們經常會看到很多燙燙燙出現,就是這個原因。
言歸正傳,CPU在執行這條int 3指令時,將自動觸發中斷處理流程(雖然這實際上不是一個真正的中斷),CPU將取出IDTR寄存器指向的中斷描述符表IDT的第3項,執行里面的中斷處理函數。
而這個中斷描述符表,早在操作系統啟動之初,就已經提前安排好了,所以執行這條指令后,操作系統的中斷處理函數將介入,來處理這一事件。
后面的過程就多了,簡單來說,操作系統會把觸發這一事件的進程凍結起來,隨后將這一事件發送到調試器,調試器拿到之后就知道目標進程觸發斷點了。這個時候,咱們程序員就能通過調試器的UI交互界面或者命令行調試接口來調試目標進程,查看堆棧、查看內存、變量都隨你。
如果我們要繼續運行,調試器將會把之前修改的int 3指令給恢復回去,然后告知操作系統:我處理完了,把目標進程解凍吧!
上面簡單描述了一下普通斷點的實現原理。現在思考一個場景:我們發現一個bug,某個全局整數型變量的值老是莫名其妙被修改,但你發現有很多線程,很多函數都有可能會去修改這個變量,你想找出到底誰干的,怎么辦?
這個時候上面的普通斷點就沒辦法了,你需要一種新的斷點: 硬件斷點 。
這時候就該本小節的主人公調試寄存器登場表演了。
在x86架構CPU內部,提供了8個調試寄存器DR0~DR7。
DR0~DR3:這是四個用于存儲地址的寄存器
DR4~DR5 :這兩個有點特殊,受前面提到的CR4寄存器中的標志位DE位控制,如果CR4的DE位是1,則DR4、DR5是不可訪問的,訪問將觸發異常。如果CR4的DE位是0,則DR4和DR5將會變成DR6和DR7的別名,相當于做了一個軟鏈接。這樣做是為了將DR4、DR5保留,以便將來擴展調試功能時使用。
DR6 :這個寄存器中存儲了硬件斷點觸發后的一些狀態信息
DR7 :調試控制寄存器,這里面記錄了對DR0-DR3這四個寄存器中存儲地址的中斷方式(是對地址的讀,還是寫,還是執行)、數據長度(1/2/4個字節)以及作用范圍等信息
通過調試器的接口設置硬件斷點后,CPU在執行代碼的過程中,如果滿足條件,將自動中斷下來。
回答前面提出的問題,想要找出是誰偷偷修改了全局整形變量,只需要通過調試器設置一個硬件寫入斷點即可。
描述符寄存器
所謂 描述符 ,其實就是一個數據結構,用來記錄一些信息,‘描述’一個東西。把很多個描述符排列在一起,組成一個表,就成了描述符表。再使用一個寄存器來指向這個表,這個寄存器就是 描述符寄存器 。
在x86/x64系列CPU中,有三個非常重要的描述符寄存器,它們分別存儲了三個地址,指向了三個非常重要的描述符表。
gdtr
: 全局描述符表寄存器,前面提到,CPU現在使用的是段+分頁結合的內存管理方式,那系統總共有那些分段呢?這就存儲在一個叫全局描述符表( GDT )的表格中,并用gdtr寄存器指向這個表。這個表中的每一項都描述了一個內存段的信息。
ldtr
: 局部描述符表寄存器,這個寄存器和上面的gdtr一樣,同樣指向的是一個段描述符表( LDT )。不同的是,GDT是全局唯一,LDT是局部使用的,可以創建多個,隨著任務段切換而切換(下文介紹任務寄存器會提到)。
GDT和LDT中的表項,就是段描述符,描述了一個內存分段的信息。
一個表項占據8個字節(32位CPU),里面存儲了一個內存分段的諸多信息:基地址、大小、權限、類型等信息。
除了這兩個段描述符寄存器,還有一個非常重要的描述符寄存器:
idtr : 中斷描述符表寄存器,指向了 中斷描述符表IDT ,這個表的每一項都是一個中斷處理描述符,當CPU執行過程中發生了硬中斷、異常、軟中斷時,將自動從這個表中定位對應的表項,里面記錄了發生中斷、異常時該去哪里執行處理函數。
IDT中的表項稱為 Gate ,中文意思為 門 ,因為這是應用程序進入內核的主要入口。雖然表的名字叫中斷描述符表,但表中存儲的不全是中斷描述符,IDT中的表項存在三種類型,對應三種類型的門:
- 任務門
- 陷阱門
- 中斷門
三種描述符中都存儲了處理這個中斷/異常/任務時該去哪里處理的地址。三種門用途不一,其中中斷門是真正意義上的中斷,而像前面提到的調試指令int 3以及老式的系統調用指令int 2e/int 80都屬于陷阱門。任務門則用的較少,要了解任務門,先了解下任務寄存器。
任務寄存器
現代操作系統,都是支持多任務并發運行的,x86架構CPU為了順應時代潮流,在硬件層面上提供了專門的機制用來支持多任務的切換,這體現在兩個方面:
CPU內部設置了一個專用的寄存器—— 任務寄存器TR ,它指向當前運行的任務。
定義了描述任務的數據結構 TSS ,里面存儲了一個任務的上下文(一系列寄存器的值),下圖是一個32位CPU的TSS結構圖:
x86CPU的構想是每一個任務對應一個TSS,然后由TR寄存器指向當前的任務,執行任務切換時,修改TR寄存器的指向即可,這是硬件層面的多任務切換機制。
這個構想其實還是很不錯的,然而現實卻打了臉,包括Linux和Windows在內的主流操作系統都沒有使用這個機制來進行線程切換,而是自己使用軟件來實現多線程切換。
所以,絕大多數情況下,TR寄存器都是指向固定的,即便線程切換了,TR寄存器仍然不會變化。
注意,我這里說的的是絕大多數情況,而沒有說死。雖然操作系統不依靠TSS來實現多任務切換,但這并不意味著CPU提供的TSS操作系統一點也沒有使用。還是存在一些特殊情況,如一些異常處理會使用到TSS來執行處理。
下面這張圖,展示了控制寄存器、描述符寄存器、任務寄存器構成的全貌:
模型特定寄存器
從80486之后的x86架構CPU,內部增加了一組新的寄存器,統稱為 MSR寄存器 ,中文直譯是模型特定寄存器,意思是這些寄存器不像上面列出的寄存器是固定的,這些寄存器可能隨著不同的版本有所變化。這些寄存器主要用來支持一些新的功能。
隨著x86CPU不斷更新換代,MSR寄存器變的越來越多,但與此同時,有一部分MSR寄存器隨著版本迭代,慢慢固化下來,成為了變化中那部分不變的,這部分MSR寄存器,Intel將其稱為Architected MSR,這部分MSR寄存器,在命名上,統一加上了IA32的前綴。
這里選取三個代表性的MSR簡單介紹一下:
- IA32_SYSENTER_CS
- IA32_SYSENTER_ESP
- IA32_SYSENTER_EIP
這三個MSR寄存器是用來實現 快速系統調用 。
在早期的x86架構CPU上,系統調用依賴于軟中斷實現,類似于前面調試用到的int 3指令,在Windows上,系統調用用到的是 int 2e ,在Linux上,用的是 int 80 。
軟中斷畢竟還是比較慢的,因為執行軟中斷就需要內存查表,通過IDTR定位到IDT,再取出函數進行執行。
系統調用是一個頻繁觸發的動作,如此這般勢必對性能有所影響。在進入奔騰時代后,就加上了上面的三個MSR寄存器,分別存儲了執行系統調用后,內核系統調用入口函數所需要的段寄存器、堆棧棧頂、函數地址,不再需要內存查表。快速系統調用還提供了專門的CPU指令sysenter/sysexit用來發起系統調用和退出系統調用。
在64位上,這一對指令升級為 syscall/sysret 。
總結
以上就是全部要介紹的寄存器了,需要說明一下的是,這并不是x86CPU全部所有的寄存器,除了這些,還存在XMM、MMX、FPU浮點數運算等其他寄存器。
這篇文章以x86/x64架構CPU為目標,通過對CPU內部寄存器的闡述,串講了CPU執行代碼機制、內存尋址技術、中斷與異常處理、多任務管理、系統調用、調試原理等多種計算機底層知識。
-
寄存器
+關注
關注
31文章
5421瀏覽量
123274 -
cpu
+關注
關注
68文章
11031瀏覽量
215931 -
X64
+關注
關注
0文章
5瀏覽量
7919 -
X86構架
+關注
關注
0文章
6瀏覽量
6580
發布評論請先 登錄
移位寄存器怎么用_如何使用移位寄存器_移位寄存器的用途
寄存器變量
如何在VHDL中實現一個簡單的寄存器

如何使用ALU,RAM,寄存器打造一個CPU 1
如何使用ALU,RAM,寄存器打造一個CPU 3

評論