C++函数调用栈从何而来

在这里插入图片描述

竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~
个人主页: rainInSunny  |  个人专栏: C++那些事儿、 Qt那些事儿

文章目录

  • 写在前面
  • 原理综述
  • x86架构函数调用栈分析
  • 如何获取rbp寄存器的值
  • 总结

写在前面

  程序员对函数调用栈是再熟悉不过了,无论是使用IDE调试还是GDB等工具进行调试,都离不开函数调用栈的分析。当我们遇到卡顿问题的时候,经常苦于没有卡顿现场,也就是函数调用栈进行分析解决。除了利用上述工具获取函数调用栈,能不能想办法在代码中记录函数调用栈,特别是卡顿的时候,还好是有办法的~

原理综述

  工具能够获取调用栈一定也是某个地方记录着这样的信息。实际上函数调用栈和函数调用过程是分不开的。函数调用过程在汇编角度分析,是由一帧一帧函数帧栈过程实现。这个过程大致包含调用现场保护、栈拉伸、参数传递、函数执行、返回值传递、栈平衡、调用现场恢复等过程。整个过程比较复杂,后续会写文章说明函数调用过程,这里只用关注与函数调用栈相关的调用现场保护过程,简单分析这个过程就能得出获取调用栈的基本原理。
在这里插入图片描述

  调用现场保护是只在函数嵌套调用的过程中需要一块内存空间来记录调用返回后仍然需要用到的数据,可以把这些需要保存的数据理解为一种调用现场,当函数调用返回时把这些数据读到对应的寄存器,函数就能愉快的执行下去了。上图是函数调用的栈帧示意图,想要获取函数调用栈,要关注FP(存储栈底地址)和LR(存储函数返回地址)寄存器,不同平台寄存器名称不一样,但都会有这样功能类似的寄存器。FP寄存器有很重要的三个作用:

  • 在FP存储栈底地址基础上增加值偏移,可以访问到父函数的栈内存数据(如这里的LR寄存器中的值)。
  • 在FP存储栈底地址基础上减少值偏移,可以访问到子函数的栈内存数据(如局部变量)。
  • FP存储栈底地址指向的内容是父函数FP的栈顶地址,用于子函数执行完毕后回到父函数时的FP寄存器还原。

  函数嵌套调用过程中,每次开辟新的函数帧栈时之前最后做的就是将PC寄存器存储下一条指令的地址压栈保存,此时LR寄存器也会存储该栈地址。进入嵌套子函数调用后第一件事就是将父函数的栈顶地址压栈保存,也就是这两者在栈空间地址是连续的,此时获取FP寄存器中地址向上偏移一个单位,再读取偏移后地址指向内容就能获取下一条指令地址。(个人理解,希望讲明白了>-<)如果用代码描述,就像这样:

while (fp) 
{pc = *(fp + 1); //pc代表存储的下一条指令地址fp = *fp; //fp指向的是父函数栈顶地址
}

x86架构函数调用栈分析

  如果看完原理还有点晕>-<,别急接下来一步步分析下x86上函数调用和调用栈相关的部分。下面是一个最简单的程序,在分析之前,回顾下上面的代码,while循环中有pc寄存器和fp寄存器,需要明确的是x86上pc寄存器名称是rip,fp寄存器的名称是rbp,指向栈底,其次这里会提到rsp寄存器,它一般指向栈顶)

int sub(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m, int n, int o) {int t = a + b;printf("The sub value is:%d\n", t);return t;
}
int main(void) {int p = sub(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);printf("the return value is:%d\n", p);return 0;
}
main:0x100003ee0 <+0>:   pushq  %rbp0x100003ee1 <+1>:   movq   %rsp, %rbp0x100003ee4 <+4>:   subq   $0x50, %rsp                # 预留 80 字节大小的栈内存空间0x100003ee8 <+8>:   movl   $0x0, -0x4(%rbp)		# 0 值写入,默认预留的大小空间,无特别场景,不会使用0x100003eef <+15>:  movl   $0x1, %edi			# 参数入 寄存器0x100003ef4 <+20>:  movl   $0x2, %esi			# 参数入 寄存器0x100003ef9 <+25>:  movl   $0x3, %edx			# 参数入 寄存器0x100003efe <+30>:  movl   $0x4, %ecx			# 参数入 寄存器0x100003f03 <+35>:  movl   $0x5, %r8d			# 参数入 寄存器0x100003f09 <+41>:  movl   $0x6, %r9d			# 参数入 寄存器0x100003f0f <+47>:  movl   $0x7, (%rsp)               # 参数入 栈内存0x100003f16 <+54>:  movl   $0x8, 0x8(%rsp)		# 参数入 栈内存0x100003f1e <+62>:  movl   $0x9, 0x10(%rsp)		# 参数入 栈内存0x100003f26 <+70>:  movl   $0xa, 0x18(%rsp)		# 参数入 栈内存0x100003f2e <+78>:  movl   $0xb, 0x20(%rsp)		# 参数入 栈内存0x100003f36 <+86>:  movl   $0xc, 0x28(%rsp)		# 参数入 栈内存0x100003f3e <+94>:  movl   $0xd, 0x30(%rsp)		# 参数入 栈内存0x100003f46 <+102>: movl   $0xe, 0x38(%rsp)		# 参数入 栈内存0x100003f4e <+110>: movl   $0xf, 0x40(%rsp)		# 参数入 栈内存0x100003f56 <+118>: callq  0x100003e90               ; sub at main.c:100x100003f5b <+123>: movl   %eax, -0x8(%rbp)0x100003f5e <+126>: movl   -0x8(%rbp), %esi0x100003f61 <+129>: leaq   0x32(%rip), %rdi          ; "the return value is:%d\n"0x100003f68 <+136>: movb   $0x0, %al0x100003f6a <+138>: callq  0x100003f78               ; symbol stub for: printf0x100003f6f <+143>: xorl   %eax, %eax0x100003f71 <+145>: addq   $0x50, %rsp0x100003f75 <+149>: popq   %rbp0x100003f76 <+150>: retq  

  上图中的汇编是main函数调用过程,看着挺多,还好我们只用关注0x100003f56 <+118>: callq 0x100003e90 ; sub at main.c:10callq指令完成函数帧的切换,实现在main函数中调用sub子函数,这个过程完成了两件事。首先将当前rip寄存器的值(下一条指令地址)保存到栈空间中,也就是下图中0x100003F5B保存在了栈最下方,然后将子函数sub的地址0x100003e90赋值给了rip寄存器,这样cpu下一条指令就会跳转到sub函数。

pushq %rip
movl <子函数内存地址> %rip

在这里插入图片描述

  下面分析sub函数,我们只用关注汇编过程的前面两行,第一行0x100003e80 <+0>: pushq %rbp pushq相当于两个过程,一个是subq $0x8, %rsp,这是一个栈拉伸的过程,相当于腾出8个字节的空间来,接下来是movl. %rbp, %rsp,这条指令把rbp寄存器里的值(也就是main函数的栈底地址)保存到刚刚腾出来的位置上。第二行0x100003e81 <+1>: movq %rsp, %rbp ,将rsp寄存器的值赋值给rbp寄存器,其实就是让rbp寄存器指向了sub函数的栈底。

  sub:0x100003e80 <+0>:  pushq  %rbp # 以上,将父函数的 rbp 值存入栈底0x100003e81 <+1>:  movq   %rsp, %rbp # 以上,将当前函数的 rsp 值赋予 rbp,此时 rbp 是子函数的栈底0x100003e84 <+4>:  subq   $0x20, %rsp # 以上,将 rsp 值减少 32 字节偏移,开辟栈预留内存空间0x100003e88 <+8>:  movl   0x50(%rbp), %eax0x100003e8b <+11>: movl   0x48(%rbp), %eax0x100003e8e <+14>: movl   0x40(%rbp), %eax0x100003e91 <+17>: movl   0x38(%rbp), %eax0x100003e94 <+20>: movl   0x30(%rbp), %eax0x100003e97 <+23>: movl   0x28(%rbp), %eax0x100003e9a <+26>: movl   0x20(%rbp), %eax0x100003e9d <+29>: movl   0x18(%rbp), %eax0x100003ea0 <+32>: movl   0x10(%rbp), %eax # 以上,根据 栈底 rbp 做增加值偏移,获取父函数的栈内存数据,即入参0x100003ea3 <+35>: movl   %edi, -0x4(%rbp)0x100003ea6 <+38>: movl   %esi, -0x8(%rbp)0x100003ea9 <+41>: movl   %edx, -0xc(%rbp)0x100003eac <+44>: movl   %ecx, -0x10(%rbp)0x100003eaf <+47>: movl   %r8d, -0x14(%rbp)0x100003eb3 <+51>: movl   %r9d, -0x18(%rbp) # 以上,将入参寄存器的值存入当前栈内存空间,做减小值偏移0x100003eb7 <+55>: movl   -0x4(%rbp), %eax0x100003eba <+58>: addl   -0x8(%rbp), %eax # 以上,完成 a + b 操作0x100003ebd <+61>: movl   %eax, -0x1c(%rbp) # 以上,将 a + b 的结果,存入栈内存空间0x100003ec0 <+64>: movl   -0x1c(%rbp), %esi0x100003ec3 <+67>: leaq   0xd4(%rip), %rdi          ; "The return value is:%d\n"0x100003eca <+74>: movb   $0x0, %al0x100003ecc <+76>: callq  0x100003f7e               ; symbol stub for: printf # 以上,调用 printf 函数开始打印 a + b 的值0x100003ed1 <+81>: movl   -0x1c(%rbp), %eax0x100003ed4 <+84>: addq   $0x20, %rsp0x100003ed8 <+88>: popq   %rbp0x100003ed9 <+89>: retq 

  注意这是一个连续的过程,所以栈上也是连续的,下图可以看出main函数最后callq指令保存指令地址0x100003F5B的栈位置就在当前rbp寄存器指向位置的上方,所以只要得到rbp寄存器指向的位置,加上1就能得到我们想要的指令地址了。还有重要的一点是rbp寄存器指向的内容是父函数(这里是main函数)的栈底地址,通过这个地址加1又能得到上一层指令地址了,如此循环往复,得到调用栈指日可待~
在这里插入图片描述

如何获取rbp寄存器的值

  不同操作系统有不同的系统调用来获取线程寄存器的状态,这里提供一个基于架构的通用思路,使用内联汇编的方式来获取。

int get_rbp_value() {int value;__asm__("movl %%rbp, %0" : "=r" (value));return value;
}

  这段代码使用GCC的内联汇编语法,通过movl指令将rbp寄存器的值移动到一个局部变量value中。"=r"是一个输出约束,表示将结果存储在提供的变量中。在这个例子中,我们使用%0来引用输出变量value。当这段代码被执行时,rbp寄存器的值就会被读取并存储在value中,然后返回给调用者。这里因为是内联汇编,为了区分寄存器和变量,所以寄存器前有两个%。
  当获取到指令地址后,就可以通过类似backtrace_symbolsaddress2line等方式获取对于的函数调用字符串形式,这块还没实践过,后续有时间研究研究>-<。

总结

  这个过程中需要核心关注的有以下几点:

  • FP寄存器指向每个函数帧栈的栈底,而当前函数栈底存储的内容就是父函数的栈底地址,这样通过FP寄存器就能循环得到每个函数的栈底地址。
  • 每次函数调用发生帧栈切换前,下一条指令的地址会被保存在调用者栈顶,这个地址和被调用者的栈底相邻,因此能够通过栈底地址偏移一个单位来获取指令地址,最后达到获取调用栈目的。

创作不易,感谢点赞、关注和收藏~

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

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

相关文章

基于cubemx的STM32的freertos的串口通信

1、任务描述 使用freertos系统实现电脑调试助手和正点原子开发板STM32F103ZET6的串口通信。 2、cubemx设置 3、程序代码 &#xff08;1&#xff09;添加usart1.c #include "usart1.h"#include "usart.h"/**********重定义函数**********/struct __FILE …

阵列信号处理2_阵列信号最优处理常用准则(CSDN_20240825)

目录 最小均方误差&#xff08;Minimum Square Error&#xff0c;MSE&#xff09;准则 最大信噪比&#xff08;Maximum Signal Noise Ratio&#xff0c;MSNR&#xff09;准则 极大似然&#xff08;Maximum Likehood, ML&#xff09;准则 最小方差无损响应&#xff08;Minim…

速通教程:如何使用Coze+剪映,捏一个爆款悟空视频

程哥最近做了一个和黑神话悟空有关的视频&#xff0c;没想到就火了&#xff0c;视频主打一个玉石风格&#xff0c;就是下面这个视频。 视频请移步飞书观看&#xff1a;黑神话悟空玉石版 制作过程不算很复杂&#xff0c;全程只需要用到Coze智能体和剪映这两个工具。 智能体用…

【JVM】亿级流量调优(一)

亿级流量调优 oop模型 前面的klass模型&#xff0c;它是Java类的元信息在JVM中的存在形式。这个oop模型是Java对象在JVM中的存在形式 内存分配策略: 1.空闲列表2.指针碰撞(jvm采用的) 2.1 top指针:执行的是可用内存的起始位置 2.2 采用CAS的方式3.TLAB 线程私有堆4.PLAB 老年…

使用DropZone+SpringBoot实现图片的上传和浏览

经常在项目中需要使用上传文件功能&#xff0c;找了不少前端上传组件&#xff0c;都不是很好用&#xff0c;今天尝试了一下DropZone&#xff0c;发现不错&#xff0c;顺便记录一下使用过程&#xff0c;方便后续查阅。在做开发的时候&#xff0c;经常需要调研一些技术&#xff0…

C# 运算符

运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C# 有丰富的内置运算符&#xff0c;分为一下六类&#xff1a; 算术运算符 关系运算符 逻辑运算符 位运算符 赋值运算符 杂项运算符 算术运算符 C# 支持的所有算术运算符。假设变量 A 的值为 10&#xff0c;变量 B 的值…

安全面试常见问题任意文件下载

《网安面试指南》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484339&idx1&sn356300f169de74e7a778b04bfbbbd0ab&chksmc0e47aeff793f3f9a5f7abcfa57695e8944e52bca2de2c7a3eb1aecb3c1e6b9cb6abe509d51f&scene21#wechat_redirect 1.1 任意文件下…

旅游社交小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;每日签到管理&#xff0c;景点推荐管理&#xff0c;景点分类管理&#xff0c;防疫查询管理&#xff0c;美食推荐管理&#xff0c;酒店推荐管理&#xff0c;周边推荐管理 微信端账…

《数据结构》顺序表+算法代码+动画演示-C语言版

目录 顺序表概念 顺序表初始化 顺序表销毁 顺序表尾插 顺序表尾删 顺序表头删 顺序表头插 顺序表pos位置插入 顺序表pos位置删除 顺序表全部代码如下&#xff1a; 顺序表概念 顺序表是用一段 物理地址连续 的存储单元依次存储数据元素的线性结构&#xff0c;一般情况下…

多个程序监听不同网卡的相同端口、相同网卡不同IP的相同端口

1 概述 一个主机上的多个程序监听同一个端口&#xff0c;是否一定存在冲突&#xff1f;如果是多网卡、单网卡多IP的情景下&#xff0c;多个程序是可以独立监听的。 2 多个程序监听不同网卡的相同端口 3 多个程序监听同一个网卡不同IP的相同端口 4 小结 多个程序监听同一个网…

【C语言】常见文件操作

文件的常见操作 #include<stdio.h>// 由于devc代码编码为ANCI&#xff0c;故读取的文件中若有中文&#xff0c;请设置文件编码为ANCI&#xff0c;否则会乱码 // 读文件 void test1() {char ch;FILE *fp; // 创建文件指针fp fopen("./file.txt", "r"…

pycharm修改文件大小限制

场景&#xff1a; 方法&#xff1a; 打开pycharm 安装目录下的idea.properties 增加配置项&#xff1a;idea.max.intellisense.filesize99999

java后端请求与响应总结

get 请求&#xff1a;将参数写在请求路径中&#xff08;请求路径跟一个&#xff1f;后面跟参数多个参数之间用&连接&#xff09; post 请求&#xff1a;将参数写在请求体中中 一、请求 1.简单参数 如 传一个或两个字符串、整数等 例如串一个用户名和密码 如果传入的数…

【自动驾驶】控制算法(四)坐标变换与横向误差微分方程

写在前面&#xff1a; &#x1f31f; 欢迎光临 清流君 的博客小天地&#xff0c;这里是我分享技术与心得的温馨角落。&#x1f4dd; 个人主页&#xff1a;清流君_CSDN博客&#xff0c;期待与您一同探索 移动机器人 领域的无限可能。 &#x1f50d; 本文系 清流君 原创之作&…

从法律风险的角度来看,项目经理遇到不清楚或不明确问题时的处理

大家好&#xff0c;我是不会魔法的兔子&#xff0c;在北京从事律师工作&#xff0c;日常分享项目管理风险预防方面的内容。 序言 在项目开展过程中&#xff0c;有时候会遇到一些不清楚或不明确的状况&#xff0c;但碍于项目进度的紧迫性&#xff0c;不得不硬着头皮做决策&…

Golang | Leetcode Golang题解之第368题最大整除子集

题目&#xff1a; 题解&#xff1a; func largestDivisibleSubset(nums []int) (res []int) {sort.Ints(nums)// 第 1 步&#xff1a;动态规划找出最大子集的个数、最大子集中的最大整数n : len(nums)dp : make([]int, n)for i : range dp {dp[i] 1}maxSize, maxVal : 1, 1fo…

CMake构建学习笔记4-libjpeg库的构建

libjpeg是一个广泛使用的开源库&#xff0c;用于处理JPEG&#xff08;Joint Photographic Experts Group&#xff09;图像格式的编码、解码、压缩和解压缩功能&#xff0c;是许多图像处理软件和库的基础。 libjpeg本身的构建没什么特别的&#xff0c;不过值得说道的是libjpeg存…

[Other]-安装ruby、ascli、ascp

最近新接到这样一个需求&#xff0c;将生物原始数据上传到某中心&#xff0c;其中用到ascp命令&#xff0c;阴差阳错的装了ruby、ascli&#xff0c;这里就都一并介绍下安装方式&#xff0c;由于服务器老旧默认安装时ruby2.0&#xff0c;又 升级到2.7等引发的一系列问题&#xf…

力扣(动态规划)-343整数拆分;96不同的二叉搜索树

整数拆分 题目&#xff1a; 给定⼀个正整数 n&#xff0c;将其拆分为⾄少两个正整数的和&#xff0c;并使这些整数的乘积最⼤化。 返回你可以获得的最⼤乘积。 示例 1: 输⼊: 2 输出: 1 解释: 2 1 1, 1 1 1。 示例 2: 输⼊: 10 输出: 36 解释: 10 3 3 4, 3 3…

Python 递归(recursion) 和 迭代(iteration)

递归 (recursion) 是指在函数的定义中使用函数自身的方法&#xff0c;直观上来看&#xff0c;就是某个函数自己调用自己。递归的基本思想就是把规模大的问题转化为规模小的相同的子问题来解决。 在函数实现时&#xff0c;因为大问题和小问题是一样的问题&#xff0c;因此大问题…