具体的配置步骤可以参考:
汇编环境搭建 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将其存到目的内存单元中。

在VS2019使用MASM编写汇编程序-LMLPHP

如果是字符怎么办?方法跟是一样的,只不过这里只需要使用eax的低8位al即可。

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

在VS2019使用MASM编写汇编程序-LMLPHP

字符串怎么办?其实这玩意就是个数组,让我们来看看如何操作数组吧

循环与数组

它们俩可是好兄弟

.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指令算是一种折中的方法。

在VS2019使用MASM编写汇编程序-LMLPHP

字符串

前面铺垫了那么多,终于到字符串了。

它也是数组

先来个朴实无华的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


在VS2019使用MASM编写汇编程序-LMLPHP

当循环体中指令第一次执行时,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的值相加。

09-23 13:16