汇编语言程序设计-3-汇编语言程序

3. 汇编语言程序

本篇笔记对应课程第三章(下图倾斜),章节划分和教材对应关系如下。


3.0 导学

对应教材第四章:介绍汇编语言程序结构、及程序调试手段。
【3.1 用汇编语言写的源程序】源程序结构。
【3.2 由源程序到程序运行】编译、链接。
【3.3 用Debug跟踪程序的执行】观察程序运行时的微观变化。

对应教材第五章:用程序设计的方法解决问题,也就是各种控制结构,如分支结构、循环结构等。
【3.4 […]和(…)】
【3.5 Loop指令】循环结构。
【3.6 Loop指令使用再例】
【3.7 段前缀的使用】介绍如何使用“段前缀”大段地引用内存数据,主要是 DS+ES+BX偏移地址

对应教材第六章:解决代码和数据的程序结构问题。
【3.8 在代码段中使用数据】
【3.9 在代码段中使用栈】
【3.10 将数据、代码、栈放入不同段】最清晰的程序结构。

3.1 用汇编语言写的源程序

图3-1 汇编程序示例

  汇编程序是包含“汇编指令”和“伪指令”的文本。如上图给出一个汇编程序的示例,下面是具体的含义介绍:

  • 伪指令:指导编译器完成编译工作的指令,没有对应的机器码,不会被CPU所执行。下面是几种伪指令。
  1. assume:假设某一个“段寄存器”和程序中的某一个用 segment … ends 定义的“段”相关联,无需深入理解。比如 assume cs:codesg是指 CS寄存器 与 codesg段 相关联,将定义的codesg当作程序的代码段使用。
  2. 段定义:一个汇编程序是多个段组成的,每个段都有段名,这些段被用来存放代码、数据或当作栈空间来使用。一个有意义的汇编程序中至少要有一个“代码段”
  3. end:汇编程序的结束标记。注意和“段”的结束标志 ends区分开来。
  4. ;为汇编程序的注释符号。
  • 汇编指令:对应有机器码的指令,可以被编译为机器指令,最终会被CPU执行。
  • 程序返回:固定套路,程序结束运行后,将CPU的控制权交还给使它得以运行的程序(常为DOS系统)。

【编程示例】编程求 23。编程步骤如下图所示:

编写汇编语言程序的两种方法:

  1. Debug直接写入指令:适用于功能简单、短小精悍的程序,只需要包含汇编指令即可。
  2. 单独编写成源文件:编写后再编译为可执行文件,适用于编写大程序。源程序由一些段构成,包括汇编指令、伪指令。

程序编程中可能犯的错误,可以用于指导debug思路:

  1. 语法错误:程序在编译时被编译器发现的错误,比较容易发现。如下左图,第一行 assume、第五行 sx拼写错误。
  2. 逻辑错误:程序在编译时不能表现出来的、在运行时发生的错误,不易发现。比如下右图第五行寄存器名称拼错。

3.2 由源程序到程序运行

图3-2 由源程序到可执行文件
  1. 编辑源程序:编辑工具有 edit.com(见工具包,经典但不好用)、EditPlus(老师)、VScode等,可以按照兴趣选择。
  2. 编译:使用工具包中的 masm.exe 进行编译,生成最终的 *.OBJ 文件。下面是中间文件的介绍、三种编译格式、编译错误的演示。
  • *.LST 文件:是一种列表文件,它包含了源代码以及编译器生成的详细信息,如行号、源代码、机器代码、符号表等。
  • *.CRF 文件:是交叉引用文件,它列出了程序中的符号(变量、标签、函数等)及其在源代码中的位置。
  • *.OBJ 文件:是目标文件,它是汇编器将源代码翻译成机器码后的产物,包含了可重定位的机器代码和符号信息。

注:编译结束时,编译器输出的最后两行会输出“警告错误 Warning Errors”、“必须要改正的错误 Severe Errors”。

  1. 连接:使用工具包中的 link.exe 进行连接,生成最终的 *.EXE 文件。“连接”的三种格式和上述“编译”相同,中间文件的含义如下。
  • *.MAP 文件:是内存映射文件,包含了程序中各个符号和段的地址分布情况,可用于了解程序在内存中的布局。
  • *.LIB 文件:是库文件,描述了不同库函数之间的调用关系。它包含了一组OBJ文件,这些模块可以在不同的程序中重用。
  • *.EXE 文件:是最终的可执行文件,包含了可以直接在操作系统上运行的机器代码。

注:当源程序中没有“栈段”时,“连接”会警告错误 no stack segment,程序简单时无需理会。

  1. 运行:直接输入生成的可执行文件名,即可执行。

工具包见:“8086汇编工作环境.zip(1.76MB)”。

【实机演示】使用图3-1所示的源代码,生成可执行文件并执行。

3.3 用Debug跟踪程序的执行

图3-3 DOS系统加载EXE文件的过程

  DOS系统加载EXE的过程如上图所示。主要找一段内存存储整个程序,并将前256个字节专门用于存储“程序段前缀(PSP)”,主要是为了DOS和程序进行通信,然后继续存放要执行的指令。于是程序加载后,关键的三个寄存器内容为:

  • DS:整个程序的起始地址,前256个字节专门存储“程序段前缀(PSP)”。
  • CS:固定为 DS+0010H,存储要执行的指令。
  • IP:初始时加载为0。注意 CS:IP 表示第一个要执行的指令地址。
  • CX:表示当前程序的长度(字节)。

我们想跟踪程序每一步的执行过程,就可以使用Debug装载运行。主要有以下三种调试方式:

  • t(Trace):逐条指令执行,适合详细调试每个指令,相当于“单步执行-步进”
  • p(Procedure step):逐过程执行,适合调试过程中不想深入子过程,相当于“单步执行-步出”。比如下面要介绍的 loop 循环中,p会直接跳过具体的循环过程,而直接给出循环的最后结果。
  • g(Go):连续执行程序,适合运行程序到下一个断点或结束。可以加上地址。

  在DOS系统启动后,计算机由“命令解释器”(程序command.com)控制,也就是命令行中的 c:\>,等待用户输入要执行的指令。最后总结程序运行的两种方式,也体现了操作系统的运行细节:

  1. 在DOS中直接运行:程序执行的“常态”。运行EXE文件时,command将程序加载入内存,并设置CPU的CS:IP指向程序的第一条指令(即程序的入口),使程序得以运行。程序运行结束后,会再返回到“命令解释器”。
  2. 在Debug中运行:程序执行处于开发周期的运行方式。在command程序中加载Debug.exe,然后将程序加载入内存,程序运行结束后,还是返回到Debug中。

3.4 […]和(…)

为了后续的表述方便,我们特别做出如下约定:

  • [...]:里面是一个数,表示偏移地址,是汇编语法。
  • (...):里面只能是寄存器名/物理地址/偏移地址,表示该寄存器或内存单元中的内容,是人为约定,而非汇编语法。当里面的内容为偏移地址时,默认取 DS 为段地址。
  • idata:表示常量,当不关心具体的数据内容时用此表示,是人为规定,而非汇编语法。
图3-4 [...]和(...)的使用示例

3.5 Loop指令

  Loop指令实现计数型循环,语法为 loop 标号循环次数为 CX。具体代码就是进入循环之前先给 CX 赋值为循环次数,然后每次执行 loop时会判断cx中的值,若 CX 不为零则转至“标号”处执行程序、若 CX 为零则向下执行。于是总结使用 CX 和 loop 指令相配合实现循环功能的三个要点:

  1. 在 CX 中存放循环次数。
  2. 用“标号”指定循环开始的位置。
  3. 在“标号”和 loop 指令的中间,写上要循环执行的程序段(循环体)。

下面按左图所示代码进行 Debug 跟踪,观察 loop 执行时,CX 和循环体的执行细节:

【代码示例】使用 loop 循环计算 212

注:asm文件中的数字没有后缀时,默认为十进制。
注:若使用 p指令,会直接跳过循环体,给出最后的循环结果。

3.6 Loop指令使用再例

【代码示例】计算 ffff:0006 字节单元中的数乘以3,并将结果存储在 DX 中。
难点:字节单元是8位,乘以3不会超过16位,使用DX存储合理,但注意不能使用低八位的 DL 存储。

注:在asm文件中,数据不能以字母开头,若有需要则需在前面加0。

3.7 段前缀的使用

图3-5 使用段前缀规避编译错误

  首先介绍一个编译的小bug。那就是对于指令 mov al,[idata],若在 Debug 模式下直接使用 a命令写入,那么执行时会将存储单元 DS:[0] 的数据存入到 AL。但如果是在源程序中,在进行编译和连接后,就等价于 mov al,idata!这显然不符合我们的期望,解决方法是加上段前缀 mov al,ds:[idata],就不会出错了。这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址的“ds:”、“cs:”、“ss:”、“es:”,在汇编语言中称为 段前缀

那既然有了段前缀,于是 loop 和 [bx] 联手就可以连续地访问内存单元

【代码示例1】计算 ffff:0 ~ ffff:b 单元(字节)中的数据的和,结果存储在 dx 中。
分析:11个8位数字相加,肯定不会超过16位,所以结果存储在 DX 中不会溢出。

assume cs:code

code segment
    mov ax,0ffffh
    mov ds,ax

    mov dx,0  ; 结果初始为0
    mov bx,0  ; 内存单元索引初始为0
    mov cx,12 ; 循环12次

 s: mov al,[bx]
    mov ah,0
    add dx,ax
    inc bx  ; 内存单元索引加1
    loop s

    mov ax,4c00h
    int 21h
code ends

end

注:关键在于不能直接使用指令 add dx,ds:[bx],否则会进行16位加法运算。

【代码示例2】将内存 ffff:0~ffff:b 中的数据拷贝到 0:200~0:20b 单元中。
难点:体会“段前缀”的作用,体会“附加段寄存器ES”作为“段前缀”带来的性能提升——循环体更小、程序执行更快。

注:CX在循环中是12递减到1,而不是0~11,所以不能使用CX作为内存单元的偏移地址。

3.8 在代码段中使用数据

  上一小节的程序中,段地址都是直接给定的。但实际上,直接在程序中指定段地址存放数据是危险的,因为每一段都有特定含义,比如 0020:0000H 实际上存放中断相关程序。如果我们想在汇编程序中安全地存放数据,就需要在程序的“段”中存放数据,就可以在程序运行时由操作系统分配空间。“段”分为“数据段”、“代码段”、“栈段”,各种段中均可以有数据。我们可以将数据、代码、栈安置到同一个段中,也可以分别放入不同的段。

【代码示例】编程计算以下8个数据的和,结果存在 ax 寄存器中
0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
难点:如何让程序从第一条指令执行——在第一条指令位置使用标号。

注:dw定义一个字、db定义一个字节、dd定义一个双字。
注:start只是大家约定俗成的标号,也可以换成别的字符,比如 begin

图3-6 程序的一般框架

3.9 在代码段中使用栈

  本小节介绍如何在代码段中使用“”。“栈”需要的内存空间,在程序中通过定义“空”数据来取得。比如下面程序中,定义了16个字 0000H 来作为栈空间:

【代码示例】利用栈,将下面的数据逆序存放。
0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
难点:先入栈、再出栈。

assume cs:code

code segment
    ; 在代码段中定义数据(define word)
    dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
    ; 定义栈空间——16word
    dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

start:  mov ax,cs
        mov ss,ax
        mov sp,30h  ; 栈底,也就是第一条指令的偏移地址

        ; 入栈
        mov bx,0  ; 数据的索引
        mov cx,8
    s0: push cs:[bx]
        add bx,2  ; 以字节为单位
        loop s0

        ; 出栈
        mov bx,0  ; 数据的索引
        mov cx,8
    s1: pop cs:[bx]
        add bx,2  ; 以字节为单位
        loop s1

    mov ax,4c00h
    int 21h
code ends
end start

注:程序运行后,栈的最后数据是乱码,这是因为出栈时 sp 可能会乱掉会有一些其他干扰,不是错误。
注:每个循环的标号命名不能相同。

3.10 将数据、代码、栈放入不同段

  上一小节的代码中,数据、代码、栈都放到了同一个“代码段”中,显然会使得程序混乱、可读性不高。于是我们改进上一小节的代码,将数据、代码、栈分别存放:

【代码示例】利用栈,将下面的数据逆序存放。
0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
难点:将数据、代码、栈分别存放到各自的栈。

assume ds:data,ss:stack,cs:code

; 数据段
data segment
    dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
data ends

; 栈段——16字
stack segment
    dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends

; 代码段
code segment
start:
    ; 数据段寄存器初始化
    mov ax,data
    mov ds,ax
    ; 栈段寄存器初始化
    mov ax,stack
    mov ss,ax
    mov sp,20h  ; 注意这里只考虑栈空间

    ; 入栈
    mov bx,0
    mov cx,8
s0: push ds:[bx]  ; 默认DS,可以不写
    add bx,2
    loop s0

    ; 出栈
    mov bx,0
    mov cx,8
s1: pop ds:[bx]  ; 默认DS,可以不写
    add bx,2
    loop s1

    mov ax,4c00h
    int 21h
code ends
end start

注:汇编程序至少包含一个段,也就是代码段,所以无需给 CS 初始化赋值。
注:数据段和栈段的段地址最好通过 u指令 查看代码。

  • 8
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虎慕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值