具体的配置步骤可以参考:

汇编环境搭建 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就可以了,这样别人看你代码时也能更清楚你都做了些什么。

要想偷懒的话可以使用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指令算是一种折中的方法。

字符串

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

它也是数组

先来个朴实无华的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 不区分大小写时,如果不相同

在VS2019使用MASM编写汇编程序的更多相关文章

  1. 用Visual Studio 2015 编写 MASM 汇编程序(二)从头开发一个Win32汇编程序

    一,建立一个VC的控制台类型的空工程: 1,从VS菜单中选择“文件”->“新建”->“项目”. 2,在新建项目中选择:“Visual c++”->"Win32"- ...

  2. win10编写8086汇编程序(dosbox)

    有部分同学反馈.在使用edit命令来编写汇编程序时遇到问题,由于模拟器没有edit程序,所以要换一种方式编写源程序.下面是完整的演示. 视频链接:http://www.bilibili.com/vid ...

  3. 汇编:采用址表的方法编写程序实现C程序的switch功能

    //待实现的C程序 1 void main() { ; -) { : printf("excellence"); break; : printf("good") ...

  4. 32位汇编第三讲,RadAsm,IDE的配置和使用,以及汇编代码注入方式

    32位汇编第三讲,RadAsm,IDE的配置和使用,以及汇编代码注入方式 一丶RadAsm的配置和使用 用了怎么长时间的命令行方式,我们发现了几个问题 1.没有代码提醒功能 2.编写代码很慢,记不住各 ...

  5. 用户手册是Yasm汇编

    本文档的用户手册是Yasm汇编. 它是介绍和通用所有Yasm用户参考. 英文的参考:http://www.cnblogs.com/coryxie/p/3959888.html 1 .介绍 Yasm b ...

  6. [抓紧小长假的尾巴] 分析一个KeyFileMe

    系统 : Windows xp 程序 : keyfileme 程序下载地址 :http://pan.baidu.com/s/1qYVfvu0 要求 : 编写KeyFile 使用工具 : OD 可在看雪 ...

  7. HLA高级汇编语言基础

    HLA高级汇编语言环境的搭建与设置 我的操作系统:WINDOWS7 需要下载的东西:MASM32:http://www.masm32.com/masmdl.htm  HLA:http://webste ...

  8. WinHex分析PE格式(1)

    最近在一直努力学习破解,但是发现我的基础太差了,就想学习一下PE结构.可是PE结构里的结构关系太复杂,看这老罗的WiN32汇编最后一章 翻两页又合上了..把自己的信心都搞没了.感觉自己的理解能力不行, ...

  9. 《深入浅出嵌入式底层软件开发》—1. ARM汇编编程基础

    1.1 ARM CPU寄存器 ARM的汇编编程,本质上就是针对CPU寄存器的编程,所以要搞清楚ARM有哪些寄存器:ARM寄存器分为两类:普通寄存器和状态寄存器:普通寄存器一共有16个,分别为R0——R ...

随机推荐

  1. 第3篇 Scrum 冲刺博客

    1.站立会议 照骗 进度 成员 昨日完成任务 今日计划任务 遇到的困难 钟智锋 确定客户端和服务器通信的形式 重新设计项目执行流程 我的规划过于混乱,对应难以同步开发 庄诗楷 绘制棋盘 游戏窗口的制作 ...

  2. Lua语言15分钟快速入门

    转载自: https://blog.csdn.net/qq_15437667/article/details/75042526 -- 单行注释 --[[ [多行注释] --]] ---------- ...

  3. JS事件——添加、移除事件

    element.addEventListener(event, function, useCapture) 方法用于向指定元素添加事件句柄.   event: 必须.字符串,指定事件名.注意: 例 使 ...

  4. JMH-大厂是如何使用JMH进行Java代码性能测试的?必须掌握!

    Java 性能测试难题 现在的 JVM 已经越来越为智能,它可以在编译阶段.加载阶段.运行阶段对代码进行优化.比如你写了一段不怎么聪明的代码,到了 JVM 这里,它发现几处可以优化的地方,就顺手帮你优 ...

  5. Vue企业级优雅实战-00-开篇

    从2018.1.开始参与了多个企业的中台建设,这些中台的技术选型几乎都是基于 Spring Cloud 微服务架构 + 基于 Vue 全家桶的前端.我前后端架构及开发我几乎各占一半的精力,在企业级前端 ...

  6. js对象数组新增、修改时的验证是否重复的逻辑

    JS代码: // 定义数据集合 const persons = [ { id: 1, name: '张三' }, { id: 2, name: '李四' } ] console.log('') con ...

  7. k8s部署mysql主从复制

    Mysql主从 准备环境 一,准备软件 官方docker_image :Mysql5.7.28 Docker Version:        19.03.4 K8s api-version:      ...

  8. docker快速搭建php7.2-nginx开发环境

    1.输入命令: docker search -s 100 php 搜索出下面图中列表,选择webdevops/php-nginx. 2.通过docker拉取webdevops/php-nginx镜像, ...

  9. 英文ubuntu中的乱码,输入法问题 、mint字体发虚

    英文ubuntu文本文件默认编码是utf-8,windows下是gbk,所以产生乱码问题. 1.前言 运行命令查看系统编码 $locale 结果如下: LANG=en_US.UTF-8 LANGUAG ...

  10. springboot入门遇到Whitelabel Error Page错误

    错误页面: 解决方法: 启动类要放在最外层,改成下面的