资源描述
附录尚匚编程序,连接程序和SPIM模拟器James R larusWi scons irr4Jadi son大学计算机科学系畏首畏尾将无法体会到自由言论和集会的压力Louis BrandeisWiitney v.California,1927A 1简介.2A 2汇编程序.8A 3连接程序.17A 4装载.18A 5内存的使用.19A 6过程调用协议.21A 7异常出错和中断.36A 8输入输出.41A 9 SPIM.43A 10 MIPS R2000汇编语言.56A 11结论.99A 12重要术语.100A 13 习题.102Al简介对于计算机来说,用二进制数对指令进行编码很自然,也很有效。然而,人们 在理解和操作二进制数时却存在许多实际的困难,比起一长串数字来人们更熟悉读 写具体的符号和单词。在第三章我们将说明,由于计算机指令能可采用许多不同的 表示方式,而不仅仅局限于在数字和单词之间选择,这样人类能读写符号,而计算 机则能执行相应的二进制指令。本附录将详细描述了如何将人们可读的高级语言翻 译成计算机可执行的指令格式,同时提供了一些编写汇编程序的帮助,并且还介绍 了如何将这些程序在SPIM模拟器上运行。其中SPIM模拟器能执行NPIS程序。如 若需要,用户可以通过网址:aww mkp.ccnXdod2e.ht喊取有关Lhi&Windows和 DOS版本的SPIN模拟器。汇编语言是计算机机器语言T进制编码的一种符号表示。由于汇编语言用符 号代替了数字位,所以汇编语言比机器语言更具有可读性。汇编语言中的符号命名 通常针对具体的操作模式,例如操作码,寄存器说明等等,人们就很容易记忆和书 写。另外,汇编语言还允许编程人员使用标号来识别、命名特定的存储宁用以保存 指令和数据。图A 1建立一个可执行文件的过程。汇编程序将汇编语言程序翻译成目标文 件,而目标文件再与其他的文件和库相连成为可执行文件。汇编程序可以将汇编语言翻译成二进制指令。与计算机的机器码相比,汇 编程序的表达方式比较友好。这种优势不仅表现在对符号名的操作和定位上,还表 现在编程的简化方面,它使程序更加清晰性。例如,在第二节将要讨论的有关宏汇 编的概念,将允许编程者通过定义一些新的操作来扩展汇编语言。汇编程序读入一个汇编语言源文件并生产一个目标文件。在目标文件中,包含 了机器指令和用于同其他目标文件链结的记录信息。图1显示了一个可执行程序 的生成过程。大多数程序有以下几个文件(也称为模块)组成:编写,编译和汇编 三个独立的模块。程序中可以调用库函数中各个子例程。各个模块通常用到调用子 例程的语句,用到定义在其他模块和函数库的数据结构。当模块的源代码中用到了 一些其他的目标文件和函数库中都没有定义的函数调用和数据结构时,该程序就无 法执行。顾名思义,目标代码连接器(linkei)就是将目标文件和库函数链接生成 可执行文件。然后,该可执行文件就可以在计算机上运行了。为了说明汇编语言的优点,我们依次观察下列所示各图。各图显示了用各种方 法计算并打印(H00的整数的平方和的例程。图A 2显示了在MIPS(Million Instructs Per Second)计算机上运行机器码。为了将这些机器指令翻译成如同图 A 3的符号,我们可以参照在第三章和第四章提供的操作码和指令格式表,但其工 作量可想而知。图A 3所示的操作码和操作数使用符号而不是用二进制数书写,所 以读写例程比较方便。然而,由于在这种汇编语言中,存储位置是通过具体指定它 们的地址,而不是通过符号来确定,所以使用时透明性较差,难以推广。图A2在MIPS机器上计算并显示0-100的整数的平方和的例程。图A4给出了用符号来表示存储地址的汇编语言。大多数程序员愿意选择这种 格式来读写程序。用句号(.)开始的名字例如:.data,.globl是汇编指令,它 的作用在于提示汇编程序如何翻译下列程序段,但它们本身并不被转化成内部的机 器指令。用冒号()结尾的名字(例如str或main)是表示下一个存储位置的标 号。除了缺少注释行,这种程序同大多数汇编语言一样具有良好的可读性。但是,由于一些简单的操作也需要通过执行任务来完成,且该语言缺乏控制流结构,无法 对程序的执行提供更多的线索,故而这种汇编语言较难使用。通过比较不难发现,图A 5所示的用C语言书写的程序简短明了。那是因为在 C语言中,每个变量有自己的存储名字,循环结构远比分支要明了的多(如若你对 C语言不太熟悉,请参考 ww mkp.coiZcod2e.htn)。其实,只有C例程才是完全 手工书写的,图示的其他例程都通过C编译程序和汇编程序间接产生。图4用汇编语言书写的例程。然而,该代码无法标识寄存器,存储单元的 具体地址,也没有包含注释语句。通常,汇编语言有以下两个作用(如图A 6所示)。首先是充当编译程序的输 出语言。编译程序的作用在于将用高级语言(如Q Pascal)书写的程序编译成相 应的机器语言或汇编语言。我们称高级语言源程序,将编译的输生叫做目标程序。其次,汇编语言的作用还在于它能直接作为一种书写规范程序的语言。这种作 用曾经是汇编语言的主要作用。然而,由于目前实现了更大的内存空间和更加完美 的编译程序,大多数程序员可以直接用高级语言书写程序,而无须仔细分析机器指 令。尽管如此,汇编语言对于书写程序还是非常有用,尤其在执行速度,占用内存 以及充分利用硬件特性等方面,高级语言还是无法与之媲美。尽管本附录着眼于MIPS上的汇编语言,汇编程序的编写在其他机器上也基本 相似。在 CISC系统结构上的机器(例如 VAX机器:参见mkp.c皿tod2e.htn)中附加了一些指令和地址模式,它们使汇编程序显得更短小,但不改变对程序的汇编过程,也不支持高级语言提供诸如类型检测,结构控 制流等功能。图4/用带标识语句,不含注释行的汇编语言书写的例程。以圆点开始的命 令行是汇编提示符(参见4 5T5刃。.text指下列语句行是指令代码 行。.data指下列语句行是数据代码行。.align n指相继的语句行以2字节对 齐,例如align 2表示以字对齐。.globl rmin表明nnin是全局符号,它对其他 文件中的代码是可见的。.asciiz则是在内存区域存储以null(空指针)结尾的 字符串。何时使用汇编语言相对简易的高级语言而言,采用汇编语言编程的主要出于程序的执行速度或占 用内存方面的考虑。例如,在设计用计算机控制具体的机械的设备(譬如说汽车的 刹车系统)时,这种要求就很重要。我们将计算机集成到另外一个设备,如汽车,称为嵌入式计算机。这种计算机系统需要实时监测和响应外界事件。因为编译程序 对指令的操作开销存在许多不确定因素,程序员也就很难利用高级语言编程保证实 时性。比如,在传感器检测到轮胎打滑时,其实已经发生在1微妙之后了。与此相 对,用汇编语言编写程序就能很好地控制指令的实时响应。另外,嵌入式的应用程 序还对程序的大小提由了严格要求。因此,用汇编语言编写的程序能满足占用更少 的内存,降低嵌入式计算机的响应时间。图A5:用C语言编写的例程图 汇编程序可以由程序员书写,也可以由编译程序生产输出混合方法,也就是说程序的大部分内容由高级语言编写,而其中的实时响应部 分由汇编语言直接实现,这样就充分利用了两者的优点。通常情况下,程序将大部 分的时间花在程序的一小部分核心代码上。这个结论也是实现高速缓存(参见第7 章第2节)的基础。通过分析一个程序可以测得其各部分的执行时间,同时也能得到程序的实时响 应部分。在很多情况下,可以通过更好的数据结构或算法来提高响应时间。而在某 些情况下,只有通过对该部分内容用汇编语言重写才能显著提高响应时间。这种性能的提高并不是说高级语言的编译程序是失败的。通常情况下,编译程 序在整个程序上实现统一的高质量机器代码要比程序员手工书写汇编程序来的好。然而,程序员的优势在于他比编译程序对程序的算法和执行上有更为深入的理解,这也就决定了程序员可以想方设法提高某一小块程序的执行效率。尤其是当程序员 在编写程序时往往需要考虑到几个进程能同时运行;而编译程序的习惯做法是分别 单独编译各个进程,并且在执行过程中固定地采用习惯方法控制使用寄存器,并严 格规定了各个进程的界限。与之相反,通过在寄存器中保留常用数值,甚至实现进 程的边界重叠,程序员可以让程序运行得更快。汇编语言的另一个优点在于它能够充分利用特定的指令,例如字符串拷贝和匹 配指令;而编译程序,通常无法判断循环代码是否可以用一条指令来代替。但是,程序员就可以比较容易用单条指令代替一个长循环。将来,随着编译技术的改进和计算机间通信的不断完善(参见第6章),程序 员的优势很可能难以继续保持下去。采用汇编语言的另一个原因在于有些特定系统结构的计算机上无法使用高级语 言。在许多旧的或特殊的计算机就没有编译程序,因此就只能选择汇编语言。汇编语言的缺点汇编语言的一些缺点阻碍了它的推广。最大的缺点在于汇编程序的编写太多依 赖具体的机器结构,而且在其他系统结构的机器上运行时往往要重新编写。第1章 我们研究讨论了计算机的高速发展意味着计算机的系统结构不断的推陈由新。这也 就说,即使不断地有更新、更快、更高效的计算机推出,原来的汇编程序还是只能 局限于原始的系统结构。汇编语言程序的另一个缺点在于汇编程序在程序编写上的长度要远远超过同等 的高级语言编写的程序。例如,图5所示的用C语言编写的程序在长度上只有11 行,而图4所示的用汇编语言程序编写的例程就有31行。在复杂的程序中,这个 比值(也称膨胀比)要远远大于3(上述例子的膨胀比)。经验数据显示,一般的 程序员每天能编写的汇编语言程序行数和高级语言程序数量基本一致。这就意味 着,如果选择采用高级语言而不是汇编语言编写程序,程序员的工作效率就会达到 x倍,这儿x值指膨胀比。由于越长的程序越难读懂,也可能存在更多的漏洞。汇编语言程序由于缺乏严 格的结构,这个问题就更加突生。常用的编程术语,例如ifthen语句,循环语句 等等,往往存在分支语句和跳转语句。因此,用户必须从当前语句由发进行结构重 组,并且同一个语句在不同的情况下还会存在微弱的差别;这样往往会导致用户很 难读懂汇编语言程序。请读者参见图AZ后回答下列问题:这里用到了哪种循环?它的上限次数和下限次数各是多少?细节(1):编译程序无需依赖汇编程序就能直接生成机器语言。这些编译程序的 执行速度通常比嵌入部分汇编程序的编译程序都要快。然而,编译程序在生产机器 语言时一定要完成一些任务。这些工作在汇编程序中同样需要处理,譬如解决地址 分配问题,指令产生二进制编码的问题等等。这就需要对编译速度和编译程序的简 洁性进行综合考虑。细节(2):尽管存在这样那样的问题,许多嵌入式应用程序还是需要用高级语言 来实现。因为有些应用程序不仅庞大复杂,还必须保证高可靠性;如果用汇编语言 来实现,不但大大增加编写程序带来的开销,同时其准确性也难以得到充分保证。事实上,国防部为了解决这种复杂的嵌入式系统的问题,在充分考虑到上述因素基 础上,已经发展了一种叫做Ada的高级编程语言用于编写嵌入式系统。A 2汇编程序编译程序的作用在于将汇编语言程序翻译成二进制机器指令和二进制数据文 件。这种翻译过程包括两个主要步骤。首先是确定标号的内存区域,以便指令翻译 过程中实现各个符号名与内存地址的一一对应。然后,对操作码实施二进制化,结 合寄存器和标号等组成合法的指令。如图A 1所示,汇编程序生成一个输出文件,我们称之为目抚文件,它包含了机器指令集,数据和记录信息。由于目标文件涉及到了其他文件中的一些子过程和数据,所以它不能单独地执 行。如果被调用对象处于其他文件中,则称该调用是外部调用(也称全局调用)。否则,如果该对象能够在文件内部得到,我们就称之为局部调用。大多数汇编程序 中,局部调用是默认的,而全局调用必须明确定义。在汇编程序中,子例程和全局 变量会被多个文件调用,所以它们需要被定义为全局调用。局部调用名字对其他模 块来说不可见。例如,C语言中的静态函数,只能被同一文件中的其他函数调用。另外,由编译程序生成的名字,譬如用于循环中起始指令的名字,就只有局部作用 域,所以编译程序无需在每一个文件中生成唯一的名字。局部标号、全局标号例题:参见A-7中的图A4子过程中有全局标号main,同时还有两个局部标 号:loop和str。局部标号只能在当前汇编文件中使用。过程的最后还包含了一条 调用全局标号的语句printf,其中printf是输由库函数,所以属于全局引用函 数。问图A 4中的哪个标号可能会调用其他文件的内容?解:因为只有全局标号才具有被其他文件调用的特性,所以只有标号main才 具有上述性质。由于汇编程序分别处理程序的每一个文件,所以只能得到局部标号的地址。它 还要利用其他工具(连接程序)将各个目标文件和库函数连结成一个可执行文件,从而解决全局标号的问题。汇编程序为连接程序提供标号列表和的全局引用,以便 生成最终的可执行程序。但局部标号也给汇编程序带来了很大困难。不同于高级语言程序的名字,汇编 中的标号可以在定义之前使用。在图A 4的例子中,指令la在标号str定义之前弓|用。这种超前引用的存在使得汇编语言在转化程序时需要分两步执行:先是找到所 有的标号,然后再生成指令。在上述例子中,当汇编程序看到指令 必时,它并不知 道哪儿能找到标号str,甚至并不知道标号str是指令还是数据。汇编程序的第一次扫描只是读取源文件的各指令行,并且将每个指令行有机分 割成各个基本单元一一词法单位。它们包括独立词,数据和标点符号。例如,在下 列指令行中ble t0,100 loop包含了 6个词法单位:操作码ble,寄存器tQ逗号,数据100,逗号和符号 loop)如果命令行以标号开始,那么汇编程序把标号名字和指令的地址记录到符号表 中。以后通过这些信息就可以计算出该命令行的指令在内存中的占用的字数。再根 据记录指令的大小,汇编程序能判断由下一条指令的位置。对于一条变长度指令,例如在VAX机的指令长度,汇编程序需要复杂计算才能确定。而对于固定长度指令 的大小,例如MIPS机指令,汇编程序则只需简单地扫描即可。相应地,数据标号 所需内存空间也需要做类似计算。当整个源文件扫描完毕时,符号表中得到的就是 标号的详细位置。第二趟扫描汇编程序利用第一次扫描得到的符号表信息产生机器码。此时汇编 程序再对文件进行逐行扫描。如果命令行中包含有指令,汇编程序就把操作码和操 作数的二进制表示(寄存器描述符或内存地址)转化为合法的指令,详细过程如第 3章第4节所描述。由于全局变量的地址并不在符号表中,所以引用到全局变量的 指令和数据在汇编期间无法实现完全汇编。而对于在其它文件中定义的标号,汇编 程序就无法引用未弓I用的变量。汇编语言是一种程序设计语言。它同诸如BASiq q JNA等高级语言的最大 不同就在于汇编语言只提供为数不多的简单数据类型和控制。汇编语言程序不能 为变量中的数据指定数据类型。反之,程序员必须为数值选择正确的操作(例如 整数或浮点数)。另外,在汇编语言中,程序必须完成的所有go to控制流。这 些就决定了无论是在MPS还是在80 x86机器上,汇编语言程序的编程比高级语言 的编写更为困难,更容易出错,细节:如果对汇编的速度要求较高,则可以采用一种叫做回补(backpatching)的技术,把汇编的两个步骤规约到一趟扫描完成。汇编程序将在期间完成(或尽可 能地完成)每一条指令的二进制化。如果指令引用到未定义的标号,汇编程序就在 符号表中记下相应的标号和指令。如若标号已经定义,则汇编程序就访问符号表,以便找到所有包含了该标号的指令。汇编程序回卷并校正它们的二进制表示,然后 将他们并入标号地址。由于汇编程序只需对汇编源程序扫描一遍,所以这种回补技 术能提高汇编速度。然而,回补过程有时需要汇编程序在内存中保存该程序的所有 的二进制码,这样才能保证指令回补的完成。这限制了源程序文件的大小。目标文件格式汇编过程负责产生目标文件。在Uiix上的目标文件由6个独立的部分组成(如图A 7):目标文件头描述了文件中其他块的大小和位置。正文段包含了在源文件中的各个子程序的机器语言码。由于这些子程 序有可能包含了一些尚未解析的引用,故可能无法独立运行。数据段包含了源文件中数据的二进制表示。由于同样用到了一些定义 在其他子程序中的引用,这些数据对于程序来说也是不完整的。重定位信息指定了依赖于绝对地址的指令和数据。当移动部分程序 时,就需要重新调整这些引用。符号表则建立外部标号与地址之间的映射股关系并建立未解析引用列 表。调试信息包含了程序被编译时的一些详细描述,以便调试程序找到指令地址与源文件中各行的对应关系,并显示这些数据结构。+3图A 7目标文件。有Uiix汇编程序产生的目标文件包含了 6个独立的部分。汇编程序产生的目标文件中包含了程序、数据以及一些附加信息(用于将程序 的各个部分连结起来)的二进制表示。因为汇编程序并不知道程序的各个子程序之 间以及数据之间的内存地址,所以重定位信息对目标文件来说至关重要。尽管说一 个文件中的子程序和数据一般存储在一块连续的内存空间中,但汇编程序并不知道 这些内存空间的绝对地址。汇编程序要完成符号表和连接程序的一一对应。并且,汇编程序必须正确记录哪些全局标号定义在哪个文件中,以及未定义引用出现的文 件。细节:为了方便起见,汇编程序假设每个文件从同一地址开始(例如,起始地址 为0,这样就使得加载代码和数据到内存时,连接程序能够对它们进行重定位。重定位信息由汇编程序产生,包含了文件中的每一个指令和数据的绝对地址。在 MIPS机器中,只有当调用子程序、存储操作指令时才用到绝对地址。而对PC相关 的寻址操作(例如分支操作)而言,则无需重定位指令。附加工具汇编程序还具备其它一些优点,使其变得简单易写。并且基本上不会改变汇编 源程序功能。例如,数据分配伪指令可以帮助程序员在描述数据时能采取比二进制 码更为简洁和自然的方式。在图A 4中,伪指令如下所示:.asci iz 64The sum from 0.100 is%i 7它包含了字符串在内存中的一些特征信息。下面我们把这条指令和它的各个字 母的ASCH值(这些字母的ASCH表示请见第3章的图3.15)进行比较:.byte 84,104,101,32,115,117,109,32.byte 102,114,111,109,32,48,32,46.byte 46,32,49,48,48,32,105,115.byte 32,37,100,10,0指令.asciiz表示字母而非数字,所以该指令很易识别,汇编程序在将字符转 化为它们所代表的二进制码时,无论在速度还是在准确率上都比人工识别高。数据 分配指令用一种人工可读的格式表示数据,它可以把汇编程序转化为二进制码。其 他指令请参见图A ia字符串指令例题:详细说明由下写指令所表达的字节序列的含义:.asci iz“The quick brown fox juips over the lazy dog解:.byte 84,104,101,32,113,117,105,99.byte 107,32,98,114,111,119,110,32.byte 102,111,120,32,106,117,109,112.byte 115,32,111,118,101,114,32,116.byte 104,101,32,108,97,122,121,32.byte 100,111,103,0宏是通过对频繁使用的指令序列的预定义来实现模式匹配和替换。使用宏时,程序员只需在源程序中定义一次,就可以多次调用它;调用时可以调用一个宏指令 语句,从而避免重复书写频繁出现的指令序列。如同子程序一样,宏允许程序员创 建并命名一条新的命令替换一段公共操作。与子程序不同之处在于,因为宏调用是 在程序被汇编时就被宏体替换,故而在程序运行时刻宏无需处理调用,也无需返回 值。在宏替换之后,汇编结果同无宏定义的程序完全相同。宏例题:在本例子中,假设用户需要输由一些数据,库函数printf支持输出字符 串和数值。编程人员能够用下列指令实现输出寄存器7中的整数:.dataint str:.asci iz 知.textla aO,inLJtr#将字符串地址送入第一个参数(load string address into first arg)mov al,7 胡数值送入第二个参数 Load value into scondargjal printf 蜩用打印库函数 Call the printf routine其中,.data指令使汇编程序将字符串存放在程序的数据段中,.text则指示 放入程序的正文段中去。但是,用这种方式打印多个数值或多次打印时比较麻烦,而且产生的程序代码 冗长、难以理解。一种恰当的方法是创建宏(printLJnt)来实现整数输生:.dataint str:.asci iz 66%T.text.macro printf int(Xarg)la aQ inutr#将字符串地址送入第一个参数imv al,arg用各宏参数(a噌送入第二个参数jal printf 蜩用打印库函数.end macroprintUnt 劭宏调用中有个参数叫ar当当宏被展开时,实际数信将代替该参数在宏调用体 中的相关变量;然后,再用展开的宏体替换程序中的相关调用部分。在第一次调用 prinjint时,参数是7,所以宏展开后的代码为:la a05 int strmov al,7jal printf在第二次调用时,假设调用命令为:prinLJnt 那么宏参数为t0,此 时宏展开结果为:la YaO,int strmov al,t0jal printf以此类推,宏调用prinOnt5。展开后的结果为什么呢?解:la a0,int strmov al,a0jal printf这个例子同时也暴露了宏的一个缺点:使用宏调用时必须知道prinjnt用到 了寄存器a0,否则就无法正确输出寄存器中的数值。硬软件接口有些汇编程序为用户提供了伪指令。伪指令就是指由汇编程序提供但硬件并不 支持的指令。在第3章的内容包括:MIPS汇编程序如何建立伪指令,如何从简单 的MIPS硬件指令集中确定寻址模式等。例如,第3章第5节介绍了如何用:sit 和bne指令来建立bit伪指令。通过拓展指令集,MPS汇编程序可以在不增加硬 件复杂性的基础上,提高编程的简易性。许多伪指令可以通过宏来实现,但在 MIPS汇编程序中用到了特定的寄存器(at),这使得汇编程序能优化产生的代 码,从而能生成更好的机器码。细节:汇编程序可以有选择地汇编部分源码,这就允许用户在程序汇编期间可以 有选择地包含或排除某些指令。这在需要汇编同一个程序的多个版本(仅有微小差 异)时特别有用。用户可以将几个版本合并成一个文件,从而无需将它们分开保 存,但其结果是增加了修改公共代码缺陷的复杂性。当某一版本的代码需要条件汇 编时,这部分代码可以暂时不进行汇编,而其他的源代码照常实施汇编即可。虽然宏和条件汇编很有用,但它们在Uiix系统下却很少用到。原因之一在于 这些操作系统下的编程人员喜欢用诸如C之类的高级语言进行编程。大多数汇编代 码是由编译程序产生,而编译程序能比宏更加方便的实现代码的复用。另一个原因 在于Unix系统下的其他工具库一如cpp,C预处理机,用(一种宏处理器)一也能 为汇编语言程序提供宏和条件汇编。A 3连接程序独立的编译程序可以将一个程序分割成几个部分存储到几个文件中。每个文件 的内容包含了一些逻辑相关的子程序和数据结构的集合,这些内容构成了整个程序 的各个模块。每个文件能够独立的进行汇编和编译,这就保证在修改某个模块时,无需重新编译整个程序。正如我们在前面提到,分开编译使得我们需要借助连接程 序将各个目标文件组合起来。图A 8所示的将各个文件合并的工具就是连接程序。它具体执行3个步骤:搜索程序库,找到被本程序调用的库函数;分配每个模块代码的内存位置,并且调整绝对引用实现指令的重定位;实现文件之间的引用。连接程序的第一步是为了保证程序中所有的标号都被事先定义好。连接程序匹 配全局符号和各个文件中的未定义的引用。文件中的全局标号使其他文件的引用成 为可能,使它们可以同时引用同一个变量。不能匹配的引用则表明虽然该符号可 用,但并没有在其他文件中定义。+8图及连接程序搜索目标文件和程序库集合,寻找全局调用,然后将它们连连 生成一个可执行文件,最后实现不同文件中的子函数之间的互相调用。在程序连接期间,发现未定义的引用并不能说明程序编写有误。程序可以引用 在目标文件中没有扫描到的库函数。在连接程序完成符号匹配之后,就可以开始搜 索系统的程序库,从而进一步扫描程序中引用的已预定义的子程序和数据结构。基 本函数库是能实现数据读写,分配及清空内存,进行数学运算的例程函数。其他函 数库则包括存取数据库,控制终端窗口的各个例程。如果程序引用了在任何一个库 中都未定义的符号,那么连结过程将由错,最终将无法实现连接。在程序引用库函 数时,连接程序从函数库中读取相应的函数段,然后将它嵌入程序正文段中。反 之,新的子函数将可能还要依赖其他子函数,因此,连接程序将依次搜索其他的库 函数直到所有的全局标号都能引用为止。当所有的标号都可以被引用时,连接程序接下来分配每个模块所占用的内存。由于每个文件是被单独汇编,所以汇编程序不可能事先知道A模块的数据和指令将 出现在B模块的哪个位置。当连接程序将一个模块换入内存,所有相关的引用都必 需重定位以保证映射正确。因为连接程序知道所有的重定位信息,所以能确定所有 的引用位置,从而能够有效地找到并回补这些引用。连接程序最终将生成一个可执行文件。可执行文件的格式基本与目标文件相 似,唯一差别在于它所包含的所有引用和重定位信息都已经合法化了。A 4装载没有错误的情况下,连结之后得到的程序就可以运行了。在运行前,可执行程 序是以文件形式存储在诸如磁盘的二级存储设备中。在Uiix操作系统上,操作系统 内核将程序装入内存后就可以运行。在开始执行之前,操作系统执行下列步骤:1、读取可执行文件头,得到正文段和数据段的大小;2为该程序创建新的地址空间。这个地址空间要足够大,用于装入正文和数据段内容,同时还需要建立一个堆栈段(详见本章A 5节)。入 将指令和数据从可执行文件拷贝到新创建的地址空间中。4 将参量搭贝到堆栈段。工 初始化机器的寄存器。通常情况下,大多数寄存器是清空的,但堆栈段指针必须指向堆栈段的栈顶地址。&转移到起始程序段。起始程序段将程序在堆栈中的内容拷贝到寄存器中去,并且调用其中的main例程。如果main例程运行结束,则该 程序还要调用系统退出命令,结束整个程序执行。A 5内存的使用接下来的内容将着重描述本书先前提到的MIPS计算机的系统结构。先前的章 节主要强调了硬件及相关软件之间的关系。而下面的章节则主要描述汇编语言程序 如何利用MIPS硬件;同时,还将提到基于MIPS系统的一些协议说明。大多数情况 下,硬件设备并不会影响这些协议;相反,通过共通遵守协议的规定,可以保证不 同的编程人员所书写程序的一致性,从而充分利用MIPS硬件设备。基于MIPS处理器的系统通常将内存分为三部分(见图A0 o我们将第一部 分,即图中起始于地址40000CU的内存空间称作正文段,用于存储程序指令。第二部分是数据段,位于正文段上面,它又可以分成两部分。静态数据(起始 地址为ioooooaex)的大小如同编译程序,其内容在整个程序执行期间有效。例如 在C语言中,全局变量是静态分配的,所以能在程序执行的任意时刻都被引用。连 接程序不仅将静态目标分配到数据段中,同时也解决了各个子程序之间的引用关 系。图A 9内存分布图静态数据段之上就是动态数据部分。顾名思义,动态数据是在程序的执行期间 动态分配的。在C程序中,库函数malloc就是用于申请新的内存块。因为编译程 序无法准确预测程序所需要的内存大小,运行时刻操作系统会扩大动态数据区域以 满足程序执行的需要。如上图向上箭头所示,malloc通过sbrk系统调用函数实现 动态扩展,而sbrk函数的作用(第7章第3节提及)则是在动态数据段之上增加 程序的虚拟地址空间。第三部分是程序的堆栈段。它驻留在虚拟地址空间(起始地址为7ffffffhex)的最上端。堆栈段类似动态数据段,程序所需要的最大堆栈空间也是无法预知的。当程序向堆栈段压入变量时,操作系统会自动向下(数据段方向)扩展堆栈段。内存空间划分的方法多种多样。但两个重要特征是不变的:两个动态可扩展的 段尽可能分开,而且能够扩展以便使用整个程序地址空间。软硬件接口由于数据段的起始地址远远高于程序的起始地址(1000000CU),装载和存储 指令无法直接通过16位偏移地址实现(请参考第3章第4节)。例如,为将数据 段1000800CU的内容装入寄存器v0中,则需要两条指令来实现:lui s0,0 x1000#0 x1000 means 1000 base 16 or4096 base 10Iw v0,ox8000#0 x10000000+0 x8000=0 x10008000其中,数字前面的Ox表示这是十六进制数。例如,ox8000表示800CU或32,768teno为了在装载和存储中避免重复使用lui这条指令,MIPS系统就将寄存器(gp)看作指向静态数据段的全局指针。该寄存器的内容是地址1000800CU故 而在装载和存储指令时能使用16位偏移区域,从而实现对静态数据段的前64KB的 存取。利用全局指针,我们能用一条指令实现上面所提到的两条指令:lw v0,0(Xgp)当然,全局指针寄存器对ioooooo(nooioocu地址空间的定位远远要比定位到 其他地址段快。MIPS编译程序通常在该区域存放全局变量,所以这些变量的定位 和匹配会比其他区域的全局数据来的好。A 6过程调用协议当程序中的各个子程序被分别编译时,关键是如何规定寄存器的使用协议。当编 译某个子程序时,编译程序必须事先知道需用到哪些寄存器,哪些寄存器的内容需要 保留等信息。我们称这些寄存器的使用规则为寄存器的使用或过程调用协议。顾名思 义,大多数情况下,这些规则主要用于约束软件,而不会受到硬件的限制。因此,大 多数的编译程序和程序员都必须遵守这些协议以免发生错误。本节描述的是gcc编译程序用到的协议。而MIPS编译程序采用更复杂的协 议,速度也有所提高。在MIPS系统中,CEU中有32个通用寄存器,它们的标号分别位 A3L寄存器 0的值通常就是ft 寄存器at(l),kOQO,kl Q刀是为汇编程序,操作系统所保留的,通常 不被源程序和编译程序占用o 寄存器53(4一刀用于将初始四个参数送到子过程中(保留的参数则送 到堆栈中)。寄存器vQvl则用于调用函数的返回值。寄存器tOWt9(A15,24,25)是一种数值调用存储寄存器,它用于存放子 程序中的临时变量,这些数值就不被其他过程调用。寄存器sOWs7(123)是用于存放能被其他过程调用的调用寄存器,它保 留的数值则长期存在。寄存器gp Q8是指向静态数据段的64KB区域的全局指针。寄存器spQ9)则是堆栈指针,它指向堆栈中的空地址。寄存器fp(3Q)是 帧指针。jal指令写寄存器ra(31),内容为来自子过程调用的返回地址。这两个 寄存器将在下一节中作详细介绍。上述用两个字母所表达的寄存器(例如sp表示stack pointer)反映了寄存 器在过程调用协议中所起的作用。在描述这些协议时,我们姑且用寄存器的名字来 代替它们的序号。图10列出了这些寄存器和它们的用途。图A 10 MPS系统下的寄存器和用途协议。过程调用下面将说明一个程序(调用者)调用另一个子程序(被调用者)的具体步骤。由于有编译程序的具体作用,程序员用C或者Pascal等高级语言编写程序时,并 不需了解子程序被调用的具体细节。但用汇编语言编写时需要明确说明每一次的调 用和返回。大多数有关过程调用的记录集中在一个叫做过程调用块的内存块中。这块内存 区域有以下几个目的:以参数的形式保存调用值。保留调用过程不希望被被调用过程修改的寄存器的值。为程序的变量提供足够的内存空间。大多数编程语言中,程序的调用和返回严格遵从后进先生(LIF3原则,因 此,这块内存可以在堆栈段实现分配和释放,这也是这些内存块有时被称作堆栈帧 的缘故。图A 11显示了一个典型的堆栈帧。帧的内存区域包括帧指针fp(指向帧的首 字)和堆栈指针sp(指向帧尾)之间的范围。由于堆栈的存储遵从从高到低的存 放顺序,所以帧指针总是高于堆栈指针。运行的程序利用帧指针能快速存取堆栈帧 中的内容。例如,堆栈帧中的参数能够用下面指令送入寄存器到我lw vO,0(Vfp)堆栈帧能用多种途径实现;但无论如何,调用过程和被调用过程必须遵从下列 步骤。下列描述的步骤是用于大多数MIPS系统上的调用协议。在过程调用中,该 协议在三个时刻发挥作用:在调用过程调用被调用过程之前的瞬间,在被调用过程 开始执行的时刻,以及在被调用过程返回到调用过程前的瞬间。首先,调用过程将 过程调用参数放到合适的位置,然后按下列步骤调用目的过程:1、传递参数。根据协议,前四个参数将被送到寄存器 中,其他参数压到堆栈中,并送到调用过程的堆栈帧的起始地址。么 保存调用程序的寄存器的值。被调用过程可以随意使用寄存器(a3,tB9),而无需先保存它们的值。如果调用程序在调用之 后还想使用这些寄存器,那么它必须在调用之前保存寄存器的值。3,执行jal指令(参见第3章第6节)。该指令把控制转移到被 调用过程的首指令,并且在寄存器出中保存返回地址。在被调用子程序运行之前,该程序必须执行以下步骤,以便设置它的堆栈 帧。1、申请栈帧所占空间,其区域为从栈指针减去栈帧大小的范围。2 把被调用者所用寄存器保存到栈中,被调用者在改变其值之前必须保存s7,fp,以及ra的值。而每个过程中如果需要创建新的栈帧 则必须保存fp而ra则仅在被调用者再调用时才保存。此外其它被调用 者专用的寄存器也要保存。人 设置栈帧指针,其值为栈帧大小减去4加上sp,和保存到fp中。图A 11堆栈帧的示意图。帧指针(懒 指向当前运行程序的堆栈帧的首 字。堆栈指针(sp)则指向该帧的尾字。前四个参数将送到寄存器,而第5 个参数就作为堆栈的首字。硬软件接口MIPS系统使用特定规范提供调用寄存器和被调用保留寄存器,因为这两种 寄存器在不同的情况下各有优点。“保留被调用过程”寄存器比较适合于保存 长期数值,譬如用户程序的变量等等。当被调用过程希望使用寄存器时,这些 寄存器才被保存起来。反之,“保留调用过程”寄存器主要用于保存短期数 值,这些数值无需在其他过程中使用,譬如计算地址使用到的数值等。在子程 序调用过程中,被调用过程同样可以使用存放临时数值的寄存器。最后被调用者执行如下步骤,将控制返回给调用者:1、如果被调用者为具有返回值的函数,则将返回值保存到YvO中。么 恢复被调用者进入时保存的所有寄存器。人 Msp中减去栈帧大小,将帧从栈中弹由o4 跳转到ra中记录的指定地址处。细节:所谓的过程递归就是指某个过程能直接或间接的调用自己。在编程语言 中,如果不支持过程递归,那么就无需在堆栈中分配帧。在非过程递归语言中,因 为在任何时刻只允许一个过程调用处于活动状态,所以每个过程的帧可以静态分 配。由于静态分配的帧在一些旧系统上产生运行速度更快的代码,故而在一些旧版 的Fort ran语言中禁止过程递归。然而,在诸如MIPS系统上的装载盖储结构中,堆栈帧的速度也可以很快。其原因是帧指针寄存器直接指向活动堆栈帧,这就使单 条的装载或存储指令能直接存取帧中的数值。另外,递归技术还是一种很有用的编 程技巧。过程调用举例:参看下面用C语言编写的程序main Iprintf t the factorial of 10 is fact(IQ);int fact(int ri)i f*1)return(1);elsereturn(nfact 1);该程序将计算并打印10!(10!=10*9*8%.*1)o过
展开阅读全文