匯編基礎程序編寫教程示例
源程序
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其它相關文章!