0xAA55 发表于 2014-8-6 09:54:08

【编译原理】调用约定及其意义

在很久很久以前,编程语言还没有诞生的时候,程序员们都是靠自己写1和0进行编程。
大概,就是在一个纸带上打孔,然后把这个纸带录入到计算机,计算机便开始运行了。相当原始。这种时期被我们称作“石器时代”。。

当蛋疼的程序员们觉得受够了这种只能用1和0说话的日子以后,有些程序员开始想法子改变生活。于是汇编助记符诞生了。这就是现在所说的汇编语言。所谓汇编助记符,就是
这些mov、add、sub、mul、div、cmp、call、ret、jmp、push、pop等符号都比10111000、00000101、00101101、11110111等直观多了。至少你看这些英文缩写的时候你知道它是在做什么:mov移动,add加法,sub减法,mul乘法,div除法,cmp比较,call调用,ret返回,jmp跳转、push压入栈、pop弹出栈等等。(所谓的“栈”,就是长得像枪的弹夹一样的内存块,你往里面先塞入的东西后出来,后塞入的东西先出来,就像手枪弹夹一样最先按进去的子弹最后打出来。)

当时的程序出现了“子程序”的概念:我需要重复使用的功能,我不需要重复编写,我只需要写一次,然后当我需要使用这个功能的时候,我调用它就行了。这是一个很大的进步。不过当时的子程序也很简单,大概就是从内存或者寄存器读取参数,没有什么调用约定这种说法。典型的就是ROM-BIOS提供的各种功能,这些程序都是靠AX、CX、DX、BX传递参数,然后通过软中断来调用。

之后出现了高级语言的概念。什么C语言、PASCAL、BASIC等一个接一个地诞生了。这些高级语言是如何变成机器可以直接运行的指令呢?首先程序员们需要将一个一个独立的源代码文件编译为中间文件,然后再用链接器把这些所有的中间文件链接起来,得到可以运行的文件。大家可能会问,为毛这么麻烦,我直接编译为可执行文件不就行了?虽然直接编译成可执行文件听起来好像简单又直观,但是这样就无法做到多种语言编写一个程序了。比如我用C语言实现了一个声卡驱动,但是我想用BASIC写一个播放器,我总不至于要完全用BASIC重新写个声卡驱动然后再写这个播放器吧?当时的程序员也意识到了这个问题。于是从那时起,程序的编译流程就是先编译各自的源代码为中间文件,然后再用链接器把中间文件组装起来得到可执行文件。这些中间文件记录了这个程序中可供其他中间文件使用的符号,以及需要从其他文件使用的符号。链接器的工作,就是根据每个中间文件互相之间的需求,把它们链接起来。

这些高级语言各自都有不同的风格,各自有各自的运行方式,如何才能保证不同的中间文件之间互相引用不会出错呢?这里就要说到调用约定了。

假设我的子程序A是用C语言写的,而且声明是_cdecl调用约定,那么当我要用别的语言的源码访问它的时候,我这个“别的语言”在调用它的时候就必须遵循_cdecl的调用约定。同样PASCAL有它自己的调用约定,因此当你用C语言调用PASCAL的子程序的时候,你需要先声明这是PASCAL的程序(pascal关键字。现在已经被淘汰)。为了保证不同语言的OBJ链接到一起的时候不会冲突,不同的编译器都会对自己导出的符号加上装饰。典型的例子就是在x86平台下VC编译器会把OBJ导出的符号前面加个下划线“_”,而对于_stdcall调用风格的函数的符号则是前面加下划线“_”并在后面加“@”,然后再加上参数所占的字节数,举个例子“int _stdcall foo(int bar,int foobar);”的参数有8个字节,那么它的符号是“_foo@8”。而_fastcall风格除了前面加的不是下划线而是“@”以外,其它行为和_stdcall一致(“int _fastcall foo(int bar,int foobar);”的符号是“@foo@8”)。C++的编译器会产生很复杂的符号,这些符号甚至包含了函数参数的详细的信息等。那么C语言和C++如何进行混合编程呢?用关键字extern"C"。在C++里用extern"C"意味着这个符号是C语言的符号。比如用VC++6写C++程序,“extern"C" int foo();”的符号是“_foo”,而不是“?foo@@YAXXZ”。而C++产生那么多奇怪的字母和标点,是为了实现函数名重载。
页: [1]
查看完整版本: 【编译原理】调用约定及其意义