社区
C语言
帖子详情
函数参数传递~~~~~~~
wq06100610
2004-04-08 01:50:05
请问~~在C/C++中,函数参数的传递到底是什么原理??比如入战出战,参数放在什么区域~能详细点举个例子吗???
...全文
406
7
打赏
收藏
函数参数传递~~~~~~~
请问~~在C/C++中,函数参数的传递到底是什么原理??比如入战出战,参数放在什么区域~能详细点举个例子吗???
复制链接
扫一扫
分享
转发到动态
举报
写回复
配置赞助广告
用AI写文章
7 条
回复
切换为时间正序
请发表友善的回复…
发表回复
打赏红包
vconan
2004-04-08
打赏
举报
回复
呵呵,看起来好复杂的
wuthering
2004-04-08
打赏
举报
回复
讲得真是好啊,学习
积木
2004-04-08
打赏
举报
回复
看看这篇文章应该对你有好处……
积木
2004-04-08
打赏
举报
回复
VC++就是好,还在难懂的汇编语句前加入了C语言的源代码。不过同时也有不少我
们不需要的代码。因此,你只需要关心红色的部分就可以了。 奇怪阿?不是参数
都用push传递了吗?怎么没看到被pop出来?问题其实是这样,当你调用Call进入
函数的时候Call背着你做了一件事。call把它下一条语句的地址push进了堆栈。(
旁人: 什么!这是为什么?)原因很简单,因为函数调用完了,要用ret返回。而
ret怎么知道返回哪里呢?对了, ret指令pop了call指令push给他的地址(搞清楚这
个关系哦),然后返回到了这个地址。call和ret配合的如此绝妙,一个PUSH一个
POP肯定不会让堆栈不平衡的(老外叫no stack unwinding)。现在明白了,如果你
来个pop eax,那eax里面是什么?当然是ret要用的返回地址了。好啦,你要是
pop eax就等于抢了ret要用的东西了。不论曾程序流程和道德标准上你做的都不对
:-P。 可是怎么在函数体里使用参数呢?问题其实并不难,既然参数在堆栈里我
们就可以使用esp(堆栈指针)来访问了。不过,我相信你也想到了。esp是个经常变
化的值。一旦,函数里出现pop或push他就会变化。这样很不容易定位参数的于内
存中的位置。因此,我们需要一个不会变化的东西作为访问参数的基准。看看函数
体的开头部分:
00401000 push ebp
00401001 mov ebp,esp
先用push ebp保存了原来ebp的值再把esp的值给ebp。原来ebp就是用来做基准的。
也难怪他被称为ebp(Base Pointer)。很自然ret返回前的pop ebp就是恢复原来
ebp的数值喽。当然一定要恢复,因为函数里也可以调用函数嘛。每个函数都用
ebp,自然要保证使用完后完璧归赵了。现在当函数执行到 mov ebp, esp后堆栈应
该变成这个样子了。
/-------------------\ Higher Address
| 参数2: 0x1100h |
+-----------------+
| 参数1: 0x8899h |
+-----------------+
| 函数返回地址 |
| 0x00401087 |
+-----------------+
| ebp |
\-------------------/ Lower Address <== stack pointer
& ebp all point to here, now
由于我们在VC++上使用的int类型是一个32位类型,ebp和函数返回值也是32位的。
因此每个量要占去4个字节。另外还需要注意堆栈的扩展方向是高地址到低地址。
有了这些指示。我们就可以分析出,第一个参数的地址是ebp + 08h,第二个参数
就是ebp + 0ch。看看反汇编的代码:
2: a = 0x4455;
00401018 mov dword ptr [ebp+8],4455h
3: b = 0x6677;
0040101F mov dword ptr [ebp+0Ch],6677h
与我们的计算吻合。之后呢:
00401031 pop ebp
00401032 ret
将ebp原来的数值完璧归赵,调用ret指令,ret指令pop出返回地址,之后返回到调
用函数的call指令的下一条语句。ret之后,堆栈应该变成这个样子了
/-------------------\ Higher Address
| 参数2: 0x1100h |
+-----------------+
| 参数1: 0x8899h |
\-------------------/ Lower Address <== stack pointer
哈哈,问题出现了,再函数返回后堆栈出现了不平衡的情况(Stack Unwinding)。
怎么办呢?好办啊,直接 pop cx pop cx 把堆栈平衡过来就好了。幸好我们只有
两个参数,要是有20个的话,那就要有20个pop cx。不说影响美观,程序效率也会
很低。所以VC++使用了这个办法解决问题:
00401082 call @ILT+5(fun) (0040100a)
00401087 add esp,8
看红色的语句,直接将esp的值加8,让堆栈变成
/-------------------\ Higher Address <== stack pointer
| 参数2: 0x1100h |
+-----------------+
| 参数1: 0x8899h |
\-------------------/ Lower Address
通过改变esp从根本上解决了Stack unwinding。(push,pop指令本质上不就是通过
改变esp来实现堆栈平衡的吗) 现在,明白了函数如何传递参数,如何调用,如何
返回。下一个问题就是看看函数如何传递返回值了。相信你早就注意到了
4: return a + b;
00401026 mov eax,dword ptr [ebp+8]
00401029 add eax,dword ptr [ebp+0Ch]
可见,函数正式用eax寄存器来保存返回值的。如果你想使用函数的返回值,那么
一定要在函数一返回就把eax寄存器的值读出来。至于为什么不用ebx,ecx...,这
个虽然没有规定,但是习惯上大家都是用eax的。而且windows程序中也明确指出了
,函数的返回值必须放入eax内。 OK,现在来解决什么是calling conversion这个
历史遗留问题。如果认真思考过,你一定想函数的参数为什么偏用堆栈转递呢,寄
存器不也可以传递吗?而且很快阿。参数的传递顺序不一定要是由后到前的,从前
到后传递也不会出现任何问题啊?再有为什么一定要等到函数返回了再处理堆栈平
衡的问题呢,能否在函数返回前就让堆栈平衡呢? 所有上述提议都是绝对可行的
,而他们之间不同的组合就造就了函数不同的调用方法。也就是你常看到或听到的
stdcall,pascal,fastcall,WINAPI,cdecl等等。这些不同的处理函数调用方式就叫
做calling convention。 默认情况下C语言使用的是cdecl方式,也就是上面提到
的。参数由右到左进栈,调用函数者处理堆栈平衡。如果你在我们刚才的程序中
fun函数前加入__stdcall,再来用上面的方法分析一下。
8: fun(0x8899,0x1100);
00401058 push 1100h ; <== 参数仍然是由右到左传递的
0040105D push 8899h
00401062 call fun (00401000)
;<== 这里没有了 add esp, 08h
1: int __stdcall fun(int a, int b) {
00401000 push ebp
00401001 mov ebp,esp
00401003 sub esp,40h
00401006 push ebx
00401007 push esi
00401008 push edi
00401009 lea edi,[ebp-40h]
0040100C mov ecx,10h
00401011 mov eax,0CCCCCCCCh
00401016 rep stos dword ptr [edi]
2: a = 0x4455;
00401018 mov dword ptr [ebp+8],4455h
3: b = 0x6677;
0040101F mov dword ptr [ebp+0Ch],6677h
4: return a + b;
00401026 mov eax,dword ptr [ebp+8]
00401029 add eax,dword ptr [ebp+0Ch]
5: }
0040102C pop edi
0040102D pop esi
0040102E pop ebx
0040102F mov esp,ebp
00401031 pop ebp
00401032 ret 8; <== ret 取出返回地址后,
; 给esp加上 8。看!堆栈平衡在函数内完成了。
; ret指令这个语法设计就是专门用来实现函数
; 内完成堆栈平衡的
于是得出结论,stdcall是由右到左传递参数,被调用函数恢复堆栈的calling
convention. 其他几种calling convention的修饰关键词分别是__pascal,
__fastcall, WINAPI(这个要包含windows.h才可以用)。现在,你可以用上面说的
方法自己分析一下他们各自的特点了。
积木
2004-04-08
打赏
举报
回复
这是一篇介绍C语言中的函数调用是如何用实现的文章。写给那些对C语言各种行为
的底层实现感兴趣人的入门级文章。如果你是C语言或者汇编、底层技术的老鸟或
是对这个问题不感兴趣,那么这篇文章只会耽误您的时间,您大可不必阅读他。当
然如果前辈们愿意为我指出不足,我将十分感谢您的指导,并对耽误您宝贵的时间
致歉。 好了,废话少说!要研究这个问题,让我们先打开VC++吧。最好是6.0的,
:-P。(什么你没有VC++,倒!....赶快装一个!@#$,要快!) 首先,让我们在VC++
里建立一个Win32 Console Application项目,并建立主文件fun.c。并输入以下内
容。
int fun(int a, int b) {
a = 0x4455;
b = 0x6677;
return a + b;
}
int main() {
fun(0x8899,0x1100);
return 0;
}
之后,最关键的是在项目设置里关闭优化功能。也就是把
Project->Setting->C/C++->Optimizations选为Disabled。编译器的优化在分析底
层实现时大多数情况不太受欢迎。 按键盘上的F10键,进入单步调试模式(Step
Over)。看到你的main函数左侧有个黄色的小箭头了吗?那个就是程序即将执行的
语句。按Alt + 8。打开反编译窗口,看到汇编语句了吗?是不是想这个样子
==> 00401078 push 1100h
0040107D push 8899h
00401082 call @ILT+5(fun) (0040100a)
00401087 add esp,8
看到两个PUSH指令了吗?再看看后面的数字,不正是我们要传递的参数吗。奇怪阿
?我们明明是先传递的0x8899怎么反倒先push 1100h呢?呵呵,这个现象就叫
Calling conversion。究竟是何方神圣,我在后面会详细的给你解释的。先别着急
。随后的Call指令的作用就是开始调用函数了。 接下来关掉反汇编窗口,在源代
码窗口按F11(Step Into)进入函数体。当看到那个黄色的小箭头指向函数名的时候
再调出反汇编窗口(Alt+8)。你会看到类似下面的代码:
1: int fun(int a, int b) {
00401000 push ebp
00401001 mov ebp,esp
00401003 sub esp,40h
00401006 push ebx
00401007 push esi
00401008 push edi
00401009 lea edi,[ebp-40h]
0040100C mov ecx,10h
00401011 mov eax,0CCCCCCCCh
00401016 rep stos dword ptr [edi]
2: a = 0x4455;
00401018 mov dword ptr [ebp+8],4455h
3: b = 0x6677;
0040101F mov dword ptr [ebp+0Ch],6677h
4: return a + b;
00401026 mov eax,dword ptr [ebp+8]
00401029 add eax,dword ptr [ebp+0Ch]
5: }
0040102C pop edi
0040102D pop esi
0040102E pop ebx
0040102F mov esp,ebp
00401031 pop ebp
00401032 ret
freefalcon
2004-04-08
打赏
举报
回复
学编译原理了吗?
学了之后就理解了
adyhvgjkiyffjj
2004-04-08
打赏
举报
回复
建议看看c++primer
被小看的面试题 - 传值 or 传引用
前几天一个正在找工作的朋友发来一个面试题,感觉挺有意思的今天发出来大家一起乐呵乐呵。这个题其实不难,但是涉及到不只一个知识点,而且平时可能不会注意到。来直接看题得到这个题...
函数
(
函数
的概念、库
函数
、自定义
函数
、形参和实参、return语句、数组做
函数
参数、嵌套调用和链式访问、
函数
的声明和定义、static和extern)
return_type fun_name(形式参数) {•:是⽤来表⽰
函数
计算结果的类型(intfloatdoublechar…),返回类型可以是void,表示什么都不返回•fun_name:是
函数
名,
函数
名尽量要根据
函数
的功能起•形式参数:
函数
的参数也可以是void,明确表⽰
函数
没有参数。如果有参数,要交代清楚参数的类型和名字,以及参数个数•{}:括起来的是
函数
体写⼀个除法
函数
,完成2个浮点型变量的除法法操作return z;int main()//输入。
第18题 - 被小看的传值 or 传引用
前几天一个正在找工作的朋友发来一个面试题,感觉挺有意思的今天发出来大家一起乐呵乐呵。这个题其实不难,但是涉及到不只一个知识点,而且平时可能不会注意到。来直接看题得到这个题...
OC 重点block-------3-------block如何作为
函数
的参数
首先说下 简化block变量的复杂定义 1)。typefy :讲一个长类型定义为一个短类型 2)。我们可以用typedef 讲长类型的block类型,定义为一个短类型 typedef void (^newType)(); 是一个block 类型,无参数无返回值的类型 typed
<More Effective C++>笔记--异常
3 异常 ~~~~~~~ 3.1 防止构造
函数
在存在异常时发生资源泄露 ========================================= 1. 如果你用对应的auto_ptr对象替代指针成员变量,就可以防止构造
函数
在存在异常时发生资源泄漏,你也不用手工在析构
函数
中释放资源,并且你还能象以前使用非const指针一样使用const指针,给其赋值。 3.2
C语言
70,039
社区成员
243,246
社区内容
发帖
与我相关
我的任务
C语言
C语言相关问题讨论
复制链接
扫一扫
分享
社区描述
C语言相关问题讨论
社区管理员
加入社区
获取链接或二维码
近7日
近30日
至今
加载中
查看更多榜单
社区公告
暂无公告
试试用AI创作助手写篇文章吧
+ 用AI写文章