---
schema_version: "library-reader-v1"
source_type: "original_book"
title: "编码 隐匿在计算机软硬件背后的语言"
author: null
edition: null
publisher: null
year: null
isbn: "9787121181184"
library_document_id: "38270854-596f-4a26-8183-db8c968c6020"
source_sha256: "249786274d83909f0bddc34d77f81ed8f0b14d1eac8843c772a37a3ecbfe2cfc"
pdf_physical_page_count: 438
pdf_physical_page_index_base: 0
range_start: 400
range_count: 10
html_url: "https://workshop.closeclaw.top/api/ai/read/38270854-596f-4a26-8183-db8c968c6020?start=400&limit=10"
markdown_url: "https://workshop.closeclaw.top/api/ai/read/38270854-596f-4a26-8183-db8c968c6020.md?start=400&limit=10"
previous_url: "https://workshop.closeclaw.top/api/ai/read/38270854-596f-4a26-8183-db8c968c6020.md?start=390&limit=10"
next_url: "https://workshop.closeclaw.top/api/ai/read/38270854-596f-4a26-8183-db8c968c6020.md?start=410&limit=10"
---

# 编码 隐匿在计算机软硬件背后的语言

> 页码规则：PDF_PHYSICAL_PAGE_INDEX 是从 0 开始的权威文件物理页索引；PRINTED_PAGE 是页面中识别出的印刷页码，可能为空，二者不得混用。

## PDF_PHYSICAL_PAGE_INDEX=400 | PRINTED_PAGE=384

注意，汇编器如何知道LXI指令必须将寄存器DE的值设置为地址0109h(Text的地址)。如果LXI指令本身被存放在地址0100h处（CP/M将程序加载至内存开始运行时的起始地址)，而0109h则是Text字符串的起始地址。一般来说，程序员在使用汇编器时有很多方便之处，其中一点就是不需要关心汇编程序各部分在内存中的存放地址。

第一个编写汇编器的人需要手工对程序汇编。如果要为机器写一个新的汇编器（或者对其修改)，则可以使用汇编语言编写该程序，然后使用原有的汇编器对其汇编。一旦新的汇编器通过了汇编，则它也就可以对自身进行汇编。

每当一种新的微处理器面世，就需要为其编写新的汇编器。然而，新的汇编器可以在已有的计算机上编写，并利用其汇编器进行汇编。这种方式称为交叉汇编(cross-assembler)，即利用计算机A的汇编器对运行在计算机B上的程序汇编。

虽然汇编器的引入消除了汇编语言编程中重复性的劳动部分（即手动汇编部分)，但汇编语言仍然存在两个主要问题。第一个问题（也许你已经意识到了)，使用汇编语言编程非常乏味，因为这是在微处理器芯片级的编程，因此不得不考虑每一个微小的细节。

汇编语言存在的第二个问题是不可“移植”（portable)。如果你为Intel 8080写了一个汇编语言程序，则该程序不能在Motorola6800上运行，你必须在6800上重写一个相同功能的汇编语言程序。编写类似程序的过程也许没有编写第一个程序那么困难，因为你应经解决了程序的组织和算法问题，但仍然还有很多工作要做。

上一章介绍了现代微处理器集成浮点运算机器码指令的原理。不可否认，这已经为我们带来了很大的便利，但仍不能令人特别满意。一种更好的方式是：完全放弃那些实现每个基本操作的机器码指令，这些指令与处理器相关，因而导致程序缺乏移植性。我们采用的替代策略是使用一些经典的数学表达式来描述复杂的数学运算。下面是一个表达式的例子：

$$
A \times S i n ( 2 \times \mathrm { P I } + B ) / C
$$

上式中的A，B，C代表数字，而PI=3.14159。

这看起来不错，为什么不动手尝试一下呢？假设在某个文本文件中有这样一个表达式，那么我们可以尝试编写一个汇编语言程序来读取该文本文件，并将其中的数学表达

编码—隐匿在计算机软硬件背后的语言

384

## PDF_PHYSICAL_PAGE_INDEX=401 | PRINTED_PAGE=385

式转换为机器码。

如果只需要计算一次该表达式，那么可以手工计算或借助计算器来完成。如果需要对A、B、C取不同的值多次计算该表达式，那么你可能要考虑使用计算机来完成这些计算。因此，代数表达式不会孤立地出现，必须考虑其前后的语句，这些语句使表达式对不同的值进行运算。

现在你所创建的东西已经触及所谓的高级程序设计语言（high-level programminglanguage)。我们一直在介绍的汇编语言称做低级语言(low-level programming language)，因为它与计算机硬件的关系相当紧密。尽管除了汇编语言以外的其他程序设计语言都可以称为“高级语言”，但它们之间还是有高低之分的，一些语言通常被认为比别的语言更高级。如果你是一家公司的总裁，坐在计算机前输入这些命令（也可能做得更轻松：口头发布这个命令)，“计算出本年度的收益和损耗，生成年度报表，最后打印出2000份送至每个股东”，你所使用的才真正是一种非常高级的语言！但在实际工作中，程序设计语言还达不到这种理想化的水平。

人类语言通常都是经历了千百年复杂的互相影响、偶然演变以及不断叶故纳新才形成的，就算一些人工语言如世界语（Esperanto)，也处处显露出与现实语言的渊源。但高级程序设计语言是经过深思熟虑的设计的，更加概念化的语言。设计程序设计语言所面临的一大挑战就是：如何让语言更具吸引力。因为语言定义了人们向计算机传送指令的方式，只有更易用的方式才能让人们对语言产生兴趣。据1993年的一项估算，从1950年到1993年大约有1000多种高级程序设计语言被发明出来并被应用。

然而，仅仅定义（define）高级语言，包括定义语言的语法（syntax）来表达该语言可以描述的一切事物，还远远不够；我们还需要为其编写一个编译器（compiler)，编译器可以将高级语言的程序语句转换成机器码指令。同汇编器类似，编译器也是逐字逐句地读取源文件并将其分解成为短语、符号和数字的，但实现过程要比汇编器更加复杂。从某些方面来看，汇编器相对简单，因为汇编语言的语句和机器码是一一对应的。而一般的高级语言却不具备这种对应关系，编译器通常必须把一条语句转换多个机器码指令。编译器的编写非常复杂，许多书都是用全部的篇幅来讲解如何设计和构造编译器。

当然，任何事物都是具有两面性的，高级语言也不例外，它有很多优势但也存在不少缺陷。高级语言最基本的优点在于它比汇编语言易于学习并且更容易编写程序，用高级语言编写的程序通常更加清晰简明与汇编语言不同，高级语言通常不依赖于特定的处理器，因此它们通常具有良好的可移植性。因为这种特点，使用高级语言的程序员不再需要关心最终运行程序的计算机的底层结构。当然，如果要在不同类型的处理器上运行程序，则需要用处理器对应的编译器将程序转换成对应的机器码。因此，最后生成的可执行文件仍然只适用于特定的处理器。

24 高级语言与低级语言

385

## PDF_PHYSICAL_PAGE_INDEX=402 | PRINTED_PAGE=386

另一方面，有一种普遍现象：一个优秀的汇编程序员所编写的程序比编译器所产生的代码更加有效率。也就是说，从高级语言程序生成的可执行程序比相同功能的汇编语言程序更大，并且运行速度更慢（但从近年的发展来看，这种差别已变得不再明显，因为微处理器变得更加复杂，而且编译器在优化代码方面也更加成熟)。

此外，虽然高级语言提高了处理器的易用性，但并没有让其变得更强大。微处理器的任何一个功能都可以通过汇编语言实现，因此汇编语言可以高度利用处理器的功能。因为高级语言必须转化成机器码，所以它只会降低微处理器的能力。事实上，如果某种高级语言具有真正意义的可移植性，那么它将不能使用某些处理器的特有功能。

例如，许多微处理器都有移位指令。如前所述，这些指令能将累加器中的字节的每一位向左或向右移动。但事实上，几乎没有哪一种高级语言包含这种操作。如果在程序中需要进行移位操作，则必须通过乘2或除2来模拟该过程（这并不是什么坏事：事实上，许多现代编译器都是利用处理器的移位指令来实现乘以或除以2的幂的)。除此之外，许多高级语言也不包括按位逻辑运算。

在早期的家用计算机中，大部分应用程序都是用汇编语言写的，而现在除了一些特殊的应用场合之外，汇编语言已经很少使用了。而今处理器引入了一些新的硬件，可以实现流水线技术—同时有若干个指令码渐次执行—这使得汇编语言变得更加复杂且不易处理。与此同时，编译器却变得更加成熟，越来越多的程序开始使用高级语言来编写。现代计算机大容量的存储器也作为一个重要的角色，推动了这种趋势：程序员不再局限于编写运行在小内存和小磁盘上的程序。

早期的计算机设计者都曾尝试用数学符号来描述问题，但公认的第一个真正可以工作的编译器是 A-0，它是为 UNIVAC 开发的编译器，于 1952 年由雷明顿兰德公司(Remington-Rand)的格瑞斯·穆雷·霍珀（Grace Murray Hopper，1906-1992)开发完成。霍珀博士的早期计算机研究工作始于1944年，那时她效力于霍华德·艾肯（Howard

编码隐匿在计算机软硬件背后的语言

386

## PDF_PHYSICAL_PAGE_INDEX=403 | PRINTED_PAGE=387

Aiken)，主要研究MarkⅠ。在她八十多岁的时候，仍然孜孜不倦地在计算机界工作，当时她在 DEC（Digital Equipment Corporation）公司从事公关事务。

FORTRAN语言是目前仍在使用的最古老的高级语言(虽然这些年来人们对其进行了大量修改)。你可能注意到了，很多计算机语言都是以大写字母命名的，这是因为它们的名字大都是由几个单词的首字母组成。FORTRAN这个名字来源于FORmula的前三个字母和TRANslation的前四个字母的组合，它由IBM在20世纪50年代中期开发，主要应用于704系列计算机。自其发布的几十年来，FORTRAN一直被认为是科学和工程应用程序开发的首选语言。它广泛地支持浮点运算，甚至支持非常复杂的数的运算（即我们上一章讲到的由实数和虚数构成的复数)。

任何一种计算机程序设计语言都有其支持者和批评者，而且人们通常只对自己喜欢的语言有热情。本书尽量以一种客观的态度来讨论某种语言，这里选取了一种语言作为原型，通过它来解释那些几乎已经销声匿迹的程序设计概念。我们的选择是ALGOL（即ALGOrithmic的缩写，有趣的是，ALGOL也是仙女座第二亮的恒星的名字)。ALGOL作为过去40年中许多曾经流行一时的通用高级语言的直接鼻祖，也非常适合用来研究高级程序设计语言的本质，该语言可看做是一粒种子，它的成长最终形成了高级语言这棵大树。直到今天，人们仍然在使用“类ALGOL”程序设计语言的概念。

ALGOL语言的原版由某国际委员会在1957至1958年间设计，它被称做ALGOL58。两年后，也就是在1960年，ALGOL58的改进版ALGOL60面世，其最终版本是ALGOL68。本章所采用的版本在 Revised Report on the Algorithmic Language ALGOL 60 说明文档中有具体描述，该文档于1962年完成并在1963年首次发行。

让我们开始写第一个ALGOL程序。假设我们使用的操作系统平台是 CP/M或MS-DOS，并且安装了一个名为ALGOL.COM的编译器。该程序是一个文本文件，命名为FIRST.ALG。注意，文件类型名是 ALG。

ALGOL程序以begin开始，以 end作为结尾，程序的主要内容被包括在这两个语句之间。下面的程序用来显示一行文本：

24 高级语言与低级语言

387

## PDF_PHYSICAL_PAGE_INDEX=404 | PRINTED_PAGE=388

通过在命令行运行ALGOL编译器对FIRST.ALG文件进行编译，其格式如下：

ALGOL FIRST.ALG

ALGOL编译器对这条命令很可能做出这种响应，在显示设备上给出以下提示信息：

Line 3: Unrecognized keyword 'ende'.

ALGOL编译器对拼写的检查非常严格，它在这一点上比传统的语文教师更甚。因为输入程序时，误把“end”拼写做“ende”，所以编译器通过提示信息告诉我们程序中有语法错误（syntax error)。当编译器检查到“ende”时，它期待能遇到一个可识别的关键字(keyword)，但由于上述错误，编译不能通过。

将程序中的错误改正之后，可以再次执行编译命令。由于系统平台和编译器版本的不同，有时编译器会直接生成一个可执行文件（CP/M平台下此文件名为FIRST.COM，MS-DOS平台下名为FIRST.EXE)；有时还需要再执行一个步骤才可以完成。不论是哪种情况，最后你都可以在命令行执行FIRST程序：

FIRST

FIRST程序会对此响应，并显示以下内容：

This is my fist ALGOL program!

注意，这里还有一个拼写错误：first被误做fist！编译器没有检查出这个错误，因此它被称为运行时错误（run-time error）—程序被执行时才出现的错误。

很明显，我们的第一个 ALGOL程序中，print 语句的功能是把一些信息显示到屏幕上，在本程序中是显示一行文本（从功能的角度来看，该程序与本章开始所给出的汇编程序是等价的)。ALGOL语言的正式规范中并不包括print语句，但我们假设所使用的特定 ALGOL编译器包括这个便利的工具，它有时候也被称做内部函数（built-in function）。除了begin和 end之外的大部分 ALGOL语句都要以分号结尾。你可能注意到了print 语句使用了向右缩进的格式，这并不是必要的，其作用只是为了让程序的结构更加清晰。

假设现在要编写一个用于两个数相乘的程序。每一种程序设计语言都包括变量（variabie）的概念。程序中的变量可以是一个字母、个短的字母序列，也可以是一个单词，由程序员自己决定。变量名实际上对应内存的一个存储单元，但在程序中是通过名字来访问该存储单元的，而不是直接使用存储单元的地址值。下面的程序定义了三个变量，分别命名为a，b，c:

编码隐匿在计算机软硬件背后的语言

388

## PDF_PHYSICAL_PAGE_INDEX=405 | PRINTED_PAGE=389

real语句称为声明（declaration）语句，用来指明程序中要定义的变量。在该程序中，变量a，b，c被定义为实数（real）类型或浮点数类型（同时，ALGOL语言也支持使用integer关键字来定义整数型变量)。程序设计语言中的变量名通常以字母开头，变量名也可以包括数字，但前提是第一个字符必须是字母。变量名不能含有空格，也不能包含除字母和数字以外的其他大部分字符。通常编译器会规定变量名的最大长度，本章用到的变量一律以单个字母命名。

假如我们使用的特定ALGOL编译器支持IEEE浮点数标准，则本程序中所定义的三个变量每一个需要4个字节的存储空间（采用单精度格式）或8个字节的存储空间（采用双精度格式)。

声明语句之后的三个语句是赋值（assignment）语句。在ALGOL语言中，赋值语句很容易被识别，因为它的格式很固定，总是在冒号后面跟着一个等号（在大多数计算机语言中，赋值语句通常只包括等号)。赋值语句的冒号左边是一个变量，而等号右边是一个表达式，表达式的计算结果将被赋值给左边的变量。前两条赋值语句指明，变量a，b将分别被赋予一个特定的值；第三条赋值语句指明，将a和b的乘积赋值给变量c。

时至今日，我们所熟悉的乘法符号“×”已经不允许出现在程序设计语言中了，因为它没有被包括在ASCII和EBDCIC字符集中。大多数程序设计语言使用星号（\*）来替代它作为程序中的乘号标记。尽管ALGOL使用了普遍使用的斜杠（/）作为除法标记，但在该语言仍然可以使用除法标记（÷)，该标记用于整数除法，用来指明被除数与除数的倍数关系。ALGOL还使用了另一个非ASCII字符“↑”，该箭头符号用来做乘方运算。

最后的print语句用来显示所有变量的值。它包含文本和变量，并以逗号分隔。print语句的主要工作并不是用来显示ASCII码值的，但本程序中却做了更多的工作：将浮点数也转换成了ASCII码并显示：

24 高级语言与低级语言

389

## PDF_PHYSICAL_PAGE_INDEX=406 | PRINTED_PAGE=390

接着会执行end语句，程序终止并将控制权交还给操作系统。

如果要将另外两个数相乘，则需要做以下工作：修改程序，改变变量的值，重新编译并重新运行程序，这将是一件非常烦琐的工作。为了避免这些重复工作，我们可以借助于另一个内部函数read。修改后的程序如下：

read语句的功能是读取从键盘键入的ASCII码值，并将其转换成浮点数。

循环（1oop）是高级语言的重要组成部分。循环使得程序可以对同一个变量的不同取值反复执行相同的操作。假设我们要写一段程序用来计算3，5，7，9各自的平方，可以这样编写程序：

for 语句将变量 a的值第一次设为 3，然后执行 do 关键字后面的语句。如果 do后面要执行的语句不止一条（如本例)，则必须将它们置于begin和 end之间，这两个关键字定义了一个语句块（block）。第一次循环之后，for语句会一次为a赋值5，7，9并执行

编码隐匿在计算机软硬件背后的语言

390

## PDF_PHYSICAL_PAGE_INDEX=407 | PRINTED_PAGE=391

相同的语句块。

下面的程序中采用了for语句的另一种使用方式，这段程序用来计算3\~99之间所有奇数的立方。

for语句将变量a初始化为3，并执行for后面的语句块。第一次循环结束后，变量a与step关键字后面的增量相加，这里是2。新得到的a的值是5，它将用于第二次执行语句块。变量a继续增加2并用于下一次循环，直到a的值超过99，这时for循环结束。

一般而言，程序设计语言对语法都有着非常严格的要求。在ALGOL60中，就关键字for而言，其语法格式是：for的后面只能跟-个变量名。而英语中的这种限制宽松的多，单词 for的后面可以跟所有类型的单词，例如“for example”，“for can”等。尽管编译器是非常复杂的程序，但其所能解释的语言显然要比人类的语言简单得多。

大部分程序设计语言的另一个重要特征体现在条件（conditional）语句的使用。条件语句的特点是，只有当某个条件成立时才会执行另一条对应的语句。在下面的例子中，我们使用ALGOL的内部函数 sqrt来计算一些数的平方根。sqrt函数的参数不能是负数，因此要在程序中通过条件测试避免这种情况。

24 高级语言与低级语言

391

## PDF_PHYSICAL_PAGE_INDEX=408 | PRINTED_PAGE=392

左尖括号（<）是小于号。如果程序的使用者输入的是一个小于0的数，if语句中的判断语句为真，因此第一个print语句将会被执行。反之，如果该数大于或等于0，则else关键字后面的语句块则会被执行。

本章目前所用到的变量都是一个变量对应一个值，我们也可以用一个变量对应多个值，数组（array）就是一个很好的选择。在ALGOL程序中可以这样声明一个数组：

该语句定义一个数组变量a，它可以用来存放100个不同的浮点数，这些数被称做数组元素。可以使用数组名加标号的方式来引用数组元素，例如，第一个数组元素是a[1]，第二个是a[2]，最后一个是a[100]。方括号中的数字称做数组下标（index)。

下面的程序用来计算1\~100所有数的平方根，将结果保存在一个数组中，然后再通过循环将这些结果显示出来。代码如下：

程序中还定义了一个整型变量i(由于它是integer的首字母，经常被程序员用做整型变量名)。第一个for循环的执行过程中，每个数组元素被赋值为其下标的平方根；第二个for循环执行过程中，数组中的每一个元素被显示出来。

变量的类型有很多，除了我们已经介绍过的实型和整型之外，变量还可以被声明为布尔型（Boolean，该名称是为了纪念第10章提到的乔治·布尔）。布尔变量的取值只可能有两种，即true和false。在本章的最后将介绍一个用到布尔数组的例子（这个例子也将用到目前所介绍的大部分内容)，来实现一个寻找素数的著名算法——爱拉托逊斯筛法(Sieve of Eratosthenes)。爱拉托逊斯（约公元前276-196年）传说是亚历山大图书馆的管理员，他因准确计算出地球的周长而永载史册。

编码隐匿在计算机软硬件背后的语言

392

## PDF_PHYSICAL_PAGE_INDEX=409 | PRINTED_PAGE=393

素数是只能被1及其本身整除的一类整数。第一个素数是2（也是唯一的偶数素数)，其他的素数还包括3，5，7，11，13，17，等等。

爱拉托逊斯方法以2开始的整数表开始，因为2是素数，因此所有可以被2整除的数都被排除掉（即除了2之外的全部偶数）。接下来是3，因为3是素数，因此所有能被3整除的数也被排除掉。因为4在第一个步骤中已经被排除掉，所以下一个要考虑的数是5，即排除所有5的倍数。按这种方式不断循环，最后剩下的都是素数。

下面的ALGOL程序用来筛选2\~10,000之间的所有素数，程序中定义了一个布尔数组，用来对所有的数进行标识。该程序如下：

第一个 for 循环将数组 a的每一个元素的初始值设置为布尔值 true。这里的 true 表示该位置的数是素数，因此现在程序默认所有的数都是素数。第二个for循环的范围是1\~100（100刚好是10000的平方根)。在第二个for循环中，如果判断条件成立，该数为素数，即 a[i]为 true，则第三个for 循环则会把该数的所有小于或等于10000的倍数（除了其本身）设置为false，因为这些数都不是素数。最后的for循环用来输出所有的素数，这里的判断条件是：若a[i]为 true，则 i 为素数。

程序设计到底是一门科学还是一门艺术呢？这的确是一个有趣的问题，一些人甚至还为此争论不休：一方面，你或许在大学里系统地学习了计算机科学（Computer Science）课程；另一方面，你又读过如唐纳德·克努斯（DonaldKnuth）的名著《计算机编程艺术系列》(The Art of Computer Programming series）等著作。然而物理学家理查德·费叶曼（Richard Feynman）曾这样写道：“从某种程度上看计算机科学像是一种工程，它的工作范畴是利用一些事物去实现其他事物。”

24 高级语言与低级语言

393

下一段：[继续读取](https://workshop.closeclaw.top/api/ai/read/38270854-596f-4a26-8183-db8c968c6020.md?start=410&limit=10)
