《我的 C 语言学习生涯记》之后记1

后记 1:

其实,有关 C 语言的点点滴滴,有好多记忆,还没讲完。于是,我借助那些零星的回忆,写点后记。

一则是有关“大型机命令语法”和“C 语言命令语法”的。其实也不能算是语法。只是当初这样听说而已。当初讲的是 DOS 里的 COPY 命令使用的格式是 COPY <源> <目标>,即先写源后写目标,是参照当时大型机的实现。而 C 语言中的一些函数,以 strcpy 为例,使用的是 strcpy(目标, 源); 这样的格式。有点像 Intel x86 汇编里面的 MOV 指令——它也采用的是“目标,源”的顺序。这在我的编程里面造成的影响就是,我经常也把目标放在左边,把源放在右边写。

二则是有关 C 语言与 Pascal 的。我一开始学计算机时,学的是 BASIC 语言。这倒也不是我自己的选择,而是环境所造成:我爸有一本 BASIC 书,一本讲述早在一九八几年的时候苹果 II 型机的书。而育才中学的电脑兴趣小组上通常也教的是 BASIC。有趣的是,初中大纲的教科书反而只教 CCDOS、WPS 和 LOGO,并不教 BASIC。在我看来,CCDOS 和 WPS 固然重要,但它们是让电脑来做无纸化中文办公用的软件,并不能用来编程(后来知道其中有个 TX.EXE 是可以当特殊显示命令库来用的)。而 LOGO 语言虽然在画图方面有其天赋,但是在编程方面功能却不强大。所以还是选择从 BASIC 入门。

但是毕竟 BASIC 这种语言有其天生的许多限制。首先传统 BASIC 不是结构化的,如果要使用栈式函数调用,常常会让程序变得很难看。其次,即使是结构化的 QBASIC,也缺乏三种非常重要的元素:一是指针,二是动态内存分配函数(唯一有的是 REDIM 命令,用于扩展数组大小,但这和 malloc 的灵活性差距太大),三是函数指针。这使得这种语言必然成为 Dijkstra 所批评的那样,不适合真正的程序员了。所幸 2001 年出现的 Visual Basic .NET 这一变种已完全克服这些限制,已成为 C# 的兄弟语言。但那已经太晚了。

而高中时候有同学参加计算机竞赛,用的都是 Pascal。Pascal 这种语言以严谨著称。它是强类型的,有指针功能,从功能上来说,和 C 语言处于同一级别。但是由于其严谨性,故而用于高中计算机竞赛的教学。Pascal 一开始最流行的是 Borland Turbo Pascal,简称 TP。后来 Borland 公司又推出了带有 Object Pascal 语言的 Turbo Pascal。Object Pascal 实现了面向对象编程,成了后来的大名鼎鼎的 RAD(Rapid Application Development,快速应用程序开发)工具 Delphi 的前身。

说实话直到 Delphi 都十分流行了,我都还不会 C 语言,也根本没学过 Pascal。因此 Pascal 对我没有产生什么大的影响。有趣的是,在学习 Windows API 编程的时候,遇见了熟悉的 PASCAL 字样。怎么回事呢?是有关 calling convention(调用协定)的。Win16 的编程在文档中指出,所有的回调函数都要显式标记为 PASCAL。以 Window Procedure(窗口过程)为例:long FAR PASCAL WndProc(HWND hWnd, WORD message, WORD wParam, LONG lParam);。

其实 calling convention 很好理解,它就是在 C 语言函数在转换为汇编代码以后,如何调用此函数的协定。不同的调用协定之间的区别主要是在传参数的方法上。对于常见的几种调用协定:__cdecl(C declaration——C 语言声明)、__stdcall(标准调用)、__pascal(PASCAL 语言)、__fastcall、__thiscall 在 x86 架构上的大致定义如下:__cdecl 是 C 语言默认的调用协定,函数参数(不包括返回值)通过压栈方式传递。压栈顺序是从右到左(举例来说 func(a, b) 的参数压栈顺序是先 b 后 a)。调用者负责在调用返回后清栈(实际不需要清理,只需要把栈顶指针 ESP 设回调用前的值)。参见 http://msdn.microsoft.com/en-US/library/zkwh89ks(v=VS.80).aspx。

__stdcall 的函数参数也通过压栈方式传递。压栈顺序从右到左。被调用的函数负责清栈。这样做的好处是编译出来的程序比较小,因为调用者清栈的时候每个函数调用的地方都需要有清栈指令,而被调用者清栈的话,只需要在被调用者里出现一次清栈指令即可。据说微软使用这种方法,外加使用了编译器的“最小代码”优化选项,便使得发布 Windows 3.1 所需要的软盘数量减到最少。

__pascal 的函数参数通过压栈方式传递。压栈顺序从左到右(有趣的是,感觉非常“标准”,但却与 __stdcall 不同)。调用者负责清栈。它与 __cdecl 的类似之处在于它也是调用者负责清栈。调用者负责清栈的一个功能上的用处是允许定义像 printf 这样的参数个数不定的函数。如果参数个数不定,那么被调用者在编译期就无法得知有多少字节的参数,因此无法完成清栈的任务。只能由调用者来清栈。

__fastcall 的头两个参数通过 ECX 和 EDX 两个寄存器传递(会过滤出 4 字节或更小的参数;占用空间更大的参数则不算)。其他的参数从右到左压栈。被调用的函数负责清栈。这种调用协定的好处是运行速度快,因为省去了头两个参数的入栈和出栈的时间。

__thiscall 也许是微软定义的一种形式。它和 __stdcall 基本类似,有一点小区别,是因为它用于 C++ 中对象的成员函数调用。它会用 ECX 寄存器来保存 this 指针的值,而不通过栈来传递。

那么返回值如何传递呢?似乎没有明显的定义。使用 __cdecl 的情况下,我用 BCC 5.5 观察下来的结果是,如果返回值占用的内存空间比较小,小于等于 4 个字节的话,就通过 EAX 这个寄存器传递。如果返回值占用的内存空间比较大,大于 4 个字节的话,会把它的地址在参数压栈之前先压栈,然后让被调用的函数像操作指针指向的对象那样对返回值进行值的复制。

还有不同调用协定下函数名修饰的问题,可以参考此文:http://blog.csdn.net/jia_xiaoxin/article/details/2868216。

在 x64 上这些调用协定都不再用了,而用一种新的调用协定,整合了 __fastcall 的速度和 __cdecl 的灵活性。参数传递先通过 rcx、rdx、r8、r9 这些整数寄存器,以及 XMM0 到 XMM3 这些浮点数寄存器,然后再用到栈,从右到左入栈。清栈由调用者负责,但是通常在函数退出前并不清栈,就让栈上空间保持被占用的状态,一次一次地调用其他函数。具体情况参见:http://msdn.microsoft.com/en-us/magazine/cc300794.aspx。

不知道是不是老谭老师的书的缘故,有一次有同学问起类似以下的问题:printf("%d, %d\n", i++, i++); 这样的一句语句中,到底会输出什么。这里面的两个 i++ 哪个先计算其实是 C 语言标准中未定义的,因此实际的输出取决于编译器——编译器生成了汇编代码后,计算的顺序就确定了,输出就确定了;不同的编译器可能生成不同的汇编代码。虽然上述 printf 语句中的两次 i++ 的顺序是未定义的,但是有趣的是,如果用 Borland C++ 5.5 来编译下述代码,会有两种不同的顺序:

#include <stdio.h>

static void __pascal test2(int a, int b);

int main(void)
{
    int i = 0;

    printf("%d, %d\n", i++, i++);   /* 情况 1 */
    test2(i++, i++);                /* 情况 2 */
    return 0;
}

static void __pascal test2(int a, int b)
{
    printf("%d, %d\n", a, b);
}


以上代码中,情况 1 处将显示“1, 0”,而情况 2 处将显示“2, 3”。通过前面讲过的 calling convention 知识,你可以猜到这是为什么了吧。编译器为了“偷懒”,每做一次计算就把参数压一次栈,然后由于 __cdecl(printf 的调用协定)和 __pascal(test2 的调用协定)的压栈顺序不同,就出现了上述结果。汇编代码如下:

; 调用 printf
    mov       edx,dword ptr [ebp-4]
    inc       dword ptr [ebp-4]
    push      edx
    mov       ecx,dword ptr [ebp-4]
    inc       dword ptr [ebp-4]
    push      ecx
    push      offset s@
    call      _printf
; 调用 test2
    mov       edx,dword ptr [ebp-4]
    inc       dword ptr [ebp-4]
    push      edx
    mov       ecx,dword ptr [ebp-4]
    inc       dword ptr [ebp-4]
    push      ecx
    call      TEST2


这里还有一点奇怪的就是 TEST2 全部是大写。其实也不奇怪,它也是 __pascal 调用协定的规定之一:函数名称要大写。


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值