理论教育 编译器设计之路:过程与函数声明

编译器设计之路:过程与函数声明

时间:2023-11-04 理论教育 版权反馈
【摘要】:过程、函数也是程序设计语言最基本的语法元素之一。例如,在Pascal中,过程体内严格禁止存在对RESULT的赋值,而函数体内却必须存在RESULT的赋值。因此,在后续章节中,在没有明确说明的情况下,过程与函数是不作区分的。这里,笔者从三个方面对Pascal的过程、函数声明作一个简单的回顾,以便进一步讨论相关语义子程序的实现。除了参数、返回值类型等信息外,实际上,过程、函数声明中还可能包含一些其他辅助信息,这些信息大多与外部函数相关。

编译器设计之路:过程与函数声明

过程、函数也是程序设计语言最基本的语法元素之一。而Pascal中的过程就是指无返回值的函数。由于Pascal没有类似于C语言的void类型,因此,Wirth教授提出了过程的概念以区别于普通的函数。实际上,从严格意义上来说,过程与函数还是存在一定差别的。例如,在Pascal中,过程体内严格禁止存在对RESULT的赋值,而函数体内却必须存在RESULT的赋值。这些语义在C语言中并不是特别强调的,C语言对于return语句的限制比较灵活。即使如此,很多人仍然质疑Pascal过程的作用,认为它的存在并没有太大的意义。当然,笔者也并不否定这个观点。确实,拥有类似于C语言的函数机制已经足以满足相关应用的需求了,过程作为一种特殊函数的存在价值并不大。不过,从设计编译器的角度而言,笔者非常尊重Wirth教授的设计思想,同时为了尽可能兼容标准Pascal,故仍保留过程机制。需要指出的是,在计算机科学中,过程与函数的概念并没有非常明确的界限。许多著作经常使用“过程”泛指一切子程序。因此,在后续章节中,在没有明确说明的情况下,过程与函数是不作区分的。

这里,笔者从三个方面对Pascal的过程、函数声明作一个简单的回顾,以便进一步讨论相关语义子程序的实现。

(1)参数传递方式。习惯上,参数传递方式可以分为三种:值传递、址传递、引用传递。其中引用传递可以视为址传递的一种特殊实现,因此,可以认为其与址传递是一脉相承的。读者最熟知的“引用传递”应该就是由C++实现的,在C++中,引用传递与传统的址传递(在C++中可以通过传递指针实现)的形式还是存在一定差异的。使用址传递方式时,由于该参数是一个指针,程序员在使用该参数时,必须通过指针运算才能获取实际所需参数值。而使用引用传递方式时,程序员不必考虑许多指针运算方面的问题,可以像使用实参一样,直接访问形参,而不必在意其指针的本质。对于程序员而言,引用传递就像是直接将实参的名字传递给了形参,可以像引用实参一样引用形参,所以引用传递亦称为名字传递。Pascal、C++、Java都支持两种参数传递方式,即值传递、引用传递。而C语言仅支持值传递。最后,笔者需指出一点,在程序设计语言领域,关于引用传递方式实际上是存在一定争议的。支持者固然是大多数,但反对者的理由也并非无中生有,他们认为引用传递可能会使程序员忽略调用函数可能产生的副作用。C#语言中加入了out方式的参数。除了一些很细小的差别之外,out方式的内核与引用方式是基本类似的。

(2)扩展信息描述。除了参数、返回值类型等信息外,实际上,过程、函数声明中还可能包含一些其他辅助信息,这些信息大多与外部函数相关。事实上,除了用户自定义过程、函数之外,还有两类函数可供用户调用,即库函数、系统函数。库函数既可以是编译器提供的预定义库函数,也可以是用户编写生成的自定义库函数。而系统函数主要指的是操作系统提供的一些系统调用或系统接口函数。库函数和系统函数都是以二进制形式存在的,它们并不需要重新编译生成。由于这些函数都是独立于本项目而存在的,因此通常将其称为外部函数。注意,关于内部、外部函数的分类标准并不统一,这里的说法仅限于本书。当然,使用外部函数的优势非常明显,本书不可能一一列举,只能简单提几点。例如,抽象与封装、功能扩展、实现跨语言共享等。而这里提到的扩展信息主要是指那些与外部函数相关的信息,例如,外部函数的来源、调用方式等信息。下面,就针对这些信息作简单介绍。

外部函数的来源一般就是指外部函数所属的库文件名及路径等信息。外部函数是以库文件形式组织的,一个库文件通常可以包含多个外部函数,例如,读者熟知的DLL文件、Lib文件皆是如此。从理论来说,用户在本项目中调用某一外部函数时,必须告知编译器所调用的外部函数所属库文件的准确路径,以便编译器生成正确的目标程序。不过,这不是绝对的。在实际情况下,来源仅仅指的就是库文件名字而已,而库文件路径信息一般是预定义的,由编译器或操作系统自动检索识别。例如,在Windows中,DLL文件的路径就是由操作系统自动检索的,Windows定义了一套明确的检索顺序。虽然来源信息并不复杂,却是非常重要的,不正确的来源信息会导致编译链接出错,甚至目标程序异常。

外部函数的调用方式主要就是讨论一个话题,即传参约定。对于支持系统栈的目标机器来说,通常都是借助于系统栈传参的,调用点通常将参数逐一压入系统栈,被调函数从系统栈内获取参数信息。传参约定的目的就是便于访问与实现外部函数,为外部函数提供一个统一调用接口是非常必要的。实际上,函数调用是一次协作的过程,调用点及被调函数双方必须遵守同样的传参约定才能保证被调函数正确获取实参。对于调用本项目的函数、过程,由于调用点与被调函数都是由同一个编译器生成目标代码的,编译器设计者完全可以定义一种内部的传参约定供双方遵守,唯一的目标只要保证正确传参即可。不过,当被调函数是外部函数时,编译器就无法作类似的内部约定了,因为外部函数已经是以二进制代码形式存在了,不可能对其作任何改变。再者,由于编写外部函数的源语言、所使用的编译器都是未知的,如果传参约定完全是由语言及编译器设计者自定义的,那么,其随意性将导致外部函数失去存在的价值。当然,计算机科学家并没有否定各种新颖的内部传参约定的存在意义,也不限制其使用范围。在这种情况下,早期的一些语言及编译器设计者仅对外部函数的传参方式作了一些约定,规定任何编译器生成外部函数、调用外部函数相关目标代码时,必须遵守相关的约定,否则无法保证调用的正确性。但这些约定并不影响各种内部函数的传参方式。例如,读者熟悉的stdcall、cdecl、fastcall就是最常见的传参约定。下面,笔者就来谈谈传参方式约定的相关组成。(www.daowen.com)

(1)实参压栈的顺序。实参压栈的顺序直接关系到被调函数访问的正确性。栈式结构的特点就是先进后出,如果调用点以顺序方式将实参压栈,则被调函数将以逆序方式访问参数。为了便于被调函数顺序访问实参,通常调用点是以逆序方式压栈的。逆序压栈得到了绝大多数编译器的共识,几乎不存在任何分歧。

(2)栈复位点。试图借助于系统栈传参,就必须保证函数调用前后系统栈的情景一致,否则程序可能产生异常。这里,有个关键问题需说明:栈式传参的过程与普通的栈应用可能存在一定的差别。虽然传参方式的通常理解是一个实参赋值给形参的过程,但事实却并非如此。根据栈式传参方式的观点,调用点将实参压栈后,被调函数直接通过计算栈基址偏移访问参数,并不需要将参数出栈,或作其他任何操作。因此,从理论上而言,直至被调函数返回并不会改变系统栈的状况,也就是说,调用函数时压入的参数仍存在于系统栈内。如此便使得函数调用前后的系统栈的状况发生了变化,这是谁都不愿意看到的,因为这可能导致函数返回异常。故目标程序就不得不将原来因函数调用需要而压栈的参数弹出,即栈复位。栈式传参方式需要做栈复位的工作是得到普遍共识的,并不存在任何异议。关键问题是由谁来完成栈复位工作。实践证明,调用点及被调函数都是可以完成这一工作的,某些观点认为应由前者完成,而另一些则支持由后者完成。例如,WINAPI的标准调用方式stdcall规定栈复位由被调函数完成。而C语言最常用的调用方式cdecl则规定栈复位由调用点完成。关于这个问题,笔者觉得并不存在本质区别,只是实现方式不同而己。

(3)各种类型数据以何种形式压栈。高级语言与汇编语言的一个重要区别就是丰富的数据类型,如数组、结构、字符串等。程序是将这类变量的首地址压栈,还是将整个存储块压栈,这是一个值得商榷的问题。

至此,简单讨论了函数传参问题,旨在说明外部函数调用方式的特点,关于更多的传参话题将在后续章节中详述。关于函数、过程的符号表处理的问题,与变量、常量类似,这里就不再详细讨论了。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈