前言
匯編語言是各種CPU提供的機器指令的助記符的集合,可以通過匯編語言直接控制硬體系統進行作業;
Q:為什么說匯編語言可以直接操作硬體?那么匯編程序還有什么意義呢?
A:匯編語言利用助記符代替機器指令的操作碼,用地址符號或標號代替指令或運算元的地址;
匯編語言是面向機器的語言而不是機器語言,但匯編語言的本質就是機器語言;
可以這樣理解,從機器語言到匯編語言僅僅只是將英語翻譯成漢語,本質上都是可以書寫并識別的語言(同物種),但是從高級語言到匯編語言就是將動物寫的“字”翻譯成人類的字(跨物種);
匯編語言和機器語言是一一對應的:每一潭訓編語言指令對應一潭訓器語言指令;而高級語言和低級語言是一對多的關系:C++的一條陳述句可以拓展成多潭訓編指令或機器指令(機器語言是機器能夠直接識別的指令代碼,所以我們常簡稱機器語言為機器指令);
本博客參考《匯編語言 第四版》王爽,因為匯編語言和具體微處理器聯系,每種微處理器的匯編語言都不同,本博客針對以8086CPU為中央處理器的PC機進行學習;
第一章 基礎知識
1.機器語言&匯編語言
機器語言是機器指令的集合,機器指令就是一臺機器可以正確執行的命令;
現在PC機中的可以執行機器指令、進行運算的芯片我們稱為CPU,CPU是一種微處理器,而每一種微處理器由于其硬體設計和內部結構不同,需要使用不同的電平脈沖來控制使其作業,故每種微處理器都有屬于自己的機器語言;
機器語言難以辨別和記憶,于是出現了匯編語言,匯編語言主要由以下三類指令構成:
- 匯編指令:機器碼的助記符,與機器碼一一對應;
- 偽指令:沒有對應的機器碼,由編譯器而不是CPU執行;
- 其他符號:如加減運算子號等,沒有對應的機器碼;
匯編語言的主體和核心是匯編指令,特決定了匯編語言的特性,匯編指令和機器指令的差別在于匯編指令是機器指令便于記憶的書寫格式;
2.記憶體地址空間
2.1 主板
每臺PC機都有一個主板,主板上的核心器件和主要器件通過總線(地址總線、資料總線和控制總線)連接:
- CPU
- 存盤器(這里指的是記憶體,也就是裝在主板上的RAM)
- 外圍芯片組
- 拓展插槽
- RAM記憶體條
- 介面卡:CPU無法直接控制外部設備,拓展插槽上的介面卡直接控制這些外部設備 —— CPU通過總線向介面卡發出命令,介面卡根據CPU的命令控制外設作業,常見的介面卡有顯卡、網卡等;
2.2 存盤器芯片
一臺PC機上安裝了多個存盤器芯片:
- RAM:主要的RAM是裝在主板上的RAM和插在拓展插槽上的RAM;
- 介面卡上的RAM:某些介面卡需要暫存大量的輸入輸出資料,典型的例子是顯存(顯卡上的) —— 將需要顯示的內容寫入顯存就能出現在顯示幕上;
- 裝有BIOS的ROM:BIOS是一類軟體系統,通過它可以利用硬體設備進行最基本的輸入和輸出,主板和某些介面卡上都插有存盤相應BIOS的ROM(主板的BIOS稱為系統BIOS);
2.3 記憶體地址空間
上述存盤器芯片在物理上是獨立的器件,但CPU在操縱它們的時候都將它們統一作為記憶體來對待(看作一個由若干存盤單元組成的邏輯存盤器,這個邏輯存盤器就是我們所說的記憶體地址空間)

當我們基于一個計算機硬體系統編程的時候必須知道這個系統中的記憶體地址空間分配情況,不同計算機系統的記憶體地址空間的分配情況不同,如下是8086PC機記憶體地址空間分配情況

第二章 暫存器
一個典型的CPU以下幾部分組成:
- 運算器進行資訊處理;
- 暫存器進行資訊存盤;
- 控制器控制各個器件進行作業;
- 內部總線連接各種器件,在它們之間進行資料傳送;
對于一個匯編程式員來說,CPU中的主要部件是暫存器 —— 暫存器是CPU中程式員可以用指令讀寫的部件,程式員通過改變各種暫存器中的內容來實作對CPU的控制;
不同的CPU其暫存器的個數、結構互不相同,8086CPU有14個暫存器,我們在之后的課程中會依次介紹;
1.通用暫存器
8086(以后默認情況下的8086都是指8086CPU)的所有暫存器都是16bit,可以存放兩個位元組;
AX、BX、CX、DX這四個暫存器通常存放一般性的資料,稱為通用暫存器;
AX通用暫存器的邏輯結構如下所示:

因為8086之前的暫存器都是8bit的,為了能夠兼容,所以8086這四個通用暫存器都可以分為兩個獨立的8bit暫存器使用;
-
AX可分為AH和AL;
-
BX可分為BH和BL;
-
CX可分為CH和CL;
-
DX可分為DH和DL;
將AX分為兩個8位暫存器情況如下,AX的低8位構成AL暫存器,高8位構成AH暫存器

8086可以一次性處理以下兩種尺寸的資料:
- 位元組:byte,1byte=8bit;
- 字:word,1word=2byte,這兩個位元組分別稱為這個字的高位位元組和低位位元組(千萬注意不是所有的CPU中的1word=2byte,字的大小取決于具體系統的總線寬度,16位微機8086中是1word=2byte)

我們先簡單介紹幾潭訓編指令以及其如何控制CPU進行作業(這里并不是指匯編指令只能控制CPU,匯編語言幾乎可以直接控制、訪問各種硬體設備)

2.物理地址
8086是16位機,也可以說8086是16位結構的CPU,16位結構描述了一個CPU具有下面幾方面特性:
-
運算器一次最多可以處理16位的資料;
-
暫存器的最大寬度為16位;
-
暫存器和運算器之間的通路為16位;
8086有20位地址線,但8086是16位結構,如果簡單的發送地址只能送16位,8086采用內部合成的方法將兩個16位地址形成一個20位地址

當8086CPU要讀寫記憶體時:
(1)CPU中的相關部件提供兩個16位的地址,一個稱為段地址,另一個稱為偏移地址;
(2)段地址和偏移地址通過內部總線送入一個稱為地址加法器(物理地址=段地址*16+偏移地址)的部件;
(3)地址加法器將兩個16位地址合成為一個20位的物理地址;
(4)地址加法器通過內部總線將20位物理地址送入輸入輸出控制電路;
(5)輸入輸出控制電路將20位物理地址送上地址總線;
(6)20位物理地址被地址總線傳送到存盤器;
段地址*16的意義是左移4位二進制位
段地址中的段不是指記憶體被劃分成一個個的段,段的劃分來自于CPU,CPU使用分段的方式來管理記憶體

在編程中根據需要,可以將若干連續記憶體單元看作一個段:
- 段地址*16定位段的起始地址 —— 段的起始地址必須是16整數倍;
- 偏移地址定位段中的記憶體單元 —— 一個段最大長度只能是64KB(針對16位偏移地址而言);
3.CS:IP暫存器
8086在訪問記憶體時需要由相關部件提供記憶體單元的段地址和偏移地址,其中段地址存放在段暫存器中,8086由CS、DS、SS和ES四個段暫存器提供段地址;
CS是代碼段暫存器,IP為指令指標暫存器,CS和IP指示了CPU當前要讀取指令的地址,8086CPU將從記憶體CS*16+IP單元開始,讀取一條指令并執行 —— 即任意時刻,8086CPU將CS:IP指向的內容當作指令執行
8086作業程序簡述如下:
(1)從CS:IP指向的記憶體單元讀取指令,讀取的指令進入指令緩沖器;
(2)IP=IP+所讀取指令的長度,從而指向下一條指令;
(3)執行指令,轉到步驟(1),重復這個程序;
8086CPU加電啟動或復位后,CS和IP被初始化為CS=FFFFH,IP=0000H,即CPU從記憶體FFFF0H單元中讀取指令并執行;
8086大部分暫存器的值可以使用mov指令來修改,mov指令稱為傳送指令;但是要改變CS、IP內容的指令被統稱為轉移指令,顯然mov不可行,我們這里介紹一個簡單的轉移指令:jmp;
“jmp 段地址:偏移地址”指令的功能為:用指令中的段地址和偏移地址修改CS和IP中的值;
“jmp 某合法暫存器”指令的功能為:用暫存器中的值修改IP中的值;
代碼段的定義:當我們將一組記憶體單元定義為一個段且將一段代碼存放在該段中,我們稱其為代碼段,如何使得代碼段中的指令被執行呢?只需要將這段代碼的首地址傳輸給CS:IP指向即可;
資料段的定義:同理,我們可以將一組連續的記憶體單元當作專門存盤資料的記憶體空間,從而定義了一個資料段,訪問資料段時,只需要用DS存放資料段的段地址,再根據相關指令訪問資料段中的具體單元即可;
注意:匯編源程式中,資料不能以字母開頭,需要在前面加0,如9138H可以直接寫為“9138H”,但是A000H在匯編源程式中只能寫成“0A000H”
4.DS和[address]
字單元簡單來說就是存放一個字型資料(16bit)的記憶體單元,由兩個地址連續的記憶體單元組成,字的低位位元組存放在低地址單元中,高位位元組存放在高地址單元中,之后我們會將起始地址為N的字單元簡稱為N地址字單元;
DS暫存器用于存放要訪問的資料的段地址,指令中的“[]”說明操作物件是一個記憶體單元,該記憶體單元=DS中的段地址+[]中的偏移地址;
mov指令訪問記憶體單元時,可以只在mov指令中給出單元的偏移地址,此時段地址默認在DS暫存器中;
push、pop指令中可以只給出偏移地址,段地址會在指令執行時CPU自動從DS取得;
[address]表示一個偏移地址為address的記憶體單元;
5.mov、add、sub指令
mov、add、sub指令都有兩個操作物件,mov指令可以有如下形式:

注意,在8086中隨意向一段記憶體空間寫入內容是很危險的(不只是針對8086,應該養成良好的編程習慣),在不確定一段記憶體空間中是否存放重要的資料或代碼的時候不能隨意向其中寫入內容;
---注意了,很多人肯定會疑惑,我們編程的時候也沒注意這么多啊?那是因為我們之前的編程一直都是在作業系統中安全、規矩的編程,使用的是作業系統分配給我們的空間;但是當我們需要直接對硬體進行編程可就是自由、直接地用匯編語言去操作真實的硬體,稍微有差錯都會導致死機等(但事實上在現在的Windows上不理會作業系統直接使用匯編語言操作硬體是不可能的,硬體已經被作業系統完全保護起來了)
在作業系統的環境中,合法地通過作業系統取得的空間都是安全的,在作業系統允許的情況下,程式可以取得任意容量的空間;
add和sub可以有如下幾種形式:

6.堆疊&堆疊段
堆疊是一種具有特殊訪問方式的存盤空間 —— 最后進入這個空間的資料最先出去;
如今的CPU都有堆疊機制,8086提供相應的指令以堆疊的方式訪問記憶體空間,也就是說基于8086編程時可以將一段記憶體當作堆疊來使用;
push ax;將暫存器ax中的資料送入堆疊中
pop ax;從堆疊頂取出資料送入ax
8086的入堆疊和出堆疊操作都是以字為單位進行;
關于CPU如何知道堆疊頂的位置,有相應的暫存器來存放堆疊頂的地址 —— 段暫存器SS和暫存器SP,堆疊頂的段地址存放在SS中,偏移地址存放在SP中;
任意時刻,SS:SP指向堆疊頂元素;
從下圖我們可以看出,入堆疊時,8086的堆疊頂從高地址向低地址方向增長;

同樣的pop操作與push操作剛好相反

注意,出堆疊后,SS:SP指向新的堆疊頂,但是2266H實際還是存在,但是此時它不屬于堆疊,再次執行push操作的時候這個資料將會被覆寫;
pop和push實質上都是記憶體傳送指令,可以在暫存器和記憶體之間傳送資料;
pop和push指令與mov指令不同之處在于push/pop訪問的記憶體單元地址不是在指令中給出,而是由SS:SP指出,且push和pop還會改變SP中的內容;
當堆疊滿的時候使用push指令入堆疊或者堆疊空的時候使用pop指令出堆疊都會發生堆疊頂超界的問題,這是很危險的事(一般來說堆疊內和堆疊外的資料不屬于同一個程式,這就會導致其他程式的資料被修改)
當然8086并沒有做出對此的解決方法,這就需要我們自己在編程的時候避免出現堆疊頂超界的問題;
仿照前面的定義,可以將長度為N(N<=64KB)的一組連續地址且起始地址為16倍數的記憶體單元定義為一個段,我們將這個段作為堆疊空間來使用從而定義了一個堆疊段(類比代碼段、資料段),以堆疊的方式進行訪問;
當然人為定義的堆疊段CPU是并不知道的,需要使用SS:SP指向我們定義的堆疊段,這樣就可以使得push、pop等堆疊操作指定訪問我們定義的堆疊段;
段總結

一段記憶體,可以既是代碼的存盤空間又是資料的存盤空間還可以是堆疊空間,也可以什么都不是,關鍵在于CPU中暫存器的設定(即CS IP SS SP DS的指向)
第三章 匯編源程式
匯編源程式是后綴名為.asm的檔案,需要經過編譯器編譯成.exe或者.com檔案才能執行,本章我們主要介紹一個完整的匯編源程式的框架;
1.源程式
assume cs:codesg
codesg segment
mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax
mov ax,4c00H
int 21H
codesg ends
end
匯編語言程式中主要包含兩種指令,一種是匯編指令,另一種是偽指令:
- 匯編指令編譯為機器代碼最終被CPU執行;
- 偽指令沒有對應的機器指令,編譯器根據偽指令進行相關的編譯作業
我們很容易看出來上述代碼中出現的偽指令:
(1)segment和ends是一對成對使用的偽指令,功能是定義一個段,該段的名稱需要標識,具體使用格式如下
段名 segment
...
段名 ends
一個匯編程式是由多個段組成的,這些段被用來存放代碼、資料或者當作堆疊空間使用:一個源程式中所有會被計算機處理的資訊:指令、資料、堆疊都被劃分到了不同的段中;
一個有意義的匯編程式至少要有一個代碼段用于存放代碼;
(2)end是一個匯編程式結束的標記,編譯器碰到了偽指令end則結束對源程式的編譯;
(3)assume的功能是假設某段暫存器和程式中的某一個segment...ends定義的段相關聯,如上面的代碼中我們使用assume cs:codesg將用作代碼段的codesg段和CPU中的段暫存器cs關聯
在匯編語言中,將源程式檔案中所有的內容稱為源程式,將源程式中最終由計算機執行、處理的指令或資料稱為程式;
源程式經過編譯、連接后成為機器碼,存盤在可執行檔案(可執行檔案由描述資訊和程式組成,程式源自源程式中的匯編指令和定義的資料,描述資訊主要是編譯、連接程式對源程式中相關偽指令進行處理得到的資訊)中,故我們現在討論可執行檔案的執行機制;
一個程式P2在可執行檔案中,則必須有一個正在運行的程式P1,將P2從可執行檔案中加載入記憶體后,將CPU的控制權交給P2,P2才能得以運行,P2開始運行后,Pl暫停運行,
而當P2運行完畢后,應該將CPU的控制權交還給使它得以運行的程式P1,此后,P1繼續運行,
現在,我們知道,一個程式結束后,將CPU的控制權交還給使它得以運行的程式,我們稱這個程序為:程式回傳,那么,如何回傳呢?應該在程式的末尾添加回傳的程式段(我們會在后面詳細介紹),
2.[BX]和loop指令
之后的課程中我們為了方便描述,使用"()"表示一個暫存器或一個記憶體單元(當然是物理地址)中的內容,(ax)表示ax中的內容,(al)表示al中的內容,(20000H)表示記憶體20000H單元中內容;
"()"中的元素可以有3種型別:
- 暫存器名
- 段暫存器名
- 記憶體單元的物理地址(20bit資料)
"(X)"表示的資料有兩種型別:
- 位元組
- 字
具體型別由暫存器名或具體的運算決定
還需要補充的是,之后我們使用idata表示常量,帶有idata的指令都是非法指令只能用于我們學習,比如mov ds,idata表示mov ds,1,mov ds,2等
2.1 [BX]
我們知道,要完整描述一個記憶體單元需要兩個資訊:
- 記憶體單元的地址;
- 記憶體單元的長度;
[BX]類似于[0],我們知道用[0]表示記憶體單元時,其偏移地址是0,段地址默認在DS中,單元的長度可由具體的指令中的其他操作物件(暫存器)指出;
[BX]表示一個記憶體單元,其偏移地址在BX中;
2.2 loop指令
loop指令顧名思義與回圈相關,其格式如下
loop 標號
通常情況下,我們使用loop指令實作回圈功能,在cx暫存器中存放回圈次數
CPU執行loop指令的時候進行如下兩步操作:
- (cx)=(cx)-1
- 判斷cx中的值,若不為0則轉至標號處執行程式,若為0則向下執行
下面我們給出一段源程式以計算212
assume cs:code
code segment
mov ax,2
mov cx,11
s:add ax,ax
loop s
mov ax,4c00H
int 21H
code ends
end
首先是標號s,匯編程式的標號代表一個地址,該地址存在一條指令:add ax,ax(當然這里的指令代碼是我們自己撰寫的)
當在回圈程序中,偏移地址需要遞增的時候,表示記憶體單元偏移地址的X應該是一個變數 —— 我們可以將偏移地址放在BX中,用[BX]的方式訪問記憶體單元,在回圈開始之前設(BX)=0,每次回圈將BX中的值加1即可
3.段前綴
指令“mov ax,[bx]”中,記憶體單元的偏移地址由bx給出,而段地址默認在ds中,我們可以在訪問記憶體單元的指令中顯式地給出記憶體單元的段地址所在的段暫存器
mov ax,ds:[bx] ;將記憶體單元中內容送入ax,這個記憶體單元長度為2位元組,偏移地址在bx中,段地址在ds中
mov ax,cs:[bx] ;偏移地址在bx中,段地址在cs中
諸如上述出現在訪問記憶體單元的指令中且顯式指明記憶體單元的段地址的"ds:""cs:"稱為段前綴
4.多個段的源程式
assume cs:code
code segment
dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H ;定義字型資料,define word,此處定義了8個字型資料,每個資料占用兩位元組
mov bx,0
mov ax,0
mov cx,8
s:add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00H
int 21H
code ends
end
;匯編程式不需要使用分號表示一條陳述句的結束,匯編程式中的注釋以分號開頭
這個代碼段存在一個問題就是將資料和代碼等全部塞在一個代碼段中,這就導致這個程式只有一個代碼段,在該代碼段中前面16個位元組是使用dw定義的字型資料,從第16個位元組開始才是匯編指令對應的機器碼;
這樣的問題就是我們只能使用debug來執行程式,因為程式的入口處不是我們希望執行的指令,我們可以直接在源程式中指明程式的入口所在
assume cs:code
code segment
dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
start:
mov bx,0
mov ax,0
mov cx,8
s:add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00H
int 21H
code ends
end start
;start這個標號在程式的第一條指令前面出現,接著在end后出現
;end除了通知編譯器程式結束,還可以通知編譯器程式的入口在什么地方
接下來我們開始使用堆疊,下面的代碼用于解決利用堆疊將程式中定義的資料逆序存放
assume cs:code
code segment
dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ;定義16個字型資料,程式加載后將取得這16個字的記憶體空間用于存放這16個資料,在后面的程式中可以將這段空間作為堆疊使用(16應該是為了避免溢位)-可以說是開辟空間
start:
mov ax,cs
mov ss,ax
mov sp,30H ;設定cs:10~cs:2F的記憶體空間作為堆疊來使用,初始狀態下堆疊為空故ss:sp指向堆疊底cs:30
mov bx,0
mov cx,8
s: push cs:[bx]
add bx,2
loop s ;將以上代碼段0~15單元中的8個字型資料依次入堆疊
mov bx,0
mov cx,8
s0: pop cs:[bx]
add bx,2
loop s0 ;將以上8個字型資料依次出堆疊
mov ax,4c00H
int 21H
code ends
end start
把資料、代碼、堆疊放在一個段中會出現以下問題:
- 一個段中顯得程式很混亂;
- 一個段不能超過64KB,如果光是資料大小都超過64KB則不允許(當然這只是8086的限制)
下面這個程式實作了和上述程式相同的功能,但是它將資料、堆疊和代碼放在了不同的段中
assume cs:code,ds:data,ss:stack ;對于不同的段,要有不同的段名,當然assume只是偽指令,CPU并不能根據assume的系結知道code data stack分別是什么段,也就不知道該如何處理
data segment
dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
data ends
stack segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ;定義16個字型資料,程式加載后將取得這16個字的記憶體空間用于存放這16個資料,在后面的程式中可以將這段空間作為堆疊使用(16應該是為了避免溢位)-可以說是開辟空間
stack ends
code segment
start:
mov ax,stack
mov ss,ax ;設定ss指向stack
mov sp,20H ;設定堆疊頂ss:sp指向stack:20
;CPU執行上述指令后將把stack段作為堆疊空間使用
mov ax,data ;將名稱為data段的段地址送入ax
mov ds,ax ;ds指向data段
mov bx,0 ;ds:bx指向data段中第一個單元
mov cx,8
s: push [bx]
add bx,2
loop s ;將以上代碼段0~15單元中的8個字型資料依次入堆疊
mov bx,0
mov cx,8
s0: pop [bx]
add bx,2
loop s0 ;將以上8個字型資料依次出堆疊
mov ax,4c00H
int 21H
code ends
end start
多個段的情況下如何訪問段中的資料呢?還是和以前一樣通過地址,地址分為兩部分:段地址和偏移地址 —— 程式中段名相當于一個標號,代表了段地址,偏移地址則需要看段中資料在段中的位置
;將程式中的data段中的資料0abcH(地址為data:6)送入bx中
mov ax,data
mov ds,ax
mov bx,ds:[6]
;注意不可以使用這樣的指令
mov ds,data ;不能直接把一個數值送入段暫存器中,此處的data在編譯的時候是會被處理成為一個表示段地址的數值(這個概念在書上前面應該已經講過了,但是那個時候沒有引起重視,但是畢竟是初學所以沒必要嚴格要求每個概念點一字不差的記錄下來,對于一些忽略的自行Google也可以)
mov bx,ds:[6]
我們只能通過以下方式將資料加載到段暫存器中:首先將資料加載到通用暫存器中,然后將其從通用暫存器移到段暫存器中;
大膽猜測一下不能這么做的原因在于工程師根本沒有創建一條可以將信號從存盤器I/O資料線饋送到段暫存器的電路路徑
第四章 定位記憶體地址
之所以需要專門介紹尋址方式,是因為合理的使用尋址方式可以設計更合理的結構來看待我們需要處理的資料,而為需要處理的資料設計一種清晰的資料結構是程式設計的一個關鍵問題;
1.and和or指令
and指令:邏輯與指令,按位進行與運算
mov al,01100011B
and al,00111011B
;執行過后al=00100011B
or指令:邏輯或指令,按位進行或運算
mov al,01100011B
or al,00111011B
;執行過后al=01111011B
2.[BX+idata]
前面已經介紹過,使用[BX]指明一個記憶體單元,這里我們給出一種更加靈活的方式指明記憶體單元:[BX+idata],表示該記憶體單元的偏移地址為(BX)+idata;
之所以介紹這種表示記憶體單元的方式,是為了引出陣列這一資料結構;
假設此時datasg段中有兩個字串,一個起始地址為0,另一個起始地址為5,我們可以將這兩個字串看作兩個陣列,一個從0地址開始存放另一個從5開始存放,我們可以使用[0+BX]和[5+BX]的方式在同一個回圈中定位這兩個字串中的字符;
3.尋址方式
SI和DI是8086中與BX功能相近的暫存器,但是SI和DI不能分成兩個8位暫存器來使用;
引入這兩個暫存器之后我們可以使用更加靈活的方式來指明一個記憶體單元:[BX+SI]或[BX+DI],以前者為例,[BX+SI]表示一個記憶體單元,其偏移地址為(BX)+(SI);
我們甚至還可以這樣表示一個記憶體單元:[BX+SI+idata],其偏移地址為(BX)+(SI)+idata;
我們這里總結前面介紹過的幾種定位記憶體地址的方法(即尋址方式)

下面我們討論一個問題,程式中需要經常進行資料的暫存,這些資料可能是在暫存器中的,也可能是在記憶體中的,但是使用暫存器一定不是一個好的方法(因為暫存器的數量是有限的),所以此時就只能選擇記憶體(將需要暫存的資料放在記憶體單元中,需要使用的時候從記憶體單元中恢復),however,在保存多個資料的時候我們需要記住資料保存在了哪個單元中,非常麻煩;
綜上,我們需要使用記憶體來暫存資料,且需要使用堆疊結構使程式結構清晰
當資料存放在記憶體中的時候,我們稱定位記憶體單元的方法為尋址,前面已經介紹過不少尋址方式,我們這里做一個總結

4.資料處理
首先我們定義描述性的符號:reg和sreg,其中reg表示一個暫存器,sreg表示一個段暫存器

-
8086中只有BX、SI、DI和BP可以用在[...]中進行記憶體單元的尋址,這四個暫存器可以單獨出現,也可以以四種組合出現:
- BX+SI
- BX+DI
- BP+SI
- BP+DI:只要在[...]中包含暫存器BP但沒有顯式給出段地址,則段地址默認在SS中
-
8086中絕大部分的機器指令都是進行
資料處理的指令,資料處理大致可以分為三類:讀取、寫入和運算;機器指令并不會關心資料的值是多少,只關心指令執行的前一刻它將要處理的資料所在的位置,可能在如下三個地方:CPU內部、記憶體和埠; -
上面已經說到資料可能所在的位置,那8086如何表示資料所在的這些位置?主要有三種表示方法:
- 立即數:對于直接包含在機器指令中的資料稱為立即數,在匯編指令中直接給出這些立即數即可
mov ax,1 add bx,2000H- 暫存器:指令要處理的資料在暫存器中,因此在匯編指令中我們需要給出相應的暫存器的名稱
mov ax,bx mov ds,ax- 段地址(SA)和偏移地址(EA):指令要處理的資料在記憶體中,在匯編指令中使用我們前面介紹過的方式給出EA和SA在某個段暫存器中
;存放段地址的暫存器可以是默認的 mov ax,[0] ;段地址默認在DS中 mov ax,[bp];段地址默認在SS中 ;當然存放段地址的暫存器可以是顯式給出的 mov ax,ds:[bp] ;SA=ds,EA=bp mov ax,cs:[BX+SI+8] ;SA=CS,EA=BX+SI+8
前面說過8086的指令可以處理兩種尺寸的資料 —— byte和word,因此我們需要在機器指令中指明指令進行的是字操作還是位元組操作
主要有兩種方法可以進行判別:
- 根據暫存器名進行處理,ax表示十六位字操作,al表示八位位元組操作;
mov ax,1
mov al,1
- 當然暫存器名并不是萬能的,很多機器指令并不存在暫存器名,這種情況下使用運算子X ptr指明記憶體單元的長度,此處X在匯編語言中可以是word或byte
mov word ptr ds:[0],1 ;word ptr指明指令訪問的記憶體單元是一個字單元
mov byte ptr ds:[0],1 ;byte ptr指明指令訪問的記憶體單元是一個位元組單元
不要覺得顯式指定需要訪問的記憶體單元的長度很雞肋(至少我們在前面一直都沒有提過這個問題),這很可能導致某些指令進行記憶體單元的修改的時候處理一個或兩個單元的內容
- 當然還有些指令只能進行字操作或位元組操作,push只能進行字操作
4.1 div指令
div是除法指令,使用的時候需要注意:
- 除數:分8bit和16bit兩種,在一個reg暫存器或記憶體單元中
- 被除數:默認放在AX或DX和AX中:
- 除數8bit,被除數16bit,默認在AX中存放;
- 除數為16bit,被除數為32bit,在DX和AX中存放,DX存放高16bit,AX存放低16bit;
- 商:
- 除數為8bit,AL存盤除法操作的商,AH存盤除法操作的余數;
- 除數為16bit,AX存盤除法操作的商,DX存盤出發操作的余數;
div byte ptr ds:[0]
;意味著(al)=(ax)/((ds)*16+0)的商
;(ah)=(ax)/((ds)*16+0)的余數
4.2 偽指令dd
前面我們分別使用db和dw來定義位元組型資料和字型資料,而dd用來定義dword雙字型資料,也就是占用兩個字的資料
4.3 dup運算子
dup與dd dw db同樣是被編譯器處理的符號,主要用于配合資料定義偽指令實作資料重復
db 3 dup(0)
;定義3個位元組,值都是0,相當于db 0,0,0
db 3 dup(0,1,2)
;定義9個位元組,相當于db 0,1,2,0,1,2,0,1,2
可見dup的指令格式如下
db 重復次數 dup(需要重復的位元組型資料)
dw 重復次數 dup(需要重復的字資料)
dd 重復次數 dup(需要重復的雙字資料)
第五章 轉移指令
前面介紹的mov等大部分指令都是傳送指令,我們定義可以修改IP或可以同時修改CS和IP的指令統稱為轉移指令,即轉移指令是可以控制CPU執行記憶體中某處代碼的指令;
8086的轉移行為有以下幾類:
- 只修改IP,稱為段內轉移,如jmp ax;根據轉移指令對IP
- 短轉移IP的修改范圍-128~127
- 近轉移IP的修改范圍-32768~32767
- 同時修改CS和IP,稱為段間轉移,如jmp 1000:0;
8086轉移指令主要分為以下幾類:
-
無條件轉移指令
-
條件轉移指令
-
回圈指令
-
程序
-
中斷
這些轉移指令轉移前提可能不同,但轉移的基本原理相同,這里主要通過學習無條件轉移指令jmp來理解CPU執行轉移指令的基本原理
1.offset運算子
offset的功能是取得標號的偏移地址
assume cs:codesg
codesg segment
start:mov ax,offset start ;等同于mov ax,0,start是代碼段中的標號,標記的指令是代碼段中的第一條指令,偏移地址為0
s:mov ax,offset s;等同于mov ax,3,s標記的指令是代碼段中第二條指令,因為第一條指令長度為3位元組,則s偏移地址為3
2.jmp指令
無條件轉移指令,可以只修改IP,也可以同時修改CS和IP
jmp指令需要的給出兩種資訊:
-
轉移的目的地址
-
轉移的距離
不同的資訊對應了不同的jmp指令格式
2.1 位移轉移
jmp short 標號
實作段內短轉移,指令中的short符號說明指令進行的是短轉移,標號是指代碼段中的標號,指明了指令要轉移的目的地 —— 轉移指令結束后,CS:IP應當指向標號處的指令
assume cs:codesg
codesg segment
start:mov ax,0
jmp short s ;執行該指令過后,跳過了add ax,1,執行s處的指令,因此最終ax的值為1而不是2
add ax,1
s:inc ax
codesg ends
ens start
CPU在執行jmp指令的時候并不需要轉移的目的地址,這就意味著CPU不需要目的地址就可以實作對IP的修改
轉移指令并沒有告訴CPU要轉移的目的地址,卻告訴了CPU要轉移的位移(比如“將當前的IP向后移動3個位元組”)

2.2 目的轉移
前面的jmp指令其對應的機器指令中并沒有轉移的目的地址,僅是相對于當前IP的轉移位移
jmp far ptr 標號
;(CS)=標號所在段的段地址
;(IP)=標號在段中的偏移地址
;far ptr指明了該指令使用標號的段地址和偏移地址來修改CS和IP
;功能:這條指令實作的是段間轉移,又稱為遠轉移,轉移的目的地址在指令中
jmp 16位reg
;功能:(IP)=(16位reg),表明轉移地址在暫存器中
下面我們再介紹兩種格式的轉移地址在記憶體中的jmp指令
第一種是段內轉移
jmp word ptr 記憶體單元地址
;功能:從記憶體單元地址處存放一個字,存放的是轉移的目的偏移地址
記憶體單元地址可以使用任一尋址方式給出
mov ax,0123H
mov ds:[0],ax
jmp word ptr ds:[0]
;執行后的(IP)=0123H
第二種是段間轉移
jmp dword ptr 記憶體單元地址
;功能:從記憶體單元地址處存放兩個字,高地址的字是轉移目的段地址,低地址的字是轉移目的偏移地址
;(CS)=(記憶體單元地址+2)
;(IP)=(記憶體單元地址)
記憶體單元地址可以用尋址方式的任一格式給出
mov ax,0123H
mov ds:[0],ax
mov word ptr ds:[2],0
jmp dword ptr ds:[0]
;執行過后(CS)=0,(IP)=0123H,CS:IP指向0000:0123
3.jcxz指令
jcxz指令是有條件轉移指令,所有的有條件轉移指令都是短轉移,在對應的機器碼中包含轉移的位移而不是目的地址,且對IP的修改范圍都是-128~127
jcxz 標號
;功能:若(CX)=0則轉移到標號處執行操作,若(CX)!=0則怎么也不做,程式向下執行
;操作:當(CX)=0時,(IP)=(IP)+8bit位移
上述8bit位移計算方法為

4.loop指令
loop指令為回圈指令,所有的回圈指令都是短轉移,在對應的機器碼中包含轉移的位移而不是目的地址,對IP的修改范圍為-128~127
loop 標號
;功能:首先計算(CX)=(CX)-1,如果(CX)!=0則(IP)=(IP)+8位位移,如果(CX)=0則什么也不做,程式向下執行
上述8bit位移計算方法為

5.ret和retf指令
ret指令使用堆疊中的資料修改IP中的內容,實作近轉移;
retf指令使用堆疊中的資料修改CS和IP中的內容,實作遠轉移;
簡單來說CPU執行ret指令的時候相當于進行pop IP,CPU執行retf指令時相當于進行pop IP pop CS
6.call指令
call指令不能實作短轉移,除此之外,call指令實作轉移的方法和jmp指令原理幾乎完全相同;
CPU執行call指令的時候進行兩步操作:
- 將當前的IP或CS和IP壓堆疊;
- 轉移;
因為我們這里是速刷匯編語言,所以針對這種指令的具體用法我們就暫且不再細講,需要的時候自行Google或看書;還有就是一些不是很重要的指令比如mul之類的我們也省略不講(因為已經匯編語言已經差不多接觸兩天了,不能再耗費更多的時間在上面了)
第六章 標志暫存器
CPU內部的暫存器中存在一種特殊的暫存器,一般來說有以下作用:
- 存盤相關指令的某些執行結果;
- 為CPU執行相關指令提供行為依據;
- 控制CPU的相關作業方式;
這樣的特殊暫存器在8086中被稱為標志暫存器,8086中的標志暫存器有16位,其中存盤的資訊通常被稱為程式狀態字PSW;
我們簡稱8086中的標志暫存器為flag,flag和其他暫存器不一樣,其他暫存器是使用整個暫存器來存放資料的,而flag暫存器按位起作用,也就是說flag暫存器的每一位都有特殊的含義、記錄特定的資訊;
8086中的flag暫存器結構如圖

其中的1、3、5、12、13、14、15在8086中并沒有使用,不具備任何含義;
1.ZF標志
flag的第六位ZF零標志位,記錄相關指令執行后其結果是否為0:
- 結果為0則ZF=1;
- 結果不為0則ZF=0;
2.PF標志
flag的第二位是PF奇偶標志位,記錄相關指令執行后其結果中的所有bit位中的1的個數是否為偶數:
- 如果1的個數為偶數則PF=1;
- 如果為奇數則PF=0;
3.SF標志
flag的第七位是SF符號標志位,記錄相關指令執行后其結果是否為負:
- 結果為負則SF=1;
- 結果非負則SF=0;
SF標志就是CPU對有符號數運算結果的一種記錄,它記錄資料的正負:
- 將資料作為有符號數來運算的時候通過SF標志得知結果的正負;
- 將資料作為無符號數來運算的時候SF的值沒有意義;
4.CF標志
flag的第零位是CF進位標志位,在進行無符號數運算的時候,它記錄了運算結果的最高有效位向更高位的進位值,或者從更高位的錯位值;
對于位數為N的無符號數來說,其對應的二進制資訊的最高位(N-1)就是它的最高有效位

5.OF標志
在進行有符號數運算的時候,如果結果超過了機器所能表示的范圍則稱為溢位,那么運算的結果將會不正確,CPU需要對指令執行后是否產生溢位進行記錄;
flag的第十一位是OF溢位標志位,OF記錄了有符號數的運算結果是否發生了溢位:
- 發生溢位則OF=1;
- 未發生溢位則OF=0;
CF是對無符號數運算有意義的標志位,OF是對有符號數運算有意義的標志位(雖然我也不知道為什么把無符號數和進位系結,把有符號數和溢位系結)
6.DF標志
flag的第十位是DF方向標志位,在串處理指令中控制每次操作后SI和DI的增減:
- DF=0則每次操作后SI和DI遞增;
- DF=1則每次操作后SI和DI遞減;
第七章 內中斷
中斷資訊可以來自CPU的內部和外部,這一章我們主要討論來自CPU內部的中斷資訊;
關于內中斷和外中斷我們在作業系統計算機作業系統 - Tintoki_blog (gintoki-jpg.github.io)或計算機組成原理計組期末復習筆記 - Tintoki_blog (gintoki-jpg.github.io)中有詳細介紹,此處不再贅述;
對于8086,當CPU內部出現如下情況將產生相應的中斷資訊:
- 除法錯誤(除法溢位等)
- 單步執行;
- 執行into指令;
- 執行int指令;
要對這四種不同型別的資訊進行處理,8086需要先知道接收到的中斷資訊的來源,因此中斷資訊中需要包含識別來源的編碼——稱為中斷型別碼,是一個位元組型資料,可以表示256種中斷資訊的來源,上面介紹的四種中斷源在8086中的中斷型別碼如下:
- 除法錯誤:0
- 單步執行:1
- into指令:4
- int指令:該指令的格式為
int n,指令中的n為位元組型立即數,是提供給CPU的中斷型別碼;
由我們編程寫的用于處理中斷資訊的程式被稱為中斷處理程式,CPU收到中斷資訊后需要轉去執行中斷處理程式,這就引出我們應該如何根據中斷資訊確定其處理程式的入口;
前面介紹的中斷資訊中的包含有標識中斷源的中斷型別碼,其作用就是定位中斷處理程式 —— 要定位中斷處理程式就需要知道它的段地址和偏移地址,如何根據8位中斷型別碼得到中斷處理程式的段地址和偏移地址呢?
1.中斷向量表
CPU使用8位中斷型別碼,通過中斷向量表找到相應的中斷處理程式的入口地址;
中斷向量表就是中斷向量的串列,中斷向量就是中斷處理程式的入口地址 —— 簡單來說中斷向量表就是中斷處理程式入口地址的串列;
中斷向量表保存在記憶體中,存放了256個中斷源對應的中斷程式的入口(事實上系統要處理的中斷事件遠沒有達到256個,中斷向量表中很多單元是空的),CPU只需要將中斷型別碼作為中斷向量表的表項號定位相應的表項,就可以得到中斷處理程式的入口地址;
中斷向量表中的一個表項存放一個中斷向量,這個中斷向量包括入口地址的段地址和偏移地址,故一個表項占兩個字:高地址字存放段地址,低地址字存放偏移地址;

那么下面的問題就是CPU如何找到中斷向量表 —— 對于8086來說中斷向量表被指定放在記憶體地址0處,記憶體0000:0000到0000:03FF這1024個單元中存放中斷向量(一般情況下0000:0200到0000:02FF的256個位元組的空間對應的中斷向量表表項都是空的);
2.中斷程序
CPU通過中斷型別碼找到中斷處理程式的入口,將CS:IP設定為該入口地址 —— 這個程序由CPU硬體自動完成,被稱為中斷程序;
CPU收到中斷資訊后,首先引發中斷程序,CS:IP指向中斷處理程式的入口之后,CPU開始執行中斷處理程式;
8086收到中斷資訊后引發的中斷程序如下:
(1)(從中斷資訊中)取得中斷型別碼;
(2)標志暫存器的值入堆疊(因為在中斷程序中要改變標志暫存器的值,所以先將其保存在堆疊中);(這一步書上說的就是標志暫存器 ,但是CS和IP的值理論上也是需要保存的-4、5步驟就是保存CS和IP的值便于CPU回頭繼續執行被中斷的程式)
(3)設定標志暫存器的第8位TF和第9位IF的值為0(這一步的目的后面將介紹);
(4)CS的內容入堆疊;
(5)IP的內容入堆疊;
(6)從記憶體地址為中斷型別碼(* 4)和中斷型別碼(* 4+2)的兩個字單元中讀取中斷處理程式的入口地址設定IP和CS;
3.中斷處理程式
中斷處理程式的常規撰寫步驟如下:
- 保存用到的暫存器;
- 處理中斷;
- 恢復用到的暫存器;
- 使用iret指令回傳:iret指令通常和硬體實作的中斷程序配合使用,中斷程序中暫存器入堆疊的順序為標志暫存器、CS、IP,iret指令的出堆疊順序是IP、CS、標志暫存器,實作了用執行中斷程式前的CPU現場恢復標志暫存器以及CS和IP的作業;
4.單步中斷
一般地,CPU執行完一條指令后,檢測標志暫存器的TF位若為1則產生單步中斷(型別碼為1)進而引發如下中斷程序:
- 取得中斷型別碼1;
- 標志暫存器入堆疊,TF、IF設定為0;
- CS、IP入堆疊
- (IP)=(1*4),CS=(1 *4+2)
CPU提供這樣的功能,一個直觀的例子就是Debug讓CPU執行一條指令后就顯示各個暫存器的狀態 —— 即Debug提供了單步中斷的中斷處理程式,其功能為顯式所有暫存器中的內容后等待輸入命令;
當然在進入中斷處理程式之前需要設定TF=0,避免CPU在執行中斷處理程式的時候發生單步中斷;
CPU提供單步中斷的原因就是為單步跟蹤程式的執行程序提供了實作機制;
5.int指令
;int指令的格式為 int n,其中n為中斷型別碼,其功能是引發中斷程序
CPU執行int n指令相當于引發一個n號中斷的中斷程序(即可以在程式中使用int指令呼叫任何一個中斷的中斷處理程序),執行程序如下:
- 取出中斷型別碼n
- 標志暫存器入堆疊,IF=0,TF=0
- CS、IP入堆疊
- (IP)=(n *4),(CS)=(n *4+2)
一般情況下,系統將一些具有一定功能的子程式,以中斷處理程式的方式提供給應用程式呼叫,當我們編程的時候可以使用int指令呼叫這些子程式(當然也可以自己撰寫一些中斷處理程式,我們稱之為中斷例程)
BIOS和DOS提供了一些常用中斷例程,系統板的ROM中存放著一條程式BIOS(基本輸入輸出系統),主要包含以下內容:
- 硬體系統的檢測和初始化程式;
- 外中斷和內中斷的中斷例程;
- 用于硬體I/O操作的中斷例程;
- 其他與硬體相關的中斷例程;
作業系統DOS提供的中斷例程實際就是作業系統向程式員提供的編程資源;
程式員在編程的時候可以用int指令直接呼叫BIOS和DOS提供的中斷例程完成某些作業,和硬體相關的DOS中斷例程中一般都會呼叫BIOS的中斷例程;
我們給出BIOS和DOS提供的中斷例程裝載到記憶體中的實體(實際上很接近作業系統中的系統啟動前的一系列操作):

2022/9/22 11:08 到此為止我們的匯編語言的學習告一段落,之后我們將繼續學習作業系統和匯編原理,如果對匯編語言感興趣想要深入可自行學習;
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/529963.html
標籤:其他
