要理解汇编语言程序是如何工作的,需要了解计算机是如何组织的,它们是如何在很低的层次上工作的。在最简单的层面上,计算机有三个主要部分。
- 主存储器或RAM,存放数据和指令。
- 一个处理器,通过执行指令来处理数据,以及
- 输入和输出(有时简称为I/O),它允许计算机与外界进行通信,并将数据存储在主存储器之外,以便以后可以取回数据。
主存储器
在大多数计算机中,内存被划分为一个个字节。每个字节包含8位。内存中的每一个字节也有一个地址,这个地址是一个数字,表示该字节在内存中的位置。内存中的第一个字节的地址是0,下一个字节的地址是1,以此类推。将内存划分为字节,使其可以进行字节寻址,因为每个字节都得到一个唯一的地址。字节存储器的地址不能用来指代一个字节的单个位。一个字节是可以寻址的最小的一块存储器。
尽管一个地址指的是内存中的一个特定的字节,但处理器允许在一行中使用几个内存字节。这个功能最常见的用途是在一行中使用2或4个字节来表示一个数字,通常是一个整数。单个字节有时也用来表示整数,但由于它们只有8位长,所以只能容纳28或256个不同的可能值。在一行中使用2或4个字节会使不同的可能值的数量分别增加到216,65536或232,4294967296。
当一个程序使用一个字节或一行中的若干个字节来表示字母、数字或其他任何东西时,这些字节被称为一个对象,因为它们都是同一事物的一部分。尽管对象都存储在相同的内存字节中,但它们被视为有一个"类型",它说明了应该如何理解这些字节:要么是一个整数,要么是一个字符,要么是其他类型(比如一个非整数值)。机器代码也可以被认为是一种类型,被解释为指令。类型的概念是非常非常重要的,因为它定义了可以对对象做什么事情,不能做什么事情,以及如何解释对象的字节。例如,在正数对象中存储一个负数是无效的,在整数中存储一个分数也是无效的。
指向(是)一个多字节对象的地址是指向该对象的第一个字节的地址,也就是地址最低的那个字节。另外,有一件重要的事情需要注意,你不能通过一个对象的地址来判断它的类型是什么,甚至不能判断它的大小。事实上,你甚至不能通过观察一个对象来判断它的类型。汇编语言程序需要跟踪哪些内存地址存放着哪些对象,以及这些对象有多大。这样做的程序是类型安全的,因为它只对对象的类型做安全的事情。一个不这样做的程序可能不会正常工作。请注意,大多数程序实际上并不显式地存储对象的类型是什么,它们只是一致地访问对象--同一对象总是被当作同一类型。
处理器
处理器运行(执行)指令,这些指令作为机器代码存储在主存储器中。除了能够访问内存进行存储外,大多数处理器都有一些小的、快速的、固定大小的空间,用于存放当前正在处理的对象。这些空间称为寄存器。处理器通常执行三种类型的指令,尽管有些指令可以是这些类型的组合。下面是x86汇编语言中每种类型的一些例子。
读取或写入内存的指令
下面的x86汇编语言指令从地址为4096(十六进制的0x1000)的字节中读取(加载)一个2字节的对象到一个名为'ax'的16位寄存器中。
mov ax, [1000h]
在这种汇编语言中,数字(或寄存器名)周围的方括号意味着该数字应被用作指向应使用的数据的地址。使用地址来指向数据的做法叫做间接性。在接下来的这个例子中,如果没有方括号,另一个寄存器bx实际上得到了加载到它的值20。
mov bx, 20
因为没有使用任何的间接性,所以实际值本身就被放到了寄存器中。
如果操作数(记号后面的东西),以相反的顺序出现,一条从内存中加载东西的指令,反而会把它写入内存。
mov [1000h], ax
这里,地址1000h的内存得到的值是ax。如果这个例子紧接着上一个例子执行,那么1000h和1001h的2个字节将是一个2字节的整数,值为20。
执行数学或逻辑运算的指令。
有些指令做的是减法或不等逻辑运算。
本文前面的机器代码例子就是用汇编语言写的这个。
加斧头,42
在这里,42和ax相加,结果又存储回ax中。在x86汇编中,也可以将内存访问和数学运算这样结合起来。
添加轴,[1000h]
该指令将存储在1000h的2字节整数的值加到ax中,并将答案存储在ax中。
或ax、bx
该指令计算寄存器ax和bx内容的or,并将结果存储回ax中。
决定下一个指令是什么的指令。
通常情况下,指令是按照它们在内存中出现的顺序执行的,也就是它们在汇编代码中输入的顺序。处理器只是一个接一个地执行它们。然而,为了让处理器做复杂的事情,它们需要根据它们所得到的数据是什么来执行不同的指令。处理器根据某件事情的结果执行不同指令的能力叫做分支。决定下一条指令是什么的指令称为分支指令。
在这个例子中,假设有人想计算他们将需要的油漆量,以油漆一定的边长的广场。然而,由于规模经济,油漆店不会卖给他们任何少于油漆100×100正方形所需的油漆量。
为了根据自己想要涂刷的方块的长度来计算出自己需要的涂料量,他们得出了这样一套步骤。
- 边长减去100
- 如果答案小于零,则将边长设置为100。
- 乘以边长
该算法可以用下面的代码表示,其中ax是边长。
移动 bx、ax 次级bx,100 膥尿 mov ax, 100 continue: mul ax
这个例子引入了一些新的东西,但前两条指令是大家熟悉的。它们将ax的值复制到bx中,然后从bx中减去100。
在这个例子中,有一个新的东西叫做标签,这是一般汇编语言中的概念。标签可以是任何程序员想要的东西(除非是指令的名称,这样会让汇编器感到困惑)。在这个例子中,标签是'继续'。它被汇编器解释为一个指令的地址。在本例中,它是mult ax的地址。
另一个新概念是标志。在x86处理器上,许多指令会在处理器中设置"标志",可以被下一条指令用来决定做什么。在这种情况下,如果bx小于100,sub将设置一个标志,表示结果小于0。
下一条指令是jge,是"如果大于或等于,则跳转"的缩写。它是一条分支指令。如果处理器中的标志指定结果大于或等于零,处理器将不直接进入下一条指令,而是跳转到继续标签处的指令,即mul ax。
这个例子很好用,但它不是大多数程序员会写的。减法指令正确地设置了标志,但它也改变了它操作的值,这就需要将ax复制到bx中。大多数汇编语言都允许比较指令不改变其传递的任何参数,但仍能正确设置标志,x86汇编也不例外。
CMP ax, 100 膥尿 mov ax, 100 continue: mul ax
现在,不是从ax中减去100,看这个数字是否小于0,然后再把它分配回ax,而是保持ax不变。标志的设置方式还是一样的,跳转的情况也还是一样。
输入和输出
虽然输入和输出是计算的一个基本部分,但在汇编语言中并没有一种方式来完成它们。这是因为I/O的工作方式取决于计算机的设置及其运行的操作系统,而不仅仅是它有什么样的处理器。在例子部分,Hello World的例子使用的是MS-DOS操作系统的调用,而后面的例子使用的是BIOS的调用。
用汇编语言是可以做I/O的。事实上,汇编语言一般可以表达计算机能够做的任何事情。然而,即使在汇编语言中,有一些加法和分支的指令,总是会做同样的事情,但汇编语言中并没有总是做I/O的指令。
要注意的是,I/O的工作方式不是任何汇编语言的一部分,因为它不是处理器工作方式的一部分。