|
|
51CTO旗下网站
|
|
移动端

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

咱们都知道函数调用是经过栈来完结的,并且知道在栈中存放着该函数的局部变量。可是关于栈的完结细节或许不一定清楚。本文将介绍一下在Linux渠道下函数栈是怎么完结的。

作者:SunnyZhang来历:itworld123|2019-06-23 17:37

咱们都知道函数调用是经过栈来完结的,并且知道在栈中存放着该函数的局部变量。可是关于栈的完结细节或许不一定清楚。本文将介绍一下在Linux渠道下函数栈是怎么完结的。有些同学或许觉得没必要了解这么深化,其实非也。依据本号多年的经历,了解体系深层次的原理对剖析疑难问题有很好的协助。

函数栈

图0 函数栈

就像了解抓包是处理网络通信问题的高档兵器相同,了解函数调用栈则是剖析程序内存问题的高档兵器。本文以Linux 64位操作体系下C言语开发为例,介绍运用程序调用栈的完结原理,并经过一个实例和GDB东西具体剖析一下某个程序的调用栈内容。在介绍具体的调用栈之前,咱们先介绍一些根底常识,这些常识是了解后续函数调用栈的根底。

X86 CPU的寄存器

CPU的寄存器是需求了解的根底常识,这是由于在X64体系中函数的参数是经过寄存器传递的。如图1是X86 CPU寄存器的列表及功用扼要阐明。

Intel X86 CPU寄存器用处

图1 Intel X86 CPU寄存器用处

咱们知道Intel的CPU在规划的时分都是向前兼容的,也便是在新一代的CPU上能够运转老一代CPU上的编译的程序。为了确保兼容性,新一代CPU保存了老一代寄存器的别号。以16位寄存器AX为例,AL表明低8位,AH表明高8位。而32位CPU面世之后,经过名为EAX的寄存器表明32位寄存器,AX仍然保存。以此类推,RAX表明一个64位寄存器。

图2 不同的寄存器称号

运用程序的地址空间

操作体系经过虚拟内存的办法为一切运用程序供给了共同的内存映射地址。如图3所示,从上到下分别是用户栈、同享库内存、运转时堆和代码段。当然这个是一个大约的分段,实践分段比这个或许略微杂乱一些,但整个格式没有大改变。

运用程序的地址空间

图3 运用程序的地址空间

从图中能够看出用户栈是从上往下生长的。也便是用户栈会先占用高地址的空间,然后占用低地址空间。现在咱们能够大体上有个了解即可,后边咱们在具体剖析用户栈的细节。

函数调用及汇编指令

为了了解函数调用栈的细节,有必要了解一下汇编程序中函数调用的完结。函数的调用首要分为2部分,一个是调用,别的一个是回来。在汇编言语中函数调用是经过call指令完结的,回来则是经过ret指令。

汇编言语的call指令相当于履行了2步操作,分别是,1)将当时的IP或CS和IP压入栈中; 2)跳转,相似与jmp指令。相同,ret指令也分2步,分别是,1)将栈中的地址弹出到IP寄存器;2)跳转履行后续指令。这个基本上便是函数调用的原理。

除了在代码间的跳动外,函数的调用往往还需求传递一个参数,而处理完结后还或许有回来值。这些数据的传递都是经过寄存器进行的。在函数调用之前经过上文介绍的寄存器存储参数,函数回来之前经过RAX寄存器(32位体系为EAX)存储回来成果。

别的一个比较重要的常识点是函数调用进程中与仓库相关的寄存器RSP和RBP,两个寄存器首要完结对栈方位的记载,具体效果如下:

  • RSP:栈指针寄存器(reextended stack pointer),其内存放着一个指针,该指针永久指向体系栈最上面一个栈帧的栈顶。
  • RBP:基址指针寄存器(reextended base pointer),其内存放着一个指针,该指针永久指向体系栈最上面一个栈帧的底部。

寄存器的称号跟体系结构是相关的,本文是64位体系,因而寄存器是RSP和RBP。如果是32位体系则寄存器的称号为ESP和EBP。

运用程序调用栈

咱们先从全体上来看一下函数调用栈的首要内容,如图4所示。在函数栈中首要包含函数参数表、局部变量表、栈的基址和函数回来地址。这儿栈的基址是上一个栈帧的基址,由于在本函数中需求运用该基址拜访栈中的内容,因而需求首要将上一个栈帧中的基址压栈。

函数调用栈概览

图4 函数调用栈概览

为了便于了解,咱们以一个具体的程序作为示例。本程序十分简略,首要是模仿了多个函数的函数调用联系和参数传递。别的,在函数func_2中界说了2个形参,以模仿多参数传递的进程。

函数栈汇编剖析

图5 函数栈汇编剖析

在本示例中,main函数调用func_1函数。咱们从main函数开端剖析,能够先看一下右侧的C言语代码。首要是函数参数的预备进程。在main函数调用func_1时顺次传入的参数为1、2、3和4+g,其间最终一个参数是需求核算的。依照赤色方框的虚线,咱们能够看到对应的汇编程序,在汇编程序中首要处理最终一个参数,然后是倒数第二个,以此类推(函数参数的处理次序在日常开发中是需求留意的内容要点)。一起,咱们看到存储参数的寄存器称号与前文是共同。

当预备完参数之后,便是调用func_1函数,这个在汇编言语中便是call func_1这一行。尽管仅仅一行汇编指令,但其实内部做了一些工作,这个咱们在前文介绍call指令的时分有所介绍,咱们能够参阅一下前文。

之后就进入func_1函数的处理逻辑。最一开端是pushq %rbp汇编程序,这句指令的效果是将RBP压入函数栈中。这句压栈及后边的更新RBP的值(moveq %rsp, %rbp)是构建本函数的栈帧头,后续对本栈帧的内容的拜访都是经过帧头(RBP)进行的。接下来是对参数压栈的进程和局部变量初始化的进程,具体散布参阅图5中的绿色方框和赤色方框。

完结函数内的运算后,最终将运算成果放入寄存器EAX中,然后调用指令leave和ret。这儿面需求阐明的是leave指令,该指令相当于下面两条汇编指令。能够比照一下函数进口的汇编指令,其实两者是对称的。leave指令将本帧的栈基址赋值给栈指针(图6中进程2),然后将其间的内容弹出到RBP中(图6中进程3)。其实便是RBP指向上一个帧(调用者)的栈帧,也便是一个恢复的进程。

  1. movl %ebp %esp 
  2. popl %ebp 

图6 函数回来示意图

这样,函数回来后寄存器RBP和RSP从被调用者的栈帧切换到了调用者的栈帧。

经过GDB剖析函数调用栈

上面是经过反汇编的办法剖析函数的调用栈和栈帧状况。咱们还能够经过gdb动态的剖析函数栈和栈帧的运用状况。咱们仍然经过main函数调用func_1函数为例来剖析。咱们这儿在函数func_1的进口处设置一个单点,然后运转程序,程序中止在断点处。如图7是咱们逐渐履行是函数栈的改变进程,具体细节咱们这儿就不再赘述,咱们能够实践操作一下。

图7 函数栈改变进程

本文的意图是让咱们对函数调用栈有个全体的了解,这样对今后程序的疑难杂症就有更多的处理思路。由于在实践出产环境中与栈相关的问题也是比较多的,比方局部变量太多导致的栈溢出,或许踩内存问题引起的栈损坏等等。因而,了解了函数栈的原理,在遇到所谓的不可思议问题的时分就会有新的思路。往往许多问题不是问题自身不可思议,而是咱们的常识储藏不行,自己感觉不可思议罢了。

【修改引荐】

  1. 你的Linux服务器果然得到保护了吗?
  2. Neofetch:在终端中显现Linux体系信息
  3. 在AppImage、Flathub和Snapcraft渠道上查找Linux运用
  4. Linux后端程序生长关键技能之底层体系结构
  5. 怎样了解Linux的软衔接和硬链接?
【责任修改:赵宁宁 TEL:(010)68476606】

点赞 0
同享:
咱们都在看
猜你喜爱

订阅专栏+更多

20个局域网建造改造事例

20个局域网建造改造事例

网络建立技巧
共20章 | 捷哥CCIE

405人订阅学习

WOT2019全球人工智能技能峰会

WOT2019全球人工智能技能峰会

通用技能、运用领域、企业赋能三大章节,13大技能专场,60+国内外一线人工智能精英大咖站台,同享人工智能的渠道东西、算法模型、语音视觉等技能主题,助力人工智能落地。
共50章 | WOT峰会

0人订阅学习

Spring Boot 爬虫查找轻松游

Spring Boot 爬虫查找轻松游

全栈式开发之旅
共4章 | 美码师

91人订阅学习

读 书 +更多

SQL Server 2005完结与保护(MCTS教程)

本书是微软认证技能专家(MCTS) 70-431考试的专用教材,全书共21章,围绕着考察方针,经过翔实的描绘、很多课程和课后测验,全面介绍了SQL S...

订阅51CTO邮刊

点击这儿检查样刊

订阅51CTO邮刊

51CTO服务号

51CTO播客