在VS2019使用MASM編寫彙編程序

具體的配置步驟能夠參考:
彙編環境搭建 Windows10 VS2019 MASM32c++

本文主要是入門向的教程,VS2019中要調用C語言函數須要加上編程

includelib ucrt.lib
includelib legacy_stdio_definitions.lib

輸出

配置好了環境以後,讓咱們開始第一個彙編程序吧數組

.686
.MODEL flat, c
.stack 100h

includelib ucrt.lib
includelib legacy_stdio_definitions.lib

;Function prototypes 
printf PROTO  arg1:PTR byte

.data
hello  byte "hello world !",0Ah, 0	;聲明變量

.code  
main   proc
       invoke printf, ADDR hello	;調用printf函數打印變量
       ret				;至關於return 0
main   endp  
end    main

.686是指明使用的指令集,向下兼容,.model flat,c中的flat表示程序使用保護模式,c表示能夠和c/c++進行鏈接。.stack以十六進制的形式聲明堆棧大小,這幾句先照抄就好。函數

若是要調用C函數記得把上面說的兩個lib加上,printf proto這句話是指明printf函數的原型,它的參數是一個指向字符串的指針。.net

.data.code就如同他們的英文名字同樣直接明瞭,數據段和代碼段。prototype

在彙編中要想使用printf,須要使用INVOKE指令。ADDR你能夠理解成給參數賦值,ADDR代表了輸出字符串的內存地址。特別注意:該指令會破壞eax,ecx,edx寄存器的值指針

hello byte "hello world !",0Ah, 0,你可能比較疑惑0Ah是幹啥的,它其實就是\n,最後面跟着個0表示字符串到此結束(你確定在C語言裏學到過)。hello是變量名,你能夠換成你喜歡的名字。不過彙編裏面變量名是不區分大小寫的code

endp表示過程(procduce)的結束,end表示程序的結束.blog

ret等同於return 0教程

整個程序若是用C來寫至關於

#include<stdio.h>
int main()
{
    printf("hello world !");
    return 0;
}

輸入

學會了輸出天然也得把輸入學會,請看下面的代碼:

.686
.MODEL flat, c
.stack 100h

includelib ucrt.lib
includelib legacy_stdio_definitions.lib

printf  PROTO  arg1:PTR byte, printlist:vararg
scanf   PROTO  arg2:ptr byte, inputlist:vararg

.data
in1fmt  byte "%d",0
msg1fmt byte 0Ah,"%s%d",0Ah,0
msg1    byte "the number is ",0
number  sdword ?

.code
main    proc
        invoke scanf, ADDR in1fmt, ADDR number	;scanf必須都加addr,相似於&
        invoke printf, ADDR msg1fmt, ADDR msg1, number
        ret
main    endp  
end     main

看着有點恐怖?對照C語言程序看一下吧

#include<stdio.h>
int main()
{
    int number;
    scanf("%d",&number);
    printf("\n%s%d\n","the number is ",number);
    return 0;
}

這段程序大致跟以前的差很少,只不過多了幾張新面孔。

.686
.model	flat, c
.stack	100h

includelib ucrt.lib
includelib legacy_stdio_definitions.lib

printf	proto arg1:ptr byte, printlist:vararg
scanf	proto arg2:ptr byte, inputlist:vararg

.data
in1fmt	byte "%d",0
msg1fmt	byte "%s%d",0Ah,0
msg1	byte "x: ",0
msg2	byte "y: ",0
x	sdword ?
y	sdword ?

.code
main	proc
	invoke scanf,ADDR in1fmt, ADDR x
	invoke printf,ADDR msg1fmt, ADDR msg1, x
	mov eax,x
	mov y,eax
	invoke printf,ADDR msg1fmt, ADDR msg2, y 
	ret
main	endp
	end    main
#include<stdio.h>
int main()
{
    int x,y;
    scanf("%d",&x);
    printf("x: %d",x );
    y=x;
    printf("y: %d",y);
    return 0;
}

對比上面兩段代碼你發現了什麼嗎?在C語言裏面,把x賦值給y只須要一句話,但在彙編裏面卻不能這樣作。由於數據不能直接從一個內存單元到另一個內存單元去,只能是經過寄存器完成相關操做。RAM中的數據先要被裝載到CPU中,再由CPU將其存到目的內存單元中。

若是是字符怎麼辦?方法跟是同樣的,只不過這裏只須要使用eax的低8位al便可。

.data
char1	byte	?
char2	byte	?
	.code
	mov char1,'A'
	mov al,char1
	mov char2,al

寄存器eax

字符串怎麼辦?其實這玩意就是個數組,讓咱們來看看如何操做數組吧

循環與數組

它們倆但是好兄弟

.data
numary	sdword	2,3,4
zeroary	sdword	3 dup(0)
empary	sdword	3 dup(?)

要想遍歷數組,循環結構是必不可少的。

for(int i=0;i<3;i++)
{
    printf("%d\n",numary[i]);
    sum += numary[i];
}
printf("%d\n",sum);

這段C語言代碼用匯編來寫是這樣的

.686
.model	flat, c

includelib ucrt.lib
includelib legacy_stdio_definitions.lib

printf	proto arg1:ptr byte, printlist:vararg

.data
msg1fmt	byte	"%d",0ah,0	;還記得吧?0ah表示換行

numary	sdword	2,5,7
sum	sdword	?

.code
main	proc 
	mov	sum,0
	mov	ecx,3
	mov	ebx,0
	.repeat	

	push	eax
	push	ecx
	push	edx

	invoke	printf,addr msg1fmt, numary[ebx]
		
	pop	edx
	pop	ecx
	pop	eax	

	mov	eax,numary[ebx]
	add	sum,eax
	add	ebx,4	;由於是雙字,4個字節
		
	.untilcxz
	invoke	printf,addr msg1fmt, sum
	ret
main	endp
	end	main

.repeat-.untilcxz該指令對作的事情就是每次循環都把ecx的值減一,直到它爲0。這裏有一個特別坑的地方:只能有126字節的指令包含在.repeat-.untilcxz循環體內,多了會報錯。

另外還有注意的是,千萬不要讓ecx值爲0進入.repeat-.untilcxz循環體,由於執行到.untilcxz語句時,ecx的值會先減1再與0比較是否相等。這就出大麻煩了,ecx的值如今爲負數,雖然不會死循環,但程序要循環40億次才能停下來。(一直減到-2147483648,下一次減一獲得的結果纔是一個正數2137483647)

鑑於上訴狀況,仍是用.while來寫循環結構比較好

;前置檢測循環while(i<=3)
mov	i,1
.while (i<=3)
inc	i	;i+=1
.endw	;循環體結束

;後置檢測循環do while
mov i,1
.repeat
inc
.untile (i>3)

棧的做用

上面那個打印數組的程序中爲何還用到了push指令?*由於invoke指令會破壞eax,ecx,edx寄存器的值,程序還須要ecx控制循環,因此在調用invoke指令以前須要利用棧將被破壞的ecx賦回原來的值,保證循環正確運行。

固然你也不須要一股腦push這麼多,上面的例子其實只須要push ecx就能夠了,這樣別人看你代碼時也能更清楚你都作了些什麼。

要想偷懶的話可使用pushadpopad來保存和恢復寄存器(eax,ecx,edx)中的值。

使用堆棧與xchg指令來實現數據交換

交換兩數在高級語言之中通常這樣寫:

temp=num1
num1=num2
num2=temp

對應到我們彙編,簡短點寫法是:

mov	eax,num1
mov	edx,num2
mov	num1,edx
mov	num2,eax

不過這裏用到了兩個寄存器,還有沒有別的比較好的辦法呢?

固然是有的,可不就是我們的標題嘛

push	num1;將num1壓棧
push	num2;將num2壓棧
pop	num1;將出棧的元素(num2)賦值給num1
pop	num2;將出棧的元素(num1)賦值給num2

;利用echg指令
mov	eax,num1
xchg	eax,num2
mov	num1,eax

搞這麼麻煩,直接xchg num1,num2不就行了嗎?

若是你這麼想就大錯特錯了!由於:數據不能直接從一個內存單元到另一個內存單元去,咱們必須藉助寄存器的幫助。

上訴三種方法中mov指令是最快的,但須要用到兩個寄存器;堆棧是最慢的,但無需使用寄存器;使用xchg指令算是一種折中的方法。

xchg指令交換兩數

字符串

前面鋪墊了那麼多,終於到字符串了。

它也是數組

先來個樸實無華的hello world

.686
.model	flat, c

includelib ucrt.lib
includelib legacy_stdio_definitions.lib

printf	proto arg1:ptr byte, printlist:vararg

.data
msg1fmt	byte	"%s",0Ah,0

string1	byte	"Hello World!",0
string2	byte	12	dup(?),0

.code
main	proc 
	mov	ecx,12
	mov	ebx,0
	.repeat
	mov	al,string1[ebx]
	mov	string2[ebx],al
	inc	ebx
	.untilcxz
	invoke	printf,addr msg1fmt,addr string2
	ret
main	endp
	end	main

使用寄存器esi和edi進行索引

.686
.model	flat, c

includelib ucrt.lib
includelib legacy_stdio_definitions.lib

printf	proto arg1:ptr byte, printlist:vararg

.data
msg1fmt	byte	"%s",0Ah,0

string1	byte	"Hello World!",0
string2	byte	12	dup(?),0

.code
main	proc 
	mov	ecx,12
	lea	esi,string1	;將string1的地址裝載到esi
	lea	edi,string2	;將string2的地址裝載到edi
	.repeat
	mov	al,[esi]	;將esi所指向的地址中的內容放入al
	mov	[edi],al	;將al中的內容放入edi所指向的地址
	inc	esi		;將esi中的內容加1
	inc	edi		;將esi中的內容加1
	.untilcxz
	invoke	printf,addr msg1fmt,addr string2
	ret
main	endp
	end	main

字符串複製

當循環體中指令第一次執行時,esi和edi分別指向String1和String2的首地址。第二次執行時,esi和edi以及分別遞增長1,esi所指00000101地址處的e會被複制到edi所指的0000010D地址中去。以後ecx減1,esi,edi遞增,指向下一個字節處。

movsb指令能夠幫助咱們簡化程序,它可用於完成單字節字符串的移動工做:首先將esi所指的字節內容複製到edi所指向的地址,接着將ecx的值減1,同時對esi和edi指向遞增或遞減操做。

雖然它是單字節移動指令,但與循環結構配合可以發揮出強大的做用。以前的代碼咱們能夠改寫成

.686
.model	flat, c

includelib ucrt.lib
includelib legacy_stdio_definitions.lib

printf	proto arg1:ptr byte, printlist:vararg

.data
msg1fmt	byte	"%s",0Ah,0

string1	byte	"Hello World!",0
string2	byte	12      dup(?),0

.code
main	proc 
	mov	ecx,12
	mov	esi,offset     string1+0	;將string1地址的值加0放入esi中
	mov	edi,offset     string2+0	;將string2地址的值加0放入edi中
	cld					;方向標誌值清零
	.repeat
	movsb
	.untilcxz
	invoke	printf,addr msg1fmt,addr string2
	ret
main	endp
	end	main

若是想要將esi和edi中的值都遞減,那麼須要將cld指令換成std指令。

字符串數組

如何複製一個字符串數組?能夠將其當作一個大字符串,這樣使用兩個循環:一個用於控制字符串數組,另外一個用於處理字符串中的每個數組,便可複製該字符串數組。

.686
.model	flat, c

includelib ucrt.lib
includelib legacy_stdio_definitions.lib

printf	proto arg1:ptr byte, printlist:vararg

.data
msg1fmt	byte	"%s",0Ah,0

names1	byte	"Abby","Fred","John","Kent","Mary"
names2	byte	20	dup(?)

.code
main	proc 
		mov	ecx,5
		lea	esi,names1	
		lea	edi,names2
		cld
		.repeat
		push	ecx	;保存寄存器ecx的值
		mov	ecx,4
		rep	movsb	;重複執行movsb直到ecx爲0
		pop	ecx	;恢復寄存器ecx的值
		.untilcxz
		invoke	printf,addr msg1fmt,addr names2
		ret
main	        endp
		end	main
前綴 意義
rep 重複操做
repe 若是相等,則重複操做
repne 若是不相等,則重複操做

前綴rep指令會對寄存器ecx的值進行遞減直到它爲0,因此程序中使用了堆棧來保護用於控制循環的ecx的值。

過程

過程又被稱爲子程序,函數。

call指令能夠用於調用過程:

call pname

以前程序裏的main就是一個過程,過程的具體格式以下

pname	proc
	;過程體
	ret
pname	endp

雖然過程的調用與返回要比直接在主程序中編寫代碼效率低,但由於相關的代碼只須要寫一次,因此節省了內存空間。

編寫過程時,最好對eax,ecx,edx進行保存恢復工做,這樣能方便須要用到這些寄存器的程序調用該過程。

宏的聲明須要放在.code以後main過程以前

mname	macro
	;宏體
	endm

宏的調用不須要call指令,你能夠就把它當成一條指令來使用。

使用堆棧與xchg指令來實現數據交換這一標題下提到的程序能夠用宏改寫爲

.code
swap	macro	p1:REQ,p2:REQ	;; :REG表示參數是必須的
	mov	ebx,p1		;;使用雙分號進行註釋,這段註釋不會在後續的宏擴展中出現
	xchg	ebx,p2
	mov	p1,ebx
	endm
main 	proc
	swap	eax,ebx
main	endp
	end	main

判斷與條件彙編

在彙編中,if語句與C語言中的沒太大區別

.if (判斷條件)
.else (判斷條件)
.endif

也支持嵌套if,只要記得用完if以後要在後面有個.endif對應便可

那條件彙編又是什麼東西呢,它與if這類的選擇結構有什麼區別?

.if語句用於控制程序執行流從哪一條路徑執行下去,條件彙編告訴程序是否將一條指令或一段代碼包含到程序中去。

addacc	macro	parm
		ifb	<parm>	;ifb if blank
		inc	eax	;若是缺乏參數就把eax的值加1
		else
		add	eax,parm;至關於eax+=parm
		endif
		endm

若是調用宏addacc時缺乏了參數,eax默認爲1,不然將參數與eax的值相加。

彙編指令 含義
if 若是(可使用EQ,NE,LT,GT,OR...)
ifb 若是爲空
ifnb 若是不爲空
ifidn 若是相同
ifidni 不區分大小寫時,若是相同
ifdif 若是不一樣
ifdifi 不區分大小寫時,若是不相同
相關文章
相關標籤/搜索