五、源文件的方式写汇编
在VS Code中搭建汇编环境
1、安装插件
打开VS Code中的扩展栏,并搜索MASM,找到MASM/TASM这个插件,并安装即可:
这个插件会把我们所需要的dosbox, dosbox-x, jsdos以及汇编编译器MASM都安装好,也不需要我们再去挂载之类的操作了。
2、设置插件
选择汇编工具为MASM
DOS环境有多钟选择,这里先选一个熟悉DOSBox,下面两种方式都可以改变DOS的模拟环境
3、编译,链接,运行/调试
下面是一段在屏幕上输出:hello world的汇编
DATA SEGMENT
PRINT DB "Hello World!", 0AH, 0DH, '$'
DATA ENDS
STACK SEGMENT STACK
DW 20 DUP(0)
STACK ENDS
CODE SEGMENT
ASSUME CS:CODE, DS:DATA, SS:STACK
START:
MOV AX, DATA
MOV DS, AX
MOV DX, OFFSET PRINT
MOV AH, 09
INT 21H
MOV AH, 4CH
INT 21H
CODE ENDS
END START
右键,然后点击运行或调试
4、四种DOS模拟环境的区别
- jsdos :类似网页
-
dosbox:单独打开一个窗口
-
dosbox-x:也是单独打开一个窗口
-
msdos player :在终端打开模拟环境
-
总结: MASM和TASM都是汇编编译器,以及不同版本的编译器可能会有细小差异。建议选择:DOSBox+MASM-v6.11。这与前面使用的是同一个东西
-
建议:前期不要太依赖一键编译、链接、运行。只用VS Code写源文件,然后在DOSBox中进行手动编译、链接、运行
编译源文件
编写源文件
在MASM文件夹下创建后缀为.asm的文件,并在里面写上测试的代码:在屏幕输出hello world
DATA SEGMENT
PRINT DB "Hello World!", 0AH, 0DH, '$'
DATA ENDS
STACK SEGMENT STACK
DW 20 DUP(0)
STACK ENDS
CODE SEGMENT
ASSUME CS:CODE, DS:DATA, SS:STACK
START:
MOV AX, DATA
MOV DS, AX
MOV DX, OFFSET PRINT
MOV AH, 09
INT 21H
MOV AH, 4CH
INT 21H
CODE ENDS
END START
使用命令查看创建的asm文件是否存在
编译
-
编译刚刚创建的asm文件。MASM.exe就是编译器,用他来编译文件
masm code // 编译code.asm文件
-
编译成功后会生成obj文件
链接
-
使用LINK.exe链接obj文件
-
链接完成后会生成一个exe文件
运行
输入文件名即可直接运行
debug模式查看exe文件
debug code.exe // 注意:一定要加上exe的后缀名
使用t命令就可以一步步查看和运行代码
源程序
参考如下: 汇编中的六大伪指令-CSDN博客
伪指令
在汇编语言源程序中,包含两种指令,一种是汇编指令,一种是伪指令。
-
汇编指令:有对应的机器码的指令,可以被编译为机器指令,最终为CPU所执行。
-
伪指令:没有对应的机器指令,最终不被CPU所执行。由编译器来执行,编译器根据伪指令来进行相关的编译工作。
标号赋值:EQU
作用:用来对一个标号赋值
格式:标号 EQU 数值
示例:
AAA EQU 00FFH ;对标号AAA赋值为0x00FF
定义存储单元:DB、DW、DD、DQ、DT
作用:用来为一个数据项分配存储单元,用一个符号名(变量名)与这个存储单元相联系,且为这个数据提供一个任选的初始值。
存储单元伪指令 | 空间大小 |
---|---|
DB | 一个字节,8位 |
DW | 一个字,16位 |
DD | 两个字,32位 |
DQ | 四个字,64位 |
DT | 十个字,16位 |
格式:(以DW为例)
-
格式一:
DW 数值1,数值2,...
示例:
DW 1234H,5678H ;默认偏移地址0x0000,存入数值:0x1234、0x4567,共两个字的空间
-
格式二:
标号 DW 数值1,数值2,...
示例:
DATA DW 1234H,5678H ;从地址DATA开始,存入数值:0x1234、0x4567,共两个字的空间
-
格式三:
标号 DW ?,?,?,...
示例:
DATA DW ?,?,? ;从地址DATA开始,开辟三个字的空间,其中每个字的数值不确定
DUP操作符
与DB、DD等数据定义伪指令配合使用的,用来进行数据的重复
格式:标号 数据定义伪指令 重复个数 DUP (待重复数据1,待重复数据2,...)
示例:
DW 3 DUP (0) ;重复定义三个字,值都为0
DW 0,0,0 ;与上面这个指令等价
DW 2 DUP (1,2) ;定义4个字,值为:1、2、1、2
DW 1,2,1,2 ;与上面指令等价
DATA DW 2 DUP(0);定义两个字,值都为0,起始地址为DATA
定义存储单元的类型:BYTE、WORD、DWORD、QWORD
作用:对存储单元的类型进行规定,多于PTR指令配合,进行类型强转
强制类型转换:PTR
示例:
assume cs:code,ds:data
data segment
d1 db 11h,22h,33h,44h
data ends
code segment
start:
mov ax,data
mov ds,ax
;mov ax,d1[0] ; 这样写是错的,因为d1的类型是byte,ax是word类型,不能直接赋值
mov ax,word ptr d1[0]; 从d1指向的地址开始,取出一个字的数据放入ax中
mov ah,4ch
int 21h
code ends
end start
-
代码运行前查看一下d1指向的数据是怎么样的
-
运行代码
确实是我们预期的效果,从d1指向的第一个数据开始,依次放入ax中,从低位开始放
段定义:SEGMENT、ENDS、ASSUME、ORG
SEGMENT和ENDS
-
作用:
- SEGMENT和ENDS是一对成对使用的伪指令,也是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令
- SEGMENT和ENDS的功能是定义一个段。SEGMENT说明一个段的开始,ENDS说明一个段的结束
-
格式:
段名 segment ... 段名 ends
-
示例:
codesg segment ;codesg段的开始 mov ax,10 ;codesg段内的代码 ... codesg ends ;codesg段的结束
ASSUME
-
作用:关联段名与段寄存器
-
格式:
ASSUME 段寄存器:段名
-
示例:
ASSUME CS:CODE, DS:DATA, SS:STACK ;将CODE与代码CS代码段寄存器关联,表示该段是代码段。DATA段与DS数据段寄存器关联,表示DATA是数据段。同理STACK表示栈段
有疑问可看该博客: 汇编语言——assume的作用_assume伪指令的作用-CSDN博客
ORG
作用:规定该伪指令后面的源程序或数据块存放的起始地址。
程序的结束和入口:END
-
作用一:END是一个汇编程序结束的标记
编译器在编译汇编程序的过程中,如果碰到了END,就结束对源程序的编译,所以在程序写完时,一定要加上END指令
-
作用二:告知CPU从什么地方开始执行程序,也就是程序入口
-
示例:
assume cs:code code SEGMENT dw 1,2,3,4 dd 1,2,3,4 start: mov ax,0 code ENDS end start ;告知程序从标号start处开始执行。如果只写一个end,那么程序默认从头执行
程序
可以将源程序文件中的所有内容称为源程序,将源程序中最终由计算机执行、处理的指令或数据,称为程序。
程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机器码,存储在可执行文件中。
标号
汇编源程序中,除了汇编指令和伪指令外,还有一些标号,比如“codesg“一个标号指代了一个地址。比如codesg在segment的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。
程序返回
示例:
mov ah,4ch
int 21h
进阶指令
LOOP指令:循环
-
格式:LOOP 标号(这个标号表示了一个地址)
CPU执行loop指令的时候,要进行两步操作
- (cx)=(cx)-1;
- 判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。
-
示例:使用汇编实现2^12
;计算2^12次方 ASSUME CS:CODESG CODESG SEGMENT MOV AX,2 ;设置初始值,十进制数 MOV CX,11 ;设置循环次数,十进制数 S: ;标号,表示一个地址 ADD AX,AX ;循环内的操作,2^12次方 LOOP S ;循环 ;退出段 MOV AX,4C00H INT 21H CODESG ENDS ;结束段 END ;结束程序
编译、链接后,使用debug查看代码,可以看到,LOOP指令就是个跳转语句
-
总结:
- 在cx中存放循环次数
- loop指令中的标号所标识地址要在前面
- 要循环执行的程序段,要写在标号和loop指令的中间
用cx和loop指令相配合实现环功能的程序框架如下
mov cx,循环次数
s:
循环执行的程序段
loop s
CALL指令:函数
CPU 执行call 指令时,进行两步操作:
- 将当前的IP或CS、IP压入栈中;
- 地址转移
根据位移进行转移的call指令
-
格式:
call 标号
将下一条指令的IP压栈后,程序转移到标号处执行指令
-
示例:
ASSUME CS:CODESG CODESG SEGMENT MOV AX,0 ;ax清零 CALL S ;将下一条指令的IP压栈,并转到标号S处执行指令 INC AX ;ax加1 S: ;标号S POP AX ;弹出压入栈内的IP,查看是否是:INC AX指令的IP MOV AX,100 ;将ax置为100,查看call指令是否是跳转到标号S处执行 ;退出段 MOV AX,4C00H INT 21H CODESG ENDS ;结束段 END ;结束程序
在debug中查看代码可知:
int ax
指令的IP地址为0x0006可以运行代码后如下
转移的目的地址在指令中的call指令
-
格式:
call far ptr 标号
- 将下一条指令的CS段地址压栈
- 将下一条指令的IP地址压栈
- 令当前CS和IP等于标号的段地址和偏移地址(也就是跳转到标号处执行指令)
-
示例:
ASSUME CS:CODESG CODESG SEGMENT MOV AX,0 ;初始化AX CALL FAR PTR S ;将下一条指令的CS地址压栈,再将下一条指令的IP地址压栈,最后转移到标号s处执行 INC AX ;AX加1 S: POP AX ;弹出存入栈内的IP地址 ADD AX,AX ;AX乘2 POP BX ;弹出存入栈内的CS地址 ADD AX,BX ;AX加BX ;退出段 MOV AX,4C00H INT 21H CODESG ENDS ;结束段 END ;结束程序
最终运行结果:ax = 0x0008 + 0x0008 + 0x076a
转移地址在寄存器中的call指令
-
格式:
call 16位的寄存器
- 将下一条指令的IP地址压栈
- 令当前IP等于16位寄存器中的值(也就是以16寄存器中的值为偏移地址,然后跳转)
-
示例:
ASSUME CS:CODESG CODESG SEGMENT MOV AX,6 ;设置call指令要跳转到的偏移地址 CALL AX ;调用call指令,跳转到偏移地址0x0006 INC AX ;退出段 MOV AX,4C00H INT 21H CODESG ENDS ;结束段 END ;结束程序
转移地址在内存中的call指令
RET和RETF指令:返回
RET:弹出栈内的数据,并赋值给IP寄存器
RETF:弹出栈顶的数据,赋值给IP寄存器;再弹出一次栈顶数据,赋值给CS寄存器
CALL与RET指令配合
这两个指令配合就实现了函数的功能,可以在程序运行到一半的时候,去其他地方执行具有其他功能的子程序。用call指令转去执行子程序,执行完毕后使用ret指令返回,继续执行主程序
如下:写一个计算某个数的12次方的函数
ASSUME CS:CODESG
CODESG SEGMENT
MOV AX,3
CALL FUNC ;计算3的12次方
MOV AX,4
CALL FUNC ;计算4的12次方
;退出段
MOV AX,4C00H
INT 21H
;函数:计算ax中的值的12次方
FUNC:
MOV CX,11
S:
ADD AX,AX
LOOP S
RET ;返回
CODESG ENDS ;结束段
END ;结束程序
定义包含多个段的程序
为何要定义多个段
- 把数据、栈、代码放到一个段中使程序显得混乱
- 如果数据、栈和代码需要的空间超过64KB,也不能放在一个段中(一个段的容量不能大于64KB,是我们在学习中所用的8086模式的限制,并不是所有的处理器都这样)。
定义多个段的方法
定义一个段的方法和定义代码段的方法没有区别,只是对于不同的段,要有不同的段名。
可以将段分为:代码段、数据段、栈段
示例:
assume cs:code, ds:data, ss:stack ;关联段寄存器
;定义数据段
data segment
dw 012H, 345H, 789H, 0ABCH, 0DEFH ;默认偏移地址为0
data ends
;定义栈段
stack segment
dw 5 dup(0) ;默认偏移地址为0
stack ends
;定义代码段
code segment
start:
mov ax,data
mov ds,ax ;ds指向data段
mov bx,0 ;设置ds:bx指向数据段的第一个字
mov ax,stack
mov ss,ax
mov sp,10 ;设置栈顶指针指向stack:0x0A
mov cx,5
s:
push [bx]
add bx,2
loop s ;将数据段的5个字依次压入栈
mov bx,0 ;重新指向data段的开头
mov cx,5
s0:
pop [bx]
add bx,2
loop s0 ;将栈顶的5个字依次弹出,并存入数据段
mov ah,4ch
int 21h ;结束程序
code ends
end start ;指明程序入口
-
编译链接后利用debug查看程序,发现data段存储在地址0x076A:0x0000处
-
查看代码运行前的数据是否与我们存的数据一致,可以看的是一致的
-
程序运行后我们预期的效果是data段中的数据颠倒顺序,可以看的是我们预期的效果
操作符
DUP:重复数据
作用:与DB、DD等数据定义伪指令配合使用的,用来进行数据的重复
格式:标号 数据定义伪指令 重复个数 DUP (待重复数据1,待重复数据2,...)
示例:
DW 3 DUP (0) ;重复定义三个字,值都为0
DW 0,0,0 ;与上面这个指令等价
DW 2 DUP (1,2) ;定义4个字,值为:1、2、1、2
DW 1,2,1,2 ;与上面指令等价
DATA DW 2 DUP(0);定义两个字,值都为0,起始地址为DATA
OFFSET:取址
作用:取得标号的偏移地址
格式:offset 标号
示例:数据段中有多个标号指向不同的数据,就可以使用offset操作符来进行取址,准确的操作想要操作的数据
assume cs:code, ds:data, ss:stack ;关联段寄存器
;定义数据段
data segment
d1 dw 012H, 345H, 789H, 0ABCH, 0DEFH
d2 dw 1111H, 2222H, 3333H, 4444H, 5555H
data ends
;定义栈段
stack segment
dw 5 dup(0) ;默认偏移地址为0
stack ends
;定义代码段
code segment
start:
mov ax,stack
mov ss,ax
mov sp,10 ;设置栈顶指针指向stack:0x0A
mov ax,data
mov ds,ax ;ds指向data段
mov bx,offset d2 ;设置ds:bx指向数据d2的偏移地址
mov cx,5
s:
push [bx]
add bx,2
loop s ;将数据段的5个字依次压入栈
mov bx,offset d2 ;重新指向数据d2的开头
mov cx,5
s0:
pop [bx]
add bx,2
loop s0 ;将栈顶的5个字依次弹出,并存入数据段
mov ah,4ch
int 21h ;结束程序
code ends
end start ;指明程序入口
-
查看d2数据的地址
-
程序运行前查看d2的数据是否与我们存的数据一致
-
程序运行后查看d2指向的数据是否成功颠倒过来
注意:offset也可以使用[]代替,如下
mov bx,offset d2
mov ax,[bx]
mov bx,d2[0] ;等价于上面的两行代码
JMP SHORT:段内短转移
作用:实现段内短距离转移,对IP的修改范围为 -128~127,
格式:jmp short 标号
示例:
assume cs:code
code segment
start:
mov ax,0
jmp short s ;移位到标号s
add ax,1
add ax,1
s:
inc ax
code ends
end start
翻译成机器码如下:
注意:jmp short 标号
实际的功能是:(IP) = (IP)+8位的位移
- 8位位移地址 = 标号处的地址 - jmp指令后第一个字节的地址
- short指明此处的位移为8位位移,所以
jmp short
指令的范围为-128~127- 这8位的位移由编译程序编译时算出
JMP NEAR PTR:段内近转移
作用:进行段内近转移,位移的范围为:-32768~32767,也就是16位的移位。与前面的jmp short指令类似,只是可位移的距离不一样
格式:jmp near ptr 标号
示例:
assume cs:code
code segment
start:
mov ax,0
jmp near ptr s ;移位到标号s,可位移范围为:-32768~+32767
add ax,1
add ax,1
s:
inc ax
code ends
end start
JMP FAR PTR:段间转移
作用:可以跨段转移,与前面的jmp不同的是,该指令是直接使用标号处的地址修改CS和IP寄存器,而不是相对于IP的移位转移
格式:jmp far ptr 标号
(CS) = 标号所在的段地址;(IP) = 标号所在的偏移地址
far ptr 指明了指令用标号的段地址和偏移地址修改CS和IP
示例:
assume cs:code
code segment
start:
mov ax,0
jmp far ptr s ;跳转到标号s,直接使用标号s的段地址和偏移地址修改cs:ip
add ax,1
add ax,1
s:
inc ax
code ends
end start
JMP WORD PTR和JMP DWORD PTR:转移地址在内存中
作用:
- JMP WORD PTR:从内存单元开始,存放着一个字,是转移目的的偏移地址
- JMP DWORD PTR:从内存单元开始,存放着两个字,高位的地址是转移目的的段地址;低位的地址是转移目的的偏移地址
格式:
jmp word ptr 内存单元地址
(段内转移)jmp dword ptr 内存单元地址
(段间转移)
示例
-
jmp word ptr 内存单元地址
(段内转移)assume cs:code code segment start: mov ax,1111h ; 设置要跳转的偏移地址 mov ds:[00ffh], ax ; 将偏移地址存入内存 jmp word ptr ds:[00ffh] ; 跳转到内存中的偏移地址 code ends end start
运行代码查看是否只修改IP,没有修改cs
-
jmp dword ptr 内存单元地址
(段间转移)assume cs:code code segment start: mov ax,1111h ; 设置要跳转的偏移地址 mov ds:[0000h], ax ; 将偏移地址存入内存 mov ax, 2222h ; 设置要跳转的段地址 mov ds:[0002h], ax ; 将段地址存入内存 jmp dword ptr ds:[0000h] ; 跳转到内存中的地址 code ends end start
运行代码后可以看到,cs:ip已成功修改
TYPE:获取数据类型
作用:查看变量的数据类型。返回当前变量占用多少个字节
格式:type 变量名
示例:
assume cs:code,ds:data
data segment
d1 db 11h,22h,33h,44h
d2 dw 5555h,6666h
d3 dd 77777777h,88888888h
data ends
code segment
start:
mov ax,data
mov ds,ax
mov ax,type d1
mov bx,type d2
mov cx,type d3
mov ah,4ch
int 21h
code ends
end start
程序运行后可以看到,d1变量占用一个字节,d2变量占用两个字节,d3变量占用3个字节
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了