具体的配置步骤可以参考:
汇编环境搭建 Windows10 VS2019 MASM32
本文主要是入门向的教程,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函数的原型,它的参数是一个指向字符串的指针。
.data
与.code
就如同他们的英文名字一样直接明了,数据段和代码段。
在汇编中要想使用printf,需要使用INVOKE指令。ADDR你可以理解成给参数赋值,ADDR表明了输出字符串的内存地址。特别注意:该指令会破坏eax,ecx,edx寄存器的值
hello byte "hello world !",0Ah, 0
,你可能比较疑惑0Ah是干啥的,它其实就是\n
,最后面跟着个0表示字符串到此结束(你肯定在C语言里学到过)。hello是变量名,你可以换成你喜欢的名字。不过汇编里面变量名是不区分大小写的
endp
表示过程(procduce)的结束,end
表示程序的结束.
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)中的值。
使用堆栈与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
指令算是一种折中的方法。
字符串
前面铺垫了那么多,终于到字符串了。
它也是数组
先来个朴实无华的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指令会对寄存器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的值相加。