汇编语言基本概念简介

汇编笔记是根据C语言中文网完成的

汇编语言是什么

汇编语言是最古老的编程语言,在所有的语言中,它与原生机器语言最为接近。它能直接访问计算机硬件,要求用户了解计算机架构和操作系统。

通过本教程有助于学习计算机体系结构、机器语言和底层编程的基本原理。可以学到足够的汇编语言,来测试其掌握的当今使用最广泛的微处理器系列的知识。在高级语言层次上,很多编程错误不容易被识别。因此,程序员经常会发现需要“深入”到程序内部,才能找出程序不工作的原因。

学习汇编可能会问的问题

需要怎样的背景知识?
在学习本教程之前,至少使用过一种结构化高级语言进行编程,如 Java、C、Python 或 C++。需要了解如何使用 IF 语句、数组和函数来解决编程问题。


什么是汇编器和链接器?
**汇编器(assembler)**是一种工具程序,用于将汇编语言源程序转换为机器语言。链接器(linker)也是一种工具程序,它把汇编器生成的单个文件组合为一个可执行程序。还有一个相关的工具,称为调试器(debugger),使程序员可以在程序运行时,单步执行程序并检查寄存器和内存状态。


汇编语言与机器语言有什么关系?
机器语言(machine language)是一种数字语言, 专门设计成能被计算机处理器(CPU)理解。所有 x86 处理器都理解共同的机器语言。
汇编语言(assembly language)包含用短助记符如 ADD、MOV、SUB 和 CALL 书写的语句。汇编语言与机器语言是一对一(one-to-one)的关系:每一条汇编语言指令对应一条机器语言指令。高级语言如 Python、C++ 和 Java 与汇编语言和机器语言的关系是一对多(one-to-many)。比如,C++ 的一条语句就会扩展为多条汇编指令或机器指令。

大多数人无法阅读原始机器代码,因此,这里探讨的是与之最接近的汇编语言。例如,下面的 C++ 代码进行了两个算术操作,并将结果赋给一个变量。假设 X 和 Y 是 整数:

1
2
int Y;
int X = ( Y + 4 ) * 3;

与之等价的汇编语言程序如下所示。这种转换需要多条语句,因为每条汇编语句只对应一条机器指令:

1
2
3
4
5
mov eax,Y  ;Y 送入 EAX 寄存器
add eax,4 ;EAX 寄存器内容加 4
mov ebx,3 ;3 送入 EBX 寄存器
imul ebx ;EAX 与 EBX 相乘
mov x,eax ;EAX 的值送入 X

**寄存器(register)**是 CPU 中被命名的存储位置,用于保存操作的中间结果。这个例子的重点不是说明 C++ 与汇编语言哪个更好,而是展示它们的关系。


汇编语言的应用(用途)

应用类型 高级语言 汇编语言
商业或科学应用程序,为单一的中型或大型平台编写 规范结构使其易于组织和维护大量代码 最小规范结构,因此必须由具有不同程度经验的程序员来维护结构。这导致对已有代码的维护困难
硬件设备驱动程序 语言不一定提供对硬件的直接访问。 即使提供了,可能也需要难以控制的编码技术,这导致维护困难 对硬件的访问直接且简单。当程序较短且文档良好时易于维护
为多个平台(不同的操作系统)编写的商业或科学应用程序 通常可移植。在每个目标操作系统上, 源程序只做少量修改就能重新编译 需要为每个平台单独重新编写代码, 每个汇编器都使用不同的语法。维护困难
需要直接访问硬件的嵌入式系统和电脑游戏 可能生成很大的可执行文件,以至于超出设备的内存容量 理想,因为可执行代码小,运行速度快

虚拟机

与只使用语言描述相比,把每一层都想象成有一台假设的计算机或者虚拟机会更容易一些。通俗地说,虚拟机可以定义为一个软件程序,用来模拟一些其他的物理或虚拟计算机的功能。

虚拟机,将其称为 VM1,可以执行 L1 语言编写的指令。虚拟机 VM0 可以执行 L0 语言编写的指令:

每一个虚拟机既可以用硬件构成也可以用软件构成。程序员可以为虚拟机 VM1 编写程序,如果能把 VM1 当作真实计算机予以实现,那么,程序就能直接在这个硬件上执行。否则,用 VM1 写出的程序就被翻译 / 解释为 VM0 程序,并在机器 VM0 上执行。

机器 VM1 与 VM0 之间的差异不能太大,否则,翻译或解释花费的时间就会非常多。如果 VM1 语言对程序员来说还不够友好到足以用于应用程序的开发呢?

可以为此设计另一个更加易于理解的虚拟机 VM2。这个过程能够不断重复,直到虚拟机 VMn 足够支持功能强大、使用方便的语言。

Java 编程语言就是以虚拟机概念为基础的。Java 编译器把用 Java 语言编写的程序翻译为 Java 字节码(Java byte code)
后者是一种低级语言,能够在运行时由 **Java 虚拟机(JVM)**程序快速执行。JVM 已经在许多不同的计算机系统上实现了,这使得 Java 程序相对而言独立于系统。

汇编语言的数据表示

汇编语言程序员处理的是物理级数据,因此他们必须善于检查内存寄存器。通常,二进制数被用于描述计算机内存的内容;有时也使用十进制和十六进制数。所以必须熟练掌握数字格式,以便快速地进行数字的格式转换。

每一种数制格式或系统,都有一个基数(base),也就是可以分配给单一数字的最大符号数。下表给岀了数制系统内可能的数字,这些系统是硬件和软件手册中最常使用的。

系统 基数 可能的数字
二进制 2 01
八进制 8 01234567
十进制 10 0123456789
十六进制 16 0123456789ABCDEF

在表的最后一行,十六进制使用的是数字 0 到 9,然后字母 A 到 F 表示十进制数 10 到 15。在展示计算机内存的内容和机器级指令时,使用十六进制是相当常见的。

二进制(bit)整数

二进制整数可以是有符号的,也可以是无符号的。有符号整数又分为正数和负数,无符号整数默认为正数,零也被看作是正数。

在书写较大的二进制数时,有些人喜欢每 4 位或 8 位插入一个点号,以增加数字的易读性。比如,1101.1110.0011.1000.0000 和 11001010.10101100

从 LSB 开始,无符号二进制整数中的每一个位代表的是 2 的加 1 次幂。下图展示的是对一个 8 位的二进制数来说,2 的幂是如何从右到左增加的:

下表列出了从 20 到 215 的十进制值。

2ⁿ 十进制值 2ⁿ 十进制值
20 1 28 256
21 2 29 512
22 4 210 1024
23 8 211 2048
24 16 212 4096
25 32 213 8192
26 64 214 16384
27 128 215 32768

二进制加法运算

两个二进制整数相加时,是位对位处理的,从最低的一对位(右边)开始,依序将每一对位进行加法运算。两个二进制数字相加,有四种结果,如下所示:

0 + 0 = 0 0 + 1 = 1
1 + 0 = 1 1 + 1 = 10

1 与 1 相加的结果是二进制的 10(等于十进制的 2)。多出来的数字向更高位产生一个进位。如下图所示,两个二进制数 0000 0100 和 0000 0111 相加:

字节(byte)简介

在 x86 计算机中,所有数据存储的基本单位都是字节(byte),一个字节有 8 位。其他的存储单位还有字(word)(2 个字节),双字(doubleword)(4 个字节)和四字(quadword)(8 个字节)。

下图展示了每个存储单位所包含的位的个数:

下表列出了所有无符号整数可能的取值范围:

类型 取值范围 按位计的存储大小 类型 取值范围 按位计的存储大小
无符号字节 0 到 28-1 8 无符号四字 0 到 264-1 64
无符号字 0 到 216-1 16 无符号八字 0 到 2128-1 128
无符号双字 0 到 232-1 32

更大的单位:

  • 1 千字节(kilobyte)(1KB)等于 210,或 1024 个字节。
  • 1 兆字节(megabyte)(1MB)等于 220,或 1 048 576 字节。
  • 1 吉字节(gigabyte)(1GB)等于 230 即 10243,或 1 073 741 824 字节。
  • 1 太字节(terabyte)(1TB)等于 240,即 10244,或 1 099 511 627 776 字节。
  • 1 拍字节(petabyte)等于 250,或 1 125 899 906 842 624 字节。
  • 1 艾字节(exabyte)等于 260,或 1 152 921 504 606 846 976 字节。
  • 1 泽字节(zettabyte)等于 270 个字节。
  • 1 尧字节(yottabyte)等于 280 个字节。

十六进制整数

大的二进制数读起来很麻烦,因此十六进制数字就提供了一种简便的方式来表示二进制数据。十六进制整数中的 1 个数字就表示了 4 位二进制位,两个十六进制数字就能表示一个字节

一个十六进制数字表示的范围是十进制数 0 到 15,所以,用字母 A 到 F 来代表十进制数 10 到 15。
下表列出了每个 4 位二进制序列如何转换为十进制和十六进制数值。

二进制 十进制 十六进制 二进制 十进制 十六进制
0000 0 0 1000 8 8
0001 1 1 1001 9 9
0010 2 2 1010 10 A
0011 3 3 1011 11 B
0100 4 4 1100 12 C
0101 5 5 1101 13 D
0110 6 6 1110 14 E
0111 7 7 1111 15 F

下面的例子说明了二进制数 0001 0110 1010 0111 1001 0100 是如何与十六进制数 16A794 等价的。

1 6 A 7 9 4
0001 0110 1010 0111 1001 0100

补码及进制转换

有符号二进制整数有正数和负数。在 x86 处理器中,MSB 表示的是符号位:0 表示正数,1 表示负数。下图展示了 8 位的正数和负数:

补码表示

负整数用补码(two`s-complement)表示时,使用的数学原理是:一个整数的补码是其加法逆元。(如果将一个数与其加法逆元相加,结果为 0。)

补码表示法对处理器设计者来说很有用,因为有了它就不需要用两套独立的电路来处理加法和减法。例如,如果表达式为 A-B,则处理器就可以很方便地将其转换为加法表达式:A+(-B)

将一个二进制整数按位取反(求补)再加 1就形成了它的补码。以 8 位二进制数 0000 0001 为例,求其补码为 1111 1111,过程如下所示

初始值 00000001
第一步:按位取反 11111110
第二步:将上一步得到的结果加 1 11111110 +00000001
和值:补码表示 11111111

**1111 1111 是 -1 的补码。**补码操作是可逆的,因此,11111111 的补码就是 0000 0001。

负整数求补码方法:

  • 符号位不变

  • 剩余位去翻后加1

    负整数与其补码相加为0

    正整数补码就是它本身

十六进制数的补码

将一个十六进制整数按位取反并加 1,就生成了它的补码。一个简单的十六进制数字取反方法就是用 15 减去该数字。下面是一些十六进制数求补码的例子:

1
2
6A3D --> 95C2 + 1 --> 95C3
95C3 --> 6A3C + 1 --> 6A3D

最大值和最小值

n 位有符号整数只用 n-1 来表示该数的范围。下表列出了有符号单字节、字、双字、四字和八字的最大值与最小值。

类型 范围 存储位数 类型 范围 存储位数
有符号字节 -27 到 +27-1 8 有符号四字 -263 到 +263-1 64
有符号字 -215 到 +215-1 16 有符号八字 -2127 到 +2127-1 128
有符号双字 -231 到 +231-1 32

二进制减法运算

执行二进制减法还有更简单的方法,即将被减去数的符号位取反,然后将两数相加。这个方法要求用一个额外的位来保存数的符号。现在以(01101-00111)为例来试一下这个方法。首先,将 00111 按位取反 11000 加 1,得到 11001。然后,把两个二进制数值相加,并忽略最高位的进位:

1
2
3
4
01101    (+13)
11001 (-7)
-------
00110 (+6)

结果正是我们预期的 +6。

字符在计算机中是如何表示的

计算机使用的是字符集,将字符映射为整数。早期,字符集只用 8 位表示。即使是现在,在字符模式(如 MS-DOS)下运行时,IBM 兼容微机使用的还是 ASCII(读为“askey”)字符集。

ASCII 是美国标准信息交换码(AmeTican Standard Code for Information Interchange)的首字母缩写。在 ASCII 中,每个字符都被分配了一个独一无二的 7 位整数。

由于 ASCII 只用字节中的低 7 位,因此最高位在不同计算机上被用于创建其专有字符集。比如,IBM 兼容微机就用数值 128〜255 来表示图形符号和希腊字符。

ANSI 字符集

美国国家标准协会(ANSI)定义了 8 位字符集来表示多达 256 个字符。前 128 个字符对应标准美国键盘上的字母和符号。后 128 个字符表示特殊字符,诸如国际字母表、重音符号、货币符号和分数。

Microsoft Windows 早期版本使用 ANSI 字符集。

Unicode 标准

当前,计算机必须能表示计算机软件中世界上各种各样的语言。因此,Unicode 被创建出来,用于提供一种定义文字和符号的通用方法。

Unicode 定义了数字代码(称为代码点(code point)),定义的对象为文字、符号以及所有主要语言中使用的标点符号,包括欧洲字母文字、中东的从右到左书写的文字和很多亚洲文字。代码点转换为可显示字符的格式有三种:

  • UTF-8 用于 HTML,与 ASCII 有相同的字节数值。
  • UTF-16 用于节约使用内存与高效访问字符相互平衡的环境中。比如,Microsoft Windows 近期版本使用了 UTF-16,其中的每个字符都有一个 16 位的编码。
  • UTF-32 用于不考虑空间,但需要固定宽度字符的环境中。每个字符都有一个 32 位的编码。

ASCII 字符串

有一个或多个字符的序列被称为字符串(string)。更具体地说,一个 ASCII 字符串是保存在内存中的,包含了 ASCII 代码的连续字节。比如,字符串“ABC123”的数字代码是 41h、42h、43h、31h、32h 和 33h。

以空字节结束(null-terminated)的字符串是指,在字符串的结尾处有一个为 0 的字节。C 和 C++语言使用的是以空字节结束的字符串,一些 Windows 操作系统函数也要求字符串使用这种格式。

汇编语言布尔表达式(NOT、AND、OR)

布尔代数(boolean algebra)定义了一组操作,其值为真(true)或假(false)。

一个布尔表达式(boolean expression)包括一个布尔运算符以及一个或多个操作数。每个布尔表达式都意味着一个为真或假的值。以下为运算符集合:

  • 非(NOT):标记为 ¬ 或 ~ 或 ’
  • 与(AND):标记为^或 ·
  • 或(OR):标记为 ∨ 或 +

NOT 是一元运算符,其他运算符都是二元的。布尔表达式的操作数也可以是布尔表达式。示例如下:

达式 说明 表达式 说明
¬X NOT X ¬X∨Y (NOT X) OR Y
X^Y X AND Y ¬(X^Y) NOT (X AND Y)
X∨Y X OR Y X^¬Y X AND (NOT Y)

NOT

NOT 运算符将布尔值取反。用数学符号书写为 ¬X,其中,X 是一个变量(或表达式),其值为真(T)或假(F)。下表列出了对变量 X 进行 NOT 运算后所有可能的输岀。 左边为输入,右边(阴影部分)为输出:

X ¬X
F T
T F

真值表中,0 表示假,1 表示真。

AND

布尔运算符 AND 需要两个操作数,用符号表示为 X ^ Y。下表列出了对变量 X 和 Y 进行 AND 运算后,所有可能的输出(阴影部分):

X Y X^Y
F F F
F T F
T F F
T T T

当两个输入都是真时,输出才为真。这与 C++ 和 Java 的复合布尔表达式中的逻辑 AND 是相对应的。

汇编语言中 AND 运算符是按位操作的。如下例所示,X 中的每一位都与 Y 中的相应位进行 AND 运算

1
2
3
X :        11111111
Y : 00011100
X ^ Y : 00011100

如下图所示,结果值 0001 1100 中的每一位表示的是 X 和 Y 相应位的 AND 运算结果。

OR

布尔运算符 OR 需要两个操作数,用符号表示为 X∨Y。下表列出了对变量 X 和 Y 进行 OR 运算后,所有可能的输出:

X Y X∨Y
F F F
F T T
T F T
T T T

当两个输入都是假时,输出才为假。这个真值表与 C++ 和 Java 的复合布尔表达式中的逻辑 OR 对应。

OR 运算符也是按位操作。在下例中,X 的每一位与 Y 的对应位进行 OR 运算,结果为 1111 1100:

1
2
3
X :    11101100
Y : 00011100
X∨Y : 11111100

如下图所示,每一位都独立进行 OR 运算,生成结果中的对应位。

运算符优先级

运算符优先级原则(operator precedence rule)用于指示在多运算符表达式中,先执行哪个运算。在包含多运算符的布尔表达式中,优先级是非常重要的。

如下表所示,NOT 运算符具有最高优先级,然后是 AND 和 OR 运算符。可以使用括号来强制指定表达式的求值顺序

表达式 运算符顺序
¬X∨Y NOT,然后 OR
¬(X^Y) OR,然后 NOT
X∨(X^Y) AND,然后 OR

注意:汇编笔记是根据C语言中文网完成的