当前位置: 首页 > news >正文

linux 函数 堆栈

1、关于栈

对于程序,编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈

  • 代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写
  • 数据段:保存初始化的全局变量和静态变量,可读可写不可执行
  • BSS:未初始化的全局变量和静态变量
  • 堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行
  • 栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,非常非常重要,可读可写可执行。如下图所示:

这里写图片描述

首先必须明确一点也是非常重要的一点,栈是向下生长的,所谓向下生长是指从内存高地址->低地址的路径延伸,那么就很明显了,栈有栈底和栈顶,那么栈顶的地址要比栈底低。对x86体系的CPU而言,其中
—> 寄存器ebp(base pointer )可称为“帧指针”或“基址指针”,其实语意是相同的。
—> 寄存器esp(stack pointer)可称为“ 栈指针”。
要知道的是:
—> ebp 在未受改变之前始终指向栈帧的开始,也就是栈底,所以ebp的用途是在堆栈中寻址用的。
—> esp是会随着数据的入栈和出栈移动的,也就是说,esp始终指向栈顶。
见下图,假设函数A调用函数B,我们称A函数为"调用者",B函数为“被调用者”则函数调用过程可以这么描述:
(1)先将调用者(A)的堆栈的基址(ebp)入栈,以保存之前任务的信息。
(2)然后将调用者(A)的栈顶指针(esp)的值赋给ebp,作为新的基址(即被调用者B的栈底)。
(3)然后在这个基址(被调用者B的栈底)上开辟(一般用sub指令)相应的空间用作被调用者B的栈空间。
(4)函数B返回后,从当前栈帧的ebp即恢复为调用者A的栈顶(esp),使栈顶恢复函数B被调用前的位置;然后调用者A再从恢复后的栈顶可弹出之前的ebp值(可以这么做是因为这个值在函数调用前一步被压入堆栈)。这样,ebp和esp就都恢复了调用函数B前的位置,也就是栈恢复函数B调用前的状态。
这个过程在AT&T汇编中通过两条指令完成,即:
leave
ret
这两条指令更直白点就相当于:
mov %ebp , %esp
pop %ebp
这里写图片描述

2、简单实例

开发测试环境:
Linux ubuntu 3.11.0-12-generic
gcc版本:gcc version 4.8.1 (Ubuntu/Linaro 4.8.1-10ubuntu8)
下面我们用一段代码说明上述过程:

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 -g Code.c ,加上-g,那么用objdump -S a.out 反汇编时可以把C代码和汇编代码穿插起来显示,这样C代码和汇编代码的对应关系看得更清楚。反汇编的结果很长,以下只列出我们关心的部分。

080483ed <bar>:
int bar(int c, int d)
{
 80483ed:	55                   	push   %ebp
 80483ee:	89 e5                	mov    %esp,%ebp
 80483f0:	83 ec 10             	sub    $0x10,%esp
	int e = c + d;
 80483f3:	8b 45 0c             	mov    0xc(%ebp),%eax
 80483f6:	8b 55 08             	mov    0x8(%ebp),%edx
 80483f9:	01 d0                	add    %edx,%eax
 80483fb:	89 45 fc             	mov    %eax,-0x4(%ebp)
	return e;
 80483fe:	8b 45 fc             	mov    -0x4(%ebp),%eax
}
 8048401:	c9                   	leave  
 8048402:	c3                   	ret    
08048403 <foo>:
int foo(int a, int b)
{
 8048403:	55                   	push   %ebp
 8048404:	89 e5                	mov    %esp,%ebp
 8048406:	83 ec 08             	sub    $0x8,%esp
	return bar(a, b);
 8048409:	8b 45 0c             	mov    0xc(%ebp),%eax
 804840c:	89 44 24 04          	mov    %eax,0x4(%esp)
 8048410:	8b 45 08             	mov    0x8(%ebp),%eax
 8048413:	89 04 24             	mov    %eax,(%esp)
 8048416:	e8 d2 ff ff ff       	call   80483ed <bar>
}
 804841b:	c9                   	leave  
 804841c:	c3                   	ret    
0804841d <main>:
int main(void)
{
 804841d:	55                   	push   %ebp
 804841e:	89 e5                	mov    %esp,%ebp
 8048420:	83 ec 08             	sub    $0x8,%esp
	foo(2, 3);
 8048423:	c7 44 24 04 03 00 00 	movl   $0x3,0x4(%esp)
 804842a:	00 
 804842b:	c7 04 24 02 00 00 00 	movl   $0x2,(%esp)
 8048432:	e8 cc ff ff ff       	call   8048403 <foo>
	return 0;
 8048437:	b8 00 00 00 00       	mov    $0x0,%eax
}
 804843c:	c9                   	leave  
 804843d:	c3                   	ret    

整个程序的执行过程是main调用foo,foo调用bar,我们用gdb跟踪程序的执行,直到bar函数中的int e = c + d;语句执行完毕准备返回时,这时在gdb中打印函数栈帧,因为此时栈已经生长到最大。

ZP1015@ubuntu:~/Desktop/c/Machine_Code$ gdb a.out 
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/ZP1015/Desktop/c/Machine_Code/a.out...done.
(gdb) start
Temporary breakpoint 1 at 0x8048423: file Code.c, line 14.
Starting program: /home/ZP1015/Desktop/c/Machine_Code/a.out 
Temporary breakpoint 1, main () at Code.c:14
14		foo(2, 3);
(gdb) s
foo (a=2, b=3) at Code.c:9
9		return bar(a, b);
(gdb) s
bar (c=2, d=3) at Code.c:3
3		int e = c + d;
(gdb) disas
Dump of assembler code for function bar:
   0x080483ed <+0>:	push   %ebp
   0x080483ee <+1>:	mov    %esp,%ebp
   0x080483f0 <+3>:	sub    $0x10,%esp
=> 0x080483f3 <+6>:	mov    0xc(%ebp),%eax
   0x080483f6 <+9>:	mov    0x8(%ebp),%edx
   0x080483f9 <+12>:	add    %edx,%eax
   0x080483fb <+14>:	mov    %eax,-0x4(%ebp)
   0x080483fe <+17>:	mov    -0x4(%ebp),%eax
   0x08048401 <+20>:	leave  
   0x08048402 <+21>:	ret    
End of assembler dump.
(gdb) si
0x080483f6	3		int e = c + d;
(gdb) 
0x080483f9	3		int e = c + d;
(gdb) 
0x080483fb	3		int e = c + d;
(gdb) 
4		return e;
(gdb) 
5	}
(gdb) bt
#0  bar (c=2, d=3) at Code.c:5
#1  0x0804841b in foo (a=2, b=3) at Code.c:9
#2  0x08048437 in main () at Code.c:14
(gdb) info registers
eax            0x5	5
ecx            0xbffff724	-1073744092
edx            0x2	2
ebx            0xb7fc4000	-1208205312
esp            0xbffff658	0xbffff658
ebp            0xbffff668	0xbffff668
esi            0x0	0
edi            0x0	0
eip            0x8048401	0x8048401 <bar+20>
eflags         0x206	[ PF IF ]
cs             0x73	115
ss             0x7b	123
ds             0x7b	123
es             0x7b	123
fs             0x0	0
gs             0x33	51
(gdb) x/20x $esp
0xbffff658:	0x0804a000	0x08048492	0x00000001	0x00000005
0xbffff668:	0xbffff678	0x0804841b	0x00000002	0x00000003
0xbffff678:	0xbffff688	0x08048437	0x00000002	0x00000003
0xbffff688:	0x00000000	0xb7e2d905	0x00000001	0xbffff724
0xbffff698:	0xbffff72c	0xb7fff000	0x0000002a	0x00000000
(gdb) 

下面从主函数开始,一步一步分析函数调用过程:

0804841d <main>:
int main(void)
{
 804841d:	55                   	push   %ebp
 804841e:	89 e5                	mov    %esp,%ebp
 8048420:	83 ec 08             	sub    $0x8,%esp
	foo(2, 3);
 8048423:	c7 44 24 04 03 00 00 	movl   $0x3,0x4(%esp)
 804842a:	00 
 804842b:	c7 04 24 02 00 00 00 	movl   $0x2,(%esp)
 8048432:	e8 cc ff ff ff       	call   8048403 <foo>

要调用函数foo先要把参数准备好,第二个参数保存在esp+4指向的内存位置,第一个参数保存在esp指向的内存位置,可见参数是从右向左依次压栈的。然后执行call指令,这个指令有两个作用:

  1. foo函数调用完之后要返回到call的下一条指令继续执行,所以把call的下一条指令的地址0x8048437压栈,同时把esp的值减4
  2. 修改程序计数器eip,跳转到foo函数的开头执行。
int foo(int a, int b)
{
 8048403:	55                   	push   %ebp
 8048404:	89 e5                	mov    %esp,%ebp
 8048406:	83 ec 08             	sub    $0x8,%esp
	return bar(a, b);
 8048409:	8b 45 0c             	mov    0xc(%ebp),%eax
 804840c:	89 44 24 04          	mov    %eax,0x4(%esp)
 8048410:	8b 45 08             	mov    0x8(%ebp),%eax
 8048413:	89 04 24             	mov    %eax,(%esp)
 8048416:	e8 d2 ff ff ff       	call   80483ed <bar>

push %ebp指令把ebp寄存器的值压栈,同时把esp的值减4。这两条指令合起来是把原来ebp的值保存在栈上,然后又给ebp赋了新值。在每个函数的栈帧中,ebp指向栈底,而esp指向栈顶,在函数执行过程中esp随着压栈和出栈操作随时变化,而ebp是不动的,函数的参数和局部变量都是通过ebp的值加上一个偏移量来访问,例如foo函数的参数a和b分别通过ebp+8和ebp+12来访问。所以下面的指令把参数a和b再次压栈,为调用bar函数做准备,然后把返回地址压栈,调用bar函数:

080483ed <bar>:
int bar(int c, int d)
{
 80483ed:	55                   	push   %ebp  
 80483ee:	89 e5                	mov    %esp,%ebp
 80483f0:	83 ec 10             	sub    $0x10,%esp
	int e = c + d;
 80483f3:	8b 45 0c             	mov    0xc(%ebp),%eax
 80483f6:	8b 55 08             	mov    0x8(%ebp),%edx
 80483f9:	01 d0                	add    %edx,%eax
 80483fb:	89 45 fc             	mov    %eax,-0x4(%ebp)
	return e;
 80483fe:	8b 45 fc             	mov    -0x4(%ebp),%eax
}
 8048401:	c9                   	leave  
 8048402:	c3                   	ret    
08048403 <foo>:

这次又把foo函数的ebp压栈保存,然后给ebp赋了新值,指向bar函数栈帧的栈底,通过ebp+8和ebp+12分别可以访问参数c和d。bar函数还有一个局部变量e,可以通过ebp-4来访问。所以后面几条指令的意思是把参数c和d取出来存在寄存器中做加法,计算结果保存在eax寄存器中,再把eax寄存器存回局部变量e的内存单元。bar函数有一个int型的返回值,这个返回值是通过eax寄存器传递的,所以首先把e的值读到eax寄存器中,然后执行leave指令,最后是ret指令。

在gdb中可以用bt命令和frame命令查看每层栈帧上的参数和局部变量,现在可以解释它的工作原理了:如果我当前在bar函数中,我可以通过ebp找到bar函数的参数和局部变量,也可以找到foo函数的ebp保存在栈上的值,有了foo函数的ebp,又可以找到它的参数和局部变量,也可以找到main函数的ebp保存在栈上的值,因此各层函数栈帧通过保存在栈上的ebp的值串起来了。

地址0x804841b处是foo函数的返回指令:

}
 804841b:	c9                   	leave  
 804841c:	c3                   	ret    

重复同样的过程,又返回到了main函数。

	return 0;
 8048437:	b8 00 00 00 00       	mov    $0x0,%eax
}
 804843c:	c9                   	leave  
 804843d:	c3                   	ret    

整个函数执行完毕。

函数调用和返回过程中的需要注意这些规则:

  1. 参数压栈传递,并且是从右向左依次压栈。
  2. ebp总是指向当前栈帧的栈底。
  3. 返回值通过eax寄存器传递。

局部变量申请栈空间时的入栈顺序
在没有溢出保护机制下的编译时,我们可以发现,所有的局部变量入栈的顺序(准确来说是系统为局部变量申请内存中栈空间的顺序)是正向的,即哪个变量先申明哪个变量就先得到空间,
也就是说,编译器给变量空间的申请是直接按照变量申请顺序执行的。
在有溢出保护机制下的编译时,情况有了顺序上的变化,对于每一种类型的变量来说,栈空间申请的顺序都与源代码中相反,即哪个变量在源代码中先出现则后申请空间;而对不同的变量来说,申请的顺序也不同,有例子可以看出,int型总是在char的buf型之后申请,不管源代码中的顺序如何(这应该来源于编译器在进行溢出保护时设下的规定)。

推荐博文:
c函数调用过程原理及函数栈帧分析

相关文章:

  • 页面参数获取与修改
  • 信息爆炸!78天闭门深造千页SpringCloud,再战京东
  • Python ML实战-工业蒸汽量预测02-数据探索
  • Android识别字符串中的简体和繁体字
  • ie11浏览器兼容性报错app.js script1003 缺少:
  • 资源管理器占用CPU过高的解决方法
  • (附源码)ssm高校实验室 毕业设计 800008
  • [CISCN2019 华东南赛区]Web4
  • 聚苯乙烯微球载金属纳米粒子/中空结构聚苯乙烯纳米微球/间隔臂羧化聚苯乙烯微球的研究
  • 软硬件一体,利尔达芯智行创新智能电摩解决方案
  • 【元宇宙欧米说】个人创作者的NFT探索之路
  • Kaggle官方·数据科学比赛经验集;百度开源·飞桨图像数据标注高效工具;C++并发编程实践第2版·笔记;前沿论文 | ShowMeAI资讯日报
  • JavaScript常用数组元素搜索或过滤的四种方法
  • java计算机毕业设计无极服装出租管理系统源码+系统+数据库+lw文档+mybatis+运行部署
  • 【算法题解】Codeforces Round #817 (Div. 4)题解
  • [译] 理解数组在 PHP 内部的实现(给PHP开发者的PHP源码-第四部分)
  • co模块的前端实现
  • Django 博客开发教程 16 - 统计文章阅读量
  • JavaScript对象详解
  • Netty+SpringBoot+FastDFS+Html5实现聊天App(六)
  • SAP云平台运行环境Cloud Foundry和Neo的区别
  • 计算机在识别图像时“看到”了什么?
  • 近期前端发展计划
  • 前端_面试
  • 前端学习笔记之原型——一张图说明`prototype`和`__proto__`的区别
  • 前嗅ForeSpider采集配置界面介绍
  • 如何优雅的使用vue+Dcloud(Hbuild)开发混合app
  • 学习HTTP相关知识笔记
  • CMake 入门1/5:基于阿里云 ECS搭建体验环境
  • RDS-Mysql 物理备份恢复到本地数据库上
  • ​ 无限可能性的探索:Amazon Lightsail轻量应用服务器引领数字化时代创新发展
  • ​configparser --- 配置文件解析器​
  • ​secrets --- 生成管理密码的安全随机数​
  • #162 (Div. 2)
  • %check_box% in rails :coditions={:has_many , :through}
  • (11)工业界推荐系统-小红书推荐场景及内部实践【粗排三塔模型】
  • (13):Silverlight 2 数据与通信之WebRequest
  • (C语言)共用体union的用法举例
  • (接口封装)
  • (五)MySQL的备份及恢复
  • (循环依赖问题)学习spring的第九天
  • (终章)[图像识别]13.OpenCV案例 自定义训练集分类器物体检测
  • (转)linux下的时间函数使用
  • (转)为C# Windows服务添加安装程序
  • ..回顾17,展望18
  • .NET Core 中插件式开发实现
  • .net 写了一个支持重试、熔断和超时策略的 HttpClient 实例池
  • .net访问oracle数据库性能问题
  • .NET框架类在ASP.NET中的使用(2) ——QA
  • .net下的富文本编辑器FCKeditor的配置方法
  • .php结尾的域名,【php】php正则截取url中域名后的内容
  • .skip() 和 .only() 的使用
  • @angular/cli项目构建--http(2)
  • [ 转载 ] SharePoint 资料
  • [.NET]桃源网络硬盘 v7.4