我把自己以往的文章彙總成為了 Github ,歡迎各位大佬 star
https://github.com/crisxuan/b...
下面我們就來介紹一下關於暫存器的相關內容。我們知道,暫存器
是 CPU 內部的構造,它主要用於資訊的儲存。除此之外,CPU 內部還有運算器
,負責處理資料;控制器
控制其他元件;外部匯流排
連線 CPU 和各種部件,進行資料傳輸;內部匯流排
負責 CPU 內部各種元件的資料處理。
那麼對於我們所瞭解的組合語言來說,我們的主要關注點就是 暫存器
。
為什麼會出現暫存器?因為我們知道,程式在記憶體中裝載,由 CPU 來執行,CPU 的主要職責就是用來處理資料。那麼這個過程勢必涉及到從儲存器中讀取和寫入資料,因為它涉及通過控制匯流排傳送資料請求並進入儲存器儲存單元,通過同一通道獲取資料,這個過程非常的繁瑣並且會涉及到大量的記憶體佔用,而且有一些常用的記憶體頁存在,其實是沒有必要的,因此出現了暫存器,儲存在 CPU 內部。
認識暫存器
暫存器的官方叫法有很多,Wiki 上面的叫法是 Processing Register
, 也可以稱為 CPU Register
,計算機中經常有一個東西多種叫法的情況,反正你知道都說的是暫存器就可以了。
認識暫存器之前,我們首先先來看一下 CPU 內部的構造。
CPU 從邏輯上可以分為 3 個模組,分別是控制單元、運算單元和儲存單元,這三部分由 CPU 內部匯流排連線起來。
幾乎所有的馮·諾伊曼型計算機的 CPU,其工作都可以分為5個階段:取指令、指令譯碼、執行指令、訪存取數、結果寫回。
取指令
階段是將記憶體中的指令讀取到 CPU 中暫存器的過程,程式暫存器用於儲存下一條指令所在的地址指令譯碼
階段,在取指令完成後,立馬進入指令譯碼階段,在指令譯碼階段,指令譯碼器按照預定的指令格式,對取回的指令進行拆分和解釋,識別區分出不同的指令類別以及各種獲取運算元的方法。執行指令
階段,譯碼完成後,就需要執行這一條指令了,此階段的任務是完成指令所規定的各種操作,具體實現指令的功能。訪問取數
階段,根據指令的需要,有可能需要從記憶體中提取資料,此階段的任務是:根據指令地址碼,得到運算元在主存中的地址,並從主存中讀取該運算元用於運算。結果寫回
階段,作為最後一個階段,結果寫回(Write Back,WB)階段把執行指令階段的執行結果資料寫回到 CPU 的內部暫存器中,以便被後續的指令快速地存取;
計算機架構中的暫存器
暫存器是一塊速度非常快的計算機記憶體,下面是現代計算機中具有儲存功能的部件比對,可以看到,暫存器的速度是最快的,同時也是造價最高昂的。
我們以 intel 8086 處理器為例來進行探討,8086 處理器是 x86 架構的前身。在 8086 後面又衍生出來了 8088 。
在 8086 CPU 中,地址匯流排達到 20 根,因此最大定址能力是 2^20 次冪也就是 1MB 的定址能力,8088 也是如此。
在 8086 架構中,所有的內部暫存器、內部以及外部匯流排都是 16 位寬,可以儲存兩個位元組,因為是完全的 16 位微處理器。8086 處理器有 14 個暫存器,每個暫存器都有一個特有的名稱,即
AX,BX,CX,DX,SP,BP,SI,DI,IP,FLAG,CS,DS,SS,ES
這 14 個暫存器有可能進行具體的劃分,按照功能可以分為三種
- 通用暫存器
- 控制暫存器
- 段暫存器
下面我們分別介紹一下這幾種暫存器
通用暫存器
通用暫存器主要有四種 ,即 AX、BX、CX、DX 同樣的,這四個暫存器也是 16 位的,能存放兩個位元組。 AX、BX、CX、DX 這四個暫存器一般用來存放資料,也被稱為 資料暫存器
。它們的結構如下
8086 CPU 的上一代暫存器是 8080 ,它是一類 8 位的 CPU,為了保證相容性,8086 在 8080 上做了很小的修改,8086 中的通用暫存器 AX、BX、CX、DX 都可以獨立使用兩個 8 位暫存器來使用。
在細節方面,AX、BX、CX、DX 可以再向下進行劃分
AX(Accumulator Register)
: 累加暫存器,它主要用於輸入/輸出和大規模的指令運算。BX(Base Register)
:基址暫存器,用來儲存基礎訪問地址CX(Count Register)
:計數暫存器,CX 暫存器在迭代的操作中會迴圈計數DX(data Register)
:資料暫存器,它也用於輸入/輸出操作。它還與 AX 暫存器以及 DX 一起使用,用於涉及大數值的乘法和除法運算。
這四種暫存器可以分為上半部分和下半部分,用作八個 8 位資料暫存器
- AX 暫存器可以分為兩個獨立的 8 位的 AH 和 AL 暫存器;
- BX 暫存器可以分為兩個獨立的 8 位的 BH 和 BL 暫存器;
- CX 暫存器可以分為兩個獨立的 8 位的 CH 和 CL 暫存器;
- DX 暫存器可以分為兩個獨立的 8 位的 DH 和 DL 暫存器;
除了上面 AX、BX、CX、DX 暫存器以外,其他暫存器均不可以分為兩個獨立的 8 位暫存器
如下圖所示。
合起來就是
AX 的低位(0 - 7)位構成了 AL 暫存器,高 8 位(8 - 15)位構成了 AH 暫存器。AH 和 AL 暫存器是可以使用的 8 位暫存器,其他同理。
在認識了暫存器之後,我們通過一個示例來看一下資料的具體儲存方式。
比如資料 19 ,它在 16 位儲存器中所儲存的表示如下
暫存器的儲存方式是先儲存低位,如果低位滿足不了就儲存高位,如果低位能夠滿足,高位用 0 補全,在其他低位能滿足的情況下,其餘位也用 0 補全。
8086 CPU 可以一次儲存兩種型別的資料
位元組(byte)
: 一個位元組由 8 bit 組成,這是一種恆定不變的儲存方式字(word)
:字是由指令集或處理器硬體作為單元處理的固定大小的資料,對於 intel 來說,一個字長就是兩個位元組,字是計算機一個非常重要的特徵,針對不同的指令集架構來說,計算機一次處理的資料也是不同的。也就是說,針對不同指令集的機器,一次能處理不用的字長,有字、雙字(32位)、四字(64位)等。
AX 暫存器
我們上面探討過,AX 的另外一個名字叫做累加暫存器或者簡稱為累加器,其可以分為 2 個獨立的 8 位暫存器 AH 和 AL;在編寫彙編程式中,AX 暫存器可以說是使用頻率最高的暫存器。
下面是幾段彙編程式碼
mov ax,20 /* 將 20 送入暫存器 AX*/
mov ah,80 /* 將 80 送入暫存器 AH*/
add ax,10 /* 將暫存器 AX 中的數值加上 8 */
這裡注意下:上面程式碼中出現的是 ax、ah ,而註釋中確是 AX、AH ,其實含義是一樣的,不區分大小寫。
AX 相比於其他通用暫存器來說,有一點比較特殊,AX 具有一種特殊功能的使用,那就是使用 DIV 和 MUL 指令式使用。
DIV 是 8086 CPU 中的除法
指令。MUL 是 8086 CPU 中的
乘法
指令。
BX 暫存器
BX 被稱為資料暫存器,即表明其能夠暫存一般資料。同樣為了適應以前的 8 位 CPU ,而可以將 BX 當做兩個獨立的 8 位暫存器使用,即有 BH 和 BL。BX 除了具有暫存資料的功能外,還用於 定址
,即尋找實體記憶體地址。BX 暫存器中存放的資料一般是用來作為偏移地址
使用的,因為偏移地址當然是在基址地址上的偏移了。偏移地址是在段暫存器中儲存的,關於段暫存器的介紹,我們後面再說。
CX 暫存器
CX 也是資料暫存器,能夠暫存一般性資料。同樣為了適應以前的 8 位 CPU ,而可以將 CX 當做兩個獨立的 8 位暫存器使用,即有 CH 和 CL。除此之外,CX 也是有其專門的用途的,CX 中的 C 被翻譯為 Counting 也就是計數器的功能。當在彙編指令中使用迴圈 LOOP 指令時,可以通過 CX 來指定需要迴圈的次數,每次執行迴圈 LOOP 時候,CPU 會做兩件事
- 一件事是計數器自動減 1
- 還有一件就是判斷 CX 中的值,如果 CX 中的值為 0 則會跳出迴圈,而繼續執行迴圈下面的指令,
當然如果 CX 中的值不為 0 ,則會繼續執行迴圈中所指定的指令 。
DX 暫存器
DX 也是資料暫存器,能夠暫存一般性資料。同樣為了適應以前的 8 位 CPU ,DX 的用途其實在前面介紹 AX 暫存器時便已經有所介紹了,那就是支援 MUL 和 DIV 指令。同時也支援數值溢位等。
段暫存器
CPU 包含四個段暫存器,用作程式指令,資料或棧的基礎位置。實際上,對 IBM PC 上所有記憶體的引用都包含一個段暫存器作為基本位置。
段暫存器主要包含
CS(Code Segment)
: 程式碼暫存器,程式程式碼的基礎位置DS(Data Segment)
: 資料暫存器,變數的基本位置SS(Stack Segment)
: 棧暫存器,棧的基礎位置ES(Extra Segment)
: 其他暫存器,記憶體中變數的其他基本位置。
索引暫存器
索引暫存器主要包含段地址的偏移量,索引暫存器主要分為
BP(Base Pointer)
:基礎指標,它是棧暫存器上的偏移量,用來定位棧上變數SP(Stack Pointer)
: 棧指標,它是棧暫存器上的偏移量,用來定位棧頂SI(Source Index)
: 變址暫存器,用來拷貝源字串DI(Destination Index)
: 目標變址暫存器,用來複制到目標字串
狀態和控制暫存器
就剩下兩種暫存器還沒聊了,這兩種暫存器是指令指標暫存器和標誌暫存器:
IP(Instruction Pointer)
: 指令指標暫存器,它是從 Code Segment 程式碼暫存器處的偏移來儲存執行的下一條指令FLAG
: Flag 暫存器用於儲存當前程式的狀態,這些狀態有- 位置 (Direction):用於資料塊的傳輸方向,是向上傳輸還是向下傳輸
- 中斷標誌位 (Interrupt) :1 - 允許;0 - 禁止
- 陷入位 (Trap) :確定每條指令執行完成後,CPU 是否應該停止。1 - 開啟,0 - 關閉
- 進位 (Carry) : 設定最後一個無符號算術運算是否帶有進位
- 溢位 (Overflow) : 設定最後一個有符號運算是否溢位
- 符號 (Sign) : 如果最後一次算術運算為負,則設定 1 =負,0 =正
- 零位 (Zero) : 如果最後一次算術運算結果為零,1 = 零
- 輔助進位 (Aux Carry) :用於第三位到第四位的進位
- 奇偶校驗 (Parity) : 用於奇偶校驗
實體地址
我們大家都知道, CPU 訪問記憶體時,需要知道訪問記憶體的具體地址,記憶體單元是記憶體的基本單位,每一個記憶體單元在記憶體中都有唯一的地址,這個地址即是 實體地址
。而 CPU 和記憶體之間的互動有三條匯流排,即資料匯流排、控制匯流排和地址匯流排。
CPU 通過地址匯流排將實體地址送入儲存器,那麼 CPU 是如何形成的實體地址呢?這將是我們接下來的討論重點。
現在,我們先來討論一下和 8086 CPU 有關的結構問題。
cxuan 和你聊了這麼久,你應該知道 8086 CPU 是 16 位的 CPU 了,那麼,什麼是 16 位的 CPU 呢?
你可能大致聽過這個回答,16 位 CPU 指的是 CPU 一次能處理的資料是 16 位的,能回答這個問題代表你的底層還不錯,但是不夠全面,其實,16 位的 CPU 指的是
- CPU 內部的運算器一次最多能處理 16 位的資料
運算器其實就是 ALU,運算控制單元,它是 CPU 內部的三大核心器件之一,主要負責資料的運算。
- 暫存器的最大寬度為 16 位
這個暫存器的最大寬度值得就是通用暫存器能處理的二進位制數的最大位數
- 暫存器和運算器之間的通路為 16 位
這個指的是暫存器和運算器之間的匯流排,一次能傳輸 16 位的資料
好了,現在你應該知道為什麼叫做 16 位 CPU 了吧。
在你知道上面這個問題的答案之後,我們下面就來聊一聊如何計算實體地址。
8086 CPU 有 20 位地址匯流排,每一條匯流排都可以傳輸一位的地址,所以 8086 CPU 可以傳送 20 位地址,也就是說,8086 CPU 可以達到 2^20 次冪的定址能力,也就是 1MB。8086 CPU 又是 16 位的結構,從 8086 CPU 的結構看,它只能傳輸 16 位的地址,也就是 2^16 次冪也就是 64 KB,那麼它如何達到 1MB 的定址能力呢?
原來,8086 CPU 的內部採用兩個 16 位地址合成的方式來傳輸一個 20 位的實體地址,如下圖所示
敘述一下上圖描述的過程
CPU 中相關元件提供兩個地址:段地址和偏移地址,這兩個地址都是 16 位的,他們經由地址加法器
變為 20 位的實體地址,這個地址即是輸入輸出控制電路傳遞給記憶體的實體地址,由此完成實體地址的轉換。
地址加法器採用 實體地址 = 段地址 * 16 + 偏移地址 的方法用段地址和偏移地址合成實體地址。
下面是地址加法器的工作流程
其實段地址 16 ,就是左移 4 位。在上面的敘述中,實體地址 = 段地址 16 + 偏移地址,其實就是基礎地址 + 偏移地址 = 實體地址 定址模式的一種具體實現方案。基礎地址其實就等於段地址 * 16。
你可能不太清楚 段
的概念,下面我們就來探討一下。
什麼是段
段這個概念經常出現在作業系統中,比如在記憶體管理中,作業系統會把不同的資料分成 段
來儲存,比如 程式碼段、資料段、bss 段、rodata 段 等。
但是這些的劃分並不是記憶體乾的,cxuan 告訴你是誰幹的,這其實是幕後 Boss CPU 搞的,記憶體當作了聲討的物件。
其實,記憶體沒有進行分段,分段完全是由 CPU 搞的,上面聊過的通過基礎地址 + 偏移地址 = 實體地址的方式給出記憶體單元的實體地址,使得我們可以分段管理 CPU。
如圖所示
這是兩個 16 KB 的程式分別被裝載進記憶體的示意圖,可以看到,這兩個程式的段地址的大小都是 16380。
這裡需要注意一點, 8086 CPU 段地址的計算方式是段地址 * 16,所以,16 位的定址能力是 2^16 次方,所以一個段的長度是 64 KB。
段暫存器
cxuan 在上面只是簡單為你介紹了一下段暫存器的概念,介紹的有些淺,而且介紹段暫存器不介紹段也有不知廬山真面目的感覺,現在為你詳細的介紹一下,相信看完上面的段的概念之後,段暫存器也是手到擒來。
我們在合成實體地址的那張圖提到了 相關部件
的概念,這個相關部件其實就是段暫存器
,即 CS、DS、SS、ES 。8086 的 CPU 在訪問記憶體時,由這四個暫存器提供記憶體單元的段地址。
CS 暫存器
要聊 CS 暫存器,那麼 IP 暫存器是你繞不過去的曾經。CS 和 IP 都是 8086 CPU 非常重要的暫存器,它們指出了 CPU 當前需要讀取指令的地址。
CS 的全稱是 Code Segment,即程式碼暫存器;而 IP 的全稱是 Instruction Pointer ,即指令指標。現在知道這兩個為什麼一起出現了吧!
在 8086 CPU 中,由 CS:IP
指向的內容當作指令執行。如下圖所示
說明一下上圖
在 CPU 內部,由 CS、IP 提供段地址,由加法器負責轉換為實體地址,輸入輸出控制電路負責輸入/輸出資料,指令緩衝器負責緩衝指令,指令執行器負責執行指令。在記憶體中有一段連續儲存的區域,區域內部儲存的是機器碼、外面是地址和彙編指令。
上面這幅圖的段地址和偏移地址分別是 2000 和 0000,當這兩個地址進入地址加法器後,會由地址加法器負責將這兩個地址轉換為實體地址
然後地址加法器負責將指令輸送到輸入輸出控制電路中
輸入輸出控制電路將 20 位的地址匯流排送到記憶體中。
然後取出對應的資料,也就是 B8、23、01,圖中的 B8、BB 都是運算元。
控制輸入/輸出電路會將 B8 23 01 送入指令快取器中。
此時這個指令就已經具備執行條件,此時 IP 也就是指令指標會自動增加。我們上面說到 IP 其實就是從 Code Segment 也就是 CS 處偏移的地址,也就是偏移地址。它會知道下一個需要讀取指令的地址,如下圖所示
在這之後,指令執行執行取出的 B8 23 01 這條指令。
然後下面再把 2000 和 0003 送到地址加法器中再進行後續指令的讀取。後面的指令讀取過程和我們上面探討的如出一轍,這裡 cxuan 就不再贅述啦。
通過對上面的描述,我們能總結一下 8086 CPU 的工作過程
- 段暫存器提供段地址和偏移地址給地址加法器
- 由地址加法器計算出實體地址通過輸入輸出控制電路將實體地址送到記憶體中
- 提取實體地址對應的指令,經由控制電路取回並送到指令快取器中
- IP 繼續指向下一條指令的地址,同時指令執行器執行指令緩衝器中的指令
什麼是 Code Segment
Code Segment 即程式碼段,它就是我們上面聊到就是 CS 暫存器中儲存的基礎地址,也就是段地址,段地址其本質上就是一組記憶體單元的地址,例如上面的 mov ax,0123H 、mov bx, 0003H。我們可以將長度為 N 的一組程式碼,存放在一組連續地址、其實地址為 16 的倍數的記憶體單元中,我們可以認為,這段記憶體就是用來存放程式碼的。
DS 暫存器
CPU 在讀寫一個記憶體單元的時候,需要知道這個記憶體單元的地址。在 8086 CPU 中,有一個 DS 暫存器
,通常用來存放訪問資料的段地址。如果你想要讀取一個 10000H 的資料,你可能會需要下面這段程式碼
mov bx,10000H
mov ds,bx
mov a1,[0]
上面這三條指令就把 10000H 讀取到了 a1 中。
在上面彙編程式碼中,mov 指令有兩種傳送方式
- 一種是把資料直接送入暫存器
- 一種是將一個暫存器的內容送入另一個暫存器
但是不僅僅如此,mov 指令還具有下面這幾種表達方式
描述 | 舉例 |
---|---|
mov 暫存器,資料 | 比如:mov ax,8 |
mov 暫存器,暫存器 | 比如:mov ax,bx |
mov 暫存器,記憶體單元 | 比如:mov ax,[0] |
mov 記憶體單元,暫存器 | 比如:mov[0], ax |
mov 段暫存器,暫存器 | 比如:mov ds,ax |
棧
棧我相信大部分小夥伴已經非常熟悉了,棧
是一種具有特殊的訪問方式的儲存空間。它的特殊性就在於,先進入棧的元素,最後才出去,也就是我們常說的 先入後出
。
它就像一個大的收納箱,你可以往裡面放相同型別的東西,比如書,最先放進收納箱的書在最下面,最後放進收納箱的書在最上面,如果你想拿書的話, 必須從最上面開始取,否則是無法取出最下面的書籍的。
棧的資料結構就是這樣,你把書籍壓入收納箱的操作叫做壓入(push)
,你把書籍從收納箱取出的操作叫做彈出(pop)
,它的模型圖大概是這樣
入棧相當於是增加操作,出棧相當於是刪除操作,只不過叫法不一樣。棧和記憶體不同,它不需要指定元素的地址。它的大概使用如下
// 壓入資料
Push(123);
Push(456);
Push(789);
// 彈出資料
j = Pop();
k = Pop();
l = Pop();
在棧中,LIFO 方式表示棧的陣列中所儲存的最後面的資料(Last In)會被最先讀取出來(First Out)。
棧和 SS 暫存器
下面我們就通過一段彙編程式碼來描述一下棧的壓入彈出的過程
8086 CPU 提供入棧和出棧指令,最基本的兩個是 PUSH(入棧)
和 POP(出棧)
。比如 push ax 會把 ax 暫存器中的資料壓入棧中,pop ax 表示從棧頂取出資料送入 ax 暫存器中。
這裡注意一點:8086 CPU 中的入棧和出棧都是以字為單位進行的。
我這裡首先有一個初始的棧,沒有任何指令和資料。
然後我們向棧中 push 資料後,棧中資料如下
涉及的指令有
mov ax,2345H
push ax
注意,資料會用兩個單元存放,高地址單元存放高 8 位地址,低地址單元存放低 8 位。
再向棧中 push 資料
其中涉及的指令有
mov bx,0132H
push bx
現在棧中有兩條資料,現在我們執行出棧操作
其中涉及的指令有
pop ax
/* ax = 0132H */
再繼續取出資料
涉及的指令有
pop bx
/* bx = */
完整的 push 和 pop 過程如下
現在 cxuan 問你一個問題,我們上面描述的是 10000H ~ 1000FH 這段空間來作為 push 和 pop 指令的存取單元。但是,你怎麼知道這個棧單元就是 10000H ~ 1000FH 呢?也就是說,你如何選擇指定的棧單元進行存取?
事實上,8086 CPU 有一組關於棧的暫存器 SS
和 SP
。SS 是段暫存器,它儲存的是棧的基礎位置,也就是棧頂的位置,而 SP 是棧指標,它儲存的是偏移地址。在任意時刻,SS:SP
都指向棧頂元素。push 和 pop 指令執行時,CPU 從 SS 和 SP 中得到棧頂的地址。
現在,我們可以完整的描述一下 push 和 pop 過程了,下面 cxuan 就給你推導一下這個過程。
上面這個過程主要涉及到的關鍵變化如下。
當使用 PUSH 指令向棧中壓入 1 個位元組單元時,SP = SP - 1;即棧頂元素會發生變化;
而當使用 PUSH 指令向棧中壓入 2 個位元組的字單元時,SP = SP – 2 ;即棧頂元素也要發生變化;
當使用 POP 指令從棧中彈出 1 個位元組單元時, SP = SP + 1;即棧頂元素會發生變化;
當使用 POP 指令從棧中彈出 2 個位元組單元的字單元時, SP = SP + 2 ;即棧頂元素會發生變化;
棧頂越界問題
現在我們知道,8086 CPU 可以使用 SS 和 SP 指示棧頂的地址,並且提供 PUSH 和 POP 指令實現入棧和出棧,所以,你現在知道了如何能夠找到棧頂位置,但是你如何能保證棧頂的位置不會越界呢?棧頂越界會產生什麼影響呢?
比如如下是一個棧頂越界的示意圖
第一開始,SS:SP 暫存器指向了棧頂,然後向棧空間 push 一定數量的元素後,SS:SP 位於棧空間頂部,此時再向棧空間內部 push 元素,就會出現棧頂越界問題。
棧頂越界是危險的,因為我們既然將一塊區域空間安排為棧,那麼在棧空間外部也可能存放了其他指令和資料,這些指令和資料有可能是其他程式的,所以如此操作會讓計算機懵逼
。
我們希望 8086 CPU 能自己解決問題,畢竟 8086 CPU 已經是個成熟的 CPU 了,要學會自己解決問題了。
然鵝(故意的),這對於 8086 CPU 來說,這可能是它一輩子的 夙願
了,真實情況是,8086 CPU 不會保證棧頂越界問題,也就是說 8086 CPU 只會告訴你棧頂在哪,並不會知道棧空間有多大,所以需要程式設計師自己手動去保證。。。
另外,我輸出了 六本 PDF,已免費提供下載,如下所示
連結: pan.baidu.com/s/1mYAeS9hI… 密碼: p9rs