組合語言-基礎功能

W&B 發表於 2021-05-03

組合語言-基礎功能

在之前我們見過了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

如果想解決這個問題我們有兩種選擇:

  1. 就是像上面的迴圈程式一樣,把bx賦值,再用mov ax,[bx]
  2. 顯式的給出段字首,如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

在前面的內容中,我們在程式中用到了資料和棧,將資料、棧和程式碼都放到了一個段裡面。我們在程式設計的時候要注意何處是資料,何處是棧,何處是程式碼。這樣做顯然有兩個問題:

  1. 把它們放到一個段中使程式顯得混亂;
  2. 面程式中處理的資料很少,用到的棧空間也小,加上沒有多長的程式碼,放到一個段裡面沒有問題。但如果資料、棧和程式碼需要的空間超過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]之外,還有很多其他的方式,下面一一介紹:

  1. [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
  1. 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]
  1. [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]

  1. [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

總結

  1. 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中。

  1. 處理資料的位置

絕大部分機器指令都是進行資料處理的指令,處理大致可分為3類:讀取、寫入、運算。在機器指令這一層來講,並不關心資料的值是多少,而關心指令執行前一刻,它將要處理的資料所在的位置。指令在執行前,所要處理的資料可以在3個地方:CPU內部、記憶體、埠(埠將在後面的課程中進行討論)。

  1. 組合語言中資料位置的表達
mov ax,10h     ;立即數
mov ax,bx  	   ;暫存器
mov ax,[bx]	   ;記憶體
  1. 定址方式
定址方式 名稱
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] 相對基址變址定址
  1. 指令要處理的資料有多長
    有三種方式:
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
  1. 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來申請。

  1. 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