汇编语言
汇编语言是一种编程语言,可以用来直接告诉计算机做什么。汇编语言几乎和计算机能理解的机器代码完全一样,只是它用文字代替了数字。计算机无法真正直接理解汇编程序。然而,它可以很容易地把程序改成机器代码,把程序中的文字换成它们所代表的数字。能做到这一点的程序叫做汇编程序。
用汇编语言编写的程序通常由指令组成,这些指令是计算机在运行程序时执行的小任务。它们之所以被称为指令,是因为程序员用它们来指示计算机做什么。计算机中遵循指令的部分就是处理器。
计算机的汇编语言是一种低级语言,也就是说,它只能用来完成计算机能够直接理解的简单任务。为了执行更复杂的任务,必须把复杂任务中的每一项简单任务告诉计算机。例如,计算机不理解如何在其屏幕上打印一句话。相反,一个用汇编编写的程序必须告诉它如何完成打印这句话所涉及的所有小步骤。
这样的汇编程序会由很多很多的指令组成,这些指令一起做一些在人类看来非常简单和基本的事情。这使得人类很难读懂汇编程序。相反,高级编程语言可能只有一条指令,如print"你好,世界!",它会告诉计算机为你执行所有的小任务。
汇编语言的开发
当计算机科学家第一次制造可编程机器时,他们直接用机器代码进行编程,机器代码是一系列的数字,指示计算机做什么。编写机器语言是非常困难的,需要很长时间,所以最终制作了汇编语言。汇编语言对人类来说更容易读懂,可以写得更快,但比起试图模仿人类语言的高级编程语言,人类使用起来还是要困难得多。
机器码编程
要用机器码编程,程序员需要知道每条指令的二进制(或十六进制)是什么样子。虽然对计算机来说,很快就能弄清楚机器码的含义,但对程序员来说却很难。每条指令都可以有几种形式,在人们看来都只是一堆数字。人们在编写机器代码时犯的任何错误,只有在计算机做错事时才会被发现。弄清错误是很难的,因为大多数人无法通过观察来判断机器代码的含义。机器代码是什么样子的例子。
05 2A 00
这个十六进制机器代码告诉x86计算机处理器将42加到累加器中。即使一个人懂得机器码,也很难读懂它。
使用汇编语言代替
用汇编语言,每条指令都可以写成一个短词,称为记号,后面再加上数字或其他短词等其他东西。记忆符的使用,使程序员不必记住告诉计算机做某事所需的机器代码中的确切数字。汇编语言中的记忆符的例子包括add,增加数据;mov,将数据从一个地方移动到另一个地方。由于'mnemonic'是一个不常见的词,所以有时会用指令类型或仅仅是指令来代替,这往往是错误的。第一个词后面的单词和数字给出了更多关于要做什么的信息。例如,add后面的东西可能是什么两个东西加在一起,mov后面的东西说的是要移动什么,放在哪里。
例如,上一节中的机器代码(05 2A 00)可以用汇编写成:?
汇编语言还允许程序员用更简单的方式编写程序使用的实际数据。大多数汇编语言都支持轻松制作数字和文本。在机器代码中,每一个不同类型的数字,如正数、负数或十进制,都必须手动转换为二进制,而文本则必须每次定义一个字母,作为数字。
汇编语言提供了所谓的机器代码的抽象。当使用汇编语言时,程序员不需要知道数字对计算机意味着什么的细节,而是由汇编器来计算。汇编语言实际上仍然可以让程序员使用处理器的所有功能,而他们可以使用机器代码。从这个意义上说,汇编语言有一个非常好的、难得的特点:它具有和它所抽象的东西(机器代码)一样的表达能力,同时又更容易使用。正因为如此,机器代码几乎从未被用作编程语言。
拆卸和调试
当程序完成后,它们已经被转化为机器代码,以便处理器能够实际运行它们。但有时,如果程序中存在bug(错误),程序员就会希望能够知道机器代码的每个部分在做什么。Disassemblers是帮助程序员做到这一点的程序,它将程序的机器代码转化回汇编语言,而汇编语言更容易理解。反汇编程序将机器代码转化为汇编语言,与汇编程序相反,后者将汇编语言转化为机器代码。
计算机组织
要理解汇编语言程序是如何工作的,需要了解计算机是如何组织的,它们是如何在很低的层次上工作的。在最简单的层面上,计算机有三个主要部分。
- 主存储器或RAM,存放数据和指令。
- 一个处理器,通过执行指令来处理数据,以及
- 输入和输出(有时简称为I/O),它允许计算机与外界进行通信,并将数据存储在主存储器之外,以便以后可以取回数据。
主存储器
在大多数计算机中,内存被划分为一个个字节。每个字节包含8位。内存中的每一个字节也有一个地址,这个地址是一个数字,表示该字节在内存中的位置。内存中的第一个字节的地址是0,下一个字节的地址是1,以此类推。将内存划分为字节,使其可以进行字节寻址,因为每个字节都得到一个唯一的地址。字节存储器的地址不能用来指代一个字节的单个位。一个字节是可以寻址的最小的一块存储器。
尽管一个地址指的是内存中的一个特定的字节,但处理器允许在一行中使用几个内存字节。这个功能最常见的用途是在一行中使用2或4个字节来表示一个数字,通常是一个整数。单个字节有时也用来表示整数,但由于它们只有8位长,所以只能容纳28或256个不同的可能值。在一行中使用2或4个字节会使不同的可能值的数量分别增加到216,65536或232,4294967296。
当一个程序使用一个字节或一行中的若干个字节来表示字母、数字或其他任何东西时,这些字节被称为一个对象,因为它们都是同一事物的一部分。尽管对象都存储在相同的内存字节中,但它们被视为有一个"类型",它说明了应该如何理解这些字节:要么是一个整数,要么是一个字符,要么是其他类型(比如一个非整数值)。机器代码也可以被认为是一种类型,被解释为指令。类型的概念是非常非常重要的,因为它定义了可以对对象做什么事情,不能做什么事情,以及如何解释对象的字节。例如,在正数对象中存储一个负数是无效的,在整数中存储一个分数也是无效的。
指向(是)一个多字节对象的地址是指向该对象的第一个字节的地址,也就是地址最低的那个字节。另外,有一件重要的事情需要注意,你不能通过一个对象的地址来判断它的类型是什么,甚至不能判断它的大小。事实上,你甚至不能通过观察一个对象来判断它的类型。汇编语言程序需要跟踪哪些内存地址存放着哪些对象,以及这些对象有多大。这样做的程序是类型安全的,因为它只对对象的类型做安全的事情。一个不这样做的程序可能不会正常工作。请注意,大多数程序实际上并不显式地存储对象的类型是什么,它们只是一致地访问对象--同一对象总是被当作同一类型。
处理器
处理器运行(执行)指令,这些指令作为机器代码存储在主存储器中。除了能够访问内存进行存储外,大多数处理器都有一些小的、快速的、固定大小的空间,用于存放当前正在处理的对象。这些空间称为寄存器。处理器通常执行三种类型的指令,尽管有些指令可以是这些类型的组合。下面是x86汇编语言中每种类型的一些例子。
读取或写入内存的指令
下面的x86汇编语言指令从地址为4096(十六进制的0x1000)的字节中读取(加载)一个2字节的对象到一个名为'ax'的16位寄存器中。
在这种汇编语言中,数字(或寄存器名)周围的方括号意味着该数字应被用作指向应使用的数据的地址。使用地址来指向数据的做法叫做间接性。在接下来的这个例子中,如果没有方括号,另一个寄存器bx实际上得到了加载到它的值20。
因为没有使用任何的间接性,所以实际值本身就被放到了寄存器中。
如果操作数(记号后面的东西),以相反的顺序出现,一条从内存中加载东西的指令,反而会把它写入内存。
这里,地址1000h的内存得到的值是ax。如果这个例子紧接着上一个例子执行,那么1000h和1001h的2个字节将是一个2字节的整数,值为20。
执行数学或逻辑运算的指令。
有些指令做的是减法或不等逻辑运算。
本文前面的机器代码例子就是用汇编语言写的这个。
在这里,42和ax相加,结果又存储回ax中。在x86汇编中,也可以将内存访问和数学运算这样结合起来。
该指令将存储在1000h的2字节整数的值加到ax中,并将答案存储在ax中。
该指令计算寄存器ax和bx内容的or,并将结果存储回ax中。
决定下一个指令是什么的指令。
通常情况下,指令是按照它们在内存中出现的顺序执行的,也就是它们在汇编代码中输入的顺序。处理器只是一个接一个地执行它们。然而,为了让处理器做复杂的事情,它们需要根据它们所得到的数据是什么来执行不同的指令。处理器根据某件事情的结果执行不同指令的能力叫做分支。决定下一条指令是什么的指令称为分支指令。
在这个例子中,假设有人想计算他们将需要的油漆量,以油漆一定的边长的广场。然而,由于规模经济,油漆店不会卖给他们任何少于油漆100×100正方形所需的油漆量。
为了根据自己想要涂刷的方块的长度来计算出自己需要的涂料量,他们得出了这样一套步骤。
- 边长减去100
- 如果答案小于零,则将边长设置为100。
- 乘以边长
该算法可以用下面的代码表示,其中ax是边长。
这个例子引入了一些新的东西,但前两条指令是大家熟悉的。它们将ax的值复制到bx中,然后从bx中减去100。
在这个例子中,有一个新的东西叫做标签,这是一般汇编语言中的概念。标签可以是任何程序员想要的东西(除非是指令的名称,这样会让汇编器感到困惑)。在这个例子中,标签是'继续'。它被汇编器解释为一个指令的地址。在本例中,它是mult ax的地址。
另一个新概念是标志。在x86处理器上,许多指令会在处理器中设置"标志",可以被下一条指令用来决定做什么。在这种情况下,如果bx小于100,sub将设置一个标志,表示结果小于0。
下一条指令是jge,是"如果大于或等于,则跳转"的缩写。它是一条分支指令。如果处理器中的标志指定结果大于或等于零,处理器将不直接进入下一条指令,而是跳转到继续标签处的指令,即mul ax。
这个例子很好用,但它不是大多数程序员会写的。减法指令正确地设置了标志,但它也改变了它操作的值,这就需要将ax复制到bx中。大多数汇编语言都允许比较指令不改变其传递的任何参数,但仍能正确设置标志,x86汇编也不例外。
现在,不是从ax中减去100,看这个数字是否小于0,然后再把它分配回ax,而是保持ax不变。标志的设置方式还是一样的,跳转的情况也还是一样。
输入和输出
虽然输入和输出是计算的一个基本部分,但在汇编语言中并没有一种方式来完成它们。这是因为I/O的工作方式取决于计算机的设置及其运行的操作系统,而不仅仅是它有什么样的处理器。在例子部分,Hello World的例子使用的是MS-DOS操作系统的调用,而后面的例子使用的是BIOS的调用。
用汇编语言是可以做I/O的。事实上,汇编语言一般可以表达计算机能够做的任何事情。然而,即使在汇编语言中,有一些加法和分支的指令,总是会做同样的事情,但汇编语言中并没有总是做I/O的指令。
要注意的是,I/O的工作方式不是任何汇编语言的一部分,因为它不是处理器工作方式的一部分。
汇编语言和可移植性
尽管汇编语言并不是直接由处理器运行的--机器代码才是,但它仍然与处理器有很大的关系。每个处理器系列都支持不同的功能、指令、指令可以做什么的规则,以及在什么地方允许什么指令组合的规则。正因为如此,不同类型的处理器仍然需要不同的汇编语言。
由于每个版本的汇编语言都与一个处理器系列相联系,所以它缺乏一种叫做可移植性的东西。具有可移植性或可移植性的东西可以很容易地从一种类型的计算机转移到另一种类型的计算机上。虽然其他类型的编程语言是可移植的,但一般来说,汇编语言是不可移植的。
汇编语言和高级语言
虽然汇编语言可以方便地使用处理器的所有功能,但由于几个原因,现代软件项目并没有使用它。
- 用汇编来表达一个简单的程序,需要花费很多精力。
- 虽然不像机器代码那样容易出错,但汇编语言仍然提供了很少的防错保护。几乎所有的汇编语言都不执行类型安全。
- 汇编语言不提倡模块化等良好的编程实践。
- 虽然每一条单独的汇编语言指令都很容易理解,但很难看出编写它的程序员的意图是什么。事实上,程序的汇编语言是如此的难懂,以至于公司并不担心人们会拆解(获取)他们的程序的汇编语言。
由于这些缺点,大多数项目都使用高级语言,如Pascal、C和C++来代替。它们允许程序员更直接地表达自己的想法,而不必担心告诉处理器每一步该怎么做。它们之所以被称为高级语言,是因为程序员在同样数量的代码中所能表达的想法更加复杂。
程序员用编译后的高级语言编写代码时,会使用一种叫做编译器的程序将他们的代码转化为汇编语言。编译器比汇编器难写得多。另外,高级语言并不总是允许程序员使用处理器的所有功能。这是因为高级语言被设计成支持所有的处理器系列。不像汇编语言只支持一种类型的处理器,高级语言是可移植的。
尽管编译器比汇编语言复杂,但几十年来对编译器的制作和研究已经使编译器非常优秀。现在,大多数项目已经没有太多理由再使用汇编语言了,因为编译器通常可以弄清楚如何用汇编语言表达程序,甚至比程序员更好。
示例程序
一个用x86汇编编写的Hello World程序。
一个使用NASM x86汇编编写的BIOS中断向屏幕打印数字的函数。模块化代码可以用汇编写,但需要额外的努力。请注意,在一行分号之后的任何内容都是注释,并被汇编器忽略。在汇编语言代码中放入注释是非常重要的,因为大型的汇编语言程序很难理解。
问题和答案
问:什么是汇编语言?答:汇编语言是一种编程语言,可以用来直接告诉计算机要做什么。它几乎与计算机能够理解的机器代码完全一样,只是它用文字代替了数字。
问:计算机如何理解一个汇编程序?
答:计算机实际上不能直接理解汇编程序,但它可以很容易地将程序改为机器码,方法是将程序中的文字替换为它们所代表的数字。这个过程是用汇编程序完成的。
问:什么是汇编语言中的指令?
答:汇编语言中的指令是计算机在运行程序时执行的小任务。它们被称为指令,因为它们指示计算机要做什么。计算机中负责遵循这些指令的部分被称为处理器。
问:汇编是什么类型的编程语言?
答:汇编语言是一种低级别的编程语言,这意味着它只能用于完成计算机能够直接理解的简单任务。为了执行更复杂的任务,必须把每项任务分解成各个组成部分,并为每个组成部分分别提供指令。
问:这与高级语言有什么不同?
答:高级语言可能有单一的命令,如PRINT "Hello, world!",这将告诉计算机自动执行所有这些小任务,而不需要像你在汇编程序中那样单独指定它们。这使得高级语言比由许多单独指令组成的汇编程序更易于人类阅读和理解。
问:为什么人类可能难以阅读汇编程序?
答:因为要完成一项复杂的任务,如在屏幕上打印东西或对数据集进行计算,必须指定许多单独的指令--这些事情在用人类自然语言表达时似乎非常基本和简单--所以可能有许多行代码组成一条指令,这使得不了解计算机内部如何在如此低的水平上工作的人类很难跟随并解释其中所发生的事情。