设为首页 加入收藏

TOP

C语言函数调用栈(二)(三)
2015-01-22 21:27:12 来源: 作者: 【 】 浏览:119
Tags:语言 函数 调用
题(如堆栈被破坏)。
?
? ? ?以Delphi调用C函数为例。Delphi函数缺省采用stdcall调用约定,而C函数缺省采用cdecl调用约定。一般将C函数声明为stdcall约定,如:int __stdcall add(int a, int b);
?
? ? ?在Delphi中调用该函数时也应声明为stdcall约定:
?
1 function add(a: Integer; b: Integer): Integer; stdcall; //参数类型应与DLL中的函数或过程参数类型一致,且引用时使用stdcall参数
2 external 'a.dll'; //指定被调DLL文件的路径和名称
? ? ?不同编译器产生栈帧的方式不尽相同,主调函数不一定能正常完成清栈工作;而被调函数必然能自己完成正常清栈,因此,在跨(开发)平台调用中,通常使用stdcall调用约定(不少WinApi均采用该约定)。
?
? ? ?此外,主调函数和被调函数所在模块采用相同的调用约定,但分别使用C++和C语法编译时,会出现链接错误(报告被调函数未定义)。这是因为两种语言的函数名字修饰规则不同,解决方式是使用extern "C"告知主调函数所在模块:被调函数是C语言编译的。采用C语言编译的库应考虑到使用该库的程序可能是C++程序(使用C++编译器),通常应这样声明头文件:
?
?View Code
? ? ?这样C++编译器就会按照C语言修饰策略链接Func函数名,而不会出现找不到函数的链接错误。
?
?
?
5.3 x86函数参数传递方法
? ? ?x86处理器ABI规范中规定,所有传递给被调函数的参数都通过堆栈来完成,其压栈顺序是以函数参数从右到左的顺序。当向被调函数传递参数时,所有参数最后形成一个数组。由于采用从右到左的压栈顺序,数组中参数的顺序(下标0~N-1)与函数参数声明顺序(Para1~N)一致。因此,在函数中若知道第一个参数地址和各参数占用字节数,就可通过访问数组的方式去访问每个参数。
?
5.3.1 整型和指针参数的传递
? ? ?整型参数与指针参数的传递方式相同,因为在32位x86处理器上整型与指针大小相同(均为四字节)。下表给出这两种类型的参数在栈帧中的位置关系。注意,该表基于tail函数的栈帧。
?
调用语句
?
参数
?
栈帧地址
?
tail(1, 2, 3, (void *)0);
?
1
?
8(%ebp)
?
2
?
12(%ebp)
?
3
?
16(%ebp)
?
(void *)0
?
20(%ebp)
?
5.3.2 浮点参数的传递
? ? ?浮点参数的传递与整型类似,区别在于参数大小。x86处理器中浮点类型占8个字节,因此在栈中也需要占用8个字节。下表给出浮点参数在栈帧中的位置关系。图中,调用tail函数的第一个和第三个参数均为浮点类型,因此需各占用8个字节,三个参数共占用20个字节。表中word类型的大小是4字节。
?
调用语句
?
参数
?
栈帧地址
?
tail(1.414, 2, 3.998e10);
?
word 0: 1.414
?
8(%ebp)
?
word 1: 1.414
?
12(%ebp)
?
2
?
16(%ebp)
?
word 0: 3.998e10
?
20(%ebp)
?
word 1: 3.998e10
?
24(%ebp)
?
5.3.3 结构体和联合体参数的传递
? ? ?结构体和联合体参数的传递与整型、浮点参数类似,只是其占用字节大小视数据结构的定义不同而异。x86处理器上栈宽是4字节,故结构体在栈上所占用的字节数为4的倍数。编译器会对结构体进行适当的填充以使得结构体大小满足4字节对齐的要求。
?
? ? ?对于一些RISC处理器(如PowerPC),其参数传递并不是全部通过栈来实现。PowerPC处理器寄存器中,R3~R10共8个寄存器用于传递整型或指针参数,F1~F8共8个寄存器用于传递浮点参数。当所需传递的参数少于8个时,不需要用到栈。结构体和long double参数的传递通过指针来完成,这与x86处理器完全不同。PowerPC的ABI规范中规定,结构体的传递采用指针方式,而不是像x86处理器那样将结构从一个函数栈帧中拷贝到另一个函数栈帧中,显然x86处理器的方式更低效。可见,PowerPC程序中,函数参数采用指向结构体的指针(而非结构体)并不能提高效率,不过通常这是良好的编程习惯。
?
?
?
5.4 x86函数返回值传递方法
? ? ?函数返回值可通过寄存器传递。当被调用函数需要返回结果给调用函数时:
?
? ? ?1) 若返回值不超过4字节(如int、short、char、指针等类型),通常将其保存在EAX寄存器中,调用方通过读取EAX获取返回值。
?
? ? ?2) 若返回值大于4字节而小于8字节(如long long或_int64类型),则通过EAX+EDX寄存器联合返回,其中EDX保存返回值高4字节,EAX保存返回值低4字节。
?
? ? ?3) 若返回值为浮点类型(如float和double),则通过专用的协处理器浮点数寄存器栈的栈顶返回。
?
? ? ?4) 若返回值为结构体或联合体,则主调函数向被调函数传递一个额外参数,该参数指向将要保存返回值的地址。即函数调用foo(p1, p2)被转化为foo(&p0, p1, p2),以引用型参数形式传回返回值。具体步骤可能为:a.主调函数将显式的实参逆序入栈;b.将接收返回值的结构体变量地址作为隐藏参数入栈(若未定义该接收变量,则在栈上额外开辟空间作为接收返回值的临时变量);c. 被调函数将待返回数据拷贝到隐藏参数所指向的内存地址,并将该地址存入%eax寄存器。因此,在被调函数中完成返回值的赋值工作。
?
? ? ?注意,函数如何传递结构体或联合体返回值依赖于具体实现。不同编译器、平台、调用约定甚至编译参数下可能采用不同的实现方法。如VC6编译器对于不超过8字节的小结构体,会通过EAX+EDX寄存器返回。而对于超过8字节的大结构体,主调函数在栈上分配用于接收返回值的临时结构体,并将地址通过栈传递给被调函数;被调函数根据返回值地址设置返回值(拷贝操作);调用返回后主调函数根据需要,再将返回值赋值给需要的临时变量(二次拷贝)。实际使用中为提高效率,通常将结构体指针作为实参传递给被调函数以接收返回值。
?
? ? ?5) 不要返回指向栈内存的指针,如返回被调函数内局部变量地址(包括局部数组名)。因为函数返回后,其栈帧空间被“释放”,原栈帧内分配的局部变量空间的内容是不稳定和不被保证的。
?
? ? ?函数返回值通过寄存器传递,无需空间分配等操作,故返回值的代价很低。基于此原因,C89规范中约定,不写明返回值类型的函数,返回值类型默认为int。但这会带来类型安全隐患,如函数定义时返回值为浮点数,而函数未声明或声明时未指明返回值类型,则
首页 上一页 1 2 3 4 下一页 尾页 3/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇C语言编程积累2 下一篇C 语言的若干问题(持续更新中)

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: