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

news2024/11/8 6:38:13

在这里插入图片描述

竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~
个人主页: 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  %rbp
  0x100003ee1 <+1>:   movq   %rsp, %rbp
  0x100003ee4 <+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:10

  0x100003f5b <+123>: movl   %eax, -0x8(%rbp)
  0x100003f5e <+126>: movl   -0x8(%rbp), %esi
  0x100003f61 <+129>: leaq   0x32(%rip), %rdi          ; "the return value is:%d\n"
  0x100003f68 <+136>: movb   $0x0, %al
  0x100003f6a <+138>: callq  0x100003f78               ; symbol stub for: printf
  0x100003f6f <+143>: xorl   %eax, %eax
  0x100003f71 <+145>: addq   $0x50, %rsp
  0x100003f75 <+149>: popq   %rbp
  0x100003f76 <+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), %eax
    0x100003e8b <+11>: movl   0x48(%rbp), %eax
    0x100003e8e <+14>: movl   0x40(%rbp), %eax
    0x100003e91 <+17>: movl   0x38(%rbp), %eax
    0x100003e94 <+20>: movl   0x30(%rbp), %eax
    0x100003e97 <+23>: movl   0x28(%rbp), %eax
    0x100003e9a <+26>: movl   0x20(%rbp), %eax
    0x100003e9d <+29>: movl   0x18(%rbp), %eax
    0x100003ea0 <+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), %eax
    0x100003eba <+58>: addl   -0x8(%rbp), %eax 
    # 以上,完成 a + b 操作
    0x100003ebd <+61>: movl   %eax, -0x1c(%rbp) 
    # 以上,将 a + b 的结果,存入栈内存空间
    0x100003ec0 <+64>: movl   -0x1c(%rbp), %esi
    0x100003ec3 <+67>: leaq   0xd4(%rip), %rdi          ; "The return value is:%d\n"
    0x100003eca <+74>: movb   $0x0, %al
    0x100003ecc <+76>: callq  0x100003f7e               ; symbol stub for: printf 
    # 以上,调用 printf 函数开始打印 a + b 的值
    0x100003ed1 <+81>: movl   -0x1c(%rbp), %eax
    0x100003ed4 <+84>: addq   $0x20, %rsp
    0x100003ed8 <+88>: popq   %rbp
    0x100003ed9 <+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.coloradmin.cn/o/2072909.html

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

相关文章

printk的原理及使用

内核驱动调试的方法&#xff0c;先从我最常用的printk的使用方法开始讲起, printk在内核源码中用来记录日志信息的函数&#xff0c;方便我们调试追踪代码&#xff0c;只能在内核源码范围内使用。 本篇内核采用5.10版本。 很多内核开发者最喜欢的调试工具之一是printk(),printk(…

分享一个基于python新闻订阅与分享平台flask新闻发布系统(源码、调试、LW、开题、PPT)

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人 八年开发经验&#xff0c;擅长Java、Python、PHP、.NET、Node.js、Android、微信小程序、爬虫、大数据、机器学习等&#xff0c;大家有这一块的问题可以一起交流&…

【目标检测】AGMF-Net:遥感目标检测的无注意力全局多尺度融合网络

《Attention-Free Global Multiscale Fusion Network for Remote Sensing Object Detection》 遥感目标检测的无注意力全局多尺度融合网络 原文&#xff1a;https://ieeexplore.ieee.org/document/10371366 摘要 遥感目标检测&#xff08;RSOD&#xff09;在复杂背景和小目标…

设计模式篇(DesignPattern - 前置知识 七大原则)(持续更新调整)

目录 前置知识 一、什么是设计模式 二、设计模式的目的 七大原则 原则一&#xff1a;单一职责原则 一、案例一&#xff1a;交通工具问题 1. 问题分析 2. 解决思路 2.1 类级别单一职责 2.2 方法级别单一职责 3. 知识小结 二、案例二&#xff1a;待更新 原则二&…

本·阿弗莱克在与詹妮弗·洛佩兹离婚期间与孩子塞拉菲娜共度时光

在詹妮弗洛佩兹提出离婚申请期间&#xff0c;本阿弗莱克被发现与塞拉菲娜阿弗莱克一起在加州观看电影。 本阿弗莱克似乎将重心放在家庭时间上&#xff0c;最近有人拍到他带着孩子塞拉菲娜阿弗莱克在一起。此前&#xff0c;他的妻子詹妮弗洛佩兹 于 8 月 20 日星期二提出离婚。 …

小黄鸟九宫格切图丨教你如何将图片九宫格切图_照片分割成9张工具

图片九宫格怎么弄&#xff1f;怎么把1张图片切割称九宫图&#xff1f;如何将一张照片切成九宫格 微博九宫图怎么做&#xff1f;你还不知道电脑上如何做微博九宫格图片? 今天用小黄鸟九宫格切割工具&#xff0c;手把手教你,搞定九宫格切图 小黄九宫格切图丨小黄鸟教你如何九宫…

如何使用ssm实现基于web的药品管理系统+vue

TOC ssm175基于web的药品管理系统vue 第1章 绪论 1.1 课题背景 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。所以各行业&…

五、Centos7-安装Jenkins--这篇废了

克隆了一个base的虚拟机&#xff0c;用来安装Jenkins 2023年11月&#xff0c;Jenkins不支持centos7了。我们只是学习用&#xff0c;先看看吧。 &#xff08; 另一个人用别的操作系统安装的jenkins&#xff0c;可以参考 版权声明&#xff1a;本文为博主原创文章&#xff0c;…

js第五天-对象

object let obj {uname: pink,age: 18,gender: w} 增 对象名.属性新值 这个和cpp不一样&#xff0c;可以在大括号外面新增属性 <script>let obj {uname: pink,age: 18,gender: w}obj.hobby footballconsole.log(obj);</script>删 delete delete obj.gender …

Spring Boot整合MyBatis-Plus的详细讲解

MyBatis Plus&#xff08;简称MP&#xff09;是一个在MyBatis基础上进行增强的工具&#xff0c;它保留了MyBatis的所有特性&#xff0c;并通过提供额外的功能和简化操作来提高开发效率。以下是对MyBatis Plus的详细介绍&#xff1a; 一、基本概述 定义&#xff1a;MyBatis Plu…

【MATLAB学习笔记】绘图——设置次刻度线的数量、设置刻度线的宽度(粗细)和长度

目录 前言设置次刻度线数量函数示例基本绘图设置次刻度线数量函数的使用 设置刻度线的长度设置刻度线和轴线的宽度总代码总结 前言 在MATLAB中&#xff0c;将XMinorTicktrue或者YMinorTicktrue设置为true可以很方便地设置X轴或者Y轴次刻度线&#xff0c;但是次刻度线的数量是MA…

代码随想录DAY25 - 回溯算法 - 08/24

目录 非递减子序列 题干 思路和代码 递归法 递归优化 全排列 题干 思路和代码 递归法 全排列Ⅱ 题干 思路和代码 方法一&#xff1a;用集合 set 去重 方法二&#xff1a;先排序&#xff0c;再用数组去重 非递减子序列 题干 题目&#xff1a;给你一个整数数组 nu…

python动画:manim中的目标位置移动,线条末端和两条线相切的位置处理

一&#xff0c;Manim中目标的位置移动 在 Manim 中&#xff0c;shift 函数用于在三维空间或二维平面上对对象进行平移。通过 shift 方法&#xff0c;用户可以快速移动场景中的物体&#xff0c;指定移动的方向和距离。方向通常由预定义的常量&#xff08;如 UP, DOWN, LEFT, RI…

opencv-python图像增强十五:高级滤镜实现

文章目录 前言二、鲜食滤镜三、巧克力滤镜三&#xff0c;冷艳滤镜&#xff1a; 前言 在之前两个滤镜文章中介绍了六种简单的滤镜实现&#xff0c;它们大多都是由一个单独函数实现的接下来介绍五种结合了之前图像增强文章提的的算法的复合滤镜。本案例中的算法来自于文章一&…

【数学建模】TOPSIS法(优劣解距离法)

TOPSIS法&#xff08;Technique for Order Preference by Similarity to Ideal Solution&#xff0c;优劣解距离法&#xff09;是一种多准则决策分析方法&#xff0c;它基于这样一个概念&#xff1a;最理想的方案应该是距离理想解最近而距离负理想解最远的方案。以下是使用TOPS…

【React原理 - 任务调度和时间分片详解】

概述 在React15的时候&#xff0c;React使用的是从根节点往下递归的方式同步创建虚拟Dom&#xff0c;由于递归具有同步不可中断的特性&#xff0c;所以当执行长任务时(通常以60帧为标准&#xff0c;即16.6ms)就会长时间占用主线程长时间无响应&#xff0c;导致页面卡顿&#x…

如何使用Gitee管理自己的项目

如何使用Gitee管理自己的项目 前言 本地创建的工程项目不利于管理&#xff0c;电脑设备丢失损坏&#xff0c;代码就找不回来了。 并且多人同时使用一个项目工程也不方便。 国内的代码托管平台&#xff0c;Gitee为我实现了远程代码管理。 并且该平台可以设置为开源和私有两种…

公司邮箱如何建立

而建立一套完善的公司邮箱系统&#xff0c;则是实现这一目标的重要一环。本文将深入探讨公司邮箱的建立过程&#xff0c;以及其在业务中的重要性。 1. 确定邮箱域名 公司邮箱的建立首先要确定一个专属的邮箱域名。域名是公司在网络上的身份标识&#xff0c;例如&#xff0c;公…

程序猿成长之路之数据挖掘篇——Kmeans聚类算法

Kmeans 是一种可以将一个数据集按照距离&#xff08;相似度&#xff09;划分成不同类别的算法&#xff0c;它无需借助外部标记&#xff0c;因此也是一种无监督学习算法。 什么是聚类 用官方的话说聚类就是将物理或抽象对象的集合分成由类似的对象组成的多个类的过程。用自己的…

VSCode插件 live Server

普通打开 安装live Server 包含端口 说明内置了服务器