一口氣看完45個寄存器,CPU核心技術大揭祕

序言

前段時間,我連續寫了十來篇CPU底層系列技術故事文章,有很多讀者私信我讓我寫一下CPU的寄存器。程序員

寄存器這個太多太複雜,不適合寫故事,拖了好久,總算是寫完了,這篇文章就來詳細聊聊x86/x64架構的CPU中那些紛繁複雜的寄存器們。web

長文預警,時速較快,請繫好安全帶~起飛~編程

自1946年馮·諾伊曼領導下誕生的世界上第一臺通用電子計算機ENIAC至今,計算機技術已經發展了七十多載。跨域

從當初專用於數學計算的龐然大物,到後來大型機服務器時代,從我的微機技術蓬勃發展,到互聯網浪潮席捲全球,再到移動互聯網、雲計算突飛猛進的當下,計算機變的形態萬千,無處不在。安全

這七十多年中,出現了數不清的編程語言,經過這些編程語言,又開發了無數的應用程序。服務器

可不管什麼樣的應用程序,什麼樣的編程語言,最終的程序邏輯都是要交付給CPU去執行實現的(固然這裏有些不嚴謹,除了CPU,還有協處理器、GPU等等)。因此瞭解和學習CPU的原理都是對計算機基礎知識的夯實大有裨益。微信

在七十多年的漫長曆程中,也涌現了很多架構的CPU。數據結構

  • MIPS
  • PowerPC
  • x86/x64
  • IA64
  • ARM
  • ······

這篇文章就以市場應用最爲普遍的x86-x64架構爲目標,經過學習瞭解它內部的100個寄存器功能做用,來串聯闡述CPU底層工做原理。多線程

經過這篇文章,你將瞭解到:架構

  • CPU指令執行原理
  • 內存尋址技術
  • 軟件調試技術原理
  • 中斷與異常處理
  • 系統調用
  • CPU多任務技術

什麼是寄存器?

寄存器是CPU內部用來存放數據的一些小型存儲區域,用來暫時存放參與運算的數據和運算結果以及一些CPU運行須要的信息。

x86架構CPU走的是複雜指令集(CISC) 路線,提供了豐富的指令來實現強大的功能,與此同時也提供了大量寄存器來輔助功能實現。這篇文章將覆蓋下面這些寄存器:

  • 通用寄存器
  • 標誌寄存器
  • 指令寄存器
  • 段寄存器
  • 控制寄存器
  • 調試寄存器
  • 描述符寄存器
  • 任務寄存器
  • MSR寄存器

通用寄存器

首當其衝的是通用寄存器,這些的寄存器是程序執行代碼最最經常使用,也最最基礎的寄存器,程序執行過程當中,絕大部分時間都是在操做這些寄存器來實現指令功能。

所謂通用,即這些寄存器CPU沒有特殊的用途,交給應用程序「隨意」使用。注意,這個隨意,我打了引號,對於有些寄存器,CPU有一些潛規則,用的時候要注意。

  • eax: 一般用來執行加法,函數調用的返回值通常也放在這裏面
  • ebx: 數據存取
  • ecx: 一般用來做爲計數器,好比for循環
  • edx: 讀寫I/O端口時,edx用來存放端口號
  • esp: 棧頂指針,指向棧的頂部
  • ebp: 棧底指針,指向棧的底部,一般用 ebp+偏移量的形式來定位函數存放在棧中的局部變量
  • esi: 字符串操做時,用於存放數據源的地址
  • edi: 字符串操做時,用於存放目的地址的,和esi兩個常常搭配一塊兒使用,執行字符串的複製等操做

在x64架構中,上面的通用寄存器都擴展成爲64位版本,名字也進行了升級。固然,爲了兼容32位模式程序,使用上面的名字仍然是能夠訪問的,至關於訪問64位寄存器的低32位。

rax rbx rcx rdx rsp rbp rsi rdi

除了擴展原來存在的通用寄存器,x64架構還引入了8個新的通用寄存器:

r8-r15

在原來32位時代,函數調用時,那個時候通用寄存器少,參數絕大多數時候是經過線程的棧來進行傳遞(固然也有使用寄存器傳遞的,好比著名的C++ this指針使用ecx寄存器傳遞,不過能用的寄存器畢竟很少)。

進入x64時代,寄存器資源富裕了,參數傳遞絕大多數都是用寄存器來傳了。寄存器傳參的好處是速度快,減小了對內存的讀寫次數。

固然,具體使用棧仍是用寄存器傳參數,這個不是編程語言決定的,而是編譯器在編譯生成CPU指令時決定的,若是編譯器非要在x64架構CPU上使用線程棧來傳參那也不是不行,這個對高級語言是無感知的。

標誌寄存器

標誌寄存器,裏面有衆多標記位,記錄了CPU執行指令過程當中的一系列狀態,這些標誌大都由CPU自動設置和修改:

  • CF 進位標誌
  • PF 奇偶標誌
  • ZF 零標誌
  • SF 符號標誌
  • OF 補碼溢出標誌
  • TF 跟蹤標誌
  • IF 中斷標誌
  • ······

在x64架構下,原來的eflags寄存器升級爲64位的rflags,不過其高32位並無新增什麼功能,保留爲未來使用。

指令寄存器

eip: 指令寄存器能夠說是CPU中最最重要的寄存器了,它指向了下一條要執行的指令所存放的地址,CPU的工做其實就是不斷取出它指向的指令,而後執行這條指令,同時指令寄存器繼續指向下面一條指令,如此不斷重複,這就是CPU工做的基本平常。

而在漏洞攻擊中,黑客想盡辦法費盡心機都想要修改指令寄存器的地址,從而可以執行惡意代碼。

一樣的,在x64架構下,32位的eip升級爲64位的rip寄存器。

段寄存器

段寄存器與CPU的內存尋址技術緊密相關。

早在16位的8086CPU時代,內存資源寶貴,CPU使用分段式內存尋址技術:

16位的寄存器能尋址的範圍是64KB,經過引入段的概念,將內存空間劃分爲不一樣的區域:分段,經過段基址+段內偏移段方式來尋址。

這樣一來,段的基地址保存在哪裏呢?8086CPU專門設置了幾個段寄存器用來保存段的基地址,這就是段寄存器段的由來。

段寄存器也是16位的。

段寄存器有下面6個,前面4個是早期16位模式就引入了,到了32位時代,又新增了fs和gs兩個段寄存器。

  • cs: 代碼段
  • ds: 數據段
  • ss: 棧段
  • es: 擴展段
  • fs: 數據段
  • gs: 數據段

段寄存器裏面存儲的內容與CPU當前工做的內存尋址模式緊密相關。

當CPU處於16位實地址模式下時,段寄存器存儲段的基地址,尋址時,將段寄存器內容左移4位(乘以16)獲得段基地址+段內偏移獲得最終的地址。

當CPU工做於保護模式下,段寄存器存儲的內容再也不是段基址了,此時的段寄存器中存放的是段選擇子,用來指示當前這個段寄存器「指向」的是哪一個分段。

注意我這裏的指向打了引號,段寄存器中存儲的並非內存段的直接地址,而是段選擇子,它的結構以下:

16個bit長度的段寄存器內容劃分了三個字段:

  • PRL: 特權請求級,就是咱們常說的ring0-ring3四個特權級。
  • TI: 0表示用的是全局描述符表GDT,1表示使用的是局部描述符表LDT。
  • Index: 這是一個表格中表項的索引值,這個表格叫 內存描述符表,它的每個表項都描述了一個內存分段。

這裏提到了兩個表,全局描述符表GDT和局部描述符表LDT,關於這兩個表的介紹,下面介紹描述符寄存器時再詳述,這裏只須要知道,這是CPU支持分段式內存管理須要的表格,放在內存中,表格中的每一項都是一個描述符,記錄了一個內存分段的信息。

保護模式下的段寄存器和段描述符到最後的內存分段,經過下圖的方式聯繫在一塊兒:


通用寄存器、段寄存器、標誌寄存器、指令寄存器,這四組寄存器共同構成了一個基本的指令執行環境,一個線程的上下文也基本上就是這些寄存器,在執行線程切換的時候,就是修改它們的內容。

控制寄存器

控制寄存器是CPU中一組至關重要的寄存器,咱們知道eflags寄存器記錄了當前運行線程的一系列關鍵信息。

那CPU運行過程當中自身的一些關鍵信息保存在哪裏呢?答案是控制寄存器!

32位CPU總共有cr0-cr4共5個控制寄存器,64位增長了cr8。他們各自有不一樣的功能,但都存儲了CPU工做時的重要信息:

  • cr0: 存儲了CPU控制標記和工做狀態
  • cr1: 保留未使用
  • cr2: 頁錯誤出現時保存致使出錯的地址
  • cr3: 存儲了當前進程的虛擬地址空間的重要信息—— 頁目錄地址
  • cr4: 也存儲了CPU工做相關以及當前人任務的一些信息
  • cr8: 64位新增擴展使用

其中,CR0尤爲重要,它包含了太多重要的CPU信息,值得單獨關注一下:

一些重要的標記位含義以下:

PG: 是否啓用內存分頁

AM: 是否啓用內存對齊自動檢查

WP: 是否開啓內存寫保護,若開啓,對只讀頁面嘗試寫入時將觸發異常,這一機制經常被用來實現寫時複製功能

PE: 是否開啓保護模式

除了CR0,另外一個值得關注的寄存器是CR3,它保存了當前進程所使用的虛擬地址空間的頁目錄地址,能夠說是整個虛擬地址翻譯中的頂級指揮棒,在進程空間切換的時候,CR3也將同步切換。

調試寄存器

在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,則DR四、DR5是不可訪問的,訪問將觸發異常。若是CR4的DE位是0,則DR4和DR5將會變成DR6和DR7的別名,至關於作了一個軟連接。這樣作是爲了將DR四、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執行代碼機制、內存尋址技術、中斷與異常處理、多任務管理、系統調用、調試原理等多種計算機底層知識。

文章寫做不容易,歡迎你們轉發支持~

往期TOP5文章

我是Redis,MySQL大哥被我害慘了!

CPU明明8個核,網卡爲啥拼命折騰一號核?

由於一個跨域請求,我差點丟了飯碗

完了!CPU一味求快出事兒了!

哈希表哪家強?幾大編程語言吵起來了!


本文分享自微信公衆號 - 編程技術宇宙(xuanyuancoding)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索