余晖落尽暮晚霞,黄昏迟暮远山寻
本站
当前位置:网站首页 > 编程知识 > 正文

软件开发 | GDB 调试器如何通过调用帧信息来确定函数调用关系

xiyangw 2023-10-09 16:11 22 浏览 0 评论

从调试器中获取函数调用关系。

在我的 上一篇文章中,我展示了如何使用debuginfo在当前指令指针(IP)和包含它的函数或行之间进行映射。该信息对于显示 CPU 当前正在执行的代码很有帮助。不过,如果能显示更多的有关当前函数调用栈及其正在执行语句的上下文对我们定位问题来说也是十分有助的。

例如,将空指针作为参数传递到函数中而导致非法内存访问的问题,只需查看当前执行函数行,即可发现该错误是由尝试通过空指针进行访问而触发的。但是,你真正想知道的是导致空指针访问的函数调用的完整上下文,以便确定该空指针最初是如何传递到该函数中的。此上下文信息由回溯提供,可以让你确定哪些函数可能对空指针参数负责。

有一点是肯定的:确定当前活动的函数调用栈不是一项简单的操作。

函数激活记录

现代编程语言具有局部变量,并允许函数可以调用自身的递归。此外,并发程序具有多个线程,这些线程可能同时运行相同的函数。在这些情况下,局部变量不能存储在全局位置。对于函数的每次调用,局部变量的位置必须是唯一的。它的工作原理如下:

  • 每次调用函数时,编译器都会生成函数激活记录,以将局部变量存储在唯一位置。
  • 为了提高效率,处理器堆栈用于存储函数激活记录。
  • 当函数被调用时,会在处理器堆栈的顶部为该函数创建一条新的函数激活记录。
  • 如果该函数调用另一个函数,则新的函数激活记录将放置在现有函数激活记录之上。
  • 每次函数返回时,其函数激活记录都会从堆栈中删除。

函数激活记录的创建是由函数中称为“序言prologue”的代码创建的。函数激活记录的删除由函数“尾声epilogue”处理。函数体可以利用堆栈上为其预留的内存来存储临时值和局部变量。

函数激活记录的大小可以是可变的。对于某些函数,不需要空间来存储局部变量。理想情况下,函数激活记录只需要存储调用 函数的函数的返回地址。对于其他函数,除了返回地址之外,可能还需要大量空间来存储函数的本地数据结构。帧大小的可变导致编译器使用帧指针来跟踪函数激活帧的开始。函数序言代码具有在为当前函数创建新帧指针之前存储旧帧指针的额外任务,并且函数尾声必须恢复旧帧指针值。

函数激活记录的布局方式、调用函数的返回地址和旧帧指针是相对于当前帧指针的恒定偏移量。通过旧的帧指针,可以定位堆栈上下一个函数的激活帧。重复此过程,直到检查完所有函数激活记录为止。

优化复杂性

在代码中使用显式帧指针有几个缺点。在某些处理器上,可用的寄存器相对较少。具有显式帧指针会导致使用更多内存操作。生成的代码速度较慢,因为帧指针必须位于寄存器中。具有显式帧指针可能会限制编译器可以生成的代码,因为编译器可能不会将函数序言和尾声代码与函数体混合。

编译器的目标是尽可能生成快速代码,因此编译器通常会从生成的代码中省略帧指针。正如 Phoronix 的基准测试所示,保留帧指针会显着降低性能。不过省略帧指针也有缺点,查找前一个调用函数的激活帧和返回地址不再是相对于帧指针的简单偏移。

调用帧信息

为了帮助生成函数回溯,编译器包含 DWARF 调用帧信息(CFI)来重建帧指针并查找返回地址。此补充信息存储在执行的 .eh_frame部分中。与传统的函数和行位置信息的debuginfo不同,即使生成的可执行文件没有调试信息,或者调试信息已从文件中删除,.eh_frame部分也位于可执行文件中。 调用帧信息对于 C++ 中的throw-catch等语言结构的操作至关重要。

CFI 的每个功能都有一个帧描述条目(FDE)。作为其步骤之一,回溯生成过程为当前正在检查的激活帧找到适当的 FDE。将 FDE 视为一张表,每一行代表一个或多个指令,并具有以下列:

  • 规范帧地址(CFA),帧指针指向的位置
  • 返回地址
  • 有关其他寄存器的信息

FDE 的编码旨在最大限度地减少所需的空间量。FDE 描述了行之间的变化,而不是完全指定每一行。为了进一步压缩数据,多个 FDE 共有的起始信息被分解出来并放置在通用信息条目(CIE)中。 这使得 FDE 更加紧凑,但也需要更多的工作来计算实际的 CFA 并找到返回地址位置。该工具必须从未初始化状态启动。它逐步遍历 CIE 中的条目以获取函数条目的初始状态,然后从 FDE 的第一个条目开始继续处理 FDE,并处理操作,直到到达覆盖当前正在分析的指令指针的行。

调用帧信息使用实例

从一个简单的示例开始,其中包含将华氏温度转换为摄氏度的函数。 内联函数在 CFI 中没有条目,因此 f2c函数的__attribute__((noinline))确保编译器将f2c保留为真实函数。

#include 

编译代码:

$ gcc -O2 -g -o f2c f2c.c

.eh_frame部分展示如下:

$ eu-readelf -S f2c |grep eh_frame
[17] .eh_frame_hdr  PROGBITS   0000000000402058 00002058 00000034  0 A  0   0  4
[18] .eh_frame      PROGBITS   0000000000402090 00002090 000000a0  0 A  0   0  8

我们可以通过以下方式获取 CFI 信息:

$ readelf --debug-dump=frames  f2c > f2c.cfi

生成 f2c可执行文件的反汇编代码,这样你可以查找f2cmain函数:

$ objdump -d f2c > f2c.dis

f2c.dis中找到以下信息来看看f2cmain函数的执行位置:

0000000000401060 

在许多情况下,二进制文件中的所有函数在执行函数的第一条指令之前都使用相同的 CIE 来定义初始条件。 在此示例中, f2cmain都使用以下 CIE:

00000000 0000000000000014 00000000 CIE
  Version:                   1
  Augmentation:              "zR"
  Code alignment factor: 1
  Data alignment factor: -8
  Return address column: 16
  Augmentation data:         1b
  DW_CFA_def_cfa: r7 (rsp) ofs 8
  DW_CFA_offset: r16 (rip) at cfa-8
  DW_CFA_nop
  DW_CFA_nop

本示例中,不必担心增强或增强数据条目。由于 x86_64 处理器具有 1 到 15 字节大小的可变长度指令,因此 “代码对齐因子” 设置为 1。在只有 32 位(4 字节指令)的处理器上,“代码对齐因子” 设置为 4,并且允许对一行状态信息适用的字节数进行更紧凑的编码。类似地,还有 “数据对齐因子” 来使 CFA 所在位置的调整更加紧凑。在 x86_64 上,堆栈槽的大小为 8 个字节。

虚拟表中保存返回地址的列是 16。这在 CIE 尾部的指令中使用。 有四个 DW_CFA指令。第一条指令DW_CFA_def_cfa描述了如果代码具有帧指针,如何计算帧指针将指向的规范帧地址(CFA)。 在这种情况下,CFA 是根据r7 (rsp)CFA=rsp+8计算的。

第二条指令 DW_CFA_offset定义从哪里获取返回地址CFA-8。在这种情况下,返回地址当前由堆栈指针(rsp+8)-8指向。CFA 从堆栈返回地址的正上方开始。

CIE 末尾的 DW_CFA_nop进行填充以保持 DWARF 信息的对齐。 FDE 还可以在末尾添加填充以进行对齐。

f2c.cfi中找到main的 FDE,它涵盖了从0x40160到(但不包括)0x401097main函数:

00000084 0000000000000014 00000088 FDE cie=00000000 pc=0000000000401060..0000000000401097
  DW_CFA_advance_loc: 4 to 0000000000401064
  DW_CFA_def_cfa_offset: 32
  DW_CFA_advance_loc: 50 to 0000000000401096
  DW_CFA_def_cfa_offset: 8
  DW_CFA_nop

在执行函数中的第一条指令之前,CIE 描述调用帧状态。然而,当处理器执行函数中的指令时,细节将会改变。 首先,指令 DW_CFA_advance_locDW_CFA_def_cfa_offsetmain401060处的第一条指令匹配。 这会将堆栈指针向下调整0x18(24 个字节)。 CFA 没有改变位置,但堆栈指针改变了,因此 CFA 在401064处的正确计算是rsp+32。 这就是这段代码中序言指令的范围。 以下是main中的前几条指令:

0000000000401060 

DW_CFA_advance_loc使当前行应用于函数中接下来的 50 个字节的代码,直到401096。CFA 位于rsp+32,直到401092处的堆栈调整指令完成执行。DW_CFA_def_cfa_offset将 CFA 的计算更新为与函数入口相同。这是预期之中的,因为401096处的下一条指令是返回指令ret,并将返回值从堆栈中弹出。

  401090:    31 c0        xor        %eax,%eax
  401092:    48 83 c4 18  add        $0x18,%rsp
  401096:    c3           ret

f2c函数的 FDE 使用与main函数相同的 CIE,并覆盖0x411900x4011c3的范围:

00000068 0000000000000018 0000006c FDE cie=00000000 pc=0000000000401190..00000000004011c3
  DW_CFA_advance_loc: 1 to 0000000000401191
  DW_CFA_def_cfa_offset: 16
  DW_CFA_offset: r3 (rbx) at cfa-16
  DW_CFA_advance_loc: 29 to 00000000004011ae
  DW_CFA_def_cfa_offset: 8
  DW_CFA_nop
  DW_CFA_nop
  DW_CFA_nop

可执行文件中 f2c函数的objdump输出:

0000000000401190 

f2c的 FDE 中,函数开头有一个带有DW_CFA_advance_loc的单字节指令。在高级操作之后,还有两个附加操作。DW_CFA_def_cfa_offset将 CFA 更改为%rsp+16DW_CFA_offset表示%rbx中的初始值现在位于CFA-16(堆栈顶部)。

查看这个 fc2反汇编代码,可以看到push用于将%rbx保存到堆栈中。 在代码生成中省略帧指针的优点之一是可以使用pushpop等紧凑指令在堆栈中存储和检索值。 在这种情况下,保存%rbx是因为%rbx用于向printf函数传递参数(实际上转换为puts调用),但需要保存传递到函数中的f初始值以供后面的计算使用。4011aeDW_CFA_advance_loc29字节显示了pop %rbx之后的下一个状态变化,它恢复了%rbx的原始值。DW_CFA_def_cfa_offset指出 pop 将 CFA 更改为%rsp+8

GDB 使用调用帧信息

有了 CFI 信息,GNU 调试器(GDB)和其他工具就可以生成准确的回溯。如果没有 CFI 信息,GDB 将很难找到返回地址。如果在f2c.c的第 7 行设置断点,可以看到 GDB 使用此信息。GDB在f2c函数中的pop %rbx完成且返回值不在栈顶之前放置了断点。

GDB 能够展开堆栈,并且作为额外收获还能够获取当前保存在堆栈上的参数 f

$ gdb f2c
[...]
(gdb) break f2c.c:7
Breakpoint 1 at 0x40119d: file f2c.c, line 7.
(gdb) run
Starting program: /home/wcohen/present/202207youarehere/f2c
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
98
converting

Breakpoint 1, f2c (f=98) at f2c.c:8
8            return c;
(gdb) where
#0  f2c (f=98) at f2c.c:8
#1  0x000000000040107e in main (argc=

调用帧信息

DWARF 调用帧信息为编译器提供了一种灵活的方式来包含用于准确展开堆栈的信息。这使得可以确定当前活动的函数调用。我在本文中提供了简要介绍,但有关 DWARF 如何实现此机制的更多详细信息,请参阅 DWARF 规范。

(题图:MJ/4004d7c7-8407-40bd-8aa8-92404601dba0)

via: https://opensource.com/article/23/3/gdb-debugger-call-frame-active-function-calls

作者:Will Cohen选题:lkxed译者:jrglinux校对:wxy

本文由 LCTT原创编译,Linux中国荣誉推出

相关推荐

前端开发之用以处理表单的jQuery控件之AJAX请求

介绍介绍我们的TFUMS的网页模板基本上都做好了,但是大家都发现了我们的模板里面的表单是不能提交的,这是因为缺少驱动程序,这个驱动程序就是指Javascript代码。在用户填写完表单项之后,点击了提交...

AJAX with JSP使用jQuery示例_ajax和jquery先学哪个

在这里,您将获得使用jQuery的JSP的AJAX示例。AJAX用于从服务器发送和接收数据,而无需重新加载页面。我们可以使用jQuery轻松实现AJAX。它为AJAX功能提供了各种方法。我使用Ecli...

华杉科技-jQuery与AJAX基础入门到实战精通

华杉科技提供的“jQuery与AJAX基础入门到实战精通”课程是一个涵盖了jQuery和AJAX技术的全面学习路径。下面是该课程的一个大致的学习大纲,以帮助你了解你将学到什么。1.jQuery基础入...

jQuery实现Ajax功能分析「与Flask后台交互」

本文实例讲述了jQuery实现Ajax功能。分享给大家供大家参考,具体如下:jQuery是一个小型的JavaScript库,它通常被用来简化DOM和JavaScript操作。通过在服务器...

jQuery - AJAX load() 方法_jqueryajax全部用法

jQueryload()方法jQueryload()方法是简单但强大的AJAX方法。load()方法从服务器加载数据,并把返回的数据放入被选元素中。语法:$(selector).load...

原生异步请求方法ajax,及jQuery相关方法,如何采用ES6封装ajax

知识已经过时了,可以直接跳到文章末尾看ES6封装ajax。怀念曾经的jQuery一.ajax方法jQuery:JavaScript代码包装成拿过来就能实现特定功能的代码库,基本淘汰了;json:简单...

JS类库Jquery(二):优雅的使用JQuery写Ajax实现前后端完美交互

Jquery虽然属于比较老的技术,但是相较于原生的JS写起来还是反方便很多,现在流行使用VUE等开源的框架,但是这并非不妨碍咱们进行Jquery的学习,前端程序员成长的过程中Jquery是必须了解的类...

Python Web详解:(Ajax+JSON+JQuery)

JOSN:JavascriptObjectNotation作用:主要约束前后端交互数据的格式JSON的格式表示单个对象使用{}采用键值对的格式保存数据键必须使用双引号引起来相当于...

JavaScript、Ajax、jQuery全部知识点,1分钟速懂!

本文将详细解读JavaScript、ajax、jQuery是什么?他们可以实现什么?1、JavaScript定义:javaScript的简写形式就是JS,是由Netscape公司开发的一种脚本语言,一...

一文读懂Ajax与Axios、jquery之间的关系与区别

1、关系1)Ajax与jQuery:jQuery提供了对Ajax技术的封装,使得使用Ajax变得更加方便。jQuery中的Ajax方法是对原生的Ajax技术(基于XMLHttpRequest对象)进行...

Javascript应用-jQuery Ajax DOM 元素、遍历、数据操作和方法

jQuery库拥有完整的Ajax兼容套件。其中的函数和方法允许在不刷新浏览器的情况下从服务器加载数据,具体如下:函数描述jQuery.ajax()执行异步HTTP(Ajax)请求。.aja...

Jquery中ajax的使用_jquery.ajax

声明:本栏目所使用的素材都是凯哥学堂VIP学员所写,学员有权匿名,对文章有最终解释权;凯哥学堂旨在促进VIP学员互相学习的基础上公开笔记。Jquery包装的ajax操作如下:$get$post操作...

全新web前端开发教程之Jquery Ajax

1、$.ajaxjquery调用ajax方法:格式:$.ajax({});参数:type:请求方式GET/POSTurl:请求地址urlasync:是否异步,默认是true表示异步data:发送到服务...

jquery对ajax的支持_ajax是什么

...

jquery中的Ajax请求详解各个参数_jquery ajax实例

比较适合初学的人$.ajax({url:"接收数据的页面地址",data:{参数:值,参数:值........},type:'post',或者getdataType:'json',async:t...

取消回复欢迎 发表评论: