发布于 2026-01-06 12 阅读
0

使用emu8086进行汇编语言入门指南

使用emu8086进行汇编语言入门指南

目录

什么是汇编语言

汇编语言是一种低级编程语言,它速度非常快,与高级语言相比资源占用更少,并且可以通过汇编器直接翻译成机器语言来执行。根据维基百科

在计算机编程中,汇编语言是指任何低级编程语言,其语言指令与体系结构的机器代码指令之间具有非常强的对应关系。

我们知道,处理器(也称为 CPU,即中央处理器)执行各种类型的操作,相当于计算机的大脑。然而,它只能识别由 0 和 1 组成的字符串。可想而知,用机器语言编写代码非常繁琐。因此,低级汇编语言应运而生,专为特定处理器系列设计,它用符号代码表示各种指令,这种符号代码更容易被人类理解。但是,正如你所料,用汇编语言进行开发既困难又不太方便。

那么,在当今世界,我们为什么要学习汇编语言呢?

你可以考虑以下几点来决定是否学习它。

  • 提升你的技能。
  • 学习除机器语言之外速度最快的语言。
  • 将汇编语言嵌入到高级语言中,以便使用高级语言不支持的功能或出于性能方面的考虑。
  • 填补知识空白,了解高级语言是如何产生的。

汇编器和编辑器

汇编器是将汇编语言代码翻译成等效机器语言代码的程序。目前市面上有许多针对不同微处理器的汇编器,例如 MASM、TASM、NASM 等。如需查看不同汇编器的列表,请访问此维基百科页面

代码编辑器是一种软件,您可以在其中编写代码、修改代码并将其保存到文件中。一些支持汇编语言的编辑器包括 VS Code、DOSBox、emu8086 等。此外,还有一些在线汇编器,例如流行的在线编辑器Ideone。我们将使用emu8086,它自带学习汇编语言所需的环境。

代码结构

我们可以直接编写汇编代码,并在 emu8086 中模拟运行,程序就能运行。但是,如果不调用退出语句或halt指令,程序会持续执行内存中的下一条指令,直到被操作系统或 emu8086 本身终止。汇编代码会保存为.asm文件类型。

还有一些最佳实践,例如在程序启动时就定义模型和栈内存大小。对于small模型,在栈之后定义数据段代码段。代码段包含要执行的代码。在本文给出的示例结构中,我创建了一个main过程(在其他编程语言中也称为函数或方法),代码从该过程开始执行。在过程结束时,我调用了一个预定义的中断语句,以表明代码已执行完毕。

.model small
.stack 100H

; Data segment
.data   ; if there is nothing in the data segment, you can omit this line.

; Code segment
.code

main PROC
    ; Write your code here

    exit:
    MOV AH, 4CH
    INT 21H
    main ENDP
END main
Enter fullscreen mode Exit fullscreen mode

第一行.model small定义了要使用的内存模型。一些常见的内存模型包括 tiny、small、medium、compact、large 等等。small内存模型支持一个数据段和一个代码段,通常足以编写小型程序。下一行.stack 100H定义了栈的大小,以十六进制数表示。等效的十进制数为256。以 开头的行或其后的部分;是注释,汇编器会忽略它们。

登记簿和旗帜

寄存器是直接连接到 CPU 的超高速存储器。emu8086 可以模拟Intel 8086微处理器的所有内部寄存器。所有这些寄存器均为 16 位,并分为以下几类:

  • 通用寄存器:共有四个通用寄存器,每个寄存器又分为低位和高位两个子组。例如,AX 寄存器分为 AL 和 AH 两个子组,每个子组均为 8 位。
    • 累加器(AX
    • 基础(BX
    • 计数器(CX
    • 数据(DX
  • 段寄存器:还有四个段寄存器。
    • 代码段(CS
    • 数据段(DS
    • 堆栈段 ( SS )
    • 额外段 ( ES )
  • 专用寄存器:有两个索引寄存器和三个指针寄存器。
    • 来源索引(SI
    • 目的地索引(DI
    • 基指针(BP
    • 栈指针(SP
    • 指令指针(IP
  • 标志寄存器:这是一个 16 位寄存器,其中 9 位由 8086 处理器用于指示处理器的当前状态。这九个标志分为两组。

    • 状态标志:六个状态标志指示当前正在执行的指令的状态。
    • 携带旗帜(CF
    • 奇偶校验标志(PF
    • 辅助旗帜(AF
    • 零标志(ZF
    • 标志旗(SF
    • 溢出标志(OF
    • 控制标志:有三个控制标志可以控制处理器的某些操作。
    • 中断标志(IF
    • 方向标志(DF
    • 陷阱标志(TF

要了解有关这些登记簿及其用途的更多信息,请访问此页面

汇编语言指令

英特尔8086微处理器共有116条指令。所有这些指令及其相关示例都可以在此链接中找到

在本文中,我将只重点介绍理解后续部分所必需的一些说明。

  • 复制数据 ( MOV ):此指令将一个字节(8 位)或一个字(16 位)从源位置复制到目标位置。两个操作数必须类型相同(字节或字)。此指令的语法为:
  MOV destination, source
Enter fullscreen mode Exit fullscreen mode

操作destination数可以是任何寄存器或内存位置,而source操作数可以是寄存器、内存地址或常量/立即数。

  • 加法 ( ADD ) 和减法 ( SUB ): ADD 指令destination将两个操作数相加source,并将结果存储在变量中destination。两个操作数必须类型相同(字或字节),否则汇编器会报错。减法指令source从变量中减去操作数destination,并将结果存储在变量中destination
  ; Addition
  ADD destination, source
  ADD BL, 10

  ; Subtraction
  SUB destination, source
  SUB BL, 10
Enter fullscreen mode Exit fullscreen mode
  • 标签:标签是紧随其后的指令地址的符号名称。它可以放在语句的开头,并作为指令操作数。前面exit:使用的就是标签。标签有两种类型。

    • 符号标签:符号标签由一个标识符或符号后跟一个冒号 ( :) 组成。由于它们具有全局作用域并会出现在目标文件的符号表中,因此只需定义一次即可。
    • 数字标签:数字标签由一个介于零 (0 0) 到九 (9 9) 之间的数字和一个冒号 (' :) 组成。它们仅用于局部引用,不会被添加到目标文件的符号表中。因此,它们的作用范围有限,并且可以重复定义。
  ; Symbolic label
  label:
  MOV AX, 5

  ; Numeric label
  1:
  MOV AX, 5
Enter fullscreen mode Exit fullscreen mode
  • 比较指令 (CMP):此指令接受两个操作数,并将其中一个操作数减去另一个操作数,然后相应地设置 OF、SF、ZF、AF、PF 和 CF 标志。结果不会被存储。
  CMP operand1, operand2
Enter fullscreen mode Exit fullscreen mode

操作operand1数可以是寄存器或内存地址,也operand2可以是寄存器值、内存值或立即数。

  • 跳转指令:跳转指令将程序控制权转移到由操作数标签指示的新指令集。跳转指令有两种类型。
    • 无条件跳转(JMP):直接跳转到指定的标签。
    • 条件跳转:这些指令用于仅在满足特定条件时跳转到指定位置,并在执行其他CMP指令后调用。该指令首先通过标志位判断条件是否满足,然后跳转到作为操作数给出的标签。它与其他编程语言中的条件语句非常相似if。8086 汇编语言中共有 31 条条件跳转指令。

使用变量

在汇编程序中,所有变量都在段中声明data。emu8086 提供了一些用于声明变量的define 指令。具体来说,本文将使用DB(define byte) 和(define word) 指令,它们分别分配 1 个字节和 2 个字节。DW

[variable-name] define-directive initial-value [,initial-value]...
Enter fullscreen mode Exit fullscreen mode

这里,variable-name是每个存储空间的标识符。汇编器会为数据段中定义的每个变量名关联一个偏移值。

以下是一个变量声明示例,其中我们初始化了 `a`num和 `b`,char它们的初始值可以稍后更改。`a` 的初始值为一个字符串,末尾output带有美元符号 ($ ) 以指示字符串结束。`b` 的初始值未知。我们可以使用`$("a", "b ...$input_char?

; Data segment
.data
num DB 31H
char DB 'A'
output DW "Hello, World!!$"
input_char DB ?
Enter fullscreen mode Exit fullscreen mode

我们现在还不能在代码段中使用这些变量!要在代码段中使用这些变量,我们必须先将数据段的地址移动到(数据段)寄存器。在代码DS段的开头使用这行代码导入所有变量。

; Storing all variables in data segment
MOV AX, @data
MOV DS, AX
Enter fullscreen mode Exit fullscreen mode

获取用户输入

emu8086 汇编器支持用户输入,方法是在寄存器中设置预定义值01然后调用中断函数。它会接收用户输入的单个字符,并将该字符的ASCII值保存到寄存器中。emu8086 模拟器以十六进制显示所有值。01HAHINTAL

; input a character from user
MOV AH, 1
INT 21h   ; the input will be stored in AL register
Enter fullscreen mode Exit fullscreen mode

显示输出

emu8086 支持单字符输出,也支持多字符或字符串输出。与输入类似,我们需要在AH寄存器中提供一个预定义的值并调用中断。单字符输出的预定义值为02`0` 或` 1` 02H,字符串输出的预定义值为`1` 或 `2`。调用中断之前,输出值必须存储在通用数据寄存器中。0909H

; Output a character
MOV AH, 2
MOV DL, 35
INT 21H

; Output a string
MOV AH, 9
LEA DX, output
INT 21H
Enter fullscreen mode Exit fullscreen mode

如代码所示,对于单个字符的输出,我们会将值存储在DL寄存器中,因为一个字符占用一个字节(8 位)。但是,对于字符串输出,情况略有不同。我们必须DX使用LEA指令将字符串变量的有效地址(带偏移量的地址)加载到寄存器中。字符串变量必须定义在数据段中。

包含变量声明、输入和输出的完整代码已在GitHub上提供。

分支或使用条件

我们可以使用条件跳转指令来模拟高级编程语言支持的 if-else 条件语句CMP。一些常用的条件跳转指令包括:

操作说明 如果跳跃 类似于
杰伊 平等的 ==
杰克·洛 较少的 <
JLE 小于或等于 <=
JG 更大的 >
JGE 大于或等于 大于等于

还有一些JMP指令的作用类似于else高级语言中的语句。以下汇编代码比较AL寄存器值,5并将相应的值设置到BL寄存器中。

; setting a test value
MOV AL, 5

; Compare
CMP AL, 5
JG greater  ; if greater
JE equal    ; else if equal
JMP less    ; else

greater:
MOV BL, 'G'
JMP after

equal:
MOV BL, 'E'
JMP after

less:
MOV BL, 'L'

after:
; Other codes
; Note: BL will contain 'E' at this point
Enter fullscreen mode Exit fullscreen mode

完整的代码可以在这个GitHub 仓库中找到。

使用循环

我们也可以在汇编语言中使用循环。然而,与高级语言不同,汇编语言没有提供不同的循环类型。虽然 emu8086 模拟器支持五种循环语法(`if`、`if` LOOP`if`、LOOPE` if` `if`),但它们在许多情况下不够灵活。我们可以使用条件语句和跳转语句来创建自定义循环。以下是汇编语言中实现的各种循环类型,它们都是等效的。LOOPNELOOPNZLOOPZ

for 循环

for循环包含初始化部分(用于初始化循环变量)、循环条件部分,以及递增/递减部分(用于在下一次迭代之前进行一些计算或更改循环变量)。以下是一个for循环的示例C

char bl = '0';
for (int cl = 0; cl < 5; cl++) {
  // body
  bl++;
}
Enter fullscreen mode Exit fullscreen mode

等效的汇编代码如下:

MOV BL, '0'

init_for:
; initialize loop variables
MOV CL, 0

for:
; condition
CMP CL, 5
JGE outside_for

; body
INC BL

; increment/decrement and next iteration
INC CL
JMP for

outside_for:
; other codes
Enter fullscreen mode Exit fullscreen mode

while 循环

与for循环不同while循环没有初始化部分。它只有一个循环条件部分,如果条件满足,则执行循环体部分。在循环体部分,我们可以在下一次迭代之前进行一些计算。以下是一个用编程语言编写的while循环示例C

char bl = '0';
int cl = 0;
while (cl < 5) {
  // body
  bl++;
  cl++;
}
Enter fullscreen mode Exit fullscreen mode

相同的汇编代码如下:

MOV CL, 0
MOV BL, '0'

while:
; condition
CMP CL, 5
JGE outside_while

; body
INC BL
INC CL

; next iteration
JMP while

outside_while:
; other codes
Enter fullscreen mode Exit fullscreen mode

Do while 循环

与while循环类似do-while循环也包含循环条件部分和循环体。唯一的区别在于,即使条件为真,循环体中的代码也至少会执行一次。以下是一个用编程语言编写的do-while循环false示例C

char bl = '0';
int cl = 0;
do {
  // body
  bl++;
  cl++;
} while (cl < 5);
Enter fullscreen mode Exit fullscreen mode

对应的汇编代码如下:

MOV CL, 0
MOV BL, '0'

do_while:
; body
INC BL
INC CL

; condition
CMP CL, 5
JL do_while

; other codes
Enter fullscreen mode Exit fullscreen mode

使用 LOOP 语法

我们可以使用预定义的循环语法,并将CX寄存器用作计数器。以下是一个循环语法示例,其功能与之前的循环相同。

MOV BL, '0'

; initialize counter
MOV CX, 5

loop1:
INC BL
LOOP loop1
Enter fullscreen mode Exit fullscreen mode

GitHub上提供了包含各种循环的完整代码

包含指令

Include指令用于访问和使用其他文件中定义的过程和宏。其语法后跟include扩展名的文件名。

include file_name
Enter fullscreen mode Exit fullscreen mode

汇编器会自动在两个位置搜索文件,如果找不到则会显示错误。这两个位置是:

  1. 源文件所在的文件夹
  2. 文件Inc

Inc文件夹中,有一个名为emu8086.inc 的文件,其中定义了一些有用的过程和宏,可以简化编码工作。要使用这些功能,我们需要在源代码的开头包含该文件。

include 'emu8086.inc'
Enter fullscreen mode Exit fullscreen mode

现在,我们可以在代码段中使用这些宏。我发现以下一些宏和过程最有用:

  • PRINT宏用于打印字符串。用法示例:PRINT output.
  • PUTC宏用于打印 ASCII 字符。用法示例:PUTC char.
  • GET_STRING过程用于从用户获取一个以空字符结尾的字符串,直到Enter按下某个键为止。在使用此过程的指令DEFINE_GET_STRING之前声明它。END
  • CLEAR_SCREEN过程用于清除整个屏幕并将光标位置设置为屏幕开头。在使用此过程的指令DEFINE_CLEAR_SCREEN之前声明它。END

emu8086.inc要了解有关文件中的宏和过程的更多信息,请访问此页面

补充:反三角形问题

让我们来解决一个运用目前所学知识的问题。任务是让用户输入一个数字(1-9),并#在控制台中打印一个倒三角形。如果用户输入了无效字符,则应显示相应的错误信息。示例输出如图所示。

逆三角形示例

请先自行尝试解决,如果无法解决,再继续阅读。

为了解决这个问题,我们需要完成以下任务:

  • 用户输入一个数字
  • 验证输入
  • 显示用户友好型消息
  • 现在到了棘手的部分。我们不能只用一个 for 循环来打印一个倒三角形。为此,我们需要使用两个循环,一个嵌套在另一个里面,也就是所谓的嵌套循环。在外层循环中,我们可以检查要打印多少行,以及在开头或结尾打印换行符。内层循环可以用来打印#

以下是嵌套循环的示例代码:

; Initialize outer loop counter
MOV BL, 0   ; counts line number starting from 0

outer_loop: ; using while loop format
CMP BL, x   ; assuming x contains user input
JE outside_loop

; Print new-line

; Initialize inner loop counter
MOV CH, 0
MOV CL, x
SUB CL, BL  ; subtract current line number from x

inner_loop:
; Print #

LOOP inner_loop

; Increment outer loop counter
INC BL
JMP outer_loop

outside_loop:
; other codes
Enter fullscreen mode Exit fullscreen mode

我的代码最终输出结果如下:

反三角形问题输出截图

完整的解决方案可以在我的GitHub 仓库中找到

概括

本文涵盖了丰富的内容。首先,我们了解了汇编语言的概念以及一些汇编器的名称。然后,我们理解了代码结构,并发现了8086微处理器中的所有寄存器和标志位。在理解了一些汇编指令之后,我们学习了如何定义变量、如何从用户获取输入以及如何在屏幕上输出内容。接着,我们学习了条件语句和循环语句,最后,我们用汇编语言解决了一个实际问题。

文章来源:https://dev.to/amritoo/a-beginners-guide-to-assemble-language-using-emu8086-2k75