【程序员的自我修养11】栈与函数调用过程

绪论

大家好,欢迎来到【程序员的自我修养】专栏。正如其专栏名,本专栏主要分享学习《程序员的自我修养——链接、装载与库》的知识点以及结合自己的工作经验以及思考。编译原理相关知识本身就比较有难度,我会尽自己最大的努力,争取深入浅出。若你希望与一群志同道合的朋友一起学习,也希望加入到我们的学习群中。文末有加入方式。

介绍

首先我们来看一张Linux进程中经典的内存布局图:

分析:

  • 内核空间:是操作系统内核运行的区域,它是内核中一个重要的部分,专门为内核以及与内核直接相关的操作和进程提供服务。用户空间无法直接访问内核空间。
  • 栈用于维护函数调用的上下文。离开了栈,函数调用就无法实现。栈通常在用户空间的最高地址处分配,通常大小为8M。往地址方向生长
  • :堆是用来容纳应用程序动态分配的内存区域,当程序使用mallocnew分配内存时,得到的内存来自堆里。往高地址生长
  • 动态库:用于程序运行时,动态链接器或在程序运行过程中通过dlopen加载的动态库,其代码段都会保存在该内存空间。
  • 可读写段:用保存程序或动态库的全局变量或静态变量。
  • 只读段:用于保存程序的代码段或只读数据区等。
  • 保留区:是对内存中受到保护而禁止访问的内存区域的总称。比如地址NULL。

通过前面的章节,我们已经介绍了可执行程序和动态库加载到内存的过程。目前只有栈和堆没有介绍,本章主要介绍Linux下,函数的调用过程及其栈的变化。强烈建议大家认真理解,本章内容在一些特定调试场景非常有用。

我们先看一个示例:

int setReg(long* reg)
{*reg = 1;return 0;
}int main()
{long reg1 = 0xfff0001;long reg2 = reg1;setReg((long*)reg2);return 0;
}

编译:

yihua@ubuntu:~/test/stack$ gcc main.c -o main
yihua@ubuntu:~/test/stack$ 

假如在这样的一个场景:你对外提供setReg接口,用于设置寄存器值。但是由于调用者传入一个非法寄存器,导致程序异常,生成coredump。如何确定传入的寄存器是多少呢?因为main程序编译过程没有增加-g选项,因此gdb无法通过info locals查看局部变量信息。如下:

yihua@ubuntu:~/test/stack$ gdb main
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
Copyright (C) 2018 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 "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from main...(no debugging symbols found)...done.
(gdb) r
Starting program: /home/yihua/test/stack/mainProgram received signal SIGSEGV, Segmentation fault.
0x000055555555461b in setReg ()
(gdb) bt
#0  0x000055555555461b in setReg ()
#1  0x0000555555554608 in main ()
(gdb) f 0
#0  0x000055555555461b in setReg ()
(gdb) info locals
No symbol table info available.

我们知道coredump 保存了进程崩溃一瞬间的所有内存信息,那么栈空间的内容理应也被保存下来了,而函数的局部变量和入参也是保存在栈空间的。顺着这个思路,我们是否可以通过了解栈空间的布局,从而解答上面的疑问呢?

堆栈帧

在x86-64环境下,栈顶是由rsp寄存器进行定位,压栈就是使rsp寄存器减小,出栈就是让rsp寄存器增大。如下:

而栈中保存了一个函数调用所有需要的维护信息,其通常称为堆栈帧活动记录,包含以下信息:

  • 函数的返回地址和参数
  • 临时变量。比如函数非静态局部变量以及编译器自动生成的其它临时变量。
  • 保存函数上下文。比如寄存器内容,需要确保函数调用后,相关寄存器恢复原样。

在x86-64环境中,一个函数的堆栈帧通过rbprsp两个寄存器划定范围。

  • rsp寄存器始终指向栈的顶部,同时也就指向了当前函数的活动记录的顶部。
  • rbp寄存器指向了活动页的固定位置,又称为帧指针

函数的调用流程总是以下流程:

  1. 把所有或一部分参数压入到栈或寄存器中。
  2. 把当前指令的下一条指令的地址压入栈中。
  3. 跳转到函数体中执行。

注:其中第二步和第三步是由指令call一起执行的。

函数体的标准开头大致如下:

  1. push rbp:把rbp寄存器压入栈中。
  2. mov rbp,rsp:即rbp=rsp(这时rbp指向栈顶,而此时栈顶就是old rbp)。
  3. 【可选】sub rsp,XXX:在栈上分配XXX字节的临时空间。
  4. 【可选】push XXX,如有必要,保存名为XXX寄存器在栈中。(可重复多个)

分析:把rbp压入栈中,是为了在函数返回的时候便于恢复以前的rbp值。而之所以可能要保存一些寄存器,在于编译器可能要求某些寄存器在调用前后保持不变,那么函数就可以在调用开始时将这些寄存器的值压入栈中,在结束后再去除。

函数体的标准结尾大致如下:

  1. 【可选】pop XXX:如有必要,恢复保存过的寄存器。(可重复多个)。
  2. mov rsp,rbp:恢复rsp,同时回收栈空间。
  3. pop rbp:从栈中恢复保存的rbp值。
  4. ret:从栈中取得返回地址,并跳转到该位置。

由以上内容,我们可知一个常见的活动页记录大致如下图:

现在我们再回过头,来分析如何从gdb中获取传入参数的值。分析:

第一步:main函数在调用setReg时,需要将入参保存到栈中或寄存器。那么我们如何确定该值是保存在寄存器还是栈中呢?分析汇编:objdump -d main,输出大致如下:

...
00000000000005fa <setReg>:5fa:   55                      push   %rbp5fb:   48 89 e5                mov    %rsp,%rbp5fe:   48 89 7d f8             mov    %rdi,-0x8(%rbp)602:   48 8b 45 f8             mov    -0x8(%rbp),%rax606:   48 c7 00 01 00 00 00    movq   $0x1,(%rax)60d:   b8 00 00 00 00          mov    $0x0,%eax612:   5d                      pop    %rbp613:   c3                      retq0000000000000614 <main>:614:   55                      push   %rbp615:   48 89 e5                mov    %rsp,%rbp618:   48 83 ec 10             sub    $0x10,%rsp61c:   48 c7 45 f0 01 00 ff    movq   $0xfff0001,-0x10(%rbp)623:   0f624:   48 8b 45 f0             mov    -0x10(%rbp),%rax628:   48 89 45 f8             mov    %rax,-0x8(%rbp)62c:   48 8b 45 f8             mov    -0x8(%rbp),%rax630:   48 89 c7                mov    %rax,%rdi633:   e8 c2 ff ff ff          callq  5fa <setReg>638:   b8 00 00 00 00          mov    $0x0,%eax63d:   c9                      leaveq63e:   c3                      retq63f:   90                      nop
...

main函数汇编可知,在callq setReg前,main函数通过mov %rax,%rdi进行传参,即将实参传入到了%rdi寄存器中。同理setReg的汇编语句中 mov %rdi,-0x8(%rbp),将%rdi寄存器的值保存到了栈中,因此基本确定mainsetReg之间是通过%rdi寄存器进行传参的。

通过info registers查看寄存器值,可知rdi寄存器值为0xfff0001,与预期相符。同理若通过汇编语句,发现实参是通过栈空间传递的,那我们可以通过x /16xg $rbp打印栈内容,从而分析实参内容。

调用惯例

上述流程能够正常执行的依据是,函数的调用方和被调用方对函数调用有着统一的理解。比如,若main函数默认通过寄存器进行传参,但是setReg认为是通过栈空间进行传参。那么最终的结果肯定是错误的。

因此函数的调用方和被调用方对于如何调用必须要有一个明确的约定,只有双方都遵守同样的约定,函数才能被正确地调用。这样的约定称为调用惯例。一般会固定以下几方面:

  • 函数参数的传递方式和方式

函数参数的传递由很多种方式,最常见的一种是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数去除。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:是从左至右,还是从右至左。有些调用惯例还允许使用寄存器传递参数,以提高性能。比如x86-64。

  • 栈的维护方式

在函数将参数压栈之后,函数体会被调用,此后需要将压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方完成,也可以由函数本身完成。

  • 名字修饰的策略

为了链接的时候对调用惯例进行区分,调用惯例需要对函数本身的名字进行修饰。不同调用惯例有不同的名字修饰策略。

常见的调用惯例如下,其中C语言默认是cdecl。

因此,为了代码的可移植性,我们应避免写出func(i++,i++)的代码,因为不同的调用惯例,实参的执行顺序是不同的,可能会导致预期不一样

总结

通过本文,大致了解了栈空间的分布,以及函数调用过程栈内存的部分。通过了解函数调用过程,我们可以在一些特定场景,去获取更多的有用信息。这在调试阶段,非常适用。可参考Linux 调试进阶(多场景覆盖)。

若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/627364.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

gradle版本中-bin与-all区别

打开android studio下载的gradle文件&#xff0c;发现-all比-bin多了一个docs文件夹和一个src文件夹。-bin是编译后的二进制发布版&#xff0c;-all还包含了源码和文档&#xff0c;比-bin大了几十兆&#xff0c;两者其余没有区别。 android开发只关注gradle功能不关注实现的情况…

基于K-Means聚类与RFM模型分析顾客消费情况【500010102】

项目说明 本数据集是生成式模拟数据&#xff0c;本项目通过可视化分析对数据进行初步探索&#xff0c;再通过时间序列针对店铺的销售额进行分析&#xff0c;对时序图进行分解&#xff0c;发现数据存在季节性&#xff0c;并且通过auto_arima自动选择参数建立了SARIMA模型&#…

IOS-高德地图路径绘制-Swift

本文展示的是在IOS开发中调用高德地图进行驾车路径绘制&#xff0c;开发语言是Swift。 IOS高德地图集成请看&#xff1a;IOS集成高德地图Api 使用路径规划功能需要集成高德地图的搜索功能。 pod AMapSearch定义AMapSearchAPI 定义主搜索对象 AMapSearchAPI &#xff0c;并继承…

AI对决:ChatGPT与文心一言的深度比较

. 个人主页&#xff1a;晓风飞 专栏&#xff1a;数据结构|Linux|C语言 路漫漫其修远兮&#xff0c;吾将上下而求索 文章目录 引言ChatGPT与文心一言的比较Chatgpt的看法文心一言的看法Copilot的观点chatgpt4.0的回答 模型的自我评价自我评价 ChatGPT的优势在这里插入图片描述 文…

Linux下的HTTP代理服务器Squid的配置和使用

Squid是一个流行的Linux下的HTTP代理服务器软件。通过Squid&#xff0c;你可以在Linux服务器上设置一个代理服务器&#xff0c;以便为客户端提供安全的网络连接和数据传输。以下是Squid的配置和使用指南。 1. 安装Squid 首先&#xff0c;你需要确保你的Linux系统上已经安装了…

国标GB28181安防视频监控平台EasyCVR视频分享页增加精简模式

智慧安防平台EasyCVR能在复杂的网络环境中&#xff08;专网、局域网、广域网、VPN、公网等&#xff09;将前端海量的设备进行统一集中接入与视频汇聚管理&#xff0c;平台支持设备通过4G、5G、WIFI、有线等方式进行视频流的快捷传输&#xff0c;可以兼容各品牌的IPC、NVR、移动…

寒假学习打字:提前实现弯道超车

寒假对于学生来说&#xff0c;通常是一个宝贵的时间段&#xff0c;可以用来放松、充实自己&#xff0c;或者提高一项重要的技能——打字。在这个数字时代&#xff0c;打字技能变得比以往任何时候都更加重要。无论是在学校的论文写作&#xff0c;还是在工作中处理电子邮件&#…

低代码高逻辑谱写IT组织和个人的第二成长曲线 | 专访西门子Mendix中国区总经理王炯

在今天快速演进的数字化转型浪潮中&#xff0c;低代码平台已经成为推动企业敏捷适应市场变化的关键引擎。在此背景下&#xff0c;西门子Mendix作为市场上的领导者&#xff0c;以其创新的低代码解决方案不断地刷新着行业标准。 近日&#xff0c;LowCode低码时代访谈了西门子Men…

C#上位机与欧姆龙PLC的通信12----【再爆肝】上位机应用开发(WPF版)

1、先上图 继上节完成winform版的应用后&#xff0c;今天再爆肝wpf版的&#xff0c;看看看。 可以看到&#xff0c;wpf的确实还是漂亮很多&#xff0c;现在人都喜欢漂亮的&#xff0c;颜值高的&#xff0c;现在是看脸时代&#xff0c;作为软件来说&#xff0c;是交给用户使用的…

【JavaEE】CAS

作者主页&#xff1a;paper jie_博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文于《JavaEE》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和精力)打造&…

μ综合设计控制器

μ综合设计控制器是一种基于μ分析的控制器设计方法&#xff0c;用于提高控制器的鲁棒性和性能。μ分析是一种数学工具&#xff0c;用于描述和比较控制系统在不同参数变化下的性能。通过μ综合设计&#xff0c;可以综合运用各种控制策略&#xff0c;以达到更好的控制效果。 μ…

【双指针】001移动零_C++

题目链接&#xff1a;移动零 目录 题目解析 代码书写 知识补充 题目解析 题目让我们求必须在不复制数组的情况下,编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 这题我们可以用双指针的方法来写&#xff1a; 我们这里将用两个数组下标来…

条码WMS仓储管理系统的价值与优势

在全球化和数字化的时代&#xff0c;企业面临着诸多挑战。在复杂的运营环境中&#xff0c;如何提高运营效率和效果&#xff0c;降低成本&#xff0c;增强竞争力&#xff0c;成为企业关注的焦点。而库存管理作为企业运营的关键环节&#xff0c;其重要性不言而喻。本文将深入探讨…

北交所交易手续费标准?哪家证券公司开通北交所券商交易手续费佣金万2?

北交所&#xff08;Beijing Exchange&#xff09;是指位于中国北京的一家金融交易所。北交所是中国政府为推动金融改革和国际化市场而设立的交易场所。它提供包括股票、债券、期货、外汇等多种金融产品的交易服务。北交所的目标是促进中国金融市场的发展&#xff0c;吸引国内外…

golang文件相对路径问题

目录结构 2.具体代码&#xff1a; const dataFile "../data/data.json"_, fileName, _, _ : runtime.Caller(1)dataPath : path.Join(path.Dir(fileName), dataFile)fmt.Println(dataPath)// open filefile, err : os.Open(dataPath)if err ! nil {log.Fatalln(err…

在IntelliJ IDEA中集成SSM项目

SSM项目&#xff1a;springMVC为控制器、spring 为事务层、 MyBatis 负责持久 首先看下集成后项目结构&#xff1a; 1、打开IntelliJ IDEA&#xff0c;点击 "File" -> "New" -> "Project"。 点击Finish&#xff0c;此时我们就已经创建了一…

postman自动化接口测试

背景描述 有一个项目要使用postman进行接口测试&#xff0c;接口所需参数有&#xff1a; appid: 应用标识&#xff1b;sign&#xff1a;请求签名&#xff0c;需要使用HMACSHA1加密算法计算&#xff0c;签名串是&#xff1a;{appid}${url}${stamp}&#xff1b;stamp&#xff1…

Idea如何重置免费使用30天

大家都知道&#xff0c;Idea的使用&#xff0c;不是免费的。需要自己购买&#xff0c;获取证书才能使用&#xff0c;那么怎么无限试用30天呢&#xff01;首次&#xff0c;自己点击 点击Evaluate按钮&#xff0c;就可以免费使用。 过了30天的试用期。重新试用30天。我们需要如下…

6-keto-PGF1α ELISA kit—ENZO LIFE SCIENCE

高灵敏ELISA试剂盒&#xff0c;3小时内可检测低至1.40 pg/ml 6-酮前列腺素F1α 6-酮-前列环素F1α&#xff08;6-keto-PGF1α&#xff09;是前列环素&#xff08;PGI2&#xff09;的稳定水解产物。由于前列环素在缓冲液中的半衰期很短&#xff08;2-3分钟&#xff09;&#xff…

vue2使用electron以及打包配置

1.创建项目 vue create vue-project 2.安装electron vue add electron-builder会自动安装相关依赖 安装成功后会在src下自动生成一个background.js文件就是相应的electron的配置信息 use strictimport { app, protocol, BrowserWindow } from electron import { createProto…