术语
Figure 3-13. 8086 Computer (Partial Model)
reg 代表寄存器。 它可以是表 3.13 中列出的任何寄存器。
imm 代表立即数【immediate】(可以理解为字面量,常量)。 术语“立即数【immediate】”用于指代直接由十进制或十六进制表示形式给出的数值,而不是包含该值的寄存器或内存位置。
4.1 The Four Field Format
根据这种格式,每个汇编语言程序都由行【line】组成。 每行【line】由四个字段/域【field】组成。 这四个字域【field】是标签域【label field】、助记符域【mnemonic field】、操作数域名【operand field】和注释域【comment field】。
- 标签域【label field】用于指定跳转指令【jump instruction】的目标的标签。 跳转【jump】与goto 指令相同, 4.4 节给出了跳转指令的示例。
- 助记符域【mnemonic field】包含指令说明符【instruction specifier】。 助记符【mnemonic】的示例有
MOV, ADD, SUB
等。助记符【mnemonic】一词表明它使机器代码更加易于记住,尽管事实并非如此; 它使得机器代码无需记住。 - 操作数域【operand field】包含指令正在操作的一个或多个对象。 如果有多个操作数,则用逗号分隔。 正如我们上面注意到的,
ADD
需要两个操作数。 另一方面,JMP
则只需要一个。 有些助记符则根本不需要操作数。 - 注释域【comment field】包含文档。 它以分号【semicolon】开头。 在任何计算机语言中,文档都很重要。 在汇编语言中,文档尤其重要,因为没有它,汇编语言程序特别难以阅读。 有时一行【line】可能只包含注释。
程序 4.1 是一个 x86 汇编语言程序的示例。 读者应该注意到它们都是四个域布局的,即使某些行【line】上的某些域【field】为空。 程序 4.1 中的许多指令都是在本书中首次出现。 这些新指令都将在本章中讨论。
;
; Greatest common divisor program
;
MOV EDX, 0 ; 0 is the only Edlinas input port
IN EAX,[DX] ; Get the user's first input
MOV ECX, EAX ; Get the input out of harm's way
IN EAX,[DX] ; Get the user's second input
MOV EDX, EAX ; Use EDX for the larger of the two inputs
ORD: SUB EAX, ECX ; Use EAX as a working copy of EDX
JZ GCD ; When equality is obtained we are done.
JNS NXT ; We want EDX to be larger. No swap needed
MOV EAX, ECX ; Swap EDX and ECX (Takes three MOV's)
MOV ECX, EDX
NXT: MOV EDX, EAX ; If there was no swap then EDX = EDX-ECX
JMP ORD ; End of the loop
GCD: MOV EAX, EDX ; The GCD is in EDX
MOV EDX, 1 ; We need EDX for the output port number
OUT [DX],EAX ; Display the answer to the user
RET
Example 4.1.
4.2 Computers from the CPU Standpoint
计算机通常被描述为由三部分组成:CPU、内存,和输入/输出【input/output】(或 I/O 系统)。 根据此图绘制的示意图如图4-1所示。 I/O 系统包括显示器、键盘、硬盘【hard drive】、打印机【printer】、调制解调器【modem】、声卡【sound card】和 CDROM。 这些都只是 I/O 设备。 这是一个以处理器【processor】为中心的视图。 正如一个群体的成员有时会根据它们与群体的关系对人进行分类一样,这张计算机的示意图根据计算机的各个部分与 CPU 的关系来定义它们。 然而,由于汇编语言都是与 CPU 一起工作的,因此它对我们来说是一个有用的视图(即以处理器为中心)。
Figure 4-1. All Computers are Divided into Three Parts
如果我们将图中的CPU想象成 386,那么我们可以将数据总线【data bus】视为一组 32 根线连接到 CPU 上的 32 个引脚,而地址总线【address bus】则视为另一组连接到 CPU 上的32根线 。 请注意,内存和 I/O 系统以基本相同的方式连接到处理器。 如果我们想象一下在总线上流动的比特位,CPU 和内存之间的数据传输看起来几乎与 CPU 和 I/O 系统之间的数据传输完全相同。 二者之间的差异实际上归结为 CPU 上的一个特定引脚。 该引脚称为 M/IO# 引脚。 当该引脚处于逻辑 1 时,它发出内存传输信号。 当它处于逻辑 0 时,则表示 I/O 传输。 如果没有 M/IO# 线,上图实际上没有任何意义。 它还需要一条 W/R# 线来指示数据是流入还是流出处理器【processor】。 这些线称为控制线【 control wires】,是控制总线【 control bus】的一部分。
强烈建议读者将图 4-1 与图 3-11 进行比较,图 3-11 更详细地显示了来自三种不同总线的电线如何连接到存储器电路【memory circuit】。
Memory vs. I/O
在表 4.1 中,我们看到 MOV 命令的两种 imm 形式与相应的 I/O 命令进行比较。 这些命令执行 AL 寄存器同内存或 I/O 设备(位于地址 12 的)之间的传输。处理器使用 W/R# 引脚发出读与写的信号,使用 M/IO# 引脚发出内存传输还是 I/O 传输的信号 。 除此之外,交易【transaction】非常相似。
Memory | I/O | |
---|---|---|
Read | MOV AL,[12] | IN AL,[12] |
Write | MOV [12],AL | OUT [12],AL |
Table 4.1. Examples of Memory and I/O Transfer Commands
x86 上的内存寻址和 I/O 寻址之间的一个显着区别是:内存地址在离开处理器之前要经过处理。 该处理有两个阶段:分段和分页。第 12 章描述了分段,第 8 章描述了分页。
I/O 系统和内存之间的另一个区别是它们不具有相同的有效地址范围。 正如第 3 章中所讨论的,有效内存地址的范围因 x86 处理器而异(这取决于处理器具有的地址引脚的数量)。另一方面,有效 I/O 地址的范围在所有 x86 处理器上都是相同的。 它是从 0 到 65,535 的 16 位范围。 顺便说一句,这意味着 I/O 系统不需要连接到所有 32 条地址线,只需连接到底部 16 条即可。这个范围内的地址称为端口【ports 】或端口号【port numbers】。
许多现有的I/O设备,如磁盘驱动器、打印机和串行端口,每个设备都使用少量端口。例如,表4.2列出了标准串口使用的8个地址。(显然,表中的端口号1到4不是我们在这里考虑的特定意义上的端口号。但以下十六进制地址是这些端口号的完美示例。)在Shanley的ISA System Architecture的附录A中可以找到标准PC上使用的相对完整的端口号列表。Linux机器上使用的I/O端口可以使用命令通过/proc目录访问
许多现有的 I/O 设备(例如磁盘驱动器、打印机和串行端口),每个设备都使用少量端口。 例如,表 4.2 列出了标准串行端口【standard serial ports】使用的八个地址 (显然,表中的端口号 1 到 4 不是我们在这里考虑的特定意义上的端口号。但以下十六进制地址是这些端口号的完美示例)。标准 PC 上使用的相对完整端口号的列表可以在 Shanley 的 ISA 系统架构的附录 A 中找到。 Linux 机器上使用的 I/O 端口可以通过 /proc 目录访问到(使用如下命令):
Com Port | I/O Addresses |
---|---|
1 | 3F8H-3FFH |
2 | 2F8H-2FFH |
3 | 3E8H-3EFH |
4 | 2F8H-2FFH |
linuxbox$ cat /proc/ioports
内存和 I/O 传输的密切相似性并没有在 x86 汇编语言中得到充分体现,x86 汇编语言有很多涉及内存访问的命令,但控制 I/O 访问的命令却很少,例如 IN 和 OUT。 使用 MOV 和许多其他命令来访问内存将在第 6 章 6.2 节中讨论。 接下来讨论 IN 和 OUT 命令。
The IN
Command
输入【input】命令将数据从 I/O 端口传输到处理器中。 该命令有六种有效形式:
IN EAX, [DX]
IN AX, [DX]
IN AL, [DX]
IN EAX,[imm]
IN AX,[imm]
IN AL,[imm]
其中 imm 是任意一字节数字【one-byte number】。 请注意,本指令遵循一般格式。 其中有效的目标寄存器是 EAX、AX 和 AL。 而端口号对应的 I/O 设备就是源【source】,其中端口号存储在 DX 寄存器中或由 imm 给出。
-
COMMAND destination, source
请注意,DX 是一个 16 位寄存器。 这是有意义的,因为端口号是一个 16 位数字。 大多数 Intel 汇编程序(包括 NASM)都不在端口号(这里说的端口号,是指的引用的值,她既可以是DX,也可以是imm)两边使用括号(这里的括号指的是[])。 但是,如果您这样做,您可以将命令 IN AL, [DX],
视为:
- let AL = [DX]
其中 [DX] 指位于地址 DX 的数据。 在 Edlinas 中这样做是为了保持一致性。 内存命令以相同的方式使用括号。 括号成为解引用运算符,就像 C 语言中的 * 一样。 在 Edlinas 和 NASM 中,所有通过地址对内存中数据的引用都使用括号括起来的地址。 NASM 不使用括号作为 I/O 引用。 因此,在使用 NASM 时,IN 和 OUT 指令中的括号被省略。
由于 IN 命令的 imm 形式仅接受一字节的端口【one-byte ports】,因此只能使用 imm 形式访问 65.536 个端口中的底部 256 个端口。 请注意,IN 命令不是作为 IN
reg, [reg] 给出的。 这是因为 IN EBX, [DX]
和 IN EAX, [CX]
等形式都是无效的(简而言之,IN
命令只能将数据从 I/O 传输到 EAX/AX/AL中的一个)。
The OUT
Command
OUT 命令将数据从处理器传输到 I/O 设备。 该命令的六种有效形式是:
OUT [DX], EAX
OUT [DX], AX
OUT [DX], AL
OUT [imm], EAX
OUT [imm], AX
OUT [imm], AL
其中 imm 是任意一字节的数字。 操作数的顺序与 IN 命令中的顺序恰好相反,因为数据流向相反。 源【source】是 EAX、AX 或 AL 寄存器,目标【target】是 I/O 设备。 同样, OUT 命令在NASM 不使用括号。
Memory Mapped I/O
事实上,当内存地址空间中的地址连接到 I/O 设备时,这称为内存映射 I/O【Memory Mapped I/O】。 标准 PC 上的视频缓冲区【video buffer】就是一个例子。 从内存地址 B8000H 开始的 4000 字节中存储的字符被输出到监视器【monitor,就是显示器】。
为 I/O 设备使用单独的地址空间并不是 I/O 寻址的唯一可行方法。 许多架构使用一组地址并简单地保留其中一些用于 I/O。
保留地址【Reserved addresses】也用于 PC 上的 ROM【只读内存,read-only memory】。 如果对只读内存进行内存写入,则处理器会将适当的写入信号发送到总线上,但不会对“存储”的数据产生任何更改。
4.3 Simple Assembly Language Programs
简单的高级语言程序通常具有三部分格式:输入数据,进行计算,然后输出结果。 使用 Edlinas,可以用 x86 汇编语言编写相同风格的简单程序。
Edlinas Ports
Edlinas 模拟的虚拟机器(假想机器)只使用两个端口号:
- 0 是键盘输入端口
- 1 是屏幕输出端口
键盘输入回显在屏幕左下方,输出显示在屏幕右下方。 当 IN EAX, [DX]
与 Edlinas 一起使用时,存储在 DX 寄存器中的值应为 0。当端口 0 被寻址到时,系统会提示用户输入输入值。 当使用 OUT [DX],EAX
时,DX 中存储的值应为 1。当使用端口 1 时,输出值【output value】以十进制为基数显示在屏幕右下方。 使用这两个端口可以用真正的 x86 汇编语言编写非常简单的程序并在 Edlinas 模拟器上运行。
使用 Edlinas 端口的程序不能在 DOS 或 Unix 下运行。 第 8 章第 8.4.7 节显示了如何通过调用 C 库函数scanf()
和 printf()
来替换这些对模拟机的 I/O 命令。 这样,使用实际 I/O 的简单Edlinas程序就可以在 Linux 下运行了。
The RET
Command
程序由操作系统启动。 当程序完成时,它必须将控制权返回给操作系统。 RET 就是这样做的。 子程序也使用 RET 将控制权返回给调用程序。 RET 使用堆栈工作,将在第 7 章 7.2 节中讨论。
Program to Add Two Numbers
我们现在拥有编写一些简单但完整的程序所需的所有命令。 这是一个 Edlinas 程序,用于将两个数字相加:
MOV EDX, 0 ;Making all 32 bits zero makes DX zero.
IN EAX, [DX] ;User enters the first number via port zero.
MOV EBX, EAX ;Get the first number out of the way.
IN EAX, [DX] ;User enters the second number.
ADD EAX, EBX ;Add the first number to the second.
MOV EDX, 1 ;The Edlinas output port is one.
OUT [DX], EAX ;The result is output to the user.
RET ;End the simulation.
Example 4.2.
4.4 Assembler Programs with Jumps
高级语言需要循环【loop】才能完成其工作。 这些循环必须由编译器转换为汇编代码。 例如,如图 4-2 所示,要以最直接的方式执行 C 语言 while 循环,需要有条件向前跳转和无条件向后跳转。
Figure 4-2. while
(test) Body;
The JMP
Command
取指-执行周期【fetch-execute cycle 】通过计算下一条指令在内存中的地址来工作。 该地址通常只需转到当前指令之后的第一个地址来确定的。 然而,为了支持高级语言中的 if-then 语句或循环,就需要跳转。 JMP
命令将执行转移到由标签指定的地址。
JMP
命令的格式为:
JMP label
标签是由程序员创建的。 设计有助于理解程序的标签是明智的。 为此,长标签很有用(言外之意不要缩写)。 另一方面,Edlinas 使用的屏幕空间非常有限,因此超过三个字符的标签在显示时会被截断,除非它们单独位于一行。 标签域中的标签指定了跳转的目标【target】,并且通常会附加一个冒号以明确它是一个标签而不是晦涩难懂的助记符。 许多跳转命令可能指向相同的目标【target】,但任何两行都不能在其标签字段中携带相同的标签。 在以下程序中,最后一行将控制转移到前一行。 该程序继续将 2 添加到 EAX 寄存器中。 这是一个无限循环。 无限循环通常都是由于写错了。
Conditional Jumps and Flags
条件跳转是根据真值【truth value】进行的跳转。 此类决策所依据的信息包含在称为标志【flags】的一位寄存器【 one-bit registers】中。 当标志【flags】包含 1 时,称为已设置【set】。 当它包含 0 时,称为被清除【 cleared】。 两个非常重要的标志【flags】是零标志【zero flag】和符号标志【sign flag】。
MOV EAX, 0
MOV EBX, 2
XYZ: ADD EAX, EBX
JMP XYZ
Example 4.3.
The Zero and Sign Flags
ADD EAX, EBX
ADD 和 SUB 命令不仅影响其目标寄存器【destination registers】,还影响许多标志【flags】。 假设命令被执行。 该命令的结果将保存在 EAX 寄存器中。 如果该结果为 0,则设置零标志【zero flag】。 如果不为 0,则零标志【zero flag】被清除。
ADD 指令后赋予符号标志【sign flag】的值是其结果的最高有效位的值。 如果结果是一个有符号数,且为负数,则该位为 1。 因此,如果执行 ADD EAX, EBX
命令给 EAX 一个负值,作为一个四字节有符号数,则设置【set】符号标志【sign flag】; 否则清除标志位。
The JZ, JS, JNZ,
and JNS
Commands
以下条件跳转命令均与JMP命令格式相同:
JZ label
JS label
JNZ label
JNS label
每一个的操作数字段都是一个标签【label】。 这些命令的含义如下:
JZ Jump if the zero flag is set.
JS Jump if the sign flag is set.
JNZ Jump if the zero flag is not set.
JNS Jump if the sign flag is not set.
以下汇编语言程序说明了如何在程序中使用 JS 指令来确定两个用户输入中哪一个较大。 程序 4.4 看起来无懈可击。 事实上它有一个错误。
; If the first input is larger output 1
; If the second input is larger output 2
; The program uses subtraction:
; B < A is true if and only if B — A is negative.
; A subtraction followed by a JS does the job
;
MOV EDX, 0
IN EAX, [DX]
MOV EBX, EAX ; The first input is now in EBX
IN EAX, [DX] ; The second input is now in EAX.
SUB EBX, EAX ; This is (first – second).
JS SIB ; Second is Bigger
MOV EAX, 1 ; Otherwise First is Bigger
JMP END ; Don't drift into the other case!
SIB: MOV EAX, 2 ;
END: MOV EDX, 1 ; Either way now EAX is ready.
OUT [DX], EAX
RET
Example 4.4.
4.5 Assembler Programs with Loops
条件跳转用于实现循环和 if-then 语句。 假设我们考虑以下程序,它使用重复加法进行乘法。 例如,要执行 7 × 5,我们可以执行 5 + 5 + 5 + 5 + 5 + 5 +5。 在程序 4.5 中,数字 1 在一行中增加,在另一行中减去。 为此有一些特殊的命令,可以占用更少的内存空间。
;
MOV EDX, 0
IN EAX,[DX] ; First input is the multiplier
MOV EBX, EAX ; Put Multiplier in EBX
IN EAX,[DX] ; Second input is the multiplied number
MOV ECX, 0 ; Initialize the running total.
RPT: ADD ECX, EAX ; Do one addition.
SUB EBX, 1 ; One less yet to be done.
JNZ RPT ; If that's not zero, do another.
MOV EAX, ECX ; Put the total in EAX
ADD EDX, 1 ;
OUT [DX], EAX ; Output the answer.
RET
Example 4.5.
The INC
and DEC
Commands
下面的自增和自减命令分别用于加减 1 :
INC reg
DEC reg
reg 可以是 24 个通用寄存器中的任何一个。 这些命令通常用于递增或递减循环计数器【loop counter】。 程序 4.5 中,SUB EBX, 1
可以替换为 DEC EBX
, ADD EDX, 1
可以替换为 INC EDX。
4.6 Signed Comparisons
Comparison-Based Jumps
高级语言经常在 if-then 语句中使用比较。 使用符号标志【sign flag】来测试减法的结果(如第 4.4 节中所做的那样)似乎是实现此测试的合理方法,因为这是一个数学事实:
- 当且仅当 B − A 为负数时,B < A
由于符号标志【sign flag】表示减法的结果的符号,我们似乎建立在坚实的基础上。 但事实并非如此。
The Overflow Flag
假设我们使用一字节的有符号数【one-byte signed numbers】,并且我们希望测试
是否成立。
在一字节寄存器中:
- 100的二进制:0110 0100
- -50的二进制:先计算50的二进制0011 0010,取反 1100 1101,再+1取补:1100 1110
- 再计算-(-50),先取反,0011 0001 ,再+1取补,0011 0010,对应的就是50的二进制
- 100 + 50 = 0110 0100 + 0011 0010 = 1001 0110
- -106的二进制:先计算106的二进制0110 1010,取反 1001 0101 ,再+1取补:1001 0110
- 100 + 50 的二进制,等于-106的二进制,其实发生了溢出
这会发生溢出【overflow】。 结果是负数。 符号标志【sign flag】被置位,比较判断为真! 使用 符号标志【sign flag】来确定跳转的话,最终会给出错误的答案。 但由于溢出【overflow】总是将结果的符号更改为与应有符号相反的符号,因此可以固定【fixed】跳转条件。
-
如果没有发生溢出,则在设置了符号标志时跳转。
-
如果发生溢出,如果未设置符号标志则跳转。
x86 处理器有一个溢出标志【overflow flag】。 它用于实现这个固定跳转条件【fixed-up jump condition】。 如果我们使用 SF 作为符号标志【sign flag】的二进制值,使用 OF 作为溢出标志【overflow flag】的二进制值,我们可以将固定跳转条件【fixed jump condition】重新表述为:
-
Jump if (SF XOR OF)
当且仅当 AL < BL,此跳转条件将在减法(如下所示)之后产生跳转:
SUB AL, BL
有一个 x86 命令用于此跳转。 它的助记符是 JL
,它代表如果小于则跳转。
The CMP
Command
当减法只是为了比较而执行时,可以使用 CMP
指令来完成。 该命令以与 SUB
以完全相同的方式设置标志【flags】,但具有不会因存储不需要的结果而浪费寄存器空间的优点。 使用 JL
和 CMP
指令,我们可以将程序 4.4 重写如下:
Example 4.6.
; If the first input is larger output 1
; If the second input is larger output 2
; The program uses subtraction:
; B < A is true if and only if B − A is negative.
MOV EDX, 0
IN EAX, [DX]
MOV EBX, EAX ;The first input is now in EBX
IN EAX, [DX] ;The second input is now in EAX.
CMP EBX, EAX ; This is first – second.
JL SIB ; Second is Bigger
MOV EAX, 1 ; Otherwise First is Bigger
JMP END ; Don't drift into the other case!
SIB: MOV EAX, 2 ;
END: MOV EDX, 1 ; Either way now EAX is ready.
OUT [DX], EAX
RET
程序 4.6 对于无符号四字节范围内的所有数字都能正确工作
More Jump Commands
在 CMP EAX, EBX
命令之后,如果条件 EAX < EBX 为真,则 JL
命令执行跳转。 命令:
CMP EAX, EBX
JL ABC
意味着:
- 将 EAX 与 EBX 进行比较并
- 如果 EAX 小于 EBX 则跳转。
这里发生的是减法和标志检查,但设计良好的语法允许我们忽略这一点。 小于【 less than 】条件并不是 CMP 命令之后方便访问的唯一条件。 这里给出了其他几个条件跳转命令的列表。
JL less than EAX < EBX
JLE less than or equal EAX ≤ EBX
JG greater than EAX > EBX
JGE greater than or equal EAX ≥ EBX
JE equal EAX = EBX
JNL not less than EAX ≮ EBX
JNLE not less than or equal EAX ≰ EBX
JNG not greater than EAX ≯ EBX
JNGE not greater than or equal EAX ≱ EBX
JNE not equal EAX ≠ EBX
由于这些不同的跳转命令很容易解释,因此它们将比较命令变成了一种语法上的意外收获。
4.7 Unsigned Comparisons
4.6 节展示了如何使用标志【flag】来确定不等式【inequalities】的真值,以便这些不等式【inequalities】可以用于条件跳转。 这些所获得的条件也适用于无符号数。
但假设我们考虑同一个例子
我们在上一节中考虑过这一点,并注意到,当用无符号数表示同样的不等式时,它就变成了:
这种不等式具有相反的真值。 因此,在这种情况下,无符号数的“如果小于则跳转【jump if less than】”命令必须执行跳转。 但 JL
命令不会这样做。 上一节介绍的有符号跳转命令不适用于无符号数。 如果 AL = 64H = 100 且 BL = CEH = 206。则 JL
将不会执行跳转,因为它其实是作用于 100 < −50。 因此需要一个无符号的“如果小于则跳转【jump if less than】”。
The Carry Flag
对于无符号数,不等式 A < B 比带符号数更容易测试。 由于无符号数 A − B 的减法不能超出上限,因此当且仅当它为负数时,它才超出范围。 无符号数的超出范围错误称为进位错误【 carry errors】。 因此,当且仅当减法 A − B 产生进位错误时,A < B 为真。 因此,进位错误的标志【flag】可以表示减法产生了负的结果。 x86 处理器有一个进位标志来指示这些错误。 使用 CF 来表示该进位标志的值,我们可以轻松地将用于实现无符号“如果小于则跳转【jump if less than】”命令的条件声明为“如果 CF 则跳转【jump if CF】”。
与符号标志【sign flag】、零标志【zero flag】和溢出标志【overflow flag】一样,进位标志【carry error】响应在使用 ADD
或 SUB
时发生的条件。 与其他三个标志不同,它不受INC
和 DEC
命令的影响。
Out of Range Flag Summary
ADD
和 SUB
等算术命令是在二进制位上执行的,没有任何信息指定这些位是用于表示有符号数还是无符号数。 软件【software】有责任对此进行跟踪。
当软件使用处理器对有符号数进行算术时,它应该查阅溢出标志【overflow flag】以确定是否发生了超出范围的错误。 当软件使用处理器处理无符号数时,软件应查阅进位标志【carry sign】以检查是否存在超出范围的错误。
硬件无法知道这些数字是有符号的还是无符号的。 这并不重要。
例如,在单字节寄存器上,将 FFH 添加到 FFH 的命令可能来自处理有符号数字的软件。 在这种情况下,总和 FEH 不是问题。 软件将刚刚发生的算术视为:
这没什么问题。
另一方面,该命令可能来自使用无符号数字的软件。 在这种情况下,软件将刚刚发生的算术视为:
- FF 255 1111 1111
- FF 255 1111 1111
- 254 1 1111 1110
这就不行了。 正确答案 510 超出了单字节无符号的范围: 0-255。 因此,将 FFH 和 FFH 做加和会清除溢出标志【overflow sign】,因为在有符号的算术中,该答案是正确的(-1 + -1 = -2),并设置进位标志【carry flag】以通知软件存在无符号、超出范围的错误【unsigned, out of range error】。
再举一个例子,假设单字节寄存器接收到将 40H 和 40H 相加的命令。 总和是80H。 如果命令来自处理无符号数字的软件,那么刚刚完成的算术将显示为:
这没什么问题。
假设该命令来自使用带符号数字的软件。 那么算术看起来像这样:
这就不行了。 正确答案 +128 超出单字节有符号范围 -128 到 +127。 硬件设置溢出标志【overflow sign】,因为这是有符号数系统中的超出范围错误,并清除进位标志【carry sign】,因为无符号系统中不存在超出范围错误。 硬件不知道软件正在使用哪个系统,因此它必须处理这两个标志。
进位标志【carry sign】和溢出标志【overflow sign】都是超出范围指示标志。 如果无符号系统中解释的算术产生了超出范围的错误,则设置进位标志【carry sign】。 如果有符号系统中解释的算术产生了超出范围的错误,则设置溢出标志【overflow sign】。
Still More Jump Commands
鉴于我们需要一个不同的“如果小于则跳转【jump if less than】”命令以用于无符号数,我们还需要一个不同的助记符。 此外,考虑到所有其他跳转条件,“大于”、“大于或等于”等,我们需要一套全新的命令和助记符。 为了使这个新集合系统化,使用 above 和 below 单词来代替短语大于【 greater than 】和小于【less than】。 因此,适用于无符号数的“如果小于则跳转【jump if less than】”命令使用助记符 JB,
表示“如果低于则跳转【jump if below】”。 这些命令的完整集如下:
JB below EAX < EBX
JBE below or equal EAX ≤ EBX
JA above EAX > EBX
JAE above or equal EAX ≤ EBX
JNB not below EAX ≮ EBX
JNBE not below or equal EAX ≰ EBX
JNA not above EAX ≯ EBX
JNAE not above or equal EAX ≱ EBX
Linux .s
files
gcc 将 C 源程序转换为 a.out 文件的中间步骤之一是汇编语言文件。 该文件是编译器【compiler】本身的输出和汇编器【assembler】gas 的输入。 要查看这些汇编程序文件,可以使用 -S 开关指示 gcc 在到达此阶段后输出汇编程序文件。 此输出的文件扩展名为 .s。 例如,考虑以下 C 程序 mult.c:
Example 4.7.
main()
{
int x, y;
register int a, b, c;
printf("Enter a number: ");
scanf("%d", &x);
printf("Multiply it by what number? ");
scanf("%d", &y);
a = x;
b = y;
c = 0;
while (b != 0)
{
c = c + a;
b = b − 1;
}
printf ("The result is %d.\n.",c);
}
假设输入数字 x = 5 和 y = −7。 该程序实际上输出了正确答案 -35,为了计算它,循环执行了 4,294,967,289 ()次。 注意:
每次运行总数【running total】 b 超过翻转值 时,就会从总数中删除 。 这种情况发生四次(因为 被乘了5次,乘第二/三/四/五次时,溢出进位,最终结果只剩下)。 b 的结果值为 ,它是 −35 的二进制补码表示形式。
汇编语言编程的主要实际用途之一是对执行多次的循环进行编码,每次执行所需的时间的改进实际上会对总运行时间产生显着的影响。 在 120 MHz Pentium 的测试中,5 × -7 计算需要 110.90 秒。 要获取该程序的汇编代码,可以使用以下命令:
linuxbox$ gcc -S mult.c
生成的文件 mult.s 是 gcc 用于生成可执行文件的汇编代码。 该汇编代码的语法与本书中使用的广泛使用的英特尔汇编语言的语法不同,但仍然可以被破译。 为此添加了注释。 第一行使用 TEST。 这是一个与CMP 相似的指令:
Example 4.8.
.L2:
testl %esi.%esi ; Is ESI = 0
jne . L4 ; If not go to L4
jmp . L3 ; otherwise go to L3
.L4:
addl %ebx,%edi ; Let EDI = EDI + EBX
decl %esi ; Let ESI = ESI - 1
jmp .L2 ; Go to L2
.L3:
第一行使用 TEST。 它是一个类似于 CMP 指令,将在下一章中讨论,%esi 指的是 ESI 寄存器。 与 CMP 类似,如果 ESI = 0,TEST 命令会设置零标志【zero flag】。许多命令末尾的 l 用于指定操作数的长度,在本例中为“long”,这意味着四个字节。 请注意,add 使用“COMMAND source destination”格式的操作数顺序,与本书中使用的顺序相反。
该循环由所示的六个指令组成。 如果我们注意到,我们可以通过在循环结束时测试 ESI 是否为零来跳出循环,那么我们可以将循环从 6 个指令减少到 3 个。唯一的改变是循环末尾的跳转。 修改后的文件是使用 gcc 编译的。
linuxbox$ gcc mult.s
Example 4.9.
.L2:
testl %esi,%esi
jne .L4
jmp .L3
.L4:
addl %ebx,%edi
decl %esi
jne .L4 ; If ESI isn't 0 go to L4
.L3:
运行生成的程序,在同一台机器上计算 5 × -7 花费了 76.14 秒。
The Pentium's Dual Pipeline
TODO