【pointer to c】Chapter2:指针、函数、引用(上):函数栈帧的建立和销毁;交换两个参数的值:函数参数分别使用值传递、地址传递、引用传递;c++引用介绍

指针、函数、引用

函数栈帧的建立和销毁

我们在初学C语言时经常听到这一句:实参的值不随形参的变化而变化。这句话不少人似懂非懂,以为懂了其实根本不知道说的是啥,就像高中学的奇变偶不变符号看象限。下面通过底层原理的理解让你彻底搞懂。首先我们需要了解一下函数栈帧的建立和销毁,以便我们后续的理解。
首先回顾汇编语法:懂得直接跳过。

汇编常见指令及用法(来自《程序是怎样跑起来的》,部分术语有差异)

操作码操作数功能
addA,B把A的值和B的值相加,并把结果存入A
cmpA,B对A和B的值进行比较,比较结果会自动存入标志寄存器中
incAA的值加1
ige标签名和cmp命令组合使用。跳转到标签行
jl标签名和cmp命令组合使用。跳转到标签行
jle标签名和cmp命令组合使用。跳转到标签行
jmp标签名将控制无条件跳转到指定标签行
movA,B把B的值赋值给A
popA从栈中读取出数值并存入A中
pushA把A的值存入栈中
ret将处理返回到调用源
xorA,BA和B的位进行异或比较,并将结果存入A中

x86常用寄存器及其功能(来自《程序是怎样跑起来的》,部分术语有差异)

寄存器名名称主要功能
eax累加寄存器运算
ebx基址寄存器存储内存地址
ecx计数寄存器计算循环次数
edx数据计数器存储数据
esi源基址寄存器存储数据发送源的内存地址
edi目标基址寄存器存储数据发送目标的内存地址
ebp扩展基址指针寄存器存储数据存储领域基点的内存地址
esp扩展栈指针寄存器存储栈中最高位数据的内存地址

esp在本篇博客里理解为栈顶指针,ebp在本篇博客里理解为栈底指针

下面我以《Linux C编程一站式学习》 19.1《函数调用》 这一节的代码来讲:

int bar(int c, int d)
{
 int e = c + d;
 return e;
}
int foo(int a, int b)
{
 return bar(a, b);
}
int main(void)
{
 foo(2, 3);
 return 0;
}

对应的汇编代码如下:

$ gcc main.c -g
$ objdump -dS a.out 
......
08048394 <bar>:
int bar(int c, int d)
{
 8048394: 55 push %ebp
 8048395: 89 e5 mov %esp,%ebp
 8048397: 83 ec 10 sub $0x10,%esp
 int e = c + d;
 804839a: 8b 55 0c mov 0xc(%ebp),%edx
 804839d: 8b 45 08 mov 0x8(%ebp),%eax
 80483a0: 01 d0 add %edx,%eax
 80483a2: 89 45 fc mov %eax,-0x4(%ebp)
 return e;
 80483a5: 8b 45 fc mov -0x4(%ebp),%eax
}
 80483a8: c9 leave 
 80483a9: c3 ret 
080483aa <foo>:
int foo(int a, int b)
{
 80483aa: 55 push %ebp
 80483ab: 89 e5 mov %esp,%ebp
 80483ad: 83 ec 08 sub $0x8,%esp
 return bar(a, b);
 80483b0: 8b 45 0c mov 0xc(%ebp),%eax
 80483b3: 89 44 24 04 mov %eax,0x4(%esp)
 80483b7: 8b 45 08 mov 0x8(%ebp),%eax
 80483ba: 89 04 24 mov %eax,(%esp)
 80483bd: e8 d2 ff ff ff call 8048394 <bar>
}
 80483c2: c9 leave 
 80483c3: c3 ret 
080483c4 <main>:
int main(void)
{
 80483c4: 8d 4c 24 04 lea 0x4(%esp),%ecx
 80483c8: 83 e4 f0 and $0xfffffff0,%esp
 80483cb: ff 71 fc pushl -0x4(%ecx)
 80483ce: 55 push %ebp
 80483cf: 89 e5 mov %esp,%ebp
 80483d1: 51 push %ecx
 80483d2: 83 ec 08 sub $0x8,%esp
 foo(2, 3);
 80483d5: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)
 80483dc: 00 
 80483dd: c7 04 24 02 00 00 00 movl $0x2,(%esp)
 80483e4: e8 c1 ff ff ff call 80483aa <foo>
 return 0;
 80483e9: b8 00 00 00 00 mov $0x0,%eax
}
 80483ee: 83 c4 08 add $0x8,%esp
 80483f1: 59 pop %ecx
 80483f2: 5d pop %ebp
 80483f3: 8d 61 fc lea -0x4(%ecx),%esp
 80483f6: c3 ret 
......

1.main函数调用foo 函数

 foo(2, 3);

 80483d5: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)
 80483dc: 00 
 80483dd: c7 04 24 02 00 00 00 movl $0x2,(%esp)
 80483e4: e8 c1 ff ff ff call 80483aa <foo>
 return 0;
 80483e9: b8 00 00 00 00 mov $0x0,%eax

这个地方的汇编代码和我之前 的汇编系列有一点语法上的不同,但是原理大致相同,首先看第一句:

movl $0x3,0x4(%esp),这句话 意思是将立即数(旧版立即数前要加$号)16位制 3 送给地址为esp+4所指向的内存位置,类似于 mov [esp+4] , 3 .第二句类似mov [esp] , 2. 注意:C语言是参数是从右向左依次压栈的。

此时的函数栈帧:
在这里插入图片描述

之后call 80483aa 是调用foo函数,这个指令有两个作用:

  1. foo函数调用完之后要返回call的下一条指令继续执行,所以把call的下一条指令的地址0x80483e9压栈,同时把esp的值减4,esp的值现在是0xbf822d18。
  2. 修改程序计数器eip,跳转到foo函数的开头执行。
    在这里插入图片描述

2.foo函数(1)

int foo(int a, int b)
{
 80483aa: 55 push %ebp
 80483ab: 89 e5 mov %esp,%ebp
 80483ad: 83 ec 08 sub $0x8,%esp

首先将ebp寄存器的值(80483e9)压栈,同时把esp的值再减4,esp的值现在是0xbf822d14。在每个函数的栈帧中,ebp指向栈底,而esp指向栈顶,在函数执行过程中esp随着压栈和出栈操作随时变化,而ebp是不动的,函数的参数和局部变量都是通过ebp的值加上一个偏移量来访问的.
然后再把esp的值-8.
现.在从ebp到esp就是foo函数的栈帧。如图:
在这里插入图片描述

到此我们总结一下:调用函数我们要做的两件事:
(1)将原函数下一条指令的地址压栈
(1)将原函数起始地址压栈(就是将ebp的值压栈)

3.foo函数调用bar函数

return bar(a, b);
 80483b0: 8b 45 0c mov 0xc(%ebp),%eax
 80483b3: 89 44 24 04 mov %eax,0x4(%esp)
 80483b7: 8b 45 08 mov 0x8(%ebp),%eax
 80483ba: 89 04 24 mov %eax,(%esp)
 80483bd: e8 d2 ff ff ff call 8048394 <bar>

第一句:mov 0xc(%ebp),%eax 将地址为ebp+12的内容送入eax寄存器,第二句将eax寄存器的内容送入地址为esp+4的位置。同理3,4句将ebp+8的内容送入地址为esp的位置。
在这里插入图片描述

call 8048394 ,见下:

在这里插入图片描述

4.bar函数(1)

int bar(int c, int d)
{
 8048394: 55 push %ebp
 8048395: 89 e5 mov %esp,%ebp
 8048397: 83 ec 10 sub $0x10,%esp
 int e = c + d;
 804839a: 8b 55 0c mov 0xc(%ebp),%edx
 804839d: 8b 45 08 mov 0x8(%ebp),%eax
 80483a0: 01 d0 add %edx,%eax
 80483a2: 89 45 fc mov %eax,-0x4(%ebp)

参照上图,push %ebp 将ebp的值压栈,mov %esp,%ebp,将esp 的值送给ebp,所以ebp的值是上图的esp的值,也就是bff1c408.
sub $0x10,%esp 将上一个esp的值-16,就是现在的bff1c3f4.然后是将地址为ebp-8的内容和地址为ebp-12 的内容加起来送到地址ebp-4。
在这里插入图片描述

在这里插入图片描述

4.bar函数(2)

 return e;
 80483a5: 8b 45 fc mov -0x4(%ebp),%eax
}
 80483a8: c9 leave 
 80483a9: c3 ret

bar函数有一个int型的返回值,这个返回值是通过eax寄存器传递的,所以首先把e的值读
到eax寄存器中。然后执行leave指令,这个指令是函数开头的push %ebp和mov %esp,%ebp的逆
操作:

  1. 把ebp的值赋给esp,现在esp的值是0xbf822d04。
  2. 现在esp所指向的栈顶保存着foo函数栈帧的ebp,把这个值恢复给ebp,
  3. 同时esp增加4,现在esp的值是0xbf822d08。

在这里插入图片描述

最后是ret指令,它是call指令的逆操作:

  1. 现在esp所指向的栈顶保存着返回地址,把这个值恢复给eip,同时esp增加4,现在esp的
    值是0xbf822d0c。
  2. 修改了程序计数器eip,因此跳转到返回地址0x80483c2继续执行。

这样我们就完成了函数栈帧的销毁,这下大家明白为什么要做这两件事了吧:
(1)将原函数下一条指令的地址压栈
(1)将原函数起始地址压栈(就是将ebp的值压栈)

在这里插入图片描述
foo函数的返回与之类似:

 80483c2: c9 leave 
 80483c3: c3 ret

完整的函数栈帧示意图
Parameter 形参 是函数定义的地方的参数,Argument实参 调用函数时的参数。比如main函数调用foo时在main函数栈帧的就是Argument,在foo函数栈帧里的就是Parameter 。如果被调函数需要使用调用函数传的参,会在被调函数的栈帧里复制一份,就像c,d复制a,b。当被调函数栈帧销毁时,c,d也就不存在了,这样无论foo函数中对c,d怎么变化也不会影响a,b.

“We will generally use parameter for a variable named in the parenthesized list in a function definition, and argument for the value used in a call of the function. The terms formal argument and actual argument are sometimes used for the same distinction.”——《The C Programming Language》Section 1.7 K&R

由于这个例子的函数没有用到返回值,建议大家学完再看看下面的博客;

程序的内存布局——函数调用栈的那点事

C语言函数调用栈(1)

C语言函数调用栈(2)

C语言函数调用栈(3)

更多关于函数栈帧的建立与销毁参考

《程序是怎样跑起来的》第10章

https://dins.site/coding-basics-assembly-function-chs/

x86函数调用过程与栈帧 - kafm - 博客园

参考资料:
《彻底搞定c指针》
《程序是怎样跑起来的》第10章

https://courses.cs.washington.edu/courses/cse333/22sp/lectures/03-c-pointers.pdf

  • 44
    点赞
  • 62
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值