CPU底層工作原理
前段時間,我連續(xù)寫了十來篇CPU底層系列技術(shù)故事文章,有不少讀者私信我讓我寫一下CPU的寄存器。
寄存器這個太多太復(fù)雜,不適合寫故事,拖了很久,總算是寫完了,這篇文章就來詳細(xì)聊聊x86/x64架構(gòu)的CPU中那些紛繁復(fù)雜的寄存器們。
長文預(yù)警,時速較快,請系好安全帶~起飛~
自1946年馮·諾伊曼領(lǐng)導(dǎo)下誕生的世界上第一臺通用電子計算機(jī)ENIAC至今,計算機(jī)技術(shù)已經(jīng)發(fā)展了七十多載。
從當(dāng)初專用于數(shù)學(xué)計算的龐然大物,到后來大型機(jī)服務(wù)器時代,從個人微機(jī)技術(shù)蓬勃發(fā)展,到互聯(lián)網(wǎng)浪潮席卷全球,再到移動互聯(lián)網(wǎng)、云計算日新月異的當(dāng)下,計算機(jī)變的形態(tài)各異,無處不在。
這七十多年中,出現(xiàn)了數(shù)不清的編程語言,通過這些編程語言,又開發(fā)了無數(shù)的應(yīng)用程序。
可無論什么樣的應(yīng)用程序,什么樣的編程語言,最終的程序邏輯都是要交付給CPU去執(zhí)行實現(xiàn)的(當(dāng)然這里有些不嚴(yán)謹(jǐn),除了CPU,還有協(xié)處理器、GPU等等)。所以了解和學(xué)習(xí)CPU的原理都是對計算機(jī)基礎(chǔ)知識的夯實大有裨益。
在七十多年的漫長歷程中,也涌現(xiàn)了不少架構(gòu)的CPU。
MIPS
PowerPC
x86/x64
IA64
······
這篇文章就以市場應(yīng)用最為廣泛的x86-x64架構(gòu)為目標(biāo),通過學(xué)習(xí)了解它內(nèi)部的100個寄存器功能作用,來串聯(lián)闡述CPU底層工作原理。
通過這篇文章,你將了解到:
CPU指令執(zhí)行原理
內(nèi)存尋址技術(shù)
軟件調(diào)試技術(shù)原理
中斷與異常處理
系統(tǒng)調(diào)用
CPU多任務(wù)技術(shù)
什么是寄存器?寄存器是CPU內(nèi)部用來存放數(shù)據(jù)的一些小型存儲區(qū)域,用來暫時存放參與運(yùn)算的數(shù)據(jù)和運(yùn)算結(jié)果以及一些CPU運(yùn)行需要的信息。
x86架構(gòu)CPU走的是復(fù)雜指令集(CISC) 路線,提供了豐富的指令來實現(xiàn)強(qiáng)大的功能,與此同時也提供了大量寄存器來輔助功能實現(xiàn)。這篇文章將覆蓋下面這些寄存器:
通用寄存器
標(biāo)志寄存器
指令寄存器
段寄存器
控制寄存器
調(diào)試寄存器
描述符寄存器
任務(wù)寄存器
MSR寄存器
通用寄存器首當(dāng)其沖的是通用寄存器,這些的寄存器是程序執(zhí)行代碼最最常用,也最最基礎(chǔ)的寄存器,程序執(zhí)行過程中,絕大部分時間都是在操作這些寄存器來實現(xiàn)指令功能。
所謂通用,即這些寄存器CPU沒有特殊的用途,交給應(yīng)用程序“隨意”使用。注意,這個隨意,我打了引號,對于有些寄存器,CPU有一些潛規(guī)則,用的時候要注意。
eax: 通常用來執(zhí)行加法,函數(shù)調(diào)用的返回值一般也放在這里面
ebx: 數(shù)據(jù)存取
ecx: 通常用來作為計數(shù)器,比如for循環(huán)
edx: 讀寫I/O端口時,edx用來存放端口號
esp: 棧頂指針,指向棧的頂部
ebp: 棧底指針,指向棧的底部,通常用ebp+偏移量的形式來定位函數(shù)存放在棧中的局部變量
esi: 字符串操作時,用于存放數(shù)據(jù)源的地址
edi: 字符串操作時,用于存放目的地址的,和esi兩個經(jīng)常搭配一起使用,執(zhí)行字符串的復(fù)制等操作
在x64架構(gòu)中,上面的通用寄存器都擴(kuò)展成為64位版本,名字也進(jìn)行了升級。當(dāng)然,為了兼容32位模式程序,使用上面的名字仍然是可以訪問的,相當(dāng)于訪問64位寄存器的低32位。
raxrbxrcxrdxrsprbprsirdi
除了擴(kuò)展原來存在的通用寄存器,x64架構(gòu)還引入了8個新的通用寄存器:
r8-r15
在原來32位時代,函數(shù)調(diào)用時,那個時候通用寄存器少,參數(shù)絕大多數(shù)時候是通過線程的棧來進(jìn)行傳遞(當(dāng)然也有使用寄存器傳遞的,比如著名的C++ this指針使用ecx寄存器傳遞,不過能用的寄存器畢竟不多)。
進(jìn)入x64時代,寄存器資源富裕了,參數(shù)傳遞絕大多數(shù)都是用寄存器來傳了。寄存器傳參的好處是速度快,減少了對內(nèi)存的讀寫次數(shù)。
當(dāng)然,具體使用棧還是用寄存器傳參數(shù),這個不是編程語言決定的,而是編譯器在編譯生成CPU指令時決定的,如果編譯器非要在x64架構(gòu)CPU上使用線程棧來傳參那也不是不行,這個對高級語言是無感知的。
標(biāo)志寄存器標(biāo)志寄存器,里面有眾多標(biāo)記位,記錄了CPU執(zhí)行指令過程中的一系列狀態(tài),這些標(biāo)志大都由CPU自動設(shè)置和修改:
CF 進(jìn)位標(biāo)志
PF 奇偶標(biāo)志
ZF 零標(biāo)志
SF 符號標(biāo)志
OF 補(bǔ)碼溢出標(biāo)志
TF 跟蹤標(biāo)志
IF 中斷標(biāo)志
······
在x64架構(gòu)下,原來的eflags寄存器升級為64位的rflags,不過其高32位并沒有新增什么功能,保留為將來使用。
指令寄存器eip: 指令寄存器可以說是CPU中最最重要的寄存器了,它指向了下一條要執(zhí)行的指令所存放的地址,CPU的工作其實就是不斷取出它指向的指令,然后執(zhí)行這條指令,同時指令寄存器繼續(xù)指向下面一條指令,如此不斷重復(fù),這就是CPU工作的基本日常。
而在漏洞攻擊中,黑客想盡辦法費盡心機(jī)都想要修改指令寄存器的地址,從而能夠執(zhí)行惡意代碼。
同樣的,在x64架構(gòu)下,32位的eip升級為64位的rip寄存器。
段寄存器段寄存器與CPU的內(nèi)存尋址技術(shù)緊密相關(guān)。
早在16位的8086CPU時代,內(nèi)存資源寶貴,CPU使用分段式內(nèi)存尋址技術(shù):
16位的寄存器能尋址的范圍是64KB,通過引入段的概念,將內(nèi)存空間劃分為不同的區(qū)域:分段,通過段基址+段內(nèi)偏移段方式來尋址。
這樣一來,段的基地址保存在哪里呢?8086CPU專門設(shè)置了幾個段寄存器用來保存段的基地址,這就是段寄存器段的由來。
段寄存器也是16位的。
段寄存器有下面6個,前面4個是早期16位模式就引入了,到了32位時代,又新增了fs和gs兩個段寄存器。
cs: 代碼段
ds: 數(shù)據(jù)段
ss: 棧段
es: 擴(kuò)展段
fs: 數(shù)據(jù)段
gs: 數(shù)據(jù)段
段寄存器里面存儲的內(nèi)容與CPU當(dāng)前工作的內(nèi)存尋址模式緊密相關(guān)。
當(dāng)CPU處于16位實地址模式下時,段寄存器存儲段的基地址,尋址時,將段寄存器內(nèi)容左移4位(乘以16)得到段基地址+段內(nèi)偏移得到最終的地址。
當(dāng)CPU工作于保護(hù)模式下,段寄存器存儲的內(nèi)容不再是段基址了,此時的段寄存器中存放的是段選擇子,用來指示當(dāng)前這個段寄存器“指向”的是哪個分段。
注意我這里的指向打了引號,段寄存器中存儲的并不是內(nèi)存段的直接地址,而是段選擇子,它的結(jié)構(gòu)如下:
16個bit長度的段寄存器內(nèi)容劃分了三個字段:
PRL: 特權(quán)請求級,就是我們常說的ring0-ring3四個特權(quán)級。
TI: 0表示用的是全局描述符表GDT,1表示使用的是局部描述符表LDT。
Index: 這是一個表格中表項的索引值,這個表格叫內(nèi)存描述符表,它的每一個表項都描述了一個內(nèi)存分段。
這里提到了兩個表,全局描述符表GDT和局部描述符表LDT,關(guān)于這兩個表的介紹,下面介紹描述符寄存器時再詳述,這里只需要知道,這是CPU支持分段式內(nèi)存管理需要的表格,放在內(nèi)存中,表格中的每一項都是一個描述符,記錄了一個內(nèi)存分段的信息。
保護(hù)模式下的段寄存器和段描述符到最后的內(nèi)存分段,通過下圖的方式聯(lián)系在一起:
通用寄存器、段寄存器、標(biāo)志寄存器、指令寄存器,這四組寄存器共同構(gòu)成了一個基本的指令執(zhí)行環(huán)境,一個線程的上下文也基本上就是這些寄存器,在執(zhí)行線程切換的時候,就是修改它們的內(nèi)容。
控制寄存器控制寄存器是CPU中一組相當(dāng)重要的寄存器,我們知道eflags寄存器記錄了當(dāng)前運(yùn)行線程的一系列關(guān)鍵信息。
那CPU運(yùn)行過程中自身的一些關(guān)鍵信息保存在哪里呢?答案是控制寄存器!
32位CPU總共有cr0-cr4共5個控制寄存器,64位增加了cr8。他們各自有不同的功能,但都存儲了CPU工作時的重要信息:
cr0: 存儲了CPU控制標(biāo)記和工作狀態(tài)
cr1: 保留未使用
cr2: 頁錯誤出現(xiàn)時保存導(dǎo)致出錯的地址
cr3: 存儲了當(dāng)前進(jìn)程的虛擬地址空間的重要信息——頁目錄地址
cr4: 也存儲了CPU工作相關(guān)以及當(dāng)前人任務(wù)的一些信息
cr8: 64位新增擴(kuò)展使用
其中,CR0尤其重要,它包含了太多重要的CPU信息,值得單獨關(guān)注一下:
一些重要的標(biāo)記位含義如下:
PG: 是否啟用內(nèi)存分頁
AM: 是否啟用內(nèi)存對齊自動檢查
WP: 是否開啟內(nèi)存寫保護(hù),若開啟,對只讀頁面嘗試寫入時將觸發(fā)異常,這一機(jī)制常常被用來實現(xiàn)寫時復(fù)制功能
PE: 是否開啟保護(hù)模式
除了CR0,另一個值得關(guān)注的寄存器是CR3,它保存了當(dāng)前進(jìn)程所使用的虛擬地址空間的頁目錄地址,可以說是整個虛擬地址翻譯中的頂級指揮棒,在進(jìn)程空間切換的時候,CR3也將同步切換。
調(diào)試寄存器在x86/x64CPU內(nèi)部,還有一組用于支持軟件調(diào)試的寄存器。
調(diào)試,對于我們程序員是家常便飯,必備技能。但你想過你的程序能夠被調(diào)試背后的原理嗎?
程序能夠被調(diào)試,關(guān)鍵在于能夠被中斷執(zhí)行和恢復(fù)執(zhí)行,被中斷的地方就是我們設(shè)置的斷點。那程序是如何能在遇到斷點的時候停下來呢?
對于一些解釋執(zhí)行(PHP、Python、JavaScript)或虛擬機(jī)執(zhí)行(Java)的高級語言,這很容易辦到,因為它們的執(zhí)行都在解釋器/虛擬機(jī)的掌控之中。
而對于像C、C++這樣的“底層”編程語言,程序代碼是直接編譯成CPU的機(jī)器指令來執(zhí)行的,這就需要CPU來提供對于調(diào)試的支持了。
對于通常的斷點,也就是程序執(zhí)行到某個位置下就停下來,這種斷點實現(xiàn)的方式,在x86/x64上,是利用了一條軟中斷指令:int 3來進(jìn)行實現(xiàn)的。
注意,這里的int不是指高級語言里面的整數(shù),而是表示interrupt中斷的意思,是一條匯編指令,int 3則表示中斷向量號為3的中斷。
在我們使用調(diào)試器下斷點時,調(diào)試器將會把對應(yīng)位置的原來的指令替換為一個int 3指令,機(jī)器碼為0xCC。這個動作對我們是透明的,我們在調(diào)試器中看到的依然是原來的指令,但實際上內(nèi)存中已經(jīng)不是原來的指令了。
順便提一句,兩個0xCC是漢字【燙】的編碼,在一些編譯器里,會給線程的棧中填充大量的0xCC,如果程序出錯的時候,我們經(jīng)常會看到很多燙燙燙出現(xiàn),就是這個原因。
言歸正傳,CPU在執(zhí)行這條int 3指令時,將自動觸發(fā)中斷處理流程(雖然這實際上不是一個真正的中斷),CPU將取出IDTR寄存器指向的中斷描述符表IDT的第3項,執(zhí)行里面的中斷處理函數(shù)。
而這個中斷描述符表,早在操作系統(tǒng)啟動之初,就已經(jīng)提前安排好了,所以執(zhí)行這條指令后,操作系統(tǒng)的中斷處理函數(shù)將介入,來處理這一事件。
后面的過程就多了,簡單來說,操作系統(tǒng)會把觸發(fā)這一事件的進(jìn)程凍結(jié)起來,隨后將這一事件發(fā)送到調(diào)試器,調(diào)試器拿到之后就知道目標(biāo)進(jìn)程觸發(fā)斷點了。這個時候,咱們程序員就能通過調(diào)試器的UI交互界面或者命令行調(diào)試接口來調(diào)試目標(biāo)進(jìn)程,查看堆棧、查看內(nèi)存、變量都隨你。
如果我們要繼續(xù)運(yùn)行,調(diào)試器將會把之前修改的int 3指令給恢復(fù)回去,然后告知操作系統(tǒng):我處理完了,把目標(biāo)進(jìn)程解凍吧!
上面簡單描述了一下普通斷點的實現(xiàn)原理?,F(xiàn)在思考一個場景:我們發(fā)現(xiàn)一個bug,某個全局整數(shù)型變量的值老是莫名其妙被修改,但你發(fā)現(xiàn)有很多線程,很多函數(shù)都有可能會去修改這個變量,你想找出到底誰干的,怎么辦?
這個時候上面的普通斷點就沒辦法了,你需要一種新的斷點:硬件斷點。
這時候就該本小節(jié)的主人公調(diào)試寄存器登場表演了。
在x86架構(gòu)CPU內(nèi)部,提供了8個調(diào)試寄存器DR0~DR7。
DR0~DR3:這是四個用于存儲地址的寄存器
DR4~DR5:這兩個有點特殊,受前面提到的CR4寄存器中的標(biāo)志位DE位控制,如果CR4的DE位是1,則DR4、DR5是不可訪問的,訪問將觸發(fā)異常。如果CR4的DE位是0,則DR4和DR5將會變成DR6和DR7的別名,相當(dāng)于做了一個軟鏈接。這樣做是為了將DR4、DR5保留,以便將來擴(kuò)展調(diào)試功能時使用。
DR6:這個寄存器中存儲了硬件斷點觸發(fā)后的一些狀態(tài)信息
DR7:調(diào)試控制寄存器,這里面記錄了對DR0-DR3這四個寄存器中存儲地址的中斷方式(是對地址的讀,還是寫,還是執(zhí)行)、數(shù)據(jù)長度(1/2/4個字節(jié))以及作用范圍等信息
通過調(diào)試器的接口設(shè)置硬件斷點后,CPU在執(zhí)行代碼的過程中,如果滿足條件,將自動中斷下來。
回答前面提出的問題,想要找出是誰偷偷修改了全局整形變量,只需要通過調(diào)試器設(shè)置一個硬件寫入斷點即可。
描述符寄存器所謂描述符,其實就是一個數(shù)據(jù)結(jié)構(gòu),用來記錄一些信息,‘描述’一個東西。把很多個描述符排列在一起,組成一個表,就成了描述符表。再使用一個寄存器來指向這個表,這個寄存器就是描述符寄存器。
在x86/x64系列CPU中,有三個非常重要的描述符寄存器,它們分別存儲了三個地址,指向了三個非常重要的描述符表。
gdtr: 全局描述符表寄存器,前面提到,CPU現(xiàn)在使用的是段+分頁結(jié)合的內(nèi)存管理方式,那系統(tǒng)總共有那些分段呢?這就存儲在一個叫全局描述符表(GDT)的表格中,并用gdtr寄存器指向這個表。這個表中的每一項都描述了一個內(nèi)存段的信息。
ldtr: 局部描述符表寄存器,這個寄存器和上面的gdtr一樣,同樣指向的是一個段描述符表(LDT)。不同的是,GDT是全局唯一,LDT是局部使用的,可以創(chuàng)建多個,隨著任務(wù)段切換而切換(下文介紹任務(wù)寄存器會提到)。
GDT和LDT中的表項,就是段描述符,描述了一個內(nèi)存分段的信息,其結(jié)構(gòu)如下:
一個表項占據(jù)8個字節(jié)(32位CPU),里面存儲了一個內(nèi)存分段的諸多信息:基地址、大小、權(quán)限、類型等信息。
除了這兩個段描述符寄存器,還有一個非常重要的描述符寄存器:
idtr: 中斷描述符表寄存器,指向了中斷描述符表IDT,這個表的每一項都是一個中斷處理描述符,當(dāng)CPU執(zhí)行過程中發(fā)生了硬中斷、異常、軟中斷時,將自動從這個表中定位對應(yīng)的表項,里面記錄了發(fā)生中斷、異常時該去哪里執(zhí)行處理函數(shù)。
IDT中的表項稱為Gate,中文意思為門,因為這是應(yīng)用程序進(jìn)入內(nèi)核的主要入口。雖然表的名字叫中斷描述符表,但表中存儲的不全是中斷描述符,IDT中的表項存在三種類型,對應(yīng)三種類型的門:
任務(wù)門
陷阱門
中斷門
三種描述符中都存儲了處理這個中斷/異常/任務(wù)時該去哪里處理的地址。三種門用途不一,其中中斷門是真正意義上的中斷,而像前面提到的調(diào)試指令int 3以及老式的系統(tǒng)調(diào)用指令int 2e/int 80都屬于陷阱門。任務(wù)門則用的較少,要了解任務(wù)門,先了解下任務(wù)寄存器。
任務(wù)寄存器現(xiàn)代操作系統(tǒng),都是支持多任務(wù)并發(fā)運(yùn)行的,x86架構(gòu)CPU為了順應(yīng)時代潮流,在硬件層面上提供了專門的機(jī)制用來支持多任務(wù)的切換,這體現(xiàn)在兩個方面:
CPU內(nèi)部設(shè)置了一個專用的寄存器——任務(wù)寄存器TR,它指向當(dāng)前運(yùn)行的任務(wù)。
定義了描述任務(wù)的數(shù)據(jù)結(jié)構(gòu)TSS,里面存儲了一個任務(wù)的上下文(一系列寄存器的值),下圖是一個32位CPU的TSS結(jié)構(gòu)圖:
x86CPU的構(gòu)想是每一個任務(wù)對應(yīng)一個TSS,然后由TR寄存器指向當(dāng)前的任務(wù),執(zhí)行任務(wù)切換時,修改TR寄存器的指向即可,這是硬件層面的多任務(wù)切換機(jī)制。
這個構(gòu)想其實還是很不錯的,然而現(xiàn)實卻打了臉,包括Linux和Windows在內(nèi)的主流操作系統(tǒng)都沒有使用這個機(jī)制來進(jìn)行線程切換,而是自己使用軟件來實現(xiàn)多線程切換。
所以,絕大多數(shù)情況下,TR寄存器都是指向固定的,即便線程切換了,TR寄存器仍然不會變化。
注意,我這里說的的是絕大多數(shù)情況,而沒有說死。雖然操作系統(tǒng)不依靠TSS來實現(xiàn)多任務(wù)切換,但這并不意味著CPU提供的TSS操作系統(tǒng)一點也沒有使用。還是存在一些特殊情況,如一些異常處理會使用到TSS來執(zhí)行處理。
下面這張圖,展示了控制寄存器、描述符寄存器、任務(wù)寄存器構(gòu)成的全貌:
模型特定寄存器從80486之后的x86架構(gòu)CPU,內(nèi)部增加了一組新的寄存器,統(tǒng)稱為MSR寄存器,中文直譯是模型特定寄存器,意思是這些寄存器不像上面列出的寄存器是固定的,這些寄存器可能隨著不同的版本有所變化。這些寄存器主要用來支持一些新的功能。
隨著x86CPU不斷更新?lián)Q代,MSR寄存器變的越來越多,但與此同時,有一部分MSR寄存器隨著版本迭代,慢慢固化下來,成為了變化中那部分不變的,這部分MSR寄存器,Intel將其稱為Architected MSR,這部分MSR寄存器,在命名上,統(tǒng)一加上了IA32的前綴。
這里選取三個代表性的MSR簡單介紹一下:
IA32_SYSENTER_CS
IA32_SYSENTER_ESP
IA32_SYSENTER_EIP
這三個MSR寄存器是用來實現(xiàn)快速系統(tǒng)調(diào)用。
在早期的x86架構(gòu)CPU上,系統(tǒng)調(diào)用依賴于軟中斷實現(xiàn),類似于前面調(diào)試用到的int 3指令,在Windows上,系統(tǒng)調(diào)用用到的是int 2e,在Linux上,用的是int 80。
軟中斷畢竟還是比較慢的,因為執(zhí)行軟中斷就需要內(nèi)存查表,通過IDTR定位到IDT,再取出函數(shù)進(jìn)行執(zhí)行。
系統(tǒng)調(diào)用是一個頻繁觸發(fā)的動作,如此這般勢必對性能有所影響。在進(jìn)入奔騰時代后,就加上了上面的三個MSR寄存器,分別存儲了執(zhí)行系統(tǒng)調(diào)用后,內(nèi)核系統(tǒng)調(diào)用入口函數(shù)所需要的段寄存器、堆棧棧頂、函數(shù)地址,不再需要內(nèi)存查表??焖傧到y(tǒng)調(diào)用還提供了專門的CPU指令sysenter/sysexit用來發(fā)起系統(tǒng)調(diào)用和退出系統(tǒng)調(diào)用。
在64位上,這一對指令升級為syscall/sysret。
總結(jié)以上就是全部要介紹的寄存器了,需要說明一下的是,這并不是x86CPU全部所有的寄存器,除了這些,還存在XMM、MMX、FPU浮點數(shù)運(yùn)算等其他寄存器。
這篇文章以x86/x64架構(gòu)CPU為目標(biāo),通過對CPU內(nèi)部寄存器的闡述,串講了CPU執(zhí)行代碼機(jī)制、內(nèi)存尋址技術(shù)、中斷與異常處理、多任務(wù)管理、系統(tǒng)調(diào)用、調(diào)試原理等多種計算機(jī)底層知識?! ?/p>
非常好我支持^.^
(39) 92.9%
不好我反對
(3) 7.1%
相關(guān)閱讀:
- [cpu] 詳細(xì)解讀GPU的起源及發(fā)展 2023-12-25
- [電子說] 對于芯片中的復(fù)位信號我們通常會有哪些特殊處理? 2023-12-25
- [存儲技術(shù)] 全面解析存儲器層次結(jié)構(gòu)原理 2023-12-25
- [接口/總線/驅(qū)動] 關(guān)于I2C總線的6個問題分析 2023-12-25
- [電子說] 【技術(shù)科普】CPU、GPU、TPU、NPU分別是什么?哪個最強(qiáng)? 2023-12-22
- [電子說] 十大IC設(shè)計企業(yè)Q3季度營收增長17.8% 2023-12-22
- [電子說] 全球IC設(shè)計廠商新排名公布 2023-12-22
- [處理器/DSP] 國產(chǎn)最高頻率記錄的自主x86處理器發(fā)布! 2023-12-22
( 發(fā)表人:彭菁 )