• 在VS2019使用MASM编写汇编程序


    具体的配置步骤可以参考:
    汇编环境搭建 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是干啥的,它其实就是 ,最后面跟着个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("
    %s%d
    ","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
    ",numary[i]);
        sum += numary[i];
    }
    printf("%d
    ",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 不区分大小写时,如果不相同
  • 相关阅读:
    mapreduce 函数入门 二
    mapreduce 函数入门 一
    Flume+Kafka+Storm+Redis 大数据在线实时分析
    mapReduce 大数据离线分析
    docker 简介
    flume安装使用+根据数据源分类
    hiho 171周
    如何新建一个空的optix工程
    读 Real-Time Rendering 收获
    hiho 1590
  • 原文地址:https://www.cnblogs.com/AD-milk/p/13711369.html
Copyright © 2020-2023  润新知