汇编语言快速入门

2019-09-27  本文已影响0人  去旅行_2016

引言

汇编语言虽然是低级语言,但具有不可替代的地位。汇编指令与硬件密切相关,因此学习汇编语言可以很好地了解计算机的结构。

机器指令、汇编指令

计算机中的电路只有两种状态:高电平和低电平,因此只能用二进制来表示各种信息。

机器指令是有特殊作用的二进制数,会触发计算机执行特定的操作,如加、减、乘、除等算术运算。机器指令由操作码和操作数两部分组成:

下图展示了 4 条机器指令,它们的作用都是修改寄存器的值。前 2 条是修改寄存器 AX 的值;后 2 条是修改寄存器 BX 的值。都是先修改为全 0,再修改为全 1

通过对比,不难发现,它们都有相同的部分 1011,表示要修改寄存器的值。前两列就是操作码,因为前 2 条和后 2 条对应不同的寄存器,所以它们的操作码不同。后面 4 列是操作数。

显然,一连串的 0 和 1,对于人类来说,难以记忆和辨别。为此,人们就用一些文字(称为助记符)来代替每条机器指令中固定不变的部分,汇编语言由此诞生。

例如,上述 4 条机器指令对应的汇编指令如下:

MOV AX, 0000H
MOV AX, FFFFH
MOV BX, 0000H
MOV BX, FFFFH

这样的写法与自然语言接近,便于阅读和记忆。

汇编指令与机器指令之间具有一一对应的关系。但汇编指令在交由计算机执行前,还是要先转换成机器指令,才能被计算机识别和运行,这个转换过程称为编译,完成这项工作的程序称为编译器。

8086 CPU

机器指令的集合称为指令集。目前被广泛采用的指令集是 Intel 研发的 x86。不同的 CPU 可能采用不同的指令集,相应的汇编指令也不同。因此汇编语言是不能跨平台的。

Intel CPU 的主要发展过程是:4004/8008、8080、8086/8088、80286、80386、80486、Pentium、Pentium Pro、Pentium 2、Pentium 3、Pentium 4、Pentium D、Core 2

8086 是具有代表性的 16 位 CPU,是第一个采用 x86 指令集的 CPU。Intel 后续推出的各型 CPU 都与其兼容。学习 8086/8088 CPU 是进一步学习和应用更先进 CPU 的基础。

几条汇编指令

8086 CPU 有 14 个寄存器,并且都是 16 位的,也就是可以放得下一个 16 位的二进制数。有 4 个是通用寄存器,用来存放一般性的数,在汇编指令中分别用 AX、BX、CX、DX 表示。

下面介绍 2 条最基本的汇编指令:

MOV 指令可用于将某个值传送到指定寄存器。下面指令将数值 5 传送到寄存器 AX 中,执行后,寄存器 AX 的值就变成 5

MOV AX, 5H

ADD 指令可用于把指定寄存器的值加上另一个值。下面指令将寄存器 AX 的值加上数值 3,执行后,寄存器 AX 的值就变成 8

ADD AX, 3H

SUB 指令可用于把指定寄存器的值减去另一个值。下面指令将寄存器 AX 的值减去数值 2,执行后,寄存器 AX 的值就变成 6

SUB AX, 2H

INC 指令可用于将指定寄存器的值加 1,下面指令相当于 ADD AX, 1

INC AX

通用寄存器又可以看作是两个相互独立的 8 位寄存器组成:

下面指令先把数值 4EH 传送到寄存器 DH,再把数值 20H 传送到寄存器 DL,相当于把 4E20H 传送到寄存器 DX

MOV DH, 4EH
MOV DL, 20H

Debug

Debug 是一个调试工具,可以直接查看和修改各个寄存器的值和内存的值,可以在机器指令的层面跟踪程序的运行。

Debug 是一个 16 位的命令行工具。DOS 和 16 位或 32 位的 Windows 都已内置 Debug,按 Win+R,输入 debug,回车即可打开。但 64 位 Windows 移除了 Debug,即使有,也无法直接运行。有两个解决方案:

不管是那种情况,启动 Debug 的方法都是一样的:打开命令提示符窗口,输入 debug,然后按下 Enter,此时命令提示符变成 -

这个时候就可以使用 Debug 提供的命令了。要使用这些命令,同样是先输入命令,再按下回车便可执行。下面介绍一些常用的命令:

在 debug 中,所有数值都以 16 进制表示,输入时结尾不需要 H

d 命令用来查看内存中的值。每次输出 8 行,每行 16 个字节,共输出 128 个字节。第一列是每行第一个字节的地址。右侧的字符是将内存中的值视为 ASCII 编码而得到的。如果没有对应可显示的字符,就用 . 代替。

u 命令将内存中的值视为机器指令,并显示对应的汇编指令,也就是反汇编。

r 命令用来查看寄存器的值。

r 命令还可以跟上某个寄存器的名称,以修改寄存器的值。Debug 会先显示寄存器的当前值,再等待输入新的值,按回车即可完成修改。

执行 q 命令,可退出 debug

段地址、偏移地址

8086 CPU 的地址线有 20 根,即地址是 20 位的二进制数。20 位的完整地址称为物理地址。但最大的寄存器也就只有 16 位,不足以保存 20 位的物理地址。为此,8086 CPU 需要两个寄存器相互配合,才能完整地表示一个地址。具体方法是:

把物理地址看成两个数的和。这两个数中,一个只有 16 位,刚好可以放在一个 16 位的寄存器中。这 16 位称为 偏移地址,保存偏移地址的寄存器称为偏移地址寄存器;另一个有 20 位,但最低 4 位都是 0,因此也只需一个 16 位的寄存器来保存它的高 16 位即可。这 16 位称为 段地址,保存段地址的寄存器称为段寄存器。

显然,段地址要先左移 4 位(相当于 × 16),再加上偏移地址,才是真正的物理地址。例如,段地址是 F492H,偏移地址是 3H,则物理地址 = F492H × 16 + 3H

在 debug 中,d 命令用来查看内存中的值。可以指定要查看的内存空间,只需在 d 后面跟上用冒号隔开的段地址和偏移地址即可。

e 命令用来修改内存的值,使用时要在 e 后面跟上用冒号隔开的段地址和偏移地址。在输入命令并按下回车后,就进入内存编辑模式。此时,debug 会从地址对应的字节开始,每次输出一个字节的当前值,并等待输入新的值,按空格,可接着修改下一个字节,按回车则结束命令。

e 命令还可以很方便地在内存中填入字符的 ASCII 编码,只需将字符放在双引号中,跟在命令后面。

a 命令和 e 命令一样,也是用来修改内存的值。不过,a 命令等待输入的是汇编指令,debug 会将输入的汇编指令转换成机器指令,再写入指定的内存空间。

代码段、数据段

CPU 通过段地址和偏移地址的组合,得到某个内存单元的地址,可看作是把内存分段,先由段地址给出某一段的起始地址,再由偏移地址指出这一段中的其中一个单元。

根据用途,段可以分为代码段和数据段。如果保存的是一系列指令,就称这一段为代码段。

要执行这些指令,只需让程序计数器指向代码段的第一个单元即可。

如果保存的是数据,就称这一段为数据段。

CS:IP 程序计数器

8086 CPU 的段寄存器有 DS, ES, SS, CS,偏移地址寄存器有 SP, BP, SI, DI, IP 以及 BX

其中 CS 为代码段寄存器,IP 为指令指针寄存器,它们共同组成程序计数器,保存下一条指令的地址。在 debug 中使用 r 命令时,debug 会同时把下一条指令显示出来。

执行 t 命令,就可以执行下一条指令。

通过修改程序计数器的值,就可以改变指令的执行顺序。在 debug 中可以用 r 命令直接修改程序计数器的值。

转移指令

专门用来修改程序计数器的汇编指令是 JMP。下面指令将程序计数器的值修改为 0730:0100,执行后,这个地址上的指令就成为下一条指令。

JMP 0730:0100

也可以只修改 IP 寄存器:JMP 命令支持把另一个寄存器的值传送给 IP 寄存器。

MOV AX, 0100
JMP AX

这类用于修改程序计数器的汇编指令称为转移指令。

有两个比较特殊的转移指令 CALLRET,这两者的搭配,可以实现「回过头」的效果:先用 CALL 跳转到别处,之后,又可以用 RET 跳转回原来的位置。

DS:[BX] 读写内存

DS 为数据段寄存器。CPU 要读写某个内存单元时,段地址写入 DS,偏移地址作为指令的一部分,放在中括号中。下面指令的作用是将 2000:0100 单元的值传送到 AL 寄存器。

MOV DX, 2000
MOV DS, DX
MOV AL, [0100]

当然,也可以反过来,从寄存器传送到内存单元。下面指令的作用是将 AL 寄存器的值传送到 2400:0200 单元。

MOV DX, 2400
MOV DS, DX
MOV [0200], AL

偏移地址可以先放在 BX 寄存器中,然后在中括号里写上 BX。下面指令的作用是将 0730:0100 单元的值传送到 AH 寄存器。

MOV DX, 0730
MOV DS, DX
MOV BX, 0100
MOV AH, [BX]

8086 CPU 的数据总线有 16 位,可以一次性传送 16 位数据,即两个字节。通常,两个字节称为一个字。

字和字节一样,都是容量的单位。8 个二进制位,称为 1 字节;而 16 个二进制位,则称为 1 字,并把低 8 位称为低位字节、高 8 位称为高位字节。

只要指令中包含 16 位寄存器,就会连续传送两个字节。

MOV DX, 0730
MOV DS, DX
MOV AX, [0100]

上面指令的作用是将 0730:0100 单元的值传送到 AX 寄存器的低 8 位;将 0730:0101 单元的值传送到 AX 寄存器的高 8 位,如下图所示:

SS:SP 堆栈指针

栈是一段特殊的内存空间,称为栈段。它的特殊之处在于:越后写入,越先读出,这种规则称为后入先出(Last In First Out,LIFO)

段寄存器 SS 和偏移地址寄存器 SP 指向这段内存。通过 PUSH 指令可以将某个寄存器的值写入栈,同时 SP 寄存器减 2,称为 入栈

SS:SP 指向的内存单元,称为 栈顶。通过 POP 指令可以将栈顶的值传送到某个寄存器,同时 SP 寄存器加 2,称为 出栈

在执行 CALL 指令时,发生了两个动作:先把下一条指令的偏移地址入栈,再把 IP 寄存器的值改为指定值。RET 指令则把栈顶的值取出,传送给 IP 寄存器。

第一个程序

一个完整的汇编程序的源代码如下:

ASSUME CS:CODE_SEGMENT

CODE_SEGMENT SEGMENT
    MOV AX, 2
    ADD AX, 2
    ADD AX, 2

    MOV AX, 4C00H
    INT 21H
CODE_SEGMENT ENDS

END

汇编程序的源代码包含两种指令:汇编指令和伪指令。汇编指令会编译成机器指令,最终被 CPU 执行。伪指令相当于 C 语言中的预处理器,编译器根据伪指令来进行编译工作。

源代码中一般还会包含一些标号。一个标号代表一个地址。放在指令前面时,就代表这个指令的地址。标号最终都会被处理成一个地址。

段的概念(代码段、数据段、栈段等)在汇编程序中得到体现。一个汇编程序由多个段组成,至少有一个代码段。

段通过伪指令 SEGMENTENDS 定义。它们总是成对使用:SEGMENT 标识段的开始;ENDS 标识段的结束。使用时,它们前面必须有一个标号,作为段名。

下面代码定义了一个名为 CODE_SEGMENT 的段,中间是一系列指令,因此是一个代码段。段名实际上就是这个代码段第一条指令的地址。

CODE_SEGMENT SEGMENT
    .
    .
    .
CODE_SEGMENT ENDS

伪指令 ASSUME 用于说明某个段与某个段寄存器存在联系,比如说明上面定义的代码段 CODE_SEGMENT 与代码段寄存器 CS 存在联系。

ASSUME CS:CODE_SEGMENT

编译器在编译过程中,如果碰到 END 指令就结束编译。

END

在 DOS 中,要运行一个程序 P1,必须有另一个程序 P2 把它加载到内存,把 CPU 的控制权交给 P1,P1 才得以运行。P1 的最后,还要把 CPU 的控制权交还给 P2,称为程序返回。代码段中的以下两条指令就是用来完成程序返回的。

    MOV AX, 4C00H
    INT 21H

现阶段只要知道,在程序末尾使用两条指令就可以实现程序返回。

编译、连接、跟踪运行

从源代码到可执行文件,要经过两个步骤:编译和连接。编译通过调用编译器完成;连接通过调用连接器完成。编译器可采用 MASM 5.0,连接器可采用 Overlay Linker 3.6

假设前面分析的源代码保存在 test.asm 中,用 MASM 5.0 编译,得到目标文件 test.obj

> masm test.asm;

用 Overlay Linker 3.6 编译,得到可执行文件 test.exe

> masm test.obj;

现在可以直接输入 test 来运行该程序,但那样不能到观察程序的运行过程。可以使用 debug 来跟踪程序的运行。只要在 debug 命令后跟上一个程序的文件名,debug 就会把这个程序加载到内存,并将程序计数器指向其第一条指令。

到了 INT 21H,要用 p 命令来执行。

上一篇下一篇

猜你喜欢

热点阅读