具體的配置步驟能夠參考:
彙編環境搭建 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
字符串怎麼辦?其實這玩意就是個數組,讓咱們來看看如何操做數組吧
.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就能夠了,這樣別人看你代碼時也能更清楚你都作了些什麼。
要想偷懶的話可使用pushad
和popad
來保存和恢復寄存器(eax,ecx,edx)中的值。
交換兩數在高級語言之中通常這樣寫:
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
指令算是一種折中的方法。
前面鋪墊了那麼多,終於到字符串了。
先來個樸實無華的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
.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 | 不區分大小寫時,若是不相同 |