emu8086

emu8086

EMU8086是學習彙編必不可少的工具,它結合了一個先進的原始編輯器、組譯器、反組譯器、具除錯功能的軟體模擬工具(虛擬PC),還有一個循序漸進的指導工具。該軟體包含了學習彙編語言的全部內容。Emu8086集原始碼編輯器,彙編/反彙編工具以及可以運行debug的模擬器(虛擬機器)於一身,此外,還有循序漸進的教程。

基本介紹

  • 外文名:emu8086
  • 性質:工具
  • 釋義:學習彙編必不可少的工具
  • 特點:循序漸進的教程
軟體簡介,使用方法,如何運行,十進制系統,二進制系統,十六進制系統,十進制到另外進制的換算,有符號數,彙編語言,CPU內部,通用暫存器,段暫存器,定址方式,MOV 指令,數組,常量,中斷,運算與邏輯指令,程式控制轉移,堆疊,

軟體簡介

EMU8086是你學習彙編必不可少的工具!
emu8086
Emu8086-MicroprocessorEmulator結合了一個先進的原始編輯器、組譯器、反組譯器、具除錯功能的軟體模擬工具(虛擬PC),還有一個循序漸進的指導工具。這對剛開始學組合語言的人會是一個很有用的工具。它會在模擬器中一步一步的編譯程式碼並執行,視覺化的工作環境讓它更容易使用。你可以在程式執行當中檢視暫存器旗標以及記憶體。模擬器會在虛擬PC中執行程式,這可以隔絕你的程式,避免它去存取實際硬體,像硬碟、記憶體,而在虛擬機器上執行組合程式,這可以讓除錯變得更加容易。這個軟體完全相容於Intel的下一代處理器,包括了PentiumII、Pentium4,而相信Pentium5也會繼續支援8086的。這種現象讓8086程式碼的可攜性相當高,它可以同時在老機器以及現代的電腦是執行,8086的另一個優勢是它的指令比較小且相當容易學習。
該軟體包含了學習彙編語言的全部內容。Emu8086集原始碼編輯器,彙編/反彙編工具以及可以運行debug的模擬器(虛擬機器)於一身,此外,還有循序漸進的教程。這套軟體對於剛開始學習彙編語言的朋友非常有幫助.它能夠編譯原始碼,並在模擬器上一步一步的執行。可視化界面令操作易如反掌.可以在執行程式的同時可觀察暫存器,標誌位和記憶體.算術和邏輯運算單元(ALU)顯示中央處理器內部的工作情況.
這個模擬器是在一台"虛擬"的電腦上運行程式的,它擁有自己獨立的“硬體”,這樣你程式就同諸如硬碟與記憶體這樣的實際硬體完全隔離開,動態調試(DEBUG)時非常方便.8086的機器代碼同INTEL下一代微處理器完全兼容,包括Pentium II 和 Pentium 4,我相信 Pentium 5 同樣也會支持 8086指令.這意味著8086代碼具有很廣泛的套用範圍,它在老式的和最新的計算機系統上都能工作.
8086指令的另外一個優點是它的指令集非常小,這樣學起來會容易得多.Emu8086 同主流彙編程式相比,語法簡單得多,但是它能生成在任何能兼容8086機器語言的代碼。注意:如果你不使用Emu8086編譯程式,那你無法在運行的時候單步跟蹤。

使用方法

如何運行

1.在開始選單選在它的圖示,或者直接運行Emu8086.EXE2.在"FILE"選單中選擇"SAMPLE"
emu8086emu8086
3.點擊"Compile and Emulate"按紐(或者按快捷鍵F5)
4.點擊"Single Step"按紐(或者按快捷鍵F8),可以查看代碼如何運行.

十進制系統

目前使用最多的是十進制.十進制系統有10個數字0,1,2,3,4,5,6,7,8,9 利用這些數字能表示任何數值,例如754這些數字是由每一位數字乘以“基數”的冪累加而成的(上一個例子中基數是10 因為十進制中有十個數字)。
位置對於每一個數字是很重要的。例如,你將上一個例子中的“7”放到結尾:547
數值就成為:
特別提醒:任何數字的0次冪都是1,0的0次冪也是1

二進制系統

計算機沒有人類聰明(至少現在是這樣),製造一個只有開關或者稱為 0,1 兩種狀態的電子機器很容易。計算機使用二進制系統,只有兩個數字 0, 1基地為2每一位二進制數稱作一位(BIT),4 BIT 組成一個半位元組(NIBBLE),8BIT組成一個位元組(BYTE),兩個位元組組成一個字(WORD),兩個字組成一個雙字(DOUBLE WORD)(很少使用):
習慣上在一串二進制後面加上“b”,這樣,我們可以知道101b是二進制表示十進制的5。
二進制10100101b表示十進制的165,計算方法如下:

十六進制系統

十六進制系統使用16個數字0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F基底是 16. 十六進制非常緊湊,便於閱讀。將二進制轉換為十六進制很容易,半位元組(4bits) 對應一位十六進制如下表
Decimal (base 10) Binary (base 2) Hexadecimal (base 16)0 0000 0 1 0001 12 0010 2 3 0011 3 4 0100 4 5 0101 5 6 0110 6 7 0111 7 8 1000 8 9 1001 9 10 1010 A 11 1011 B 12 1100 C 13 1101 D 14 1110 E 15 1111 F
習慣上我們在一個十六進制數的後面加上 "H",以便和其他進制區別, 這樣我們就知道 5Fh是一個十六進制數表示十進制的 95。習慣上,我們也在以字母開頭(從A到F)的十六進制數前面 加上"0" 例如: 0E120h. 十六進制 1234h 等於 4660:

十進制到另外進制的換算

在換算中,將十進制數不斷除以目標進制的基底,每一次都要記錄下商和餘數,直到商0。 餘數用來表示結果。
下面是一個十進制39(基底是10)到十六進制(基底是16)的換算:
結果為 27H
上例中所有的餘數都小於10,不必使用字母。再舉一個更複雜的例子:十進制 43868 換算為十六進制:
結果是 0AB5Ch, 使用 上面提到的表 將大於9的數字替換成字母。 運用同樣的原理,我們可以換算為二進制(用2作除數),或者是先換算成十六進制,再用上面的表 換算成二進制:
於是,得到二進制: 1010101101011100b

有符號數

對於十六進制數 0FFh 無法確定它是正數還是負數,因為它可以表示十進制的"255" 或者  "- 1"。
8位可以表示256個狀態,於是,我們可以假定前128個表示正數(從0到127),接下來的128個數(從128到256)表示負數 。如果想表示"- 5",我們從256中減去5,即  256 - 5 = 251。用這種複雜的方法表示一個負數有著數學依據的,數學上"- 5" 加上 "5"等於0。當我們將兩個8位的數字 5 和 251相加時,結果超過255,溢出處理為0!
128到256高位始終是1,這個可以作為數字元號的標記 對於字(16位),16位有65536個狀態,頭32768個狀態(從0到32767)用來表示正數,下面的32768個狀態(從32767到65535) 表示負數
Emu8086 帶有數制轉換工具,也可以計算各種數值表達式。選擇選單 Math 項:
Number Convertor (數制轉換)可以實現任意數制之間的轉換。在文本框中填寫源
數值,將自動轉換到任意的數制。 可以作 8 位 或者 16 位轉換。Expression Evaluator(表達式計算)可以用來計算不同數制的計算以及從一個進制到另一個進制的轉換。輸入表達式,按下回車,結果就會以你選定的進制表示。最長可以進行32位的計算。當在Signed打鉤選中時(除了八進制和雙字),最前面的一位將被認作是符號位。這樣以來,0FFFFFFFFh 將被認為是十進制的 -1。例如,你計算 0FFFFh * 10h + 0FFFFh ( 8086 CPU所能訪問的最大記憶體地址)。如果你選中Signed 和 Word 選項,結果是 -17 (因為表達式被認為是 (-1) * 16 + (-1) )。如果想按照無符號數計算,請不要選擇 Signed 表達式為 65535 * 16 + 65535 計算結果將是1114095 同樣你可以使用Number Convertor將非十進制換算為有符號的十進制,然後根據十進制計算。支持如下運算:
~ not  (inverts all bits).
* multiply.
/ divide.
% modulus.
+ sum.
- subtract (and unary -).
<< shift left.
>> shift right.
& bitwise AND.
^ bitwise XOR.
| bitwise OR.
二進制必須有“b”作結尾,例如00011011b 十六進制必須有"h"作結尾,另外,當第一位是字母時,最前面必須加上0,例如:0ABCDh八進制必須有"o"作結尾,例如:77o

彙編語言

彙編語言是底層程式語言。為了學習這門語言,你需要對於計算機結構有所了解。計算機系統模型如下:
emu8086emu8086
系統匯流排 system bus(圖中黃色部分)是將計算機各個部分連線到一起的部件。CPU是計算機的心臟,大部分的運算都是在CPU中完成的。RAM是讀取並且存放將要執行的程式的地方。

CPU內部

通用暫存器

8086CPU有8個通用暫存器,每一個暫存器都有自己的名稱:
AX 累加暫存器 accumulator register(分為 AH / AL). BX 基址暫存器 base address register (分為 BH / BL). CX 計數暫存器 count register(分為 CH / CL ). DX 數據暫存器 data register (分為 DH / DL). SI 源變址暫存器 source index register. DI 目的變址暫存器 destination index register. BP 基址指針暫存器 base pointer. SP 堆疊暫存器 stack pointer.
編程中,由程式設計師決定通用暫存器的具體用途。暫存器的主要目 的是保存數值(變數)。上面提到的暫存器是16位的,意思是: 0011000000111001b (二進制),或者12345(十進制形式)。4個通用暫存器(AX, BX, CX, DX) 在使用時分為兩個8位暫存器,例如 假設AX= 0011000000111001b,AH=00110000b AL=00111001b。
當你修改其中任意8位值,整個16位暫存器的值同樣改變。同樣對於其他的3個暫存器,“H”表示高8位,“L”表示低8位。暫存器在CPU內部,訪問中它們速度遠遠超過記憶體。因為,訪問記憶體需要經過系統匯流排,所以時間要長一些。而訪問暫存器中的數據幾乎不需要時間。於是,編程中,應當儘量在暫存器中保存數據。雖然暫存器很小,並且這些暫存器都有具體用途,但他們依然是存放計算中臨時數據的好地方。

段暫存器

CS 代碼段暫存器,用來存放當前正在運行的指令 DS 數據段暫存器,用來存放當前運行程式所用的數據 ES 附加段暫存器,由程式設計師決定用途 SS 堆疊段暫存器,指出堆疊所在區域
儘管容許在段暫存器中存放任何數據,但是這決不是 一個好主意。段暫存器有著非常特別的目的--指出可以訪問記憶體塊的地址。段暫存器與通用暫存器協同工作就可以訪問任意的記憶體區域。例如,如果我們打算訪問物理地址是12345h(十六進制)的記憶體單元,我們應設定DS = 1230h SI = 0045h 這樣以來,我們便能訪問超過一個暫存器(16位)所能表示的記憶體地址的範圍。CPU計算物理地址的方法是將段暫存器乘以10H在加上一個特定的通用暫存器。(1230h * 10h + 45h = 12345h):
這種,由兩個暫存器生成的地址被稱為有效地址 (effective address)
默認下,BX, SI 及 DI 與 DS協同工作,BP SP 與 SS 暫存器協同工作。其餘的通用暫存器不能形成有效地址!同樣,儘管BX可以形成有效地址,但是BH BL不能!控制暫存IP 指令指針暫存器 instruction pointer 、Flags Register 狀態標誌暫存器
IP 始終同CS 協同工作,指出當前執行的指令。 Flags Register 完成一次數學運算後,由CPU自動修改,通過它可以得到當前結果類型,也可以作為跳轉語句條件。通常你無法直接訪問它們。

定址方式

我們可以通過下面的四個暫存器來定址 BX, SI, DI, BP.
通過計算[]符號中的值,我們可以訪問到不同記憶體單元的值。具體組合請看下表:
[BX + SI]
[BX + DI]
[BP + SI]
[BP + DI]
[SI]
[DI]
d16 (variable offset only)
[BX]
[BX + SI] + d8
[BX + DI] + d8
[BP + SI] + d8
[BP + DI] + d8
[SI] + d8
[DI] + d8
[BP] + d8
[BX] + d8
[BX + SI] + d16
[BX + DI] + d16
[BP + SI] + d16
[BP + DI] + d16
[SI] + d16
[DI] + d16
[BP] + d16
[BX] + d16
d8 - 表示8位偏移量 d16 - 表示16位偏移量
偏移量可以是一個立即數或者是一個變數的偏移,或者二者兼備。這取決於編譯器如何計算單獨的立即數。偏移量可以在[]符號裡面或者外面,這不影響編譯器生成相同的機器碼。偏移量是一個有符號數,可以是正數或者負數。一般說來,8位或者16位,對於編譯後的結果是有影響的。例如,假定 DS = 100, BX = 30, SI = 70。
如下定址方式 [BX + SI] + 25 計算物理地址為100 * 16 + 30 + 70 + 25 = 1725
默認下,DS 暫存器套用在除了BP暫存器之外的所有物理地址計算中,暫存器是和SS暫存器一起工作的。用過下面的表,你可以和輕鬆記住誰和誰是關聯在一起使用的。
上表中,你可以從每一列中選擇一個或者忽略任意一個列。比如,可以看到,BX 和 BP始終不會選到一起。SI 和 DI不會選到一起。這是一個計算地址模式[BX+5] 段暫存器(CS, DS, SS, ES) 中數值被稱作 "段偏移" 。目的暫存器(BX, SI, DI, BP) 中數值被稱作"偏移量"
比如,ds中數值為1234h,si中數值為7890h,可以記作 1234:7890 物理地址為 1234h * 10h + 7890h = 19BD0h在編譯過程中使用如下聲明數據類型
BYTE PTR - 表示位元組 ; WORD PTR - 表示字(2個位元組)
例如:BYTE PTR [BX] ;按位元組訪問 ; WORD PTR [BX] ;按字訪問
Emu8086 容許使用如下更簡潔的前綴
b. - 等價於上面的 BYTE PTR ; w. - 等價於上面的 WORD PTR
有時,編譯器可以自動計算出數據類型,但是如果一個參與運算的數是立即數,這種方法就不可靠了。

MOV 指令

將第二個運算元(源)拷貝到第一個運算元(目的)指定位值 ,源運算元可以是立即數通用暫存器或者記憶體單元,目的暫存器可以是通用暫存器或者記憶體單元 ,源和目的必須是同樣大小,要么都是位元組要么都是字
操作類型如下:
MOV REG, memory
MOV memory, REG
MOV REG, REG
MOV memory, immediate
MOV REG, immediate
REG: AX, BX, CX, DX, AH, AL, BL, BH, CH, CL, DH, DL, DI, SI, BP, SP.
memory: [BX], [BX+SI+7],變數, 等等
immediate: 5, -24, 3Fh, 10001101b, 等等.
mov 指令只支持如下段暫存器
MOV SREG, memory
MOV memory, SREG
MOV REG, SREG
MOV SREG, REG
SREG: DS, ES, SS, 注意 CS 只能作操作源
REG: AX, BX, CX, DX, AH, AL, BL, BH, CH, CL, DH, DL, DI, SI, BP, SP.
memory: [BX], [BX+SI+7], variable, 等等
MOV指令不能用來設定CS和IP暫存器的值。
下面是一個使用 MOV 指令的例子:
#MAKE_COM#   ; 表示,這個是一個com程式
ORG 100h   ;COM 程式必須的
MOV AX, 0B800h  ; 將ax設定為 B800h.
MOV DS, AX  ; 將 AX 值拷貝到 DS.
MOV CL, 'A'  ; 將ASCII 碼 'A'的值傳送到cl,這個值是 41h.
MOV CH, 01011111b  ; 將ch設定為二進制的01011111b
MOV BX, 15Eh   ; 將 BX 設定成 15Eh.
MOV [BX], CX   ; 將 CX 放到 bx 指出的記憶體單元 B800:015E
RET   ; 返回作業系統
你可以將上面的程式貼入Emu8086代碼編輯器,接下來按下[complie and emulate] (或者按F5) 模擬視窗將顯示這個程式已經調入,點擊[single step]觀察暫存器數值變化,你可以猜到 ";" 表示注釋,編譯器忽略在";"後面的一切,程式結束後,你可以看到如下視窗
事實上,上面程式是將字元直接寫入顯示記憶體
通過上面的例子,你可以發現 MOV 指令是非常有用的。
變數
變數是一個記憶體地址。對於編程者來說,使用諸如名稱為“var1”這樣的 變數保存數據遠遠比使用5a73:235b這樣的地址容易的多。特別是當你使用10個以上的變數的時侯。
編譯器支持這兩種變數 BYTE 和 WORD.(位元組和字)
聲明變數的方法:
name DB value 名稱 DB 值
name DW value 名稱 DW 值
DB - stays for Define Byte.
DW - stays for Define Word.
name -可以是任何字母與數字構成,但是必須由字母開頭。可以通過不命名來聲明一個 沒有名稱的的變數(這個變數只有地址,沒有名稱)
value - 可以是任何數值支持三種進制(十六進制,二進制十進制),你可以使用"?"符號表示初始值沒有確定。
你可能從第二章了解到, MOV 指令是將數值從源拷貝到目的。 讓我們再看一個 MOV 指令的例子
#MAKE_COM#
ORG 100h
MOV AL, var1
MOV BX, var2
RET ; stops the program.
VAR1 DB 7
var2 DW 1234h
將上面的代碼拷貝到emu8086源程式編輯器中,按下F5鍵編譯 並在模擬器中執行。你會看到如下畫面
從畫面可以看出,反編譯後的代碼同源程式很相似,不同的是變數被具體的記憶體地址取代。當編譯器生成機器代碼它會自動將變數名稱用該變數的便宜量代替。默認情況下,DS 暫存器存放段偏移(當執行com檔案的時侯,DS 暫存器的值同 CS 暫存器(代碼段)的值一樣)。記憶體第一列是偏移(offset),第二列是一個十六進制值(hexadecimal value),第三列是十進制(decimal value),最後一列是 ASCII 字元。編譯器是非大小寫敏感的,所以 “VAR1” 同 “var1” 都是同一個變數。
VAR1變數的偏移是0108h,物理地址是0b56:0108
var2 變數的偏移是0109h,物理地址是 0b56:0109
這個變數是字,它占用2位元組。這裡假定低位元組存放在低地址,所以34h位於12h前面。
你可以看到,在RET指令後面還有一些指令,這樣是因為反編譯工具無法判斷數據從什麼地方開始。同樣,你可以寫出直接使用DB的程式.
#MAKE_COM#
ORG 100h
DB 0A0h
DB 08h
DB 01h
DB 8Bh
DB 1Eh
DB 09h
DB 01h
DB 0C3h
DB 7
DB 34h
DB 12h
將上面的代碼拷貝到emu8086原代碼編輯器,按下F5鍵編譯,並在模擬器中運行,你可以看到同樣的反彙編結果,得到同樣的功能。根據上面,你可以猜測,編譯器將源程式轉化為一些位元組的集合,這個集合被稱作機器代碼(machine code),處理器懂得他們,並且執行它們。ORG 100是一個編譯指令(它告訴編譯器如何處理原始碼)當你使用變數的時侯,這條指令特別重要。它通知編譯器可執行程式將被調入偏移量是100h(256位元組)的位置,有了它,編譯器就可以計算出所有變數的正確地址,然後用這些地址(偏移量)來代替變數名稱。上面的這些指令不會真正的編譯為任何機器代碼。為何可執行程式總是被裝入偏移量100h?作業系統在CS暫存器(代碼段)存儲著程式信息,比如命令行方式下的參數等等。儘管上面只是一個COM檔案的例子,EXE檔案調入在偏移量0000的位置,他使用特定的段保存變數。我們在下面會學習到關於EXE檔案的知識。

數組

數組可以看作是變數鏈。一個字元串是一個位元組數組的例子,其中每一個字元都當作一個ASCII碼的值(0....255)下面是一些定義數組的例子
a DB 48h, 65h, 6Ch, 6Ch, 6Fh, 00h
b DB 'Hello', 0
b是一個數組,當編譯器發現引用了字元串值後,會自動將這些字元轉化為對應的位元組。下面圖表表示的就是聲明數組後在記憶體中的分布:
你可以使用方括弧做下標直接訪問到數組中的值,例如:
MOV AL, a[3] 同樣,你還可以使用任意一個記憶體索引暫存器BX, SI, DI, BP,例如:
MOV SI, 3
MOV AL, a[SI]
如果你想聲明比較複雜的數組,你可以使用DUP指令 形式如下number DUP ( value(s) )
number - 重複的數量(任意常數)
value - 將要複製的表達式
例如:c DB 5 DUP(9) 就相當於如下定義:c DB 9, 9, 9, 9, 9 另外一個例子:d DB 5 DUP(1, 2) 等同於d DB 1, 2, 1, 2, 1, 2, 1, 2, 1, 2 當然,如果需要存放超過255或者小於-128的數值,你還可以使用DW來代替 DB。但是DW不能用於聲明字元串。DUP命令展開後不能超過1020個字元(上一個例子中展開之後是13個字元),如果需要聲明請將它們分成兩行(這樣,記憶體中得到的仍然是一個大數組)。取得變數地址 LEA指令(Load Effective Address 讀取有效地址)或者OFFSET指令。OFFSET 和 LEA二者都能夠獲得變數的偏移量。LEA在使用中更有效,這是因為它能返回索引變數的地址。取得變數地址在很多情況下是非常有用的,例如你打算向一個過程傳遞參數。注意:在編譯過程中使用如下聲明數據類型 BYTE PTR - 表示位元組 ;WORD PTR - 表示字(2個位元組)
例如:
BYTE PTR [BX] ;按位元組訪問 ; WORD PTR [BX] ;按字訪問
Emu8086 容許使用如下更簡潔的前綴
b. - 等價於上面的 BYTE PTR ;w. - 等價於上面的 WORD PTR
有時,編譯器可以自動計算出數據類型,但是如果一個參與運算的數是立即數,這種方法就不可靠了。
第一個例子:
ORG 100h
MOV AL, VAR1 ; 將變數var1的數值放入al以便檢查
LEA BX, VAR1 ; 將var1的地址存入 BX.
MOV BYTE PTR [BX], 44h ; 修改變數var1的內容
MOV AL, VAR1 ; 將變數VAR1的數值放入AL以便檢查
RET
VAR1 DB 22h
END
下面是另外一個例子,用OFFSET指令代替LEA:
ORG 100h
MOV AL, VAR1 ; 將變數VAR1的值放入AL以便檢查.
MOV BX, OFFSET VAR1 ; 將變數VAR1的地址放入 BX.
MOV BYTE PTR [BX], 44h ; 修改變數VAR1內容
MOV AL, VAR1 ;將變數VAR1的值放入 AL以便檢查.
RET
VAR1 DB 22h
END
上面例子的功能相同。
這些語句:
LEA BX, VAR1
MOV BX, OFFSET VAR1
都將生成同樣的機器代碼: MOV BX, num,num 是16位變數偏移
請注意,只有這些暫存器可以放入方括弧中(作為記憶體指針)BX, SI, DI, BP(請參考本教程前述章節)

常量

常量同變數很相似,但是它一直存在。定義一個變數之後,它的值 不會改變。使用EQU定義常量:name equ <任意表達式> 例如:
k EQU 5
MOV AX, k
上面的例子等同於如下代碼:
MOV AX, 5
在程式執行過程中你可以選擇模擬器"View"選單下的"Variables"
你可以點一個變數然後設定Elements屬性為數組大小來查看數組。彙編語言對於數據類型並不嚴格,這樣以來所有的變數都可以被看 作是數組。變數可以顯示為下列進制
HEX - 十六進制 hexadecimal (基底 16).
BIN - 二進制 (基底 2).
OCT - 八進制 (基底 8).
SIGNED - 有符號十進制 (基底 10).
UNSIGNED - 無符號十進制 (基底 10).
CHAR - ASCII 碼 (一共有256個符號,其中一些符號是不可見的).
程式運行的時侯,你可以通過雙擊它來編輯變數值,或者選中之後點Edit按鈕。
十六進制數值以"h"結尾,二進制以"b" 結尾,八進制以"o" 結尾,十進制沒有結尾。字元串用這樣的方式表示:'hello world',0
(結尾以0表示)
數組按照如下輸入:1, 2, 3, 4, 5
(數組可以是一組位元組或者字,這取決於你想以位元組還是字的方式編輯)
表達式會自動計算,例如,輸入如下表達式5 + 2會自動計算為7。等等....

中斷

中斷是一系列功能調用。這些功能調用使得編程更加容易。比如,你想在印表機上輸出一個字元,你只需要簡單的調用中斷,它將幫你完成所有的事情。另外還有控制磁碟和其他硬體工作的中斷。我們將這些功能調用稱作軟體中斷。不同的硬體同樣可以觸發中斷,這些中斷稱作硬體中斷。這裡,我們只介紹軟體中斷(software interrupts)。觸發一個軟體中斷,需要使用INT指令,它的使用方式非常簡單:
INT value
上面value的取值範圍是從 0 到 255 (或者0到0ffh),通常我們使用十六進制。 你也許猜測只有256箇中斷調用,但是這是不正確的。因為每一個中斷都有子功能。
在調用一個中斷的子功能之前,需要設定AH暫存器。每一個中斷最多可以擁有256個子功能(於是,我們有256*256=65536個功能調用)。一般情況下使用AH暫存器,但是一些情況下可能使用另外的暫存器。通常,其他的暫存器是用來傳遞數據和參數的。
下面的例子調用了 INT 10h中斷0Eh子功能輸出字元串‘Hello!'。這個功能作用是在螢幕上顯示一個字元,然後游標進一,如果需要還滾屏。
#MAKE_COM# ; 生成com檔案的指令
ORG 100h
;我們使用的這個子功能沒有返回值,
;所以我們只用設定就可以了。
MOV AH, 0Eh ; 選擇子功能
;int 10h/0eh 子功能,輸出放在
;AL暫存器中的ASCII碼對應的字元
MOV AL, 'H' ; ASCII碼: 72
INT 10h ; 輸出
MOV AL, 'e' ; ASCII 碼: 101
INT 10h ; 輸出
MOV AL, 'l' ; ASCII 碼: 108
INT 10h ; 輸出
MOV AL, 'l' ; ASCII 碼: 108
INT 10h ; 輸出
MOV AL, 'o' ; ASCII 碼: 111
INT 10h ; 輸出
MOV AL, '!' ; ASCII 碼: 33
INT 10h ; 輸出
RET ; 返回作業系統
將上述程式拷貝貼上到Emu8086代碼編輯器,點擊 [Compile and Emulate] 按鈕,運行!
常用函式館 - emu8086.inc
通過引用一些常用函式,可以使你編程更加方便。在你的程式中使用其他檔案中的函式的方法是
INCLUDE後面接上你要引用的檔案名稱。編譯器
會自動在你源程式所在的資料夾中查找你引用的檔案,如果沒有找到,它將搜尋Inc 資料夾。通常你無法完全理解 emu8086.inc(位於Inc資料夾)但是這沒有關係,你只用知道它能做什麼就足夠了。要使用emu8086.inc中的函式,你應當在你程式的開頭加上
include 'emu8086.inc'
emu8086.inc 定義了如下的宏:
PUTC char - 將一個ascii字元輸出到游標當前位值,只有一個參數的宏
GOTOXY col, row - 設定當前游標位置,有兩個參數
PRINT string - 輸出字元串,一個參數
PRINTN string - 輸出字元串,一個參數。與print功能相同,不同在於輸出之後自動回車
CURSOROFF - 關閉文本游標
CURSORON - 打開文本游標
使用上述宏的方法是:在你需要的位值寫上宏名稱加上參數。例如:
include emu8086.inc
ORG 100h
PRINT 'Hello World!'
GOTOXY 10, 5
PUTC 65 ; 65 - ASCII 碼的 'A'
PUTC 'B'
RET ; 返回作業系統
END ; 停止編譯器
當編譯器運行你的代碼時,它首先找到聲明中的emu8086.inc檔案,然後將代碼中的宏用實際的代碼替換掉。通常來說,宏都是比較小的代碼段,經常使用宏會使得你的可執行程式特別大(對於降低檔案大小來說使用過程更好)
emu8086.inc 同樣定義了如下過程:
PRINT_STRING - 在當前游標位置輸出一個字元串字元串地址由DS:SI 暫存器給出使用時,需要在END前面聲明DEFINE_PRINT_STRING 才能使用.
PTHIS - 在當前游標位置輸出一個字元串(同 PRINT_STRING)一樣,不同之處在於它從堆疊接收字元串。字元串終止符
應在call之後定義。例如
CALL PTHIS
db 'Hello World!', 0
使用時,需要在 END 前面聲明
DEFINE_PTHIS 。GET_STRING - 從用戶輸入得到一個字元串,輸入的字元串寫入 DS:DI 指出的緩衝,緩衝區的大小由 DX設定。回車作為輸入結束。使用時,需要在END前面聲明
DEFINE_GET_STRING 。CLEAR_SCREEN - 清屏過程(滾過整個螢幕),然後將游標設定在左上角. 使用時,需要在END前面聲明DEFINE_CLEAR_SCREEN 。
SCAN_NUM - 取得用戶從鍵盤輸入的多位有符號數,並將輸入存放在CX暫存器。 使用時,需要在 END前面聲明 DEFINE_SCAN_NUM。
PRINT_NUM - 輸出AX暫存器中的有符號數。使用時,需要在END 前面聲明 DEFINE_PRINT_NUM以及 DEFINE_PRINT_NUM_UNS.
PRINT_NUM_UNS - 輸出AX暫存器中的無符號數。使用時,需要在END 前面聲明DEFINE_PRINT_NUM_UNS.
使用上述過程,必須在你源程式的底部(但是在END之前!!!)聲明這些函式,使用CALL指令後面接上過程名稱來調用。例如:
include 'emu8086.inc'
ORG 100h
LEA SI, msg1 ; 要求輸入數字
CALL print_string ;
CALL scan_num ; 讀取數字放入cx
MOV AX, CX ; CX存放數值拷貝到AX; 輸入如下字元
CALL pthis
DB 13, 10, 'You have entered: ', 0
CALL print_num ; 輸出 AX中的字元
RET ; 返回作業系統
msg1 DB 'Enter the number: ', 0
DEFINE_SCAN_NUM
DEFINE_PRINT_STRING
DEFINE_PRINT_NUM
DEFINE_PRINT_NUM_UNS ; print_num函式要求的
DEFINE_PTHIS
END ; 結束
首先,編譯器運行聲明(對於宏只是展開)。當編譯器遇到CALL指令,它 將用過程聲明中的地址來替代過程名。程式在執行過程中遇到這個過程,便會直接跳轉到過程。這是非常有用的,比如,即使在你的代碼中執行100次一個過程,編譯後的執行檔也不會因此而增大多少。這樣看起來很划算,是不是?後面你會學到更多的,現在只需要了解一點點基本原理。

運算與邏輯指令

大多數運算與邏輯指令影響處理器的狀態標記暫存器
從上圖可以看到,這是狀態標記暫存器是一個16位暫存器,每一位稱作一個標誌位,可以取值 1 或者 0 。
進位標誌 Carry Flag (CF) - 出現無符號(unsigned overflow)溢出該位設定成1。例如,計算 255+1(結果超出0...255)。沒有溢出時該位為0。
零標誌 Zero Flag (ZF) - 當結果為 0 時設定為1,結果不為 0 時設定為0。
符號標誌 Sign Flag (SF) - 結果為負置1,結果為正置為0。事實上該位對於結果特別重要。
溢出標誌 Overflow Flag (OF) - 當出現有符號數溢出設定為1。例如,計算100+50(結果超出-128-127的範圍)。
奇偶標誌 Parity Flag (PF) - 當結果運算元中1的個數為偶時置1,否則為0注意,如果結果是一個字,該標誌只指示低8位。
輔助進位標誌 Auxiliary Flag (AF) - 低4位向上進位時置1,否則為0(記錄運算時第3位(半個位元組)產生的進位值。例如,執行加法指令時,最高有效位有進位時置1,否則置0
中斷標誌 Interrupt enable Flag (IF) - 當cpu容許中斷時為1,否則為0
Direction Flag (DF) - 方向標誌,在串處理指令中控制處理信息的方向用。當DF為1時,每次操作後使變址暫存器SI和DI減量,這樣就使串處理從高地址向低地址方向處理。當DF為0時,則使SI和DI增量,使串處理從低地址向高地址方向處理。
這裡有3組指令.
第一組: ADD, SUB,CMP, AND, TEST, OR, XOR
支持如下運算元:
REG, memory
memory, REG
REG, REG
memory, immediate
REG, immediate
REG(暫存器): AX, BX, CX, DX, AH, AL, BL, BH, CH, CL, DH, DL, DI, SI, BP, SP.
memory(記憶體): [BX], [BX+SI+7], 變數,等等...
immediate(立即數): 5, -24, 3Fh, 10001101b, 等等...
執行之後,結果經常存放在第一個運算元中。CMP和TEST指令只影響標誌位,並不返回數值(這兩條指令是用來在程式運行中判斷的)上述指令只影響如下標誌位 :
CF, ZF, SF, OF, PF, AF.
ADD - 將第二個運算元加至第一個運算元上
SUB - 從第一個運算元中減去第二個運算元
CMP - 從第一個運算元中減去第二個運算元,但只影響標誌位.
AND - 兩個運算元各個位邏輯與運算。運算法則如下
1 AND 1 = 1 1 AND 0 = 0
0 AND 1 = 0 0 AND 0 = 0
只有當兩個運算元都是1時,運算結果才是1。
TEST - 和上面的and 操作一樣,但是只影響標誌位。
OR - 兩個運算元各個位邏輯或運算。運算法則如下
1 OR 1 = 1 1 OR 0 = 1
0 OR 1 = 1 0 OR 0 = 0
如果運算元中有1那么結果一定是1。
XOR - 兩個運算元各個位邏輯異或運算。運算法則如下
1 XOR 1 = 0 1 XOR 0 = 1
0 XOR 1 = 1 0 XOR 0 = 0
當兩個運算元不同時,結果為1。
第二組: MUL, IMUL, DIV, IDIV
支持如下運算元:
REG
memory
REG(暫存器): AX, BX, CX, DX, AH, AL, BL, BH, CH, CL, DH, DL, DI, SI, BP, SP.
memory(記憶體): [BX], [BX+SI+7], variable, etc...
MUL and IMUL 指令只影響 CF, OF標誌位。
運算後如果結果超出範圍,這些標記位置1,如果沒有超過範圍,置0
DIV 和 IDIV 指令對於標誌位無影響
MUL - 無符號乘:
運算元是位元組時: AX = AL * 運算元.
當運算元是字時: (DX AX) = AX * 運算元.
IMUL - 有符號乘法:
當運算元是位元組時: AX = AL * 運算元.
當運算元是字時: (DX AX) = AX * 運算元.
DIV - 無符號除法:
當運算元是位元組時: AL = AX / 運算元 AH = 餘數(取模後的餘數) .
當運算元是字時:
AX = (DX AX) / 運算元 DX = 餘數(取模後的餘數) IDIV - 有符號除法:
運算元是位元組時:
AL = AX / 運算元 AH =餘數(取模後的餘數)
當運算元是字時:
AX = (DX AX) / 運算元 DX = 餘數(取模後的餘數) .
第三組: INC, DEC, NOT, NEG
支持如下運算元:
REG
memory
REG(暫存器): AX, BX, CX, DX, AH, AL, BL, BH, CH, CL, DH, DL, DI, SI, BP, SP.
memory(記憶體): [BX], [BX+SI+7], variable, etc...
INC, DEC 指令只影響如下標誌位: ZF, SF, OF, PF, AF.
NOT 指令不影響任何標誌位!
NEG i指令只影響如下操作位: CF, ZF, SF, OF, PF, AF.
NOT - 對與運算元每一位取反
NEG - 對運算元取反
實際上它對每一位取反然後在最後一位加1。例如5會變成-5,-2會變成2。(這裡所說運算應當是計算機內部的補碼運算)

程式控制轉移

對於編程來說控制程式走向是非常重要的事情,它是你的程式根據條件作出判斷,跳轉到相應的位值。
無條件跳轉
控制程式轉向的最基本的指令是JMP.
使用形式如下: JMP label
在程式中聲明/label/的方法很簡單,只要在它名字後面加上“:”,
label可以由任何字元混合而成但是不能由數字開頭,例如,下面是3個合法的label
label1:
label2:
a:
label可以在一條指令的前面聲明,例如:
x1:
MOV AX, 1
x2: MOV AX, 2
下面是一個 JMP 指令的例子:
ORG 100h
MOV AX, 5 ; 將 AX 設定為 5.
MOV BX, 2 ; 將 BX 設定為 2.
JMP calc ; 跳轉到 'calc'.
back: JMP stop ; 跳轉到 'stop'.
calc:
ADD AX, BX ; 將 BX 加到 AX.
JMP back ; 返回 'back'.
stop:
RET ; 返回作業系統
END
當然有更簡單的計算這兩個數字之和的方法,但是上面是一個JMP指令的很好的例子。
從例子中可以看出,JMP可以控制向前和向後。它可以轉移到當前代碼段的任意位置(65535位元組)。短條件轉移與JMP這一無條件轉移指令不同,還有一些有條件跳轉指令(只有在條件成立的時侯才跳轉)。這些指令分為三組,第一組是只檢測單獨標記位,第二組比較有符號數,第三組比較無符號數。檢測單獨標記位的轉移指令
指令
說明
條件
相反指令
JZ , JE
如果為0(相等),轉移 .
ZF =1
JNZ, JNE
JC , JB, JNAE
如果進位 (小於, 不大於等於),轉移
CF = 1
JNC, JNB, JAE
JS
如果是負數,轉移
SF = 1
JNS
JO
如果溢出,轉移
OF = 1
JNO
JPE, JP
如果是偶數,轉移
PF = 1
JPO
JNZ , JNE
如果不為0(不相等),轉移
ZF = 0
JZ, JE
JNC , JNB, JAE
如果沒有進位(大於, 大於等於),轉移
CF = 0
JC, JB, JNAE
JNS
如果不是負數,轉移
SF = 0
JS
JNO
如果沒有溢出,轉移
OF = 0
JO
JPO, JNP
如果不是偶數,轉移
PF = 0
JPE, JP
可以看到一些指令功能相同,對,他們編譯之後生成相同機器碼所以很容易理解為什麼你編譯 JE 指令而反編譯得到的卻是JZ.使用不同的名稱是為了使程式更容易理解。比較有符號數轉移指令
指令
說明
條件
相反指令
JE , JZ
如果等於 (=),如果為0,跳轉
ZF = 1
JNE, JNZ
JNE , JNZ
如果不等於 (<>),如果不等於0,跳轉
ZF = 0
JE, JZ
JG , JNLE
如果大於 (>) 如果不小於等於 (not <=),跳轉
ZF = 0
SF = OF
JNG, JLE
JL , JNGE
如果小與Jump if Less (<) 如果不大於等於 (not >=),跳轉
SF <> OF
JNL, JGE
JGE , JNL
如果大於等於 (>=),如果不小於 (not <),跳轉
SF = OF
JNGE, JL
JLE , JNG
如果小於等於 (<=),如果不大於 (not >),跳轉
ZF = 1或者SF <> OF
JNLE, JG
<> - 符號表示不等於. 比較無符號數轉移指令
指令
說明
條件
相反指令
JE , JZ
如果等於 (=).,如果為0,跳轉
ZF = 1
JNE, JNZ
JNE , JNZ
如果不等於(<>),如果不為0,跳轉
ZF = 0
JE, JZ
JA , JNBE
如果大於 (>),如果不小於等於(not <=),跳轉
CF = 0
and
ZF = 0
JNA, JBE
JB , JNAE, JC
如果小於 (<),如果不大於等於(not >=),如果進位,跳轉
CF = 1
JNB, JAE, JNC
JAE , JNB, JNC
如果大於等於(>=),如果不小於 (not <),如果沒有進位,跳轉
CF = 0
JNAE, JB
JBE , JNA
如果小於或者等於(<=),如果不大於 (not >),跳轉
CF = 1or
ZF = 1
JNBE, JA
一般來說,需要使用CMP指令來比較數值(該指令與 SUB(減法)
指令相近,只不過不保存結果,而只修改標值位)上面說法的意思是,例如:需要比較5 和2, 5-2 =3結果不是0(0標值位設定為 0)另一個例子比較 7和7
7 - 7 = 0結果為0! (0標值位設定為1。 JZ 或者 JE 會轉移).
下面是一個 CMP 指令和條件轉移指令的例子:
include emu8086.inc
include emu8086.inc
ORG 100h
MOV AL, 25 ; 設定AL為 25.
MOV BL, 10 ; 設定BL為10.
CMP AL, BL ; 比較 AL - BL.
JE equal ; 如果 AL = BL (ZF = 1) 跳轉
PUTC 'N' ; 如果到這裡,說明 AL <> BL,
JMP stop ; 列印'N', 跳轉到結束
equal: ; 如果到這裡
PUTC 'Y' ; 則 AL = BL,列印'Y'.
stop:
RET
END
請用用不同的數字試驗取代上述 AL 和 BL,點擊[FLAGS]鍵打開標誌,使用[Single Step]觀察發生了什麼,不要忘記每一次修改之後重新編譯運行(快捷鍵F5)。
全部的條件轉移指令都有一個很大的限制,就是與 JMP 指令不同,他們只能向前跳轉127位元組或者向後跳轉128位元組(注意大多數指令編譯之後是3個或者更多位元組)我們可以用如下小技巧解決這一問題:
從上述表中找到一條相反條件的轉移指令,令其跳轉到 label_x. 用JMP指令跳轉到你想要的地方在JMP指令後面定義label_x: label_x: - 可以是任意合法標號.
下面是一個例子:
include emu8086.inc
ORG 100h
MOV AL, 25 ; 設定 AL 為 25.
MOV BL, 10 ; 設定 BL 為 10.
CMP AL, BL ; 比較 AL - BL.
JNE not_equal ; 如果 AL <> BL (ZF = 0),轉移
JMP equal
not_equal:
; 假定這裡還有編譯之後超過127位元組的程式
PUTC 'N' ; 如果執行到這裡,說明 AL <> BL,
JMP stop ; 列印 'N', 轉移到程式結束。
equal: ; 如果執行到這裡,
PUTC 'Y' ; 說明 AL = BL, 列印 'Y'.
stop:
RET ; 上述都要執行這一條
END
另外,可以使用立即數來代替標號。立即數前使用“$”編譯器將直接得到偏移。例如:
ORG 100h
; 無條件向前轉移
; 跳過後面2位元組
JMP $2
a DB 3 ; 1 byte.
b DB 4 ; 1 byte.
; JCC 跳過 7 位元組:
; (JMP 本身占用 2 位元組)
MOV BL,9
DEC BL ; 2 bytes.
CMP BL, 0 ; 3 bytes.
JNE $-7
RET
END

堆疊

堆疊是記憶體中用於保存臨時數據的一片區域.當使用CALL指令時,堆疊用於保存過程的返回地址,RET指令能夠從堆疊中取得該地址並使程式返回到那裡。當使用INT指令,發生的也與此類似。
堆疊保存標誌暫存器代碼段偏移量。IRET指令用來從中斷返回。 我們同樣可以使用堆疊保存任何數據。對於堆疊的操作只有兩條:
PUSH - 將16位數值壓入堆疊. POP - 將16位數值從堆疊中彈出
PUSH 指令的使用方法:
PUSH REG
PUSH SREG
PUSH memory
PUSH immediate
REG(暫存器): AX, BX, CX, DX, DI, SI, BP, SP.
SREG(段暫存器): DS, ES, SS, CS.
memory(記憶體): [BX], [BX+SI+7], 16 位變數, 等等...
immediate(立即數): 5, -24, 3Fh, 10001101b,等等...
POP 指令的使用方法:
POP REG
POP SREG
POP memory
REG(暫存器): AX, BX, CX, DX, DI, SI, BP, SP.
SREG(段暫存器): DS, ES, SS, (除了 CS).
memory(記憶體): [BX], [BX+SI+7], 16位變數, 等等...
注意:
PUSH and POP 都只操作16位數據!
注意:
在80186其極以後的CPU中才能使用 PUSH 立即數這樣的指令堆疊使用LIFO(後進先出)算法,意思是:加入我們按照如下順序壓入數值: 1, 2, 3, 4, 5
再使用POP指令彈出,結果將是 5 4 3 2 1
注意,有多少條PUSH指令就要對應有多少條POP指令,否則堆疊會被占用,無法正確返回作業系統。前面講過使用RET指令返回作業系統,所以在程式開始時會將返回地址壓入堆疊(通常都是0000h)I PUSH 和 POP指令在我們暫存器不夠用的時侯特別有用,我們有如下技巧:
暫存器原始數值存入堆疊(使用 PUSH)使用暫存器從堆疊中彈出暫存器原先數值再放入暫存器(使用POP) 下面是一個例子:
ORG 100h
MOV AX, 1234h
PUSH AX ; 將 AX 存入堆疊.
MOV AX, 5678h ; 修改 AX 值
POP AX ;返回 AX 原先的值
RET
END
堆疊的另外一個作用是交換數值,下面是一個這樣的例子:
ORG 100h
MOV AX, 1212h ; 將 1212h 存入 AX.
MOV BX, 3434h ; 將 3434h 存入 BX
PUSH AX ; 將 AX 數值存入堆疊.
PUSH BX ; 將 BX 數值存入堆疊
POP AX ; BX原值存入AX
POP BX ; AX原值存入BX
RET
END
之所以能這樣是因為堆疊是用LIFO(後進先出)算法,當我們壓入1212h和3434h之後,使用pop彈出我們首先得到的是3434h然後才是1212h
堆疊的記憶體區域由SS暫存器(堆疊段),SP暫存器(棧指針)設定設定。一般來說作業系統在程式開始時會設定這些。 "PUSH 源" 指令做如下工作:
將SP暫存器減 2
將源的值寫入記憶體SS:SP地址處
"POP 目的" 指令做如下工作:
記憶體SS:SP地址處數值寫入目的
將SP暫存器加2
由 SS:SP 指出的地址稱作堆疊
對於COM檔案,堆疊段通常就是代碼段堆疊指針設定為
0FFFEh.在地址SS:0FFFEh處存放程式結束時RET指令返回地址。你可以點擊[stack]按鈕直接觀察堆疊操作。堆疊頂由“<”符號標記。
宏與過程很相似,但並不是完全相似。宏看起來像過程,但是當你的代碼編譯完成之後就消失了,取而代之的是真正的代碼。如果你聲明一個宏,而在代碼中從來沒有調用,編譯器在編譯過程中將忽略它。
宏的定義 :
name MACRO [參數,]
<指令>
ENDM
與過程不同,宏要求定義參數並使用。例如:
MyMacro MACRO p1, p2, p3
MOV AX, p1
MOV BX, p2
MOV CX, p3
ENDM
ORG 100h
MyMacro 1, 2, 3
MyMacro 4, 5, DX
RET
上述代碼在編譯過程中將展開成:
MOV AX, 00001h
MOV BX, 00002h
MOV CX, 00003h
MOV AX, 00004h
MOV BX, 00005h
MOV CX, DX
關於宏與過程需要注意如下要點:
當你想使用一個過程,你應該使用CALL指令,例如:CALL MyProc 當你想使用一個宏,你只需要輸入它的名稱。例如:MyMacro
過程是存在於記憶體中某一特定位值的,即使你調用這個過程100次,cpu只是執行記憶體中這一段的代碼。在遇到RET指令後還會回到調用該過程的位值。這是通過使用堆疊保存返回地址來實現的。CALL指令占用3位元組,無論調用多少次過程,最終輸出的執行檔並不會因此而顯著增大。
宏會在程式代碼中展開。如果你使用相同的宏100次,輸出的執行檔將會變得越來越大,因為每一次調用宏中的指令都會插入到調用宏的位值。
你可以使用堆疊或者通用暫存器來向過程傳遞參數向宏傳遞參數的方法是在宏名稱後面直接接上參數。例如: MyMacro 1, 2, 3
用ENDM指令結束宏就足夠了標記過程結束,你需要在ENDP指令前加上過程名稱
宏會直接在代碼中展開,因此,如果你在宏中使用標記,當宏被調用2次或兩次以上的時侯就會出現
"Duplicate declaration"(重複定義) 這一錯誤。為了避免該錯誤
在變數,標記或者過程名稱之前加上“local”指令。例如:
MyMacro2 MACRO
LOCAL label1, label2
CMP AX, 2
JE label1
CMP AX, 3
JE label2
label1:
INC AX
label2:
ADD AX, 2
ENDM
ORG 100h
MyMacro2
MyMacro2
RET
I若過你打算在很多程式中使用宏,將所有的宏存放在一個檔案中不失為一個好辦法。將那個檔案放在INC目錄下,使用 INCLUDE 檔案名稱 就可以在你的程式中調用宏了。

相關詞條

熱門詞條

聯絡我們