汇编语言基本概念简介
汇编笔记是根据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 | int Y; |
与之等价的汇编语言程序如下所示。这种转换需要多条语句,因为每条汇编语句只对应一条机器指令:
1 | mov eax,Y ;Y 送入 EAX 寄存器 |
**寄存器(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 | 6A3D --> 95C2 + 1 --> 95C3 |
最大值和最小值
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 | 01101 (+13) |
结果正是我们预期的 +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 | X : 11111111 |
如下图所示,结果值 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 | X : 11101100 |
如下图所示,每一位都独立进行 OR 运算,生成结果中的对应位。
运算符优先级
运算符优先级原则(operator precedence rule)用于指示在多运算符表达式中,先执行哪个运算。在包含多运算符的布尔表达式中,优先级是非常重要的。
如下表所示,NOT 运算符具有最高优先级,然后是 AND 和 OR 运算符。可以使用括号来强制指定表达式的求值顺序:
表达式 | 运算符顺序 |
---|---|
¬X∨Y | NOT,然后 OR |
¬(X^Y) | OR,然后 NOT |
X∨(X^Y) | AND,然后 OR |
注意:汇编笔记是根据C语言中文网完成的