使用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
第一行.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
操作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
-
标签:标签是紧随其后的指令地址的符号名称。它可以放在语句的开头,并作为指令操作数。前面
exit:使用的就是标签。标签有两种类型。- 符号标签:符号标签由一个标识符或符号后跟一个冒号 (
:) 组成。由于它们具有全局作用域并会出现在目标文件的符号表中,因此只需定义一次即可。 - 数字标签:数字标签由一个介于零 (0
0) 到九 (99) 之间的数字和一个冒号 (':) 组成。它们仅用于局部引用,不会被添加到目标文件的符号表中。因此,它们的作用范围有限,并且可以重复定义。
- 符号标签:符号标签由一个标识符或符号后跟一个冒号 (
; Symbolic label
label:
MOV AX, 5
; Numeric label
1:
MOV AX, 5
- 比较指令 (CMP):此指令接受两个操作数,并将其中一个操作数减去另一个操作数,然后相应地设置 OF、SF、ZF、AF、PF 和 CF 标志。结果不会被存储。
CMP operand1, operand2
操作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]...
这里,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 ?
我们现在还不能在代码段中使用这些变量!要在代码段中使用这些变量,我们必须先将数据段的地址移动到(数据段)寄存器。在代码DS段的开头使用这行代码导入所有变量。
; Storing all variables in data segment
MOV AX, @data
MOV DS, AX
获取用户输入
emu8086 汇编器支持用户输入,方法是在寄存器中设置预定义值01,然后调用中断函数。它会接收用户输入的单个字符,并将该字符的ASCII值保存到寄存器中。emu8086 模拟器以十六进制显示所有值。01HAHINTAL
; input a character from user
MOV AH, 1
INT 21h ; the input will be stored in AL register
显示输出
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
如代码所示,对于单个字符的输出,我们会将值存储在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
完整的代码可以在这个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++;
}
等效的汇编代码如下:
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
while 循环
与for循环不同,while循环没有初始化部分。它只有一个循环条件部分,如果条件满足,则执行循环体部分。在循环体部分,我们可以在下一次迭代之前进行一些计算。以下是一个用编程语言编写的while循环示例C。
char bl = '0';
int cl = 0;
while (cl < 5) {
// body
bl++;
cl++;
}
相同的汇编代码如下:
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
Do while 循环
与while循环类似,do-while循环也包含循环条件部分和循环体。唯一的区别在于,即使条件为真,循环体中的代码也至少会执行一次。以下是一个用编程语言编写的do-while循环false示例。C
char bl = '0';
int cl = 0;
do {
// body
bl++;
cl++;
} while (cl < 5);
对应的汇编代码如下:
MOV CL, 0
MOV BL, '0'
do_while:
; body
INC BL
INC CL
; condition
CMP CL, 5
JL do_while
; other codes
使用 LOOP 语法
我们可以使用预定义的循环语法,并将CX寄存器用作计数器。以下是一个循环语法示例,其功能与之前的循环相同。
MOV BL, '0'
; initialize counter
MOV CX, 5
loop1:
INC BL
LOOP loop1
GitHub上提供了包含各种循环的完整代码。
包含指令
Include指令用于访问和使用其他文件中定义的过程和宏。其语法后跟带include扩展名的文件名。
include file_name
汇编器会自动在两个位置搜索文件,如果找不到则会显示错误。这两个位置是:
- 源文件所在的文件夹
- 文件
Inc夹
在Inc文件夹中,有一个名为emu8086.inc 的文件,其中定义了一些有用的过程和宏,可以简化编码工作。要使用这些功能,我们需要在源代码的开头包含该文件。
include 'emu8086.inc'
现在,我们可以在代码段中使用这些宏。我发现以下一些宏和过程最有用:
- 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
我的代码最终输出结果如下:
概括
本文涵盖了丰富的内容。首先,我们了解了汇编语言的概念以及一些汇编器的名称。然后,我们理解了代码结构,并发现了8086微处理器中的所有寄存器和标志位。在理解了一些汇编指令之后,我们学习了如何定义变量、如何从用户获取输入以及如何在屏幕上输出内容。接着,我们学习了条件语句和循环语句,最后,我们用汇编语言解决了一个实际问题。
文章来源:https://dev.to/amritoo/a-beginners-guide-to-assemble-language-using-emu8086-2k75

