匯編語言-基礎功能
在之前我們見過了mov,pop,push,add等指令,很顯然這些都是最基礎的指令,只能執行一些很簡單的功能,若要想實現復雜的功能,只用那這些指令是很難辦到的,接下來將繼續介紹更多的基礎指令
[bx]寄存器和loop指令
在之前,我們從內存中取數據到寄存器都是固定數字,如mov ax,[idata]
,除此之外,還可以mov ax,[bx]
,這條指令的作用是將DS:(bx中儲存的數據)所指向的內存單元的值賦給AX。默認的段地址是DS,也可以手動設定,如mov ax,ES:[bx]
。
Loop指令的格式為 Loop 標號
, CPU執行了loop指令時,會執行兩步操作,首先將CX寄存器中的值減一,隨后判斷CX寄存器中的值是否為零,如果為不零則跳到標號處,如果為零則向下直行。
可以發現CX寄存器控制着loop指令執行的次數,通常我們用loop指令來實現循環,在CX寄存器中儲存着循環次數。
在之前的時候,我們想實現二的三次方的計算,會用到下面的代碼:
mov ax,2
add ax,ax
add ax,ax
當我們想實現更高次方的計算高次方的計算,如2的12次方計算,就需要不斷重復add ax,ax
。
如果改用loop指令實現,發現代碼大大減少:
mov ax,2
mov cx,11
s : add ax,ax
loop s
注意: 標號一定要在 Loop 標號
前被定義。
debug看看loop是怎么執行的
比如我們要實現將內存單元 ffff:6所指向的字節乘以三倍放在DX中
assume cs:code
code segment
mov ax,0ffffh ;數據不能以字母開頭,記得加0
mov ds,ax
mov bx,6
mov al,[bx]
mov ah,0
mov dx,0
mov cx,3
s : add dx,ax
loop s
mov ax,4c00h
int 21H
code ends
end
Debug程序之后,我們可以用U命令來查看載入的程序,我們發現 LOOP 標號
此時已經變成了LOOP 0012
,通過觀察我們可以看到add dx,ax
的IP地址為0012H,也就是說,在載入loop指令后,IP指向了下一條指令,隨后又經過對CX的判斷,將IP修改為0012H。
在之前的程序里,在debug的過程中,程序很簡短,我們用單步式調T命令執行完了整個程序,現在有了循環功能,很有可能按段手指也跑不完,G這一命令可以直接執行到程序結束。
在debug中直接寫程序和masm編譯程序的區別
在之前的debug中,我們想將DS:6所指向的內存數據,放到AX,會用到以下代碼,在之前也測試過確實是可行的,但在masm中,這樣的寫法是無法達到我們的目的。
debug模式:如願以償
mov ax,0FFFFH
mov ds,ax
mov ax,[6]
masm編譯:會吧6直接賦值給AX
assume cs:code
code segment
mov ax,0FFFFH
mov ds,ax
mov ax,[6]
mov ax,4c00h
int 21h
code ends
end
如果想解決這個問題我們有兩種選擇:
- 就是像上面的循環程序一樣,把bx賦值,再用
mov ax,[bx]
。 - 顯式的給出段前綴,如
mov ax,ds:[6]
。
有四個段前綴,分別是SS,DS,CS,ES
安全的使用內存
那之前的代碼例子中為了方便,使用debug直接向內存中寫入數據,但是在實際的電腦中,這樣的行為是非常危險,你無法確定你所選擇的內存單元是否被其他程序占用。
比如以下程序
assume cs:code
code segment
mov ax,0
mov ds,ax
mov ds:[26h],ax
mov ax,4c00h
int 21h
code ends
end
運行到mov ds:[26h],ax
會造成系統死機(dosbox會卡死,無法再輸入操作)。可見,在不能確定一段內存空間中是否存放着重要的數據或代碼的時候,不能隨意向其中寫入內容。我們是在操作系統的環境中工作,操作系統管理所有的資源,也包括內存。如果我們需要向內存空間寫入數據的話,要使用操作系統給我們分配的空間,而不應直接用地址任意指定內存單元,向里面寫入。
一般情況下00200h——002ffh這段內存單元不包含代碼和數據。
使用段前綴
嘗試將內存ffff:0-ffff:b單元中的數據復制到0:20-0:20b單元中。
在四個段前綴中,DS指向數據段,SS指向堆棧段,CS指向代碼轉。
ffff:0和0:20相差超過64kb,無法用同一個段前綴表示,想要實現復制,可以把DS反復賦值為FFFFh和0020h,這樣做明顯不聰明,注意到,四個段前綴中我們只使用了三個,還有ES擴展段沒有用到。
assume cs:code
code segment
mov ax,Offffh
mov ds,ax
mov ax,0020h、
mov es,ax
mov bx,0
mov cx,12
s:mov dl,[bx]
mov es:[bx],dl
inc bx
loop s
movax,4c00h
int 21h
code ends
end
多個段的程序
前面的程序中,只有一個代碼段。現在有一個問題是,如果程序需要用其他空間來存放數據,使用哪里呢?前面,我們講到要使用一段安全的空間。可哪里安全呢我們說0:200-0:2FF是相對安全的,可這段空間的容量只有256個字節,如果我們需要的空間超過256個字節該怎么辦呢?
在操作系統的環境中,合法地通過操作系統取得的空間都是安全的,因為操作系統不會讓一個程序所用的空間和其他程序以及系統自己的空間相沖突。在操作系統允許的情況下,程序可以取得任意容最的空間。
程序取得所需空間的方法有兩種,一是在加載程序的時候為程序分配,再就是程序在執行的過程中向系統申請。加載程序的時候為程序分配空間,我們在前面已經有所體驗,比如我們的程序在加載的時候,取得了代碼段中的代碼的存儲空間。
考慮一個問題,實現多個在內存中的數據累加,結果保存在ax中:
數據為0123h、0456h、0789h、0abch、0defh、0fedh、0cbah、0987h
可以用下面這段代碼實現
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
mov ax,0
mov bx,0
mov cx,8
s:add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
code ends
end
在上面的這段代碼中,我們將需要用的數據保存在了代碼段中,debug上面的程序可以驗證。但這樣會有一個問題, IP默認指向CS段開始的數據,會錯誤的將我們上面的數據當做是代碼去執行。為了解決這一問題,一種做法是用start偽指令強制標識程序開始。(dw 也是偽指令,是告訴編譯器,分配一個word=2byte的空間來保存一個數據,上面的8個數據,在內存中占用了16byte=8word,除了dw 外還有db申請單byte,dd用來申請雙字數據2word=4byte)
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
start: mov ax,0
mov bx,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的作用。end除了通知編譯器程序結束外,還可以通知編譯器程序的入口在什么地方。在程序中我們用end指令指明了程序的入口在標號start處,也就是說,"mov ax,0"是程序的第一條指令。
下面嘗試一下在代碼段中使用棧,實現儲存在CS:0—F的數據0123h、0456h、0789h、0abch、0defh、0fedh、0cbah、0987h,逆序存放。一種在代碼段中使用棧的操作是,dw出一塊空的區域,用來當做堆棧。
可以用下面這段代碼實現:
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
dw 0,0,0,0,0,0,0,0
start: mov ax,cs
mov ss,ax
mov sp,20h
mov bx,0
mov cx,8
s: push cs:[bx]
add bx,2
loop s
mov bx,0
mov cx,8
s1: pop cs:[bx]
add bx,2
loop s1
mov ax,4c00h
int 21h
code ends
end start
在前面的內容中,我們在程序中用到了數據和棧,將數據、棧和代碼都放到了一個段里面。我們在編程的時候要注意何處是數據,何處是棧,何處是代碼。這樣做顯然有兩個問題:
- 把它們放到一個段中使程序顯得混亂;
- 面程序中處理的數據很少,用到的棧空間也小,加上沒有多長的代碼,放到一個段里面沒有問題。但如果數據、棧和代碼需要的空間超過64KB,就不能放在一個段中(一個段的容量不能大於64KB,是我們在學習中所用的8086模式的限制,並不是所有的處理器都這樣)。
所以,應該考慮用多個段來存放數據、代碼和棧。我們用和定義代碼段一樣的方法來定義多個段,然后在這些段里面定義需要的數據,或通過定義數據來取得棧空間。具體做法如下面的程序所示,這個程序實現了和上面程序一樣的功能,
不同之處在於它將數據、棧和代碼放到了不同的段中。
assume cs:code,ss:stack,ds:data
data segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends
stack segment
dw 0,0,0,0,0,0,0,0
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,20h
mov ax,data
mov ds,ax
mov bx,0
mov cx,8
s: push [bx]
add bx,2
loop s
mov bx,0
mov cx,8
s1: pop [bx]
add bx,2
loop s1
mov ax,4c00h
int 21h
code ends
end start
更靈活的定位內存地址的方法
AND和OR指令,這里短暫介紹下,有計算機基礎的都會很容易想到,這2個指令的作用,直接給例子
mov al, 011OOO11B
and al, 00111011B
;執行后:al = 00100011B
;可將操作對象的相應位設為,其他位不變
mov al,01100011B
or al,00111011B
;執行后:a1 =01111011B
;通過該指令可將操作對象的相應位設為1,其他位不變。
任何數據在計算機中都是二進制儲存的,字符也不例外,這里說一下ASCII碼在計算機中的儲存。世界上有很多編碼方案,有一種方案叫做ASCII編碼,是在計算機系統中通常被采用的。簡單地說,所謂編碼方案,就是一套規則,它約定了用什么樣的信息來表示現實對象。比如說,在ASCII編碼方案中,用61H表示"a",62H表示"b"。一種規則需要人們遵守才有意義。
一個文本編輯過程中,就包含着按照ASCII編碼規則進行的編碼和解碼。在文本編輯過程中,我們按一下鍵盤的a鍵,就會在屏幕上看到"a"。這是怎樣一個過程呢?我們按下鍵盤的a鍵,這個按鍵的信息被送入計算機,計算機用ASCII碼的規則對其進行編碼,將其轉化為61H存儲在內存的指定空間中;文本編輯軟件從內存中取出61H,將其送到顯卡上的顯存中。
工作在文本模式下的顯卡,用ASCII碼的規則解釋顯存中的內容,61H被當作字符"a"'顯卡驅動顯示器,將字符"a"的圖像畫在屏幕上。我們可以看到,顯卡在處理文本信息的時候,是按照ASCII碼的規則進行的。這也就是說,如果我們要想在顯示器上看到"a"'就要給顯卡提供"a"的ASCII碼,61H。如何提供?當然是寫入顯存中。
下面給出如何保存字符串在數據段中:
assume cs:code,ss:stack,ds:data
data segment
db 'unIX'
db 'foRK'
data ends
code segment
start: mov ax,'a'
mov ax,4c00h
int 21h
code ends
end start
在上面的代碼中,db 'unIX'
相當於db 75H,6EH,49H,58H
,分別對應了每個字符的ascii碼。mov ax,'a'
相當於mov ax,61H
。
通過上面的知識儲備,已經可以實現字符串的大小寫轉換了,可能突然有點懵,但確實如此,仔細一想自己的變成經歷,你會發現當初做大小寫轉換,用到過'A'和'a'之間正好差了32=2^5,也就是二進制的第六位。'A' = 41H(0100 0001),'a' = 61H(0110 0001),所以只要檢查二進制的第6位就可以確定是大寫還是小寫字母。
把第一行數據變為大寫,第二行變成小寫的程序
assume cs:code,ds:data
data segment
db 'BaSic'
db 'iNfOrMaTiOn'
data ends
code segment
start: mov ax,data
mov ds,ax
mov cx,5
mov bx,0
s: mov al,[bx]
and al,11011111B
mov [bx],al
inc bx
loop s
mov cx,11
s1: mov al,[bx]
or al,00100000B
mov [bx],al
inc bx
loop s1
mov ax,4c00h
int 21h
code ends
end start
對內存操作除了前面說到的立即數(idata)和[bx]之外,還有很多其他的方式,下面一一介紹:
- [bx+idata]在bx的基礎上加上一個立即數對應的地址
對前面的字符串大小寫轉換,如果兩個字符串長度是相同的,可以用[bx+idata]實現一個循環就可解決。
assume cs:code,ds:data
data segment
db 'BaSic'
db 'iNfOr'
data ends
code segment
start: mov ax,data
mov ds,ax
mov cx,5
mov bx,0
s: mov al,[bx]
and al,11011111B
mov [bx],al
mov al,[bx+5]
or al,00100000B
mov [bx+5],al
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end start
- SI和DI
SI和DI是8086CPU中和bx功能相近的寄存器,SI和DI不能夠分成兩個8位寄存器來使用。下面的3組指令實現了相同的功能。
mov bx,0
mov ax,[bx]
mov si,0
mov ax,[si]
mov di,0
mov ax,[di]
下面的3組指令也實現了相同的功能
mov bx,0
mov ax,[bx+123]
mov si,0
mov ax,[si+123]
mov di,0
mov ax,[di+123]
- [bx+si]或[bx+di]
在前面,我們用[bx(si或di)]和[bx(si或di)+idata]的方式來指明一個內存單元,我們還可以用更為靈活的方式:[bx+si]和[bx+di
[bx+si]和[bx+di]的含義相似,我們以[bx+si]為例進行講解。[bx+si]表示一個內存單元,它的偏移地址為(bx)+(si)(即bx中的數值加上si中的數值)。
指令mov ax,[bx+si]
的含義如下:
將一個內存單元的內容送入ax,這個內存單元的長度為2字節(字單元),存放一個字,偏移地址為bx中的數值加上si中的數值,段地址在ds中。該指令也常被寫作mov ax,[bx][si]
。
- [bx+si+idata]和[bx+di+idata]
相當於在上面的基礎是又加了立即數。
mov ax,[bx+200+si]
mov ax,[200+bx+si]
mov ax,200[bx][si]
mov ax,[bx].200[si]
mov ax,[bx][si].200
一個使用尋址小試驗
介紹了這么多的尋址方式,現在嘗試通過這些來實現一個任務,盡可能用最少的代碼:
在數據段中保存了這些字符串,每個字符串長16Byte,嘗試把他們的前4個字母變成大寫。
db '1. display '
db '2. brows '
db '3. replace '
db '4. modify '
分析一下問題,有4個字符串,每個字符串需要修改4位,需要循環嵌套使用,循環依靠cx中的值實現控制次數,如果我們單純的直接從外循環進入內循環,沒有保存進入內循環前的cx值,等到內循環結束繼續外循環會發現進行不下去了(退出內循環的條件是cx=0,若為保存進入內之前的cx值,外循環只進行一次),所以進入內循環前cx的值需要保存,那么保存到哪里?在簡單的情況下,可以直接保存在寄存器中,當所有的寄存器都被用了的時候很這個辦法行不通,一個好的方法是使用堆棧去保存cx,使用棧出入不需要其他的寄存器,只用到cx。
觀察可以發現第一個字母都在第4個byte,下標為3,所以我們用bx保存每個字符串的開頭,si指向要修改的第幾個字母,用立即數3修正下標。
assume cs:code,ss:stack,ds:data
data segment
db '1. display '
db '2. brows '
db '3. replace '
db '4. modify '
data ends
stack segment
dw 0,0,0,0,0,0,0,0
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,60h
mov ax,data
mov ds,ax
mov bx,0
mov cx,4
s: push cx
mov cx,4
mov si,0
s1: mov al,[bx+si+3]
and al,11011111B
mov [bx+si+3],al
inc si
loop s1
add bx,16
pop cx
loop s
mov ax,4c00h
int 21h
code ends
end start
總結
- bx,si,di,bp
前 3 個寄存器我們已經用過了, 現在我們進行一下總結。在 8086CPU 中, 只有這4 個寄存器可以用在"[...]" 中來進行內存單元的尋址。比如下面的指令都是正確的:
mov ax , [ bx ]
mov ax , [ bx +si]
mov ax , [ bx +di]
mov ax , [ bp ]
mov ax , [ bp +si]
mov ax , [ bp+di ]
而下面的指令是錯誤的:
mov a x , [ cx ]
mov a x , [ ax ]
mov a x , [ dx ]
mov a x , [ ds ]
在[...]中,這4個寄存器可以單個出現,或只能以4種組合出現:bx和si、bx和di、bp和si、bp和di。比如下面的指令是正確的:
mov ax,[bx]
mov ax,[si]
mov ax,[di]
mov ax,[bp]
mov ax,[bx+si]
mov ax,[bx+di]
mov ax,[bp+si]
mov ax,[bp+di]
mov ax,[bx+si+idata]
mov ax,[bx+di+idata]
mov ax,[bp+si+idata]
mov ax,[bp+di+idata]
下面的指令是錯誤的:
mov ax,[bx+bp]
mov ax,[si+di]
只要在[...]中使用寄存器bp,而指令中沒有顯性地給出段地址,段地址就默認在ss中。
- 處理數據的位置
絕大部分機器指令都是進行數據處理的指令,處理大致可分為3類:讀取、寫入、運算。在機器指令這一層來講,並不關心數據的值是多少,而關心指令執行前一刻,它將要處理的數據所在的位置。指令在執行前,所要處理的數據可以在3個地方:CPU內部、內存、端口(端口將在后面的課程中進行討論)。
- 匯編語言中數據位置的表達
mov ax,10h ;立即數
mov ax,bx ;寄存器
mov ax,[bx] ;內存
- 尋址方式
尋址方式 | 名稱 |
---|---|
mov ax, [idata] | 直接尋址 |
-------------- | ------------- |
mov ax, [bx] | 寄存器間接尋址 |
mov ax, [si] | 寄存器間接尋址 |
mov ax, [di] | 寄存器間接尋址 |
mov ax, [bp] | 寄存器間接尋址 |
-------------- | ------------- |
mov ax, [bx+idata] | 寄存器相對尋址 |
mov ax, [si+idata] | 寄存器相對尋址 |
mov ax, [di+idata] | 寄存器相對尋址 |
mov ax, [bp+idata] | 寄存器相對尋址 |
-------------- | ------------- |
mov ax, [bx+si] | 基址變址尋址 |
mov ax, [bx+di] | 基址變址尋址 |
mov ax, [bp+si] | 基址變址尋址 |
mov ax, [bp+di] | 基址變址尋址 |
-------------- | ------------- |
mov ax, [bx+si+idata] | 相對基址變址尋址 |
mov ax, [bx+di+idata] | 相對基址變址尋址 |
mov ax, [bp+si+idata] | 相對基址變址尋址 |
mov ax, [bp+di+idata] | 相對基址變址尋址 |
- 指令要處理的數據有多長
有三種方式:
mov ax,[1834] ;通過寄存器指定大小,ax為2byte
mov al,[1834] ;通過寄存器指定大小,al為1byte
mov word ptr ds:[0], 1 ;word ptr指定是字操作
mov byte ptr ds:[0], 1 ;word ptr指定是byte操作
push ax ;這種指令指定了數據長度為word
- div指令
div是除法指令,使用div做除法的時候應注意以下問題。
(1)除數:有8位和16位兩種,在一個reg或內存單元中。
(2)被除數:默認放在AX或DX和AX中,如果除數為8位,被除數則為16位,默認在AX中存放;如果除數為16位,被除數則為32位,在DX和AX
中存放,DX存放高16位,AX存放低16位。
(3)結果:如果除數為8位,則AL存儲除法操作的商,AH存儲除法操作的余數;如果除數為16位,則AX存儲除法操作的商,DX存儲除法操作的余數。
32位的被除數可以用dd
來申請。
- dup操作符
dup也是一個操作符,和dd,dw,db一樣由編譯器識別處理,他和dd,dw,db一起使用,用來實現數據的重復。
db 3 dup(0) ; 相當於db 0,0,0
db 3 dup(1,2); 相當於db 1,2,1,2,1,2
;像之前去申請堆棧一樣
dw 8 dup(0) ; 相當於dw 0,0,0,0,0,0,0,0,0