五、源文件的方式写汇编

在VS Code中搭建汇编环境

1、安装插件

打开VS Code中的扩展栏,并搜索MASM,找到MASM/TASM这个插件,并安装即可:

image

这个插件会把我们所需要的dosbox, dosbox-x, jsdos以及汇编编译器MASM都安装好,也不需要我们再去挂载之类的操作了。

2、设置插件

image

选择汇编工具为MASM

image

DOS环境有多钟选择,这里先选一个熟悉DOSBox,下面两种方式都可以改变DOS的模拟环境

  1. image

  2. image

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

右键,然后点击运行或调试

image

4、四种DOS模拟环境的区别

  • jsdos :类似网页

image

  • dosbox:单独打开一个窗口

    image

  • dosbox-x:也是单独打开一个窗口

    image

  • msdos player :在终端打开模拟环境

image

  • 总结: MASM和TASM都是汇编编译器,以及不同版本的编译器可能会有细小差异。建议选择:DOSBox+MASM-v6.11。这与前面使用的是同一个东西

  • 建议:前期不要太依赖一键编译、链接、运行。只用VS Code写源文件,然后在DOSBox中进行手动编译、链接、运行

编译源文件

编写源文件

在MASM文件夹下创建后缀为.asm的文件,并在里面写上测试的代码:在屏幕输出hello world

image

  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文件是否存在

image

编译

  1. 编译刚刚创建的asm文件。MASM.exe就是编译器,用他来编译文件

    masm code	// 编译code.asm文件
    

    image

  2. 编译成功后会生成obj文件

    image

链接

  1. 使用LINK.exe链接obj文件

    image

  2. 链接完成后会生成一个exe文件

    image

运行

输入文件名即可直接运行

image

debug模式查看exe文件

debug code.exe	// 注意:一定要加上exe的后缀名

image

使用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
  1. 代码运行前查看一下d1指向的数据是怎么样的

    image

  2. 运行代码

    image

    确实是我们预期的效果,从d1指向的第一个数据开始,依次放入ax中,从低位开始放

段定义:SEGMENT、ENDS、ASSUME、ORG

SEGMENT和ENDS

  • 作用:

    1. SEGMENT和ENDS是一对成对使用的伪指令,也是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令
    2. 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的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。

程序返回

image

示例:

mov ah,4ch
int 21h

进阶指令

LOOP指令:循环

  • 格式:LOOP 标号(这个标号表示了一个地址)

    CPU执行loop指令的时候,要进行两步操作

    1. (cx)=(cx)-1;
    2. 判断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指令就是个跳转语句

    image

  • 总结:

    1. 在cx中存放循环次数
    2. loop指令中的标号所标识地址要在前面
    3. 要循环执行的程序段,要写在标号和loop指令的中间

    用cx和loop指令相配合实现环功能的程序框架如下

    ​ mov cx,循环次数
    s:
    ​ 循环执行的程序段
    ​ loop s

CALL指令:函数

CPU 执行call 指令时,进行两步操作:

  1. 将当前的IP或CS、IP压入栈中;
  2. 地址转移

根据位移进行转移的call指令

image

  • 格式: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

    image

    可以运行代码后如下

    image

转移的目的地址在指令中的call指令

image

  • 格式:call far ptr 标号

    1. 将下一条指令的CS段地址压栈
    2. 将下一条指令的IP地址压栈
    3. 令当前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             ;结束程序   
    

    image

    最终运行结果:ax = 0x0008 + 0x0008 + 0x076a

    image

转移地址在寄存器中的call指令

image

  • 格式:call 16位的寄存器

    1. 将下一条指令的IP地址压栈
    2. 令当前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指令

image

image

RET和RETF指令:返回

image

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             					;结束程序   

定义包含多个段的程序

为何要定义多个段

  1. 把数据、栈、代码放到一个段中使程序显得混乱
  2. 如果数据、栈和代码需要的空间超过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     ;指明程序入口
  1. 编译链接后利用debug查看程序,发现data段存储在地址0x076A:0x0000处

    image

  2. 查看代码运行前的数据是否与我们存的数据一致,可以看的是一致的

    image

  3. 程序运行后我们预期的效果是data段中的数据颠倒顺序,可以看的是我们预期的效果

    image

操作符

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     ;指明程序入口
  1. 查看d2数据的地址

    image

  2. 程序运行前查看d2的数据是否与我们存的数据一致

    image

  3. 程序运行后查看d2指向的数据是否成功颠倒过来

    image

注意: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

翻译成机器码如下:

image

注意:jmp short 标号实际的功能是:(IP) = (IP)+8位的位移

  1. 8位位移地址 = 标号处的地址 - jmp指令后第一个字节的地址
  2. short指明此处的位移为8位位移,所以jmp short指令的范围为-128~127
  3. 这8位的位移由编译程序编译时算出

image

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

image

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

    image

  • 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已成功修改

    image

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个字节

image

posted @   7七柒  阅读(58)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示