Linux后端程序员必备技能函数堆栈

发布者:上海IT外包来源:http://www.lanmon.net点击数:1031

每个人都知道函数调用是通过堆栈实现的,函数的局部变量存储在堆栈中。但是堆栈的实现细节可能并不清楚。本文将介绍如何在Linux平台上实现函数堆栈。有些学生可能觉得没有必要深入理解,事实上并非如此。根据这个数字多年的经验,了解系统的深层原理对于分析困难问题非常有帮助。
功能堆栈
图0功能堆栈
正如熟悉数据包捕获是解决网络通信问题的高级武器一样,熟悉函数调用堆栈是分析程序内存问题的先进武器。本文以Linux 64位操作系统下的C语言开发为例,介绍了应用程序调用栈的实现原理,并通过实例和GDB工具分析了程序的调用栈内容。在介绍特定的调用堆栈之前,让我们先介绍一些基本知识,这是了解后续函数调用堆栈的基础。
X86 CPU寄存器
CPU寄存器是需要理解的基础知识,因为X64体系结构中的函数参数通过寄存器传递。图1显示了X86 CPU寄存器列表及其功能的简要说明。
Intel X86 CPU寄存器使用情况
图1 Intel X86 CPU寄存器的使用情况
我们知道英特尔的CPU在设计时是向前兼容的,也就是说,它们可以在新一代CPU上运行旧版CPU上的编译程序。为确保兼容性,新一代CPU保留了旧一代寄存器的别名。以16位寄存器AX为例,AL表示低8位,AH表示高8位。引入32位CPU后,32位寄存器由名为EAX的寄存器表示,并保留AX。通过类比,RAX代表一个64位寄存器。
图2不同的寄存器名称
应用程序地址空间
操作系统通过虚拟内存为所有应用程序提供统一的内存映射地址。如图3所示,从上到下是用户堆栈,共享库内存,运行时堆和代码段。当然这是一个粗略的部分,实际的细分可能比这稍微复杂,但整体模式没有太大变化。
应用程序地址空间
图3应用程序的地址空间
从图中可以看出,用户堆栈从上到下增长。也就是说,用户栈首先占用高地址的空间,然后占用低地址空间。目前,我们可以有一个大致的了解,稍后我们将详细分析用户堆栈的细节。函数调用和汇编指令
为了理解函数调用堆栈的细节,有必要了解汇编程序中函数调用的实现。函数调用主要分为两部分,一部分是调用,另一部分是返回。在汇编语言中,函数调用由调用指令完成,返回由ret指令完成。
汇编语言调用指令相当于分别执行2步操作,1)将当前IP或CS和IP推送到堆栈上; 2)跳转,类似于jmp指令。类似地,ret指令也分为2个步骤,1)将堆栈中的地址弹出到IP寄存器; 2)跳转执行后续指令。这基本上是函数调用的原理。
除了代码之间的跳动之外,函数调用通常需要传递参数,并且在处理完成之后可能存在返回值。这些数据的传输是通过寄存器完成的。该函数在函数调用之前存储在上述寄存器中,并在函数返回之前通过RAX寄存器(EAX的32位系统)存储结果。
另一个重要的知识点是函数调用期间与堆栈相关的寄存器RSP和RBP。这两个寄存器主要记录堆栈位置。具体功能如下:
RSP:重新扩展的堆栈指针,它包含一个指针,该指针始终指向系统堆栈顶部堆栈的堆栈顶部。
RBP:重新扩展的基指针,它包含一个指针,该指针始终指向系统堆栈顶部堆栈帧的底部。
寄存器的名称与架构有关。本文是一个64位系统,因此寄存器是RSP和RBP。在32位系统的情况下,寄存器的名称是ESP和EBP。
应用程序调用栈
让我们看一下函数调用堆栈的主要内容,如图4所示。在函数堆栈中,它主要包括函数参数表,局部变量表,堆栈的基址和功能返回地址。这里堆栈的基地址是前一个堆栈帧的基地址,因为在此函数中需要基址来访问堆栈的内容,因此需要先推送先前堆栈帧中的基址。
函数调用堆栈概述
图4函数调用堆栈概述
为了便于理解,我们以一个特定的程序为例。这个程序很简单,主要是模拟函数调用关系和多个函数的参数传递。另外,在函数func_2中定义了两个形式参数来模拟多参数传递的过程。
功能堆栈组装分析
图5功能堆栈组装分析
在此示例中,main函数调用func_1函数。让我们从主函数开始。我们来看看右边的C代码。首先是函数参数的准备。当main函数调用func_1时,顺序传递的参数是1,2,3和4 + g,其中计算最后一个参数。根据红色框的虚线,我们可以看到相应的汇编程序,首先处理汇编程序中的最后一个参数,然后是倒数第二个,依此类推(函数参数的处理顺序是在日常开发中需要注意的) )内容重点)。同时,我们看到存储参数的寄存器名称与前一个相同。参数准备就绪后,将调用func_1函数,该函数是汇编语言中的调用函数func_1。虽然它只是一系列汇编指令,但事实上,有些事情是在内部完成的。这是在我们在前一篇文章中介绍了调用指令时引入的。您可以参考上一篇文章。
然后输入func_1函数的处理逻辑。第一个是pushq%rbp汇编程序,用于将RBP推入函数堆栈。推送栈的值和后续更新RBP(moveq%rsp,%rbp)是函数的堆栈头。随后对堆栈帧的内容的访问由帧头(RBP)执行。接下来是参数堆栈和局部变量初始化的过程。具体分布指的是图5中的绿色和红色框。
在完成函数操作后,最后将操作结果放入寄存器EAX中,然后调用指令leave和ret。这里需要解释的是leave指令,它相当于以下两个汇编指令。您可以比较函数入口的汇编指令,实际上两者是对称的。 leave指令将该帧的堆栈基址分配给堆栈指针(图6中的步骤2),然后将内容弹出到RBP中(图6中的步骤3)。事实上,RBP指向前一帧(调用者)的堆栈帧,这是一个恢复过程。
Movl%ebp%esp
Popl%ebp
图6函数返回原理图
因此,在函数返回后,寄存器RBP和RSP从调用者的堆栈帧切换到调用者的堆栈帧。
通过GDB分析函数调用堆栈
以上是通过反汇编功能的调用堆栈和堆栈帧的细分。我们还可以通过gdb动态分析函数堆栈和堆栈帧的使用。我们仍然使用main函数来调用func_1函数作为分析的示例。这里我们在函数func_1的入口处设置一个点,然后运行程序,程序在断点处停止。如图7所示,我们的逐步实现是函数堆栈的更改过程。详情将不在此处描述。你实际上可以做到这一点。
图7功能堆栈更改过程
本文的目的是让每个人都对函数调用堆栈有一个大致的了解,这样就可以解决未来程序难以处理的问题。因为在实际生产环境中存在与堆栈相关的更多问题,例如由太多局部变量引起的堆栈溢出,或者由内存问题引起的堆栈损坏。因此,了解函数堆栈的原理,遇到所谓的莫名其妙的问题时会有新的想法。通常很多问题都不是莫名其妙,但我们的知识还不够,我们感到莫名其妙。
IT外包
>
400-635-8089
立即
咨询
电话咨询
服务热线
400-635-8089
微信咨询
微信咨询
微信咨询
公众号
公众号
公众号
返回顶部