浅析函数调用过程

本文通过一个简单的C代码示例,结合gdb调试工具,详细解析了函数调用的过程,包括返回值如何返回、堆栈变化以及参数传递等细节。通过分析main函数的汇编代码和堆栈状态,揭示了函数调用时的内存布局和操作步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一个函数到底是怎么调用的,返回值是如何返回的,这里面学问真的很多,让我们来小小的分析一下。。。。

我们看一个很简单的C代码,通过gdb调试来分析函数调用过程。

平台和工具:ubuntu12.04+gcc 4.6.3+GNU gdb  7.4-2012.04

cs.c:

int add(int a,int b)
{
	return a+b;
}

int main()
{
int a=1;
int b=2;
int c=add(a,b);
}

编译: gcc -g cs.c

调试:gdb a.out

我们看一下:main函数的汇编代码:

(gdb) disas main
Dump of assembler code for function main:
01   0x080483c1 <+0>:	push   %ebp
02   0x080483c2 <+1>:	mov    %esp,%ebp
03   0x080483c4 <+3>:	sub    $0x18,%esp
04   0x080483c7 <+6>:	movl   $0x1,-0xc(%ebp)
05   0x080483ce <+13>:	movl   $0x2,-0x8(%ebp)
06   0x080483d5 <+20>:	mov    -0x8(%ebp),%eax
07   0x080483d8 <+23>:	mov    %eax,0x4(%esp)
08   0x080483dc <+27>:	mov    -0xc(%ebp),%eax
09   0x080483df <+30>:	mov    %eax,(%esp)
10   0x080483e2 <+33>:	call   0x80483b4 <add>
11   0x080483e7 <+38>:	mov    %eax,-0x4(%ebp)
12   0x080483ea <+41>:	leave  
13   0x080483eb <+42>:	ret    
End of assembler dump.

为了方便我在每行前面加了序号(本来是没有的)

堆栈情况如下:

                    

                                      图1

下面的数字代表main函数反编译的行号
1.帧指针ebp入栈
2.帧指针ebp指向当前栈指针esp
3.分配24(ox18)字节空间,所以esp下移24字节,如图1
4,5.局部变量a,b入栈,这里可以看到变量分配是按地址增大方向,所以虽然a先定义,但a的地址小,如图1
这里,你可能会问那C在哪呢?按地址增大方向,c应该在b的上面,说的很对,图1中
ebp-4是不是什么都没有,那就是c的所在地址,我们将在11行得到证明
6,7.将add的参数b入栈
8,9.将add的参数a入栈,如图1
10.调用add子程序,我们反编译add函数,

(gdb) disas add
Dump of assembler code for function add:
   0x080483b4 <+0>:	push   %ebp
   0x080483b5 <+1>:	mov    %esp,%ebp
   0x080483b7 <+3>:	mov    0xc(%ebp),%eax
   0x080483ba <+6>:	mov    0x8(%ebp),%edx
   0x080483bd <+9>:	add    %edx,%eax
   0x080483bf <+11>:	pop    %ebp
   0x080483c0 <+12>:	ret    
End of assembler dump.
我们看到调用的是add函数的地址: 0x080483b4
call做了两个操作,一,将返回地址入栈,那返回地址是什么呢?
返回地址,顾名思义就是调用子程序后返回的地址,那就应该是call下一条指令
的地址,即 0x080483e7 二,跳转到子程序的起始处
调用add后,堆栈如下图所以。

      

                      图2

我们将验证图2的返回地址就是0x080483e7
给两函数添加断点

gdb) break main
Breakpoint 1 at 0x80483c7: file cs.c, line 8.
(gdb) break add
Breakpoint 2 at 0x80483b7: file cs.c, line 3.
(gdb) r
Starting program: /home/yihaibo/c/a.out 
Breakpoint 1, main () at cs.c:8
8	int a=1;
(gdb) n
9	int b=2;
(gdb) n
10	int c=add(a,b);
(gdb) disas main
Dump of assembler code for function main:
   0x080483c1 <+0>:	push   %ebp
   0x080483c2 <+1>:	mov    %esp,%ebp
   0x080483c4 <+3>:	sub    $0x18,%esp
   0x080483c7 <+6>:	movl   $0x1,-0xc(%ebp)
   0x080483ce <+13>:	movl   $0x2,-0x8(%ebp)
=> 0x080483d5 <+20>:	mov    -0x8(%ebp),%eax
   0x080483d8 <+23>:	mov    %eax,0x4(%esp)
   0x080483dc <+27>:	mov    -0xc(%ebp),%eax
   0x080483df <+30>:	mov    %eax,(%esp)
   0x080483e2 <+33>:	call   0x80483b4 <add>
   0x080483e7 <+38>:	mov    %eax,-0x4(%ebp)
   0x080483ea <+41>:	leave  
   0x080483eb <+42>:	ret    
End of assembler dump.
调试过程中,可通过disas main,参看执行到main函数哪条指令(箭头所指地址)
(gdb) stepi
0x080483d8	10	int c=add(a,b);
(gdb) disas main
Dump of assembler code for function main:
   0x080483c1 <+0>:	push   %ebp
   0x080483c2 <+1>:	mov    %esp,%ebp
   0x080483c4 <+3>:	sub    $0x18,%esp
   0x080483c7 <+6>:	movl   $0x1,-0xc(%ebp)
   0x080483ce <+13>:	movl   $0x2,-0x8(%ebp)
   0x080483d5 <+20>:	mov    -0x8(%ebp),%eax
=> 0x080483d8 <+23>:	mov    %eax,0x4(%esp)
   0x080483dc <+27>:	mov    -0xc(%ebp),%eax
   0x080483df <+30>:	mov    %eax,(%esp)
   0x080483e2 <+33>:	call   0x80483b4 <add>
   0x080483e7 <+38>:	mov    %eax,-0x4(%ebp)
   0x080483ea <+41>:	leave  
   0x080483eb <+42>:	ret    
End of assembler dump.
可通过命令:stepi,执行汇编代码一条指令,和上面对比,发现的确执行一条指令。
(gdb) disas main
Dump of assembler code for function main:
   0x080483c1 <+0>:	push   %ebp
   0x080483c2 <+1>:	mov    %esp,%ebp
   0x080483c4 <+3>:	sub    $0x18,%esp
   0x080483c7 <+6>:	movl   $0x1,-0xc(%ebp)
   0x080483ce <+13>:	movl   $0x2,-0x8(%ebp)
   0x080483d5 <+20>:	mov    -0x8(%ebp),%eax
   0x080483d8 <+23>:	mov    %eax,0x4(%esp)
   0x080483dc <+27>:	mov    -0xc(%ebp),%eax
   0x080483df <+30>:	mov    %eax,(%esp)
=> 0x080483e2 <+33>:	call   0x80483b4 <add>
   0x080483e7 <+38>:	mov    %eax,-0x4(%ebp)
   0x080483ea <+41>:	leave  
   0x080483eb <+42>:	ret    
End of assembler dump.
(gdb) stepi
add (a=1, b=2) at cs.c:2
2	{
(gdb) print /x *(int*)($ebp-28)
$2 = 0x80483e7
通过多次stepi,当执行call指令时,我们可以打印出返回地址的值。
很容易计算出,返回地址所在栈地址是($ebp-28),所以通过print /x *(int*)($ebp-28)命令打印出($ebp-28)中存储的值(x 16进制),我们发现的确是0x080483e7,的确是call的下一条指令所在的地址。

这时程序将跳转到add子程序,
(gdb) disas add
Dump of assembler code for function add:
=> 0x080483b4 <+0>:	push   %ebp
   0x080483b5 <+1>:	mov    %esp,%ebp
   0x080483b7 <+3>:	mov    0xc(%ebp),%eax
   0x080483ba <+6>:	mov    0x8(%ebp),%edx
   0x080483bd <+9>:	add    %edx,%eax
   0x080483bf <+11>:	pop    %ebp
   0x080483c0 <+12>:	ret    
End of assembler dump.
跳转之后的堆栈如图2所示。
我们看到:
mov    0xc(%ebp),%eax  取得形参b的值2
mov    0x8(%ebp),%edx  取得形参a的值1
然后将两值相加,此时%eax中的值就是add的返回值。
pop  %ebp  弹出%ebp
ret   弹出返回地址,并跳转到返回地址,此时堆栈有恢复到图1。
指令执行如下:
(gdb) disas main
Dump of assembler code for function main:
   0x080483c1 <+0>:	push   %ebp
   0x080483c2 <+1>:	mov    %esp,%ebp
   0x080483c4 <+3>:	sub    $0x18,%esp
   0x080483c7 <+6>:	movl   $0x1,-0xc(%ebp)
   0x080483ce <+13>:	movl   $0x2,-0x8(%ebp)
   0x080483d5 <+20>:	mov    -0x8(%ebp),%eax
   0x080483d8 <+23>:	mov    %eax,0x4(%esp)
   0x080483dc <+27>:	mov    -0xc(%ebp),%eax
   0x080483df <+30>:	mov    %eax,(%esp)
   0x080483e2 <+33>:	call   0x80483b4 <add>
=> 0x080483e7 <+38>:	mov    %eax,-0x4(%ebp)
   0x080483ea <+41>:	leave  
   0x080483eb <+42>:	ret    
End of assembler dump.
此时将add返回值eax赋给-0x4(%ebp),即上面所说c的地址
(gdb) print &c
$6 = (int *) 0xbffff0e4
(gdb) print $ebp-4
$7 = (void *) 0xbffff0e4
(gdb) print /x *(int*)($ebp-4)
$4 = 0x3
(gdb) print c
$5 = 3
通过上面我们可以看出c的地址的确是$ebp-4,在本次实验中eax是函数的返回值

这个过程分析就over了.....


在Vue2中,可以使用watch来调用函数。具体有三种方式可以实现: 第一种方式是直接在watch中定义一个监听处理函数,当所监听的数据发生变化时,该函数会被触发。例如,在Vue实例中可以这样写: ``` watch: { cityName(newName, oldName) { // 在这里执行函数的逻辑 } } ``` 第二种方式是在watch中使用字符串形式的方法名来指定要调用的函数。例如,在Vue实例中可以这样写: ``` watch: { cityName: 'Namechange' } ``` 这样,当cityName的值发生变化时,会调用Vue实例中的Namechange函数。 第三种方式是使用$watch函数进行调用。$watch函数是Vue的内置方法,用于添加一个数据监听,当所监听的数据变化时,执行指定的回调函数。例如,在Vue实例中可以这样写: ``` this.$watch('cityName', function(newName, oldName) { // 在这里执行函数的逻辑 }) ``` 总结起来,Vue2中watch可以通过定义监听处理函数、使用字符串形式的方法名或使用$watch函数来调用函数。这些方法都可以实现在数据变化时执行相应的逻辑操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [vue的watch监听函数](https://blog.csdn.net/wwf1225/article/details/106590301)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [浅析vue 函数配置项watch及函数 $watch 源码分享](https://download.csdn.net/download/weixin_38663167/12949455)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值