ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

汇编语言:编写汇编程序

汇编语言:编写汇编程序

目录
  • 汇编程序的生命周期
  • 指令、伪指令与结构
    • 伪指令
    • 程序返回
    • 标准程序框架
  • 用 vscode 编辑源程序
  • 编译和连接
  • 程序执行过程的跟踪
  • 实验 3
    • 程序编写
    • Debug 分析
    • 查看 PSP 内容
  • 参考资料

之前的指令是在 Debug 中输入并执行的,接下来将编写完整的、可被操作系统加载的汇编源程序。

汇编程序的生命周期

汇编程序从编写完成到运行,需要通过三个步骤:

  1. 编写源程序:使用如 Edit.com 或 VS Code 文本编辑器编写 .asm 文件,此时程序是包含汇编指令和伪指令的纯文本。
  2. 编译与连接:编译器负责将汇编指令翻译成机器码,并处理段定义等伪指令,具体为使用 MASM.EXE.asm 文件编译成 .obj 目标文件。连接器负责处理多个目标文件的拼接,并加入程序加载、初始化等相关描述信息,具体为使用 LINK.EXE.obj 文件连接成 .exe 可执行文件。
  3. 执行:在 DOS(或 DOSBox)中运行 .exe 文件,操作系统根据文件头中的信息将程序加载到内存,设置 CS:IP 指向入口点后程序开始运行。

image

指令、伪指令与结构

以如下汇编程序为例:

ssume cs:codesgcodesg segment
    mov ax,0123H
    mov bx,0456H
    add ax,bx
    add ax,ax
    
    mov ax,4c00H
    int 21H
    
codesg ends
end

伪指令

伪指令不生成机器码,仅用于指导编译器工作。上面的程序包含 3 个伪指令:

伪指令 功能
XXX segment ... XXX ends 定义逻辑段,一个程序至少有一个段(代码段)。segment 标记段开始,ends 标记段结束。
end 标记整个程序的结束,编译器遇到 end 才知道停止编译。注意区分 end(程序结束)和 ends(段结束)
assume 建立段寄存器与逻辑段的关联。例如 assume cs:codesg 告诉编译器,将 codesg 段作为代码段,运行时应将其地址装入 CS 寄存器。这只是一个假设或声明,实际的寄存器赋值仍需在代码中完成。不过对于代码段,操作系统会根据 end 后的入口点自动设置 CS:IP。

codesg 的标号是地址的代称,它是一个段名,同时也代表该段的段地址,编译后它会被转换为具体的数值。

程序返回

在单任务操作系统 DOS 中,我们的程序(子进程)必须由另一个程序加载,通常是 COMMAND.COM 父进程。程序运行结束后,必须执行程序返回,将 CPU 控制权交还给父进程,否则系统会死机。标准的返回代码是:

mov ax, 4c00H
int 21H

这两句调用了 21H 号中断例程的 4CH 号功能,实现程序退出。

标准程序框架

一个标准的汇编程序框架如下:

assume cs:codecode segment; 数据定义(如果需要)
start:; 代码逻辑mov ax, 4c00hint 21h
code endsend start ; end 标记程序结束,start 标记程序入口

start: 是一个标号,用于指定程序入口,配合 end start 使用。

编译器能对程序的语法错误进行检查,如拼写错误(aume 而不是 assume)、缺少 end 等,这类错误阻止编译生成 .obj 文件。逻辑错误编译器无法发现,通常在运行时出错。最典型的就是忘记写程序返回语句,程序虽然能编译连接成功,也能运行,但运行后无法正确返回 DOS,会导致系统环境异常。

在 Debug 中直接写指令(如 A 命令)时,我们不需要 assumesegmentend,也不需要 mov ax,4c00h/int 21h。因为 Debug 是一个交互式调试环境,它本身接管了控制权,我们输入的指令直接由 Debug 加载到内存并执行,退出 Debug 即意味着返回。而 .exe 程序是独立的,必须包含返回逻辑才能将控制权交还给 COMMAND.COM。

用 vscode 编辑源程序

在 DOSBox 环境下,可以使用 edit 命令启动编辑器编写代码,保存文件时需要使用 .asm 后缀。
image

虽然 DOSBox 自带了 EDIT.COM 编辑器,但它不支持现代编辑习惯,如无语法高亮、快捷键晦涩等。VSCode 提供了 x86 Assembly 语法高亮,方便区分指令、寄存器、注释,以及智能缩进以及现代化的查找替换功能,可以提升编写 .asm 源代码的效率。在 VS Code 的扩展商店中搜索并安装 MASM/TASMx86 and x86_64 Assembly,安装后 .asm 文件会自动高亮。使用 VSCode 编写,只需确保文件编码为 ANSI(或 GBK),且后缀正确,保存后 DOSBox 即可识别。
image

安装完成后进行配置,点击扩展设置或直接编辑 settings.json:

vscode 配置 选项 作用
Assembler(汇编器) MASM 使用微软的宏汇编器
Emulator(模拟器) dosbox 编译连接后的程序需要通过 DOSBox 来运行
Mode(模式) single file(单文件模式) 实验程序通常都是一个单独的 .asm文件,暂时不需要复杂的工程管理

image

配置完成后,在 VSCode 中打开 .asm 源文件,在编辑区右键点击选择 运行当前程序(汇编+链接+运行)(Run ASM file)。扩展会自动在后台依次调用 MASM.EXE 进行编译、LINK.EXE 进行连接,最后启动 DOSBox 加载生成的 .EXE 文件,能省去我们在命令行中手动敲入 masmlink 命令的过程。
image

执行上述命令后 DOSBox 窗口会自动弹出并运行程序,程序执行完毕后返回 DOS 提示符。
image

编译和连接

编译阶段使用的是微软的 masm.exe,它对汇编程序进行语法分析和翻译后生成目标文件。masm 除了要求输入源程序文件名外,还会依次询问目标文件(.obj)、列表文件(.lst)和交叉引用文件(.crf)的名称。其中只有 .obj 是后续必需的,其余两个属于编译过程中的中间产物,通常直接按 Enter 忽略即可。
image
编译成功后将看到 .obj 文件,如果出现 Severe Errors 则不会生成目标文件,需要回到源程序排查语法或伪指令错误。
image

接着需要通过 link.exe 进行连接,该操作用于将机器码模块组合成一个符合 DOS 规范的可执行文件。link 的交互过程同样会询问可执行文件(.exe)、映像文件(.map)和库文件(.lib)的名称,对于最简单的单文件程序只需生成 .exe 文件。
image
连接阶段常会遇到一个典型警告 “no stack segment”,这是因为在早期的 8086 汇编中,如果程序没有显式定义栈段,连接器无法自动分配栈空间因此给出警告。学习阶段的程序结构较为简单,暂时忽略该警告不会影响运行。
image

masm 和 link 都支持在命令行末尾加分号(;)来跳过所有中间文件的询问,例如masm c:\1;link 1;
image

生成 .exe 文件之后可以直接运行。尝试运行 4-2.exe 时屏幕上没有任何输出,这因为该程序只包含数据传送和运算指令,并未涉及任何向显示器输出字符的系统调用。程序执行完毕后,控制权交还操作系统,重新显示命令提示符。
image

程序执行过程的跟踪

在 DOS 中,负责把程序装进内存并启动的是 command.com,它是 DOS 的 shell 命令解释器。当输入程序名并回车时,command 负责找到 .exe 文件并将其加载进内存,然后设置 CS:IP 指向程序入口。接着暂停自身交出 CPU 控制权,待程序运行结束后 CPU 控制权又交还给 command 以等待下一条命令。

与 command 直接加载运行不同,可以使用Debug 观察程序的执行过程。Debug 在加载程序后不会放弃对 CPU 的控制,允许通过单步指令(如 t)来跟踪执行。在 Debug 加载.exe文件后,寄存器 CX 中保存了程序代码的长度,DS 指向程序所在内存段的段地址。
image

在程序实际代码之前,DOS 会预留 256 字节(100H)的区域作为 PSP(程序段前缀),用于程序和操作系统通信。因此程序的真正物理地址是DS:0100,Debug 中看到的 CS:IP 从这个位置开始。在跟踪结束时,程序通常会调用 int 21(配合AH=4CH)来实现返回。
image
image

在 Debug 中执行中断指令时必须使用 P 命令,否则会陷入中断例程内部。当看到 “Program terminated normally” 时,说明程序正常返回到了 Debug 环境下,使用q命令则可以最终退出Debug 回到 command。

从现代视角看,该流程是基于 16 位实模式DOS MZ 格式的。PSP 的概念在现代 Windows 的 PE 格式或 Linux 的 ELF 格式中已不存在,但它们本质上都承担着加载器(Loader)与进程通信的功能。

实验 3

程序编写

将给出的程序保存为 t1.asm

assume cs:codesgcodesg segmentmov ax,2000Hmov ss,axmov sp,0add sp,10pop axpop bxpush axpush bxpop axpop bxmov ax,4c00Hint 21H
codesg ends
end

在 vscode 编写完代码后右键选择“调试当前程序(汇编+链接+调试)”,即可生成 t1.exe 并进入 Debug 进行调试。
image

加载完成后,DS 和 ES 寄存器指向 PSP 的段地址,CS 指向程序代码的段地址(通常 CS = DS + 10H),IP 初始为 0000。
image

Debug 分析

执行 mov ax,2000HAX=2000H,此时 SS 和 SP 尚未改变,栈顶指向 PSP 后的区域。执行 mov ss,axSS=2000H,修改 SS 的指令会屏蔽中断,且下一条指令 mov sp,0会紧接着自动执行。执行mov sp,0SP=0000H,此时栈顶逻辑地址为 2000:0000。执行 **add sp,10** 后 SP=000AH`,栈顶向低地址方向移动了 AH 个字节,相当于在栈中预留了空间。
image

执行 pop axSP=000CH,从栈顶 2000:000A 处读取一个字送入 AX。由于栈中数据未知,AX 变为该内存单元的值。
image

执行 pop bxSP=000EH,同理 BX 被赋值为栈顶 2000:000E 处的值。
image

执行 push axSP=000CH,将 AX 的值压回栈顶 2000:000C
image

执行 push bxSP=000AH。将 BX 的值压回栈顶 2000:000A
image

执行 pop axSP=000CH,将栈顶 2000:000A 的值弹出给 AX。由于上一步刚将 BX 压入,此时 AX=BX
image

执行 pop bxSP=000EH,将栈顶 2000:000C 的值弹出给 BX。由于之前将 AX 压入,此时 BX=AX。经过几次 POP 和 PUSH 操作,AX 和 BX 的内容进行了互换。执行 mov ax,4c00HAX=4C00H,准备调用 21H 中断返回 DOS,执行 int 21H 后程序结束。
image

查看 PSP 内容

在 Debug 中加载程序后,用 d ds:0 命令查看内存。PSP 位于 DS 所指向的段的开头,占用 256 个字节。内存数据中最开始的头两个字节是 CD 20,对应机器码 INT 20H
image

INT 20H 是一条终止程序的中断指令,DOS 在加载程序时将在 PSP 的开头放置该指令,作为一种兼容机制和程序退出的备用手段。现代程序通常使用 INT 21H 的 4CH 功能。

参考资料

《汇编语言(第四版)》,王爽 编著,清华大学出版社

返回列表