匯編基礎程序編寫教程示例

源程序

1.1 構成

寄存器與段的關聯假設

 assume:含義為“假設”。

它假設某一段寄存器和程序中的某一個用 segment … ends 定義的段相關聯。

通過assume說明這種關聯,在需要的情況下 ,編譯程序可以將段寄存器和某一個具體的段相聯系。

標號

一個標號指代瞭一個地址。

codesg:放在segment的前面,作為一個段的名稱,這個段的名稱最終將被編譯、連接程序處理為一個段的段地址。

定義一個段

segment和ends的功能是定義一個段,segment說明一個段開始,ends 說明一個段結束。

segment和ends是一對成對使用的偽指令

一個段必須有一個名稱來標識,使用格式為:

段名 segment

段名 ends

一個匯編程序是由多個段組成的,這些段被用來存放代碼、數據或當作棧空間來使用。

一個有意義的匯編程序中至少要有一個段,這個段用來存放代碼。

程序結束標記

End 是一個匯編程序的結束標記,編譯器在編譯匯編程序的過程中,如果碰到瞭偽指令 end,就結束對源程序的編譯。

如果程序寫完瞭,要在結尾處加上偽指令end 。否則,編譯器在編譯程序時,無法知道程序在何處結束。

註意:不要搞混瞭end和ends。

程序返回

一個程序結束後,將CPU的控制權交還給使它得以運行的程序,我們稱這個過程為:程序返回。

如何返回

應該在程序的末尾添加返回的程序段。

mov ax,4c00H

int 21H

程序運行

DOS是一個單任務操作系統。

一個程序P2在可執行文件中,則必須有一個正在運行的程序P1,將P2從可執行文件中加載入內存後,將CPU的控制權交給P2,P2才能得以運行。P2開始運行後,P1暫停運行。

而當P2運行完畢後,應該將CPU的控制權交還給使它得以運行的程序P1,此後,P1繼續運行。

1.2 源程序中的“程序”

匯編源程序:

偽指令 (編譯器處理)

匯編指令(編譯為機器碼)

程序:源程序中最終由計算機執行、處理的指令或數據。

註意

我們可以將源程序文件中的所有內容稱為源程序,將源程序中最終由計算機執行處理的指令或數據 ,成為程序。

程序最先以匯編指令的形式存在源程序中,經編譯、連接後轉變為機器碼,存儲在可執行文件中,

 

1.3 段結束、程序結束、程序返回

1.4 語法錯誤和邏輯錯誤

語法錯誤

程序在編譯時被編譯器發現的錯誤

邏輯錯誤

程序在編譯時不能表現出來的、在運行時發生的錯誤

2 程序執行的過程

2.1 一個匯編語言程序從寫出到最終執行的簡要過程:

2.2 連接

作用

當源程序很大時,可以將它分為多個源程序文件來編譯,每個源程序編譯成為目標文件後,再用連接程序將它們連接到一起,生成一個可執行文件;

程序中調用瞭某個庫文件中的子程序,需要將這個庫文件和該程序生成的目標文件連接到一起,生成一個可執行文件;

一個源程序編譯後,得到瞭存有機器碼的目標文件,目標文件中的有些內容還不能直接用來生成可執行文件,連接程序將這此內容處理為最終的可執行信息。

所以,在隻有一個源程序文件,而又不需要調用某個庫中的子程序的情況下,也必須用連接程序對目標文件進行處理,生成可執行文件。

註意,對於連接的過程,可執行文件是我們要得到的最終結果。

使用匯編語言編譯程序對源程序文件中的源程序進行編譯,產生目標文件;再用連接程序對目標文件進行連接,生成可在操作系統中直接運行的可執行文件。

2.3 可執行文件

可執行文件中包含兩部分內容:

  •  程序(從原程序中的匯編指令翻譯過來的機器碼)和數據(源程序中定義的數據)
  •  相關的描述信息(比如:程序有多大、要占多少內存空間等)

執行可執行文件中的程序

  • 在操作系統中,執行可執行文件中的程序。
  • 操作系統依照可執行文件中的描述信息,將可執行文件中的機器碼和數據加載入內存,並進行相關的初始化(比如:設置CS:IP指向第一條要執行的指令),然後由CPU執行程序。

 

可執行文件中的程序裝入內存並運行的原理

  • 在DOS中,可執行文件中的程序P1若要運行,必須有一個正在運行的程序P2 ,將 P1 從可執行文件中加載入內存,將CPU的控制權交給它,P1才能得以運行;
  • 當P1運行完畢後,應該將CPU的控制權交還給使它得以運行的程序P2

exe的執行過程

實際過程

(1)我們在提示符“C:\masm”後面輸入可執行文件的名字“1”,按Enter鍵。

(2)1.exe中的程序運行;

(3)運行結束,返回,再次顯示提示符“C:\masm”。

操作過程

操作系統是由多個功能模塊組成的龐大 、復雜的軟件系統。任何通用的操作系統 ,都要提供一個稱為shell(外殼)的程序 ,用戶(操作人員)使用這個程序來操作計算機系統工作。

DOS中有一個程序command.com ,這個程序在 DOS 中稱為命令解釋器,也就是DOS系統的shell。

(1)我們在DOS中直接執行 1.exe 時,是正在運行的command將1.exe中的程序加載入內存。

(2)command設置CPU的CS:IP指向程序的第一條指令(即程序的入口),從而使程序得以運行。

(3)程序運行結束後,返回到command中,CPU繼續運行command。

2.4 程序執行過程的跟蹤

Debug 可以將程序加載入內存,設置CS:IP指向程序的入口,但Debug並不放棄對CPU 的控制,這樣,我們就可以使用Debug 的相關命令來單步執行程序 ,查看每條指令指令的執行結果。

我們在 DOS中用 “Debug 1.exe” 運行Debug對1.exe進行跟蹤時,程序加載的順序是:command加載Debug,Debug加載1.exe。

返回的順序是:從1.exe中的程序返回到Debug,從Debug返回到command。

EXE文件中的程序的加載過程

 

總結

程序加載後,ds中存放著程序所在內存區的段地址,這個內存區的偏移地址為 0 ,則程序所在的內存區的地址為:ds:0;

這個內存區的前256 個字節中存放的是PSP,dos用來和程序進行通信。

從 256字節處向後的空間存放的是程序。

所以,我們從ds中可以得到PSP的段地址SA,PSP的偏移地址為 0,則物理地址為SA×16+0。

因為PSP占256(100H)字節,所以程序的物理地址是:

SA×16+0+256= SA×16+16×16=(SA+16)×16+0

可用段地址和偏移地址表示為:SA+10:0。

3 程序編寫

3.1 兩個基本的問題

計算機是進行數據處理、運算的機器,那麼有兩個基本的問題就包含在其中:

(1)處理的數據在什麼地方?

(2)要處理的數據有多長?這兩個問題,在機器指令中必須給以明確或隱含的說明,否則計算機就無法工作。

為瞭描述上的簡潔,在以後的課程中,我們將使用兩個描述性的符號 reg來表示一個寄存器,用sreg表示一個段寄存器。

reg的集合包括:ax、bx、cx、dx、ah、al、bh、bl、ch、cl、dh、dl、sp、bp、si、di;

sreg的集合包括:ds、ss、cs、es。

3.2 數據在哪裡

機器指令處理的數據所在位置

  • 絕大部分機器指令都是進行數據處理的指令,處理大致可分為三類:讀取、寫入、運算
  • 在機器指令這一層來講,並不關心數據的值是多少,而關心指令執行前一刻,它將要處理的數據所在的位置。
  • 指令在執行前,所要處理的數據可以在三個地方:CPU內部、內存、端口
  • 指令舉例

 

匯編語言中數據位置的表達

匯編語言中用三個概念來表達數據的位置。

 立即數(idata)

對於直接包含在機器指令中的數據(執行前在cpu 的指令緩沖器中),在匯編語言中稱為:立即數(idata ) ,在匯編指令中直接給出。例如:

mov ax,1

add bx,2000h

or bx,00010000b

mov al,’a’

 

寄存器

指令要處理的數據在寄存器中,在匯編指令中給出相應的寄存器名。例如:

mov ax,bx

mov ds,ax

push bx

mov ds:[0],bx

push ds

mov ss,ax

mov sp,ax

mov ax,bx

對應機器碼:89D8

執行結果:(ax) = (bx)

段地址(SA)和偏移地址(EA)

指令要處理的數據在內存中,在匯編指令中可用[X]的格式給出EA,SA在某個段寄存器中。

存放段地址的寄存器可以是默認的。

mov ax,[0]

mov ax,[bx]

mov ax,[bx+8]

mov ax,[bx+si]

mov ax,[bx+si+8]

段地址默認在ds中

存放段地址的寄存器也可以顯性的給出。

mov ax,[bp]

mov ax,[bp+8]

mov ax,[bp+si]

mov ax,[bp+si+8]

段地址默認在ss中

顯性的給出存放段地址的寄存器

尋址方式

當數據存放在內存中的時候,我們可以用多種方式來給定這個內存單元的偏移地址,這種定位內存單元的方法一般被稱為尋址方式。

3.3 指令處理的數據有多長

8086CPU的指令,可以處理兩種尺寸的數據,byte和word。所以在機器指令中要指明,指令進行的是字操作還是字節操作

對於這個問題,匯編語言中用以下方法處理。

(1)通過寄存器名指明要處理的數據的尺寸。

(2)在沒有寄存器名存在的情況下,用操作符X ptr指明內存單元的長度,X在匯編指令中可以為word或byte。

(3)其他方法

下面的指令中,寄存器指明瞭指令進行的是字節操作:

 mov al,1

mov al,bl

mov al,ds:[0]

mov ds:[0],al

inc al

add al,100

下面的指令中,寄存器指明瞭指令進行的是字操作:

mov ax,1

mov bx,ds:[0]

mov ds,ax

mov ds:[0],ax

inc ax add ax,1000

在沒有寄存器參與的內存單元訪問指令中,用word ptr或byte ptr顯性地指明所要訪問的內存單元的長度是很必要的。

否則,CPU無法得知所要訪問的單元是字單元,還是字節單元

下面的指令中,用word ptr指明瞭指令訪問的內存單元是一個字單元:

mov word ptr ds:[0],1

inc word ptr [bx]

inc word ptr ds:[0]

add word ptr [bx],2

下面的指令中,用byte ptr指明瞭指令訪問的內存單元是一個字節單元:

mov byte ptr ds:[0],1

inc byte ptr [bx]

inc byte ptr ds:[0]

add byte ptr [bx],2

有些指令默認瞭訪問的是字單元還是字節單元,

比如:push [1000H]就不用指明訪問的是字單元還是字節單元

因為push指令隻進行字操作

3.4 數據處理

在代碼段中使用數據

考慮這樣一個問題,編程計算以下8個數據的和,結果存在ax 寄存器中:

0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H。

在前面的課程中,我們都是累加某些內存單元中的數據,並不關心數據本身。

可現在我們要累加的就是已經給定瞭數值的數據。

 

程序第一行中的 “dw”的含義是定義字型數據。dw即define word。

在這裡,我們使用dw定義瞭8個字型數據(數據之間以逗號分隔),它們所占的內存空間的大小為16個字節。

程序中的指令就要對這8個數據進行累加,可這8個數據在哪裡呢?

由於它們在代碼段中,程序在運行的時候CS中存放代碼段的段地址,所以我們可以從CS中得到它們的段地址

這8個數據的偏移地址是多少呢?

  • 因為用dw定義的數據處於代碼段的最開始,所以偏移地址為0,這8 個數據就在代碼段的偏移0、2、4、6、8、A、C、E處。
  • 程序運行時,它們的地址就是CS:0、CS:2、CS:4、CS:6、CS:8、CS:A、CS:C、CS:E。

程序中,我們用bx存放加2遞增的偏移地址,用循環來進行累加。

在循環開始前,設置(bx)=0,cs:bx指向第一個數據所在的字單元。

每次循環中(bx)=(bx)+2,cs:bx指向下一個數據所在的字單元。

如何讓這個程序在編譯後可以存系統中直接運行呢?我們可以在源程序中指明界序的入口所在

探討end的作用:

end 除瞭通知編譯器程序結束外,還可以通知編譯器程序的入口在什麼地方。

有瞭這種方法,我們就可以這樣來安排程序的框架:

在代碼段中使用棧

完成下面的程序,利用棧,將程序中定義的數據逆序存放

 assume cs:codesg

codesgsegment

dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

?

code ends end

程序的思路大致如下:

程序運行時,定義的數據存放在cs:0~cs:15單元中,共8個字單元。依次將這8個字單元中的數據入棧,然後再依次出棧到這 8 個字單元中,從而實現數據的逆序存放。

問題是,我們首先要有一段可當作棧的內存空間。如前所述,這段空間應該由系統來分配。我們可以在程序中通過定義數據來取得一段空間,然後將這段空間當作棧空間來用

mov ax,cs

mov ss,ax

mov sp,32

我們要講 cs:16 ~ cs:31 的內存空間當作棧來用,初始狀態下棧為空,所以 ss:sp要指向棧底,則設置ss:sp指向cs:32。

比如對於:

dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H

我們可以說,定義瞭8個字型數據,也可以說,開辟瞭8個字的內存空間,這段空間中每個字單元中的數據依次是:

0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H。

因為它們最終的效果是一樣的

將數據、代碼、棧放入不同的段

在前面的內容中,我們在程序中用到瞭數據和棧,我們將數據、棧和代碼都放到瞭一個段裡面。我們在編程的時候要註意何處是數據,何處是棧,何處是代碼。這樣做顯然有兩個問題:

(1)把它們放到一個段中使程序顯得混亂;

(2)前面程序中處理的數據很少,用到的棧空間也小,加上沒有多長的代碼,放到一個段裡面沒有問題。

但如果數據、棧和代碼需要的空間超過64KB,就不能放在一個段中(一個段的容量不能大於64 KB,是我們在學習中所用的8086模式的限制,並不是所有的處理器都這樣)。

所以,我們應該考慮用多個段來存放數據、代碼和棧。

我們用和定義代碼段一樣的方法來定義多個段,然後在這些段裡面定義需要的數據,或通過定義數據來取得棧空間。

程序中“data”段中的數據“0abch”的地址就是:data:6。

我們要將它送入bx中,就要用如下的代碼:

 mov ax,data

mov ds,ax

mov bx,ds:[6]

我們不能用下面的指令:

mov ds,data

mov ax,ds:[6]

其中指令“mov ds,data” 是錯誤的,因為8086CPU不允許將一個數值直接送入段寄存器中。

程序中對段名的引用,如指令“mov ds,data”中的“data”,將被編譯器處理為一個表示段地址的數值。

“代碼段”、“數據段”、“棧段”完全是我們的安排

我們在源程序中用偽指令

“assume cs:code,ds:data,ss:stack”將cs、ds和ss分別和code、data、stack段相連。

這樣做瞭之後,CPU是否就會將 cs指向 code,ds 指向 data,ss 指向stack,從而按照我們的意圖來處理這些段呢?

當然也不是,要知道 assume 是偽指令,是由編譯器執行的,也是僅在源程序中存在的信息,CPU並不知道它們。

若要CPU按照我們的安排行事,就要用機器指令控制它,源程序中的匯編指令是CPU要執行的內容

CPU如何知道去執行它們?

我們在源程序的最後用“end start”說明瞭程序的入口,這個入口將被寫入可執行文件的描述信息,可執行文件中的程序被加載入內存後,CPU的CS:IP被設置指向這個入口,從而開始執行程序中的第一條指令。

標號“start”在“code”段中,這樣CPU就將code段中的內容當作指令來執行瞭。

我們在code段中,使用指令:

mov ax,stack

mov ss,ax

mov sp,16 設置ss指向stack,設置ss:sp指向stack:16, CPU 執行這些指令後,將把stack段當做棧空間來用。 CPU若要訪問data段中的數據,則可用 ds 指向 data 段,用其他的寄存器(如:bx)來存放 data段中數據的偏移地址

總之,CPU到底如何處理我們定義的段中的內容,是當作指令執行,當作數據訪問,還是當作棧空間,完全是靠程序中具體的匯編指令,和匯編指令對CS:IP、SS:SP、DS等寄存器的設置來決定的。

3.5 模塊化實現:call 和 ret 指令

功能:call和ret 指令都是轉移指令,它們都修改IP,或同時修改CS和IP。

ret

 ret指令用棧中的數據,修改IP的內容,從而實現近轉移;

CPU執行ret指令時,進行下面兩步操作:

(1)(IP)=((ss)*16+(sp))

(2)(sp)=(sp)+2

retf

retf指令用棧中的數據,修改CS和IP的內容,從而實現遠轉移;

CPU執行retf指令時,進行下面兩步操作:

(1)(IP)=((ss)*16+(sp))

(2)(sp)=(sp)+2

(3)(CS)=((ss)*16+(sp))

(4)(sp)=(sp)+2

可以看出,如果我們用匯編語法來解釋ret和retf指令,則:

CPU執行ret指令時,相當於進行:

pop IP

CPU執行retf指令時,相當於進行:

pop IP

pop CS

示例

ret指令

程序中ret指令執行後,(IP)=0,CS:IP指向代碼段的第一條指令。

retf指令

程序中retf指令執行後,CS:IP指向代碼段的第一條指令。

call 指令

CPU執行call指令,進行兩步操作:

(1)將當前的 IP 或 CS和IP 壓入棧中

(2)轉移

主要應用格式

call 指令不能實現短轉移,除此之外,call指令實現轉移的方法和 jmp 指令的原理相同

依據位移進行轉移的call指令

call 標號(將當前的 IP 壓棧後,轉到標號處執行指令)

CPU執行此種格式的call指令時,進行如下的操作:

(1) (sp) = (sp) – 2 ((ss)*16+(sp)) = (IP)

(2) (IP) = (IP) + 16位位移

call 標號

16位位移=“標號”處的地址-call指令後的第一個字節的地址;

16位位移的范圍為 -32768~32767,用補碼表示;

16位位移由編譯程序在編譯時算出。

從上面的描述中,可以看出,如果我們用匯編語法來解釋此種格式的 call指令,則: CPU 執行指令“call 標號”時,相當於進行: push IP jmp near ptr 標號

轉移的目的地址在指令中的call指令

前面講解的call指令,其對應的機器指令中並沒有轉移的目的地址 ,而是相對於當前IP的轉移位移。

指令“call far ptr 標號”實現的是段間轉移。

CPU執行“call far ptr 標號”這種格式的call指令時的操作:

(1) (sp) = (sp) – 2 ((ss) ×16+(sp)) = (CS) (sp) = (sp) – 2 ((ss) ×16+(sp)) = (IP)

(2) (CS) = 標號所在的段地址 (IP) = 標號所在的偏移地址

從上面的描述中可以看出,如果我們用匯編語法來解釋此種格式的 call 指令,則: CPU 執行指令 “call far ptr 標號” 時,相當於進行: push CS push IP jmp far ptr 標號

轉移地址在寄存器中的call指令

指令格式:call 16位寄存器

功能:

(sp) = (sp) – 2

((ss)*16+(sp)) = (IP)

(IP) = (16位寄存器)

匯編語法解釋此種格式的 call 指令,CPU執行call 16位reg時,相當於進行: push IP jmp 16位寄存器

轉移地址在內存中的call指令

轉移地址在內存中的call指令有兩種格式:

(1) call word ptr 內存單元地址

匯編語法解釋: push IP jmp word ptr 內存單元地址 比如下面的指令: mov sp,10h mov ax,0123h mov ds:[0],ax call word ptr ds:[0] 執行後,(IP)=0123H,(sp)=0EH

(2) call dword ptr 內存單元地址

匯編語法解釋: push CS push IP jmp dword ptr 內存單元地址 比如,下面的指令: mov sp,10h mov ax,0123h mov ds:[0],ax mov word ptr ds:[2],0 call dword ptr ds:[0] 執行後,(CS)=0,(IP)=0123H,(sp)=0CH

call 和 ret 的配合使用

我們看一下程序的主要執行過程:

(1)前三條指令執行後,棧的情況如下:

(2)call 指令讀入後,(IP) =000EH,CPU指令緩沖器中的代碼為 B8 05 00; CPU執行B8 05 00,首先,棧中的情況變為:

 

然後,(IP)=(IP)+0005=0013H。

(3)CPU從cs:0013H處(即標號s處)開始執行。

(4)ret指令讀入後:(IP)=0016H,CPU指令緩沖器中的代碼為 C3;CPU執行C3,相當於進行pop IP,執行後,棧中的情況為:

 

(IP)=000EH;

(5)CPU回到 cs:000EH處(即call指令後面的指令處)繼續執行。

我們發現,可以寫一個具有一定功能的程序段,我們稱其為子程序,在需要的時候,用call指令轉去執行

 call指令轉去執行子程序之前,call指令後面的指令的地址將存儲在棧中,所以可以在子程序的後面使用 ret 指令,用棧中的數據設置IP的值,從而轉到 call 指令後面的代碼處繼續執行。

這樣,我們可以利用call和ret來實現子程序的機制。

子程序的框架

標號: 指令 ret 具有子程序的源程序的框架:

參數和結果傳遞的問題

子程序一般都要根據提供的參數處理一定的事務,處理後,將結果(返回值)提供給調用者。

其實,我們討論參數和返回值傳遞的問題,實際上就是在探討,應該如何存儲子程序需要的參數和產生的返回值。

我們設計一個子程序,可以根據提供的N,來計算N的3次方。

這裡有兩個問題:

(1)我們將參數N存儲在什麼地方?

(2)計算得到的數值,我們存儲在什麼地方?

很顯然,我們可以用寄存器來存儲,可以將參數放到 bx 中 ;

因為子程序中要計算 N×N×N ,可以使用多個 mul 指令,為瞭方便,可將結果放到 dx 和 ax中。

子程序

說明:計算N的3次方

參數: (bx)=N

結果: (dx:ax)=N∧3

cube:mov ax,bx

mul bx ;用ax與bx相乘

mul bx

ret

用寄存器來存儲參數和結果是最常使用的方法。對於存放參數的寄存器和存放結果的寄存器,調用者和子程序的讀寫操作恰恰相反:

調用者將參數送入參數寄存器,從結果寄存器中取到返回值;

子程序從參數寄存器中取到參數,將返回值送入結果寄存器。

以上就是匯編基礎程序編寫教程示例的詳細內容,更多關於匯編語言基礎程序編寫的資料請關註WalkonNet其它相關文章!

推薦閱讀: