引子
打算寫幾篇稍近底層或者說是基礎的博文,淺要介紹或者說是回顧一些基礎知識,
自然,還是得從最基礎的開始,那就從彙編語言開刀吧,
從彙編語言開刀的話,我們必須還先要了解一些其他東西,
像 CPU ,記憶體這些知識點還是了解深刻一點的比較好,
是以這一篇博文就繞着 80x86 CPU 中寄存器的基礎部分下手,至于其他的一些将會在後續的博文中介紹 。
同時在這裡說明一下,本篇博文介紹的算是比較詳細的了,而且介紹的知識點也是比較多的,是以造成博文長度過長,
如果有興趣想了解這一塊的話,還請自行斟酌好閱讀比例,建議分 3 次以上閱覽 。
讀者定位
本博文主要将介紹的是 8086 CPU 中的寄存器,既然是 8086 CPU 寄存器簡介的話,
自然,面向的是初級一些的讀者,其中不會涉及太多難點,同時,所有的介紹,我也會盡可能的從基礎開始,
然後循序漸進的介紹,同時也會盡量的将知識點介紹詳細,
介紹的過程中也會涉及到一些彙程式設計式代碼,當然,采用的是最簡單的方式介紹而已,
本篇博文也就是回顧一些基礎知識,讀者主要定位于想對 8086 CPU 有所了解,
希望對整個程式設計的底層有所了解的朋友,而且讀者最好是擁有一定的計算機基礎和彙編語言基礎 。
開頭
首先淺要介紹一下 Intel CPU 的發展史吧:
Intel CPU 系列,最初是 4 位微處理器 4004,然後到到 8 位微處理器的 8008 ,
再到 8 微微處理器 8080,以及稍後的 16 位微處理器 8086,
由 8086 開始,Intel 進入現在所謂的 x86 時代 。
Intel 8086 為 16 位 CPU ,而因為在 8086 之前的 CPU 都是 8 位 CPU,這樣也就造成了很多的外設也隻支援 8 位,
是以 Intel 緊接着就退出了 8 位的 8088 CPU,是以 Intel 8088 也就可以看做是 8086 的 8 位版本;
如果是但從彙編語言的角度上來說,8086 和 8088 是沒有差別的,即 8086 上跑的程式可以不加修改的移植到 8088 ,
8088 上跑的程式也可以不加修改的移植到 8086 上,
當然,還是有些特殊的地方是不同的,而這些基本上在這裡可以忽略掉,
在 8088 CPU 之後,Intel 又推出了 80186 ,80286 ,這兩款 CPU 均是 16 位 CPU ,
而對于 80186 來說,其與 8086 的差別可以簡單的看做是 80186 多了幾條指令而已,
而 80286 則不同,80286 的位址總線數目有了變化,
在 8086 , 8088 , 80186 上,CPU 的位址總線都是 20 根,即可最大尋址 220 即達到 1MB 的尋址能力,
而對于 80286 CPU 來說,其位址總線數目達到了 24 根,進而最大尋址能力為 224 即 16MB,
由于支援更多的實體記憶體尋址,是以 80286 便開始成為了多任務,多使用者系統的核心。
而後來,Intel 又推出了 80386 ,80386 為 32 位微處理器,Intel 80x86 家族的 32 位微處理器始于 80386;
同時 80386 也完全相容先前的 8086/8088,80186,80286,并且 80386 全面支援 32 位資料類型和 32 位操作,
并且 80386 的資料總線根數和位址總線根數均達到了 32 根,進而可以最大實體尋址為 232 即 4GB 。
而之後的 80486 也是 32 位微處理器,而後又出來了 Pentium 和 Pentium Pro 等等第五代微處理器,
這些處理器雖然也是 32 位微處理器,但是他們的資料總線和位址總線都有所擴充,
比如 Pentium 的資料總線達到 64 位,而 Pentium Pro 的位址總線位數達到了 36 位 。
好,關于 Intel CPU 的介紹就到這裡了,下面就要開始回歸中心,看 CPU 中的寄存器了,
首先,從學習的角度來說,從 8086/8088 CPU 下手是不錯的選擇,而我這裡選擇的也是 8086 CPU 而已,
說實在的,像 80386 CPU 我也還沒有研究過,像奔騰這些,呵呵,扯更遠了,
說到底也就隻能拿 8086 出來曬曬而已,當然,從 8086 開始也是學習的最佳路徑 。
說了這麼久,到底寄存器是什麼呢?其實很簡單,寄存器就是個存儲資訊的單元或者說是器件又或者說是容器而已,
就比如記憶體也是一個存儲媒體或者說是存儲單元而已,其實寄存器從了解上來說和記憶體差不多,
隻不過寄存器(這裡讨論的寄存器都是 CPU 中的寄存器,不包括外設上的寄存器)位于 CPU 内部,而記憶體位于 CPU 外部,
而且,寄存器比記憶體可是珍貴得多啊,就拿記憶體和硬碟來比,肯定是記憶體在使用上珍貴得多,是 PC 中的稀有資源,
而寄存器是 CPU 中的稀有資源,記憶體和寄存器相比就像硬碟和記憶體相比一樣 。
而對于一個彙程式設計式員來說,CPU 中主要可以使用的也就是寄存器而已,彙程式設計式員可以使用指令來讀寫 CPU 中的寄存器,
進而可以實作對于 CPU 的控制,當然,不同的 CPU ,寄存器的個數和結構都是不一樣的,
比如 8086 CPU 中,寄存器的個數也就 14 個而已,
并且 8086 CPU 中所有的寄存器的結構為 16 位,即一個寄存器中可以存放下 2B 即 2 個位元組,
而到了 80386 CPU 中,寄存器的個數也比 8086 增多了,比如在 80386 中添加了系統位址寄存器等寄存器,
同時寄存器的結構也變了,比如在 80386 中絕大多數的寄存器為 32 位,而有些寄存器則是 16 位 。
8086
CPU 中寄存器總共為 14 個,且均為 16 位 。
即 AX,BX,CX,DX,SP,BP,SI,DI,IP,FLAG,CS,DS,SS,ES 共 14 個。
而這 14 個寄存器按照一定方式又分為了通用寄存器,控制寄存器和段寄存器。
通用寄存器:
AX,BX,CX,DX
稱作為資料寄存器:
AX (Accumulator):累加寄存器,也稱之為累加器;
BX (Base):基位址寄存器;
CX (Count):計數器寄存器;
DX (Data):資料寄存器;
SP
和 BP 又稱作為指針寄存器:
SP (Stack Pointer):堆棧指針寄存器;
BP (Base Pointer):基指針寄存器;
SI
和 DI 又稱作為變址寄存器:
SI (Source Index):源變址寄存器;
DI (Destination Index):目的變址寄存器;
控制寄存器:
IP (Instruction Pointer):指令指針寄存器;
FLAG:标志寄存器;
段寄存器:
CS (Code Segment):代碼段寄存器;
DS (Data Segment):資料段寄存器;
SS (Stack Segment):堆棧段寄存器;
ES (Extra Segment):附加段寄存器;
通用寄存器
從上面可以知道,在 8086 CPU 中,通用寄存器有 8 個,分别是 AX,BX,CX,DX,SP,BP,SI,DI ,
至于為什麼給它們取名做通用寄存器,那是因為,這些個寄存器每一個都有自己專門的用途,
比如 CX 作為計數寄存器,則是在使用 LOOP 指令循環時用來指定循環次數的寄存器,
如果它們每一個都隻有一個專用的作用,那就它們隻能稱之為專用寄存器了,
正是因為這些個寄存器還可以用來傳送資料和暫存資料,是以才稱它們為通用寄存器 。
下面就按順序來一一介紹這幾個通用寄存器了:
資料寄存器(AX,BX,CX,DX):
資料寄存器有 AX,BX,CX,DX 四個組成,
由于在 8086 之前的 CPU 為 8 位 CPU,是以為了相容以前的 8 位程式,
在 8086 CPU 中,每一個資料寄存器都可以當做兩個單獨的寄存器來使用,
由此,每一個 16 位寄存器就可以當做 2 個獨立的 8 位寄存器來使用了 。
AX 寄存器可以分為兩個獨立的 8 位的 AH 和 AL 寄存器;
BX 寄存器可以分為兩個獨立的 8 位的 BH 和 BL 寄存器;
CX 寄存器可以分為兩個獨立的 8 位的 CH 和 CL 寄存器;
DX 寄存器可以分為兩個獨立的 8 位的 DH 和 DL 寄存器;
除了上面 4 個資料寄存器以外,其他寄存器均不可以分為兩個獨立的 8 位寄存器 ;
注意在上面标志中的“獨立”二字,這兩個字表明 AH 和 AL 作為 8 位寄存器使用時,
可以看做它們是互不相關的,也就是看做兩個完全沒有聯系的寄存器 X 和 Y 即可,
比如指令 MOV AH , 12H ,CPU 在執行時根本就不會知道 AL 中是什麼鬼東西,因為它隻認識 AH 。
下面給出一幅 16 位資料寄存器的結構圖:
表示 16 位 寄存器 AX 可以表示成兩個 8 位寄存器,
其中 AH 表示高位的 8 位寄存器,AL 表示低位的 8 位寄存器 。
AX
寄存器:
如上所說,AX 的另外一個名字叫做累加寄存器或者簡稱為累加器,其可以分為 2 個獨立的 8 位寄存器 AH 和 AL;
在寫彙程式設計式時,AX 寄存器可以說是使用率最高的寄存器(不過,總共才那麼 14 個寄存器,哪一個不經常使用咯?),
既然 AX 是資料寄存器的話,那麼理所當然,其可以用來存放普通的資料,由于其是 16 位寄存器,
自然也就可以存放 16 位資料,但是因為其又可以分為 2 個獨立的 8 位寄存器 AH 和 AL ,
是以,在 AH 和 AL 中又可以獨立的存放 2 個 8 位的資料,
可以有以下代碼(即将 AX 當做普通的寄存器使用,即可以用來暫存資料):
而既然 AX 又被稱作為累加器,自然其還有一點點特殊的地方的:
AX 寄存器還具有的特殊用途是在使用 DIV 和 MUL 指令時使用,
DIV 在 8086 CPU 中是除法指令,而在使用除法的時候有兩種情況,即除數可以是 8 位或者是 16 位的,
而且除數可以存放在寄存器中或者是記憶體單元中,而至于被除數的話,自然,應該由 AX 來代替了,
當除數是 8 位時,被除數一定會是 16 位的,并且預設是放在 AX 寄存器中,
而當除數是 16 位時,被除數一定是 32 位的,因為 AX 是 16 位寄存器,自然,放不下 32 位的被除數,
是以,在這裡還需要使用另一個 16 位寄存器 DX ,
其中 DX 存放 32 位的被除數的高 16 位,而 AX 則存放 32 位的被除數的低 16 位,
同時,AX 的作用還不僅僅是用來儲存被除數的,當除法指令執行完成以後,
如果除數是 8 位的,則在 AL 中會儲存此次除法操作的商,而在 AH 中則會儲存此次除法操作的餘數,
當然,如果除數是 16 位的話,則 AX 中會儲存本次除法操作的商,而 DX 則儲存本次除法操作的餘數。
上面介紹的是 AX 寄存器在除法操作中的應用,下面還需要介紹一下 AX 在乘法操作中的應用,
當使用 MUL 做乘法運算時,兩個相乘的數要麼都是 8 位,要麼都是 16 位,
如果兩個相乘的數都是 8 位的話,則一個預設是放在 AL 中,
而另一個 8 位的乘數則位于其他的寄存器或者說是記憶體位元組單元中,
而如果兩個相乘的數都是 16 位的話,則一個預設存放在 AX 中,
另一個 16 位的則是位于 16 的寄存器中或者是某個記憶體字單元中。
同時,當 MUL 指令執行完畢後,如果是 8 位的乘法運算,則預設乘法運算的結果是儲存在 AX 中,
而如果是 16 位的乘法運算的話,則預設乘法運算的結果有 32 位,
其中,高位預設儲存在 DX 中,而低位則預設儲存在 AX 中。
AX 寄存器在 DIV 指令中的使用:
4
條語句的執行過程如下:
AX 寄存器在 MUL 指令中的使用:
BX 寄存器:
首先可以明确的是,BX 作為資料寄存器,表明其是可以暫存一般的資料的,
即在某種程度上,它和 AX 可以暫存一般性資料的功能是一樣的,
其同樣為了适應以前的 8 位 CPU ,而可以将 BX 當做兩個獨立的 8 位寄存器使用,即有 BH 和 BL,
除了暫存一般性資料的功能外,BX 作為通用寄存器的一種,BX 主要還是用于其專屬功能 – 尋址(尋址實體記憶體位址)上,
BX 寄存器中存放的資料一般是用來作為偏移位址使用的,何為偏移位址呢?
既然是偏移位址的話,當然得有一個基位址了,而這個基位址其實就是段位址,這裡就涉及到了段寄存器,
當然,在介紹 BX 寄存器的時候,我不會去介紹段寄存器,上面提到 BX 的主要功能是用在尋址上,
那麼,其是如何尋址的呢?
對于尋址這個話題,我會在我的下一篇博文中作出詳細的介紹,
而這裡,我隻點一下,在 8086 CPU 中,CPU 是根據 <段位址:偏移位址> 來進行尋址操作的,
而 BX 中存放的資料表示的是偏移位址的話,自然,便可以通過 <段位址:[BX]> 的方式來完成尋址操作了。
為了介紹 BX 在尋址當中的作用,下面我給出一副示意圖:
上面的示意圖表示:可以令 BX = 2,然後通過 DS : [BX] 來通路到記憶體中段位址為 DS,且偏移量為 2 的記憶體單元了。
上面介紹的這種尋址方式是 BX 在尋址中最最簡單的應用了,而對于稍微複雜的尋址方式,
還可以依賴于 SI,DI,BP 等寄存器來一起完成,當然,這會是下一篇博文将要介紹的内容了。
BX 寄存器在尋址中的使用:
3
從上圖可以看出,在偏移位址為 5 時的記憶體單元中的資料位 BBH,
而從這幅圖上面就可以看出,确實通過 [BX] 找到了偏移位址為 5 處的記憶體單元,并且将記憶體單元移入了 AH 中。
CX
CX 寄存器作為資料寄存器的一種呢,其同樣具有和 AX,BX 一樣的特點,即可以暫存一般性的資料,
同時還可以将其當做兩個獨立的 8 位寄存器使用,即有 CH 和 CL 兩個 8 位寄存器,
當然,CX 也是有其專門的用途的,CX 中的 C 被翻譯為 Counting 也就是計數器的功能,
當在彙編指令中使用循環 LOOP 指令時,可以通過 CX 來指定需要循環的次數,
而 CPU 在每一次執行 LOOP 指令的時候,都會做兩件事:
一件就是令 CX = CX – 1,即令 CX 計數器自動減去 1;
還有一件就是判斷 CX 中的值,如果 CX 中的值為 0 則會跳出循環,而繼續執行循環下面的指令,
當然如果 CX 中的值不為 0 ,則會繼續執行循環中所指定的指令 。
CX 寄存器在循環中的使用(輸出 5 個白底藍字的 A):
語句的執行過程如下:
DX
DX 寄存器作為資料寄存器的一種,同樣具有和 AX,BX,CX 一樣的特點,即可以暫存一般性的資料,
同時還可以将其當做兩個獨立的 8 位寄存器使用,極有 DH 和 DL,
同時,DX 作為一個通用寄存器的話,自然其還有其他的用途,而關于 DX 在其他方面的用途,
其實在前面介紹 AX 寄存器時便已經有所介紹了,
即當在使用 DIV 指令進行除法運算時,如果除數為 16 位時,被除數将會是 32 位,而被除數的高 16 位就是存放在 DX 中,
而且執行完 DIV 指令後,本次除法運算所産生的餘數将會儲存在 DX 中,
同時,在執行 MUL 指令時,如果兩個相乘的數都是 16 位的話,
那麼相乘後産生的結果顯然需要 32 位來儲存,而這 32 位的結果的高 16 位就是存放在 DX 寄存器中 。
DX 寄存器在 DIV 指令中的使用(即 2293812 / 256 = 8960 餘數為 52):
可以看到在語句結束以後,AX = 2300H 即十進制的 8960,而 DX = 34H即十進制的 52 和我們的結果是一緻的。
DX 寄存器在 MUL 指令中的使用則各位可以參考在 AX 中 MUL 運算的使用,這裡就不貼出來了。
指針寄存器(BP,SP):
BP
8086 CPU 中的指針寄存器包括兩個,即 SP 和 BP ,在這裡呢,我先隻對 BP 寄存器做介紹,
因為 SP 寄存器實質上必須和 SS 段寄存器一起使用,是以,我将會把 SP 寄存器留到後面和 SS 段寄存器一起作介紹。
BP (Base Pointer)也就是基指針寄存器,它和其他的幾個用來進行尋址操作所使用的寄存器(還有 BX,SI,DI)沒有太大的差別,
關于 SI 和 DI 寄存器的下面請見下文。
首先,BP 寄存器作為通用寄存器的一種,說明其是可以暫存資料的,而後,BP 又不是資料寄存器,
也就意味着其不能分割成 2 個獨立的 8 位寄存器使用,
而後當以 […] 的方式通路記憶體單元而且在 […] 中使用了寄存器 BP 的話,
那麼如果在指令中沒有明确或者說是顯示的給出段位址時,
段位址則使用預設的 SS 寄存器中的值(BX,SI,DI 會預設使用 DS 段寄存器),
比如 DS:[BP] 則在這裡明确給出了段位址位于 DS 中,
是以,這裡代表的記憶體單元即是段位址為 DS ,偏移量為 BP 寄存器中的值的記憶體單元,
而如果單單是使用 [BP] 的話,則代表的記憶體單元是段位址為 SS,偏移量為 BP 寄存器中的值的記憶體單元。
并且 BP 寄存器主要适用于給出堆棧中資料區的偏移,進而可以友善的實作直接存取堆棧中的資料,
至于堆棧的話,會在後面的博文中介紹。
在 8086 CPU 中,隻有 4 個寄存器可以以 […] 的方式使用,這四個寄存器分别是 BX,SI,DI,BP。
下面的 Demo 是 BX 寄存器在尋址中的使用:
變址寄存器(SI,DI):
首先,變址寄存器和上面介紹的指針寄存器(也就是 BP 和 SP),它們的功能其實都是用于存放某個存儲單元位址的偏移,
或者是用于某組存儲單元開始位址的偏移,即作為存儲器指針使用,當然,由于變址寄存器和指針寄存器都是屬于通用寄存器,
是以它們也可以儲存算術結果或者說是具有暫存資料的功能,但是因為它們不是資料寄存器,是以無法分割成 2 個獨立的 8 位寄存器使用,
關于變址寄存器和指針寄存器的詳細使用,筆者将會在下一篇博文中作出最詳細的介紹,
SI (Source Index) 是源變址寄存器,DI (Destination Index) 即是目的變址寄存器,
8086 CPU 中的 SI 寄存器和 DI 寄存器其實和 BX 寄存器的功能是差不多的,
隻不過 SI 寄存器和 DI 寄存器均不是資料寄存器,是以它們不能夠拆分為 2 個獨立的 8 位寄存器,
而這也就是 SI 寄存器和 DI 寄存器與BX 寄存器所不同的地方,
既然,SI,DI 兩個寄存器的功能和 BX 差不多,自然,SI 和 DI 中也是可以暫存一般性資料的,
同時,通過使用 SI 和 DI 寄存器也是可以用來完成尋址操作的。
比如下面的代碼就是可行的:
其他寄存器(CS,IP,SS,SP,DS,ES)
由于段寄存器總是和其他一些像指針寄存器,變址寄存器,控制寄存器一起使用,
是以在這裡,我并不會單獨介紹段寄存器,而是将段寄存器和一些其他的常用寄存器搭配介紹 。
由于下面的介紹中會涉及到很多關于段和棧的概念,而段和棧的介紹又都必須關系到實體記憶體,
是以在介紹段寄存器以及其他一些呈協作關系的寄存器之前,還是先來介紹一下這幾個基本的概念比較好。
8086
CPU 通路記憶體(實體位址):
當 CPU 需要通路一個記憶體單元時,需要給出記憶體單元的位址,
而每一個記憶體單元在實體記憶體空間中都有一個唯一的位址,
即可以通過這個位址定位到記憶體單元,而這個位址即為實體位址。
CPU 通過位址總線将一個記憶體單元的實體位址送入存儲器,
而後 CPU 便可以通過這個實體位址來通路這個實體位址所指向的記憶體單元了。
那麼這個實體位址在 CPU 中是如何形成的呢?
首先,我們知道 8086 CPU 的位址總線是 20 根,
即每次都可以傳輸 20 位的位址,進而尋址能力有 220 也就是 1MB 的大小,
但是 8086 CPU 的寄存器隻有 16 位,也就是在 8086 CPU 的内部,
一次性處理,傳輸,暫存的位址都隻能是 16 位,
即 8086 CPU 不能完整的儲存下一個實體位址(實體位址為 20 位),
如果單單以最簡單的方式(即直接用 16 位寄存器來儲存實體位址)的話,那麼,尋址能力隻有 216 ,也就是 64KB,
如果真以如此簡單的方式的話,那麼位址總線還需要 20 根幹嘛呢?而且,難不成我們以後的記憶體就是 64KB 了嗎?
當然不是的,8086 CPU 在這裡采取了一定的措施進而使其尋址能力達到 1MB 。
8086 CPU 在内部通過兩個 16 位的位址進行合成進而形成一個 20 位的實體位址,由此,8086 CPU 的尋址能力便可以達到 1MB 。
那麼 8086 CPU 又是如何将兩個 16 位的位址合成為一個20 位的實體位址的呢?
當 CPU 在通路記憶體時,其會使用一個 16 位的基位址,然後再使用一個 16 位的偏移位址,
通過将基位址和偏移位址傳入 8086 CPU 的位址加法器中進行合成即可以構造出 20 位的實體位址。
至于合成的方式如下:
基位址其實是通過一個 16 位的段位址來形成的,将一個段位址左移 4 位即形成了基位址,
而至于偏移位址的話,自然不必多說,為 16 位,通過将基位址和偏移位址相加便形成了 20 位的實體位址 。
下面給出一幅示意圖來表示實體位址的合成:
段:
至于段的話,其實在實體記憶體中是沒有段這一概念的,事實上,段的概念來自于 CPU ,
因為 CPU 擁有段寄存器,既然在 CPU 中擁有了段寄存器,自然,在 CPU 中就肯定有段的概念了,
其實段也就是在程式設計時,我們将若幹個位址連續的記憶體單元看做是一個段,
然後通過将一個段位址左移 4 位形成基位址,再通過這個基位址來定位這個段的起始位址,
然後,再通過偏移位址便可以精确定位到段中的記憶體單元了,由于段的起始位址是一個段位址左移 4 位,
是以很明顯,段的起始位址肯定是 16 的倍數,而且由于一個段内部,隻能通過偏移位址來定位,
而偏移位址為 16 位,是以一個段的長度也就是 216 也就是 64KB 的大小。
在程式設計時,可以講一段記憶體定義成為一個段,而這裡,我們又可以引出資料段,代碼段,棧段這三種類型的段 。
何為資料段呢?其實就是我們自個兒定義一段記憶體(當然段起始位址肯定是 16 的倍數,并且段長度 <= 64KB),
然後我們在這個段裡頭存放我們所需要使用的資料,這就是資料段;
何為代碼段呢?其實也很簡單,也是我們自己在程式設計的時候定義一段記憶體,然後這段記憶體用來存放我們的代碼(也就是指令),
既然是存放的代碼,自然就稱之為代碼段;
何為棧段呢?至于棧段的話,有接觸過資料結構的朋友應該是很清楚棧的,而這裡我們也就是在記憶體中配置設定出一個段,
然後将這個段當做棧來使用,對于棧的介紹,詳見下文;
這裡呢,順便還點出幾個關于段寄存器的内容,當然下文還會詳細介紹的,
首先,對于任何一個段來說,均有段位址,而這些段位址是存放在段寄存器中(段寄存器的作用也在于此),
但是對于不同的段,它們預設的段位址存放在不同的段寄存器中,像
資料段來說,它的段位址存放在 DS (Data Segment)寄存器中,
代碼段的段位址存放在 CS (Code Segment)寄存器中,
棧段的段位址存放在 SS (Stack Segment)寄存器中 。
下面給出一幅在段中尋址的示意圖:
上面的示意圖中,通過将段位址左移四位,然後與偏移位址相加便可以得到 20 位的實體位址了 。
棧:
8086 CPU 中提供了對棧的支援,并且其還提供了相應的指令來以棧的方式通路記憶體空間 。
什麼是棧?
通過上面在段中的介紹,棧其實就是一個段,再說白一點,也就是一塊記憶體,當然,這塊記憶體是一塊連續的記憶體 。
既然棧是一個段的話,那麼當然就可以以使用段的方式來使用棧,當然,除了像段一樣的使用棧以外,
棧還提供了其特殊的通路方式(如果和段一模一樣的話,那還需要棧幹嗎呢?),
衆所周知,棧是先進後出類型的資料結構,在 8086 CPU 中也是如此,
可以通過 ”PUSH“ 指令将資料壓入棧中,然後再通過 ”POP“
指令将棧頂的元素取出來 。
下面給出一幅示意圖來描述棧:
即通過 PUSH 10 來将元素 10 放入棧中,因為,先前棧中沒有任何資料,是以,10 就會作為棧頂元素存在,
然後再在棧中壓入元素 20 ,此時,棧頂中的元素就是 20 了,然後再使用 POP 指令将棧頂元素取出,
此時取出的棧頂元素是 20 ,取出 20 後,棧中便隻剩下 10 了,自然 10 就成為了棧頂,
最後再通過 POP 指令将棧頂 10 取出,此時,棧便變成了空棧了 。
好了,在介紹段寄存器之前的基礎知識介紹就到這裡了,下面開始正式介紹段寄存器以及與它們協作使用的寄存器。
CS
寄存器 和 IP 寄存器:
經過前面對段的介紹,相信各位朋友對段寄存器應該也有一定的了解了,
下面将要介紹的是一組非常非常重要的寄存器,即 CS:IP 。
CS:IP 兩個寄存器訓示了 CPU 目前将要讀取的指令的位址,其中 CS 為代碼段寄存器,而 IP 為指令指針寄存器 。
什麼叫做訓示了 CPU 目前将要讀取的指令呢?在 8086 CPU 中,為什麼 CPU 會自動的執行指令呢?
這些指令肯定是存放在記憶體中的,但是 CPU 怎麼知道這些指令存放在記憶體的那個位置呢?
比如,我有下面的兩條指令要執行:
而假設這兩條指令在記憶體中存放為:
很顯然, 1000H:0000H 指向的是 MOV AX,1234H 的首位址,
如果 CPU 要讀取到我的指令的話,很顯然,必須要知道位址 1000H:0000H ,
然後 CPU 就可以根據這個首位址,将彙編指令 MOV AX,1234H 所對應的機器碼讀入到 CPU 的指令寄存器中,
最後便可以在 CPU 中進行處理了。
但關鍵是 CPU 如何知道我的 1000H:0000H 這個首位址?
其實這就需要使用到 CS:IP 這個寄存器組了 。
當我們運作一個可執行檔案時,很明顯,我們需要另外一個程式來将這個可執行檔案加載到記憶體當中,
關于這個加載可執行檔案的程式,我們在這裡不管他,點一下即可,
一般是通過作業系統的外殼程式(也就是傳說中的 Shell 程式),
Shell 将可執行檔案加載到記憶體中以後,就會設定 CPU 中的兩個寄存器,
即設定 CS:IP 兩個寄存器指向可執行檔案的起始位址,此後 CPU 便從這個起始位址開始讀取記憶體中的指令,并且執行,
比如我們在寫彙程式設計式時,通常會使用 START 标記,其實這個标記就是用來标記起始位址的,
當将一個彙程式設計式編譯,連接配接成可執行檔案以後,再通過作業系統的 Shell 程式将可執行檔案加載到記憶體中以後,
這個 START 所标記處的位址就是整個可執行檔案的起始位址了 。
也就是說,當一個可執行檔案加載到記憶體中以後,CS:IP 兩個寄存器便指向了這個可執行檔案的起始位址,
然後 CPU 就可以從這個起始位址開始往下讀取指令,
當讀取完指令後,CS:IP 将會自動的改變,基本上是改變 IP ,進而指向下一條要讀取的指令,這樣就可以執行這個可執行檔案了 。
最後再對 CS:IP 總結一下:
你想讓 CPU 執行哪行指令,你就讓 CS:IP 指向儲存有指令的那塊記憶體即可。
任何時候,CS:IP 指向的位址中的内容都是 CPU 目前執行的指令。
下面我們來看一個 Demo,并詳細觀察其執行的過程:
從上面的截圖中可以看出,當我使用 Shell (在 DOS 下也就是 Command 指令解釋器)将可執行檔案加載進記憶體後,
可以看到,整個程式的起始位址為 0C54H : 0000 H ,并且,可以看到 CS 的位址為 0C54H ,IP 的位址為 0000H,
這正好吻合我們上面對 CS:IP 的分析,很明顯,CPU 将會讀取 MOV AX ,1234H 到 CPU 中并且執行 ,
然後我們繼續向下看:
可以看到,我們單步執行後,AX 中的值編成了 1234H ,而 IP 寄存器中的值變成了 0003H,
對于 AX 中的值的改變,我們是能夠了解的,但是 IP 中的值為什麼會從 0000H 變到 0003H 呢?
從最上面的一幅關于指令在記憶體中的存放可以看出 MOV AX ,1234H 在記憶體中需要 3 個記憶體單元存放,
也就是 CPU 為了執行 MOV AX ,1234H 這條指令,已經将記憶體中相對應的 3 個記憶體單元讀入記憶體中了,
執行完這條指令後,自然,CPU 就要将偏移位址向下移動 3 個單元,進而使得 CS:IP 指向下一條需要執行的指令了 ,
為了更深刻的了解,我們再來繼續看執行過程,
從最上面的一幅關于指令在記憶體中的存放可以看出 MOV BX ,AX 在記憶體中隻占 2 個記憶體單元,
這也就是為什麼 IP 這一次隻向下移動了 2 個單元的緣故 。
關于
CS: IP 的遐想:
從上面關于 CS:IP 的介紹中,我們可以大膽的猜想,我們隻需要通過手動的改變 CS:IP 所指向的記憶體位址,
讓 CS:IP 指向我們另外的代碼,那麼我們就可以讓 CPU 執行我們自己指定的代碼了 。
即可以通過修改 CS:IP 來達到我們想要讓 CPU 幹什麼它就幹什麼的目的 。
上面的雖然是遐想,但是大家要相信,我們寫的是彙編,不是 JAVA 也不是 NET ,
是以我們還真的可以達到上面的目的,也就是說我們的遐想其實是可以實作的,當然這還是有一定的限制的 ,
關于這個遐想呢,可能會在我後續的博文中有所介紹,不過感興趣的當然可以自己去嘗試了,蠻有味的哦 。
SS
寄存器和 SP 寄存器:
根據前面對棧的介紹,相信各位對棧也肯定是有一定了解了的,更何況,估計大家也是職場打滾多年的,
要是棧都沒用過的話,那也确實蠻悲劇的 ,是以,我在這裡也不會對棧做十分詳細的介紹了,
但是,最基本的介紹還是要的,畢竟在底層的話,不像進階語言那麼友善,可以直接一個 Stack 就 OK 的,
在底層涉及的是棧在記憶體中的具體實作 。
不知道,大夥有沒有注意筆者在本篇博文的上面介紹關于棧的知識時,我并沒有提到如何找到這個棧,
我隻提到了一個棧就是先進後出操作,同時可以使用 ”PUSH“ 和 ”POP“ 指令,
然後就是稍微帶了一下 SS 這個寄存器的介紹,
我們雖然在記憶體中是可以友善的定義一個棧了,但是,我們為什麼要定義這麼一個棧呢?
自然,是為了操作友善,同時提供給 CPU 使用的,
既然 CPU 要使用的話,自然,CPU 又必須根據一定的方式找到這個棧,
而這就需要使用 SS 和 SP 寄存器了 。
同時,一個棧也就是一塊記憶體區域,通過上面的介紹,我們也知道了如果要在一塊記憶體中精确地定位到記憶體單元的話(尋址),
我們必須要有基位址(也就是段位址左移 4 位)和偏移位址,自然,要在一個棧中尋址的話,也需要段位址和偏移位址,
而對于一個棧來說,我們使用的最多的是什麼呢?
當然是棧頂了,因為隻有棧頂可以用來存取資料,是以對于一個棧來說,我們隻需要有棧頂的段位址和偏移位址即可,
而對于棧頂的段位址,其是存放在段寄存器 SS 中的,而對于棧頂的偏移位址,其則是存放在 SP 寄存器中的 。
記住,在任何時刻,SS:SP 都是指向棧頂元素 。
其實關于棧的使用還是比較簡單的,但是要注意的是 8086 CPU 并不會保證我們對棧的操作會不會越界 。
是以我們在使用棧的時候需要特别注意棧的越界問題 。
當使用 PUSH 指令向棧中壓入 1 個位元組單元時,SP = SP - 1;即棧頂元素會發生變化;
而當使用 PUSH 指令向棧中壓入 2 個位元組的字單元時,SP = SP – 2 ;即棧頂元素也要發生變化;
當使用 POP 指令從棧中彈出 1 個位元組單元時, SP = SP + 1;即棧頂元素會發生變化;
當使用 POP 指令從棧中彈出 2 個位元組單元的字單元時, SP = SP + 2 ;即棧頂元素會發生變化;
下面通過一個 Demo 來介紹棧的使用:
然後我們來看棧在記憶體中的結構圖:
首先我們來看尚未執行上述任何指令時棧中的資料情況:
然後我們再來依次執行上述指令:
從上副截圖中可以看出已經設定好了 SS:SP ,也就是棧已經設定 OK 了,
下面開始往棧中壓入資料了,
由于我們壓入棧中的資料為字資料,即占 2 個記憶體單元,是以,每次 SP = SP – 2 ;
将 5 個字型資料壓入棧中後,我們可以來檢視棧中的資料了,
是以,在記憶體中的一個好看點的結構圖如下所示:
下面開始進行出棧操作了
由于我們彈出棧時的資料為字資料,即占 2 個記憶體單元,是以,每次 SP = SP + 2 ;
将 5 個字型資料全部彈出棧中後,我們可以來檢視棧中的資料了,
可以看到 SP 變成了初始狀态了,也就是說棧中所有的資料已經全部彈出了,雖然我們檢視記憶體時看到的不是 0 ,
但是我們看到的這些資料都是無效的,我們這裡不理會 。
DS
寄存器和 ES 寄存器:
DS 寄存器和 ES 寄存器都屬于段寄存器,其實它們和 CS 寄存器以及 SS 寄存器用起來差別不大,
既然是段寄存器的話,自然它們存放的就是某個段位址了 。
通過上面對基礎知識的介紹呢,我們已經知道,如果 CPU 要通路一個記憶體單元時,
我們必須要提供一個指向這個記憶體單元的實體位址給 CPU ,
而我們也知道在 8086 CPU 中,實體位址是由段位址左移 4 位,然後加上偏移位址形成的,
是以,我們也就隻需要提供段位址和偏移位址即 OK 。
8086 CPU 呢,提供了一個 DS 寄存器,并且通常都是通過這個 DS 段寄存器來存放要通路的資料的段位址 。
DS(Data Segment):很顯然,DS 中存放的是資料段的段位址 。
但是這裡不得不再點一下,那就是我們對段的支援是在 CPU 上展現的,而不是在記憶體中實作了段,
是以事實上我們使用的段其實是一個邏輯概念,即是我們自己定義的,
再說白了,我定義一個段,我說它是資料段那它就是資料段,我說它是代碼段那麼它就是代碼段,
它們其實都是一塊連續的記憶體而已,至于為什麼要區分為資料段和代碼段,
很明顯,是用來給我們程式設計提供友善的,即我們在自己的思想上或者說是編碼習慣上規定,
資料放資料段中,代碼放代碼段中 。而我們在使用資料段的時候,為了友善或者說是代碼的編寫友善起見,
我們一般把資料段的段位址放在 DS 寄存器中,當然,如果你硬要覺得 DS 不順眼,那你可以換個 ES 也是一樣的,
至于 ES(Extra Segment) 段寄存器的話,自然,是一個附加段寄存器,如果再說得過分點,
就當它是個擴充吧,當你發現,你幾個段寄存器不夠用的時候,你可以考慮使用 ES 段寄存器,
在使用方式上,則和其他的段寄存器沒什麼差別 。
下面看一個介紹使用 DS 寄存器的 Demo:
上面的代碼所做的事情,就是循環将 1,2,3,4,5 寫入到位址 1000H:0000H ,1000H:0001H,
1000H:0002H,1000H:0003H,1000H:0004H 中,
而當循環執行完成以後,我們再來看記憶體 1000H:0000H 處的值:
在這裡,我們可以看到确實達到了我們預期的效果,但是大家注意看代碼:
這裡可以看到,我們在 [BX] 中并沒有給其指定段位址,而隻有一個偏移位址,
但是根據我們一開始的介紹,必須要有段位址和偏移位址才能夠定位記憶體單元,
莫非這裡出問題了?
其實不是的,因為我們在最前面定義了段位址 DS 為 1000H,
當我們定義好段位址後,每一次 CPU 執行到 [BX] 時,便會自動或者說是預設的從 DS 中取值,
并且将取得的值作為段位址,是以,當 [BX] 為 0001H 時,CPU 會從 DS 中取得一個 1000H ,
由這兩個一合成即可以得到正确的實體位址 1000H:0000H 。
最後還提醒一點,那就是 8086 CPU 不支援直接将一個資料送入段寄存器中,
也就是下面的做法是錯誤的:
标志寄存器(FLAG):
前面呢,已經介紹了 8086 CPU 14 個寄存器中的 13 個了,下面我們将介紹最後一個寄存器也就是 FLAG 寄存器,
FLAG 寄存器之是以放到最後一個介紹,是因為其和其他的一些寄存器不同,像 AX,BX,CX,DX 這些寄存器來說,
它們都是用來存放資料的,當然 FLAG 中存放的也是資料啦,
呵呵,不過,AX,BX 這些寄存器中的資料是作為一個整體使用的,
最多也就分成一個 AL 和 AH 使用而已,但是在 FLAG 中,資料是按位起作用的,
也就是說,FLAG 中的每一個位都表示不同的狀态,
由于一個位也就能表示 0 和 1 ,自然,FLAG 中的每一個位就是用來描述狀态的,
而且 FLAG 寄存器中存儲的資訊通常又被稱作程式狀态字(PSW) 。
下面我給出一幅 FLAG 寄存器中各個位的示意圖:
從上面這幅圖中可以看出,FLAG 的第 0 個位表示的是 CF ,第 2 個位表示的是 PF ,與此類推 . . . .
首先,我們來看一個清單:
上面的這個表怎麼看呢?我們通過看下面一幅截圖就知道了 。
從上面的标記中可以看出,從左到右依次代表 OF,DF,SF,ZF,PF,CF 标志位的值,
再通過與上面的表格相對照可以知道:
OF = 0 ;
DF = 0 ;
SF = 0 ;
ZF = 0 ;
PF = 0 ;
CF = 0 ;
至于為什麼我們在 Debug 模式下,使用 R 指令時,隻會列出這幾個标志位,我菜的話是因為相對來說,
列出的這幾個标志位更為常用,其他的幾個标志位并不經常使用的緣故吧 。
下面我們就按不同的位來分别介紹這些位所描述的狀态,以及它們代表的意義:
CF(Carry
FLag) - 進位标志(第 0 位):
CF: 進位标志是用來反映計算時是否産生了由低位向高位的進位,或者産生了從高位到低位的借位 。
PF(Parity
FLag) - 奇偶标志(第 2 位):
PF: 奇偶标志是用來記錄相關指令執行後,其結果的所有的 Bit 位中 1 的個數是否為偶數 。
AF(Auxiliary
Carry FLag) - 輔助進位标志(第 4 位):
AF: 用來輔助進位标志 。
ZF(Zero
FLag) – 零标志(第 6 位):
ZF: 記錄的是相關的指令執行完畢後,其執行的結果是否為 0 。
SF(Sign
FLag) - 符号标志(第 7 位):
SF: 符号标志,其記錄相關指令執行完以後,其結果是否為負數 。
TF(Trap
FLag) - 追蹤标志(第 8 位):
TF: 追蹤标志,主要是用于調試時使用 。
IF(Interrupt-Enable
FLag) - 中斷允許标志(第 9 位):
IF: 中斷允許标志,其決定 CPU 是否能夠響應外部可屏蔽中斷請求(以後會做詳細介紹) 。
DF(Direction
FLag) - 方向标志(第 10 位):
DF: 方向标志,其用于在串處理指令中,用來控制每次操作後 SI 和 DI 是自增還是自減 。
OF(OverFlow
FLag) - 溢出标志(第 11 位):
OF: 溢出标志,其通常記錄了有符号數運算的結果是否發生了溢出 。
總結
上面呢,從最簡單的開始,循序漸進的介紹了 8086 CPU 中的各個寄存器,
同時也通過一些 Demo 來列舉了各個寄存器的使用,
由于寫的比較基礎,而且量也比較多,是以,造成博文過長了,讀者需一定耐心才能看完,
寫本篇博文呢,并不是說将來要用彙編去開發個什麼東東,
實質上,筆者學習彙編的目的也不在此,隻是因為先前在接觸到底層的寄存器以及記憶體時,
筆者總有一絲不爽的感覺,總是感覺不得要領,是以才會開始彙編的學習,
此次推出本系列博文,本意也并不是說要學習彙編做開發,隻是為了提升内功而已 。