函数栈的原理

对于软件工程师来说,堆栈这个词虽然经常听说,但要是让你说清其具体原理,可能很多人猛然一下还真答不上来。归根结底还是缺少对其本质的认识,今天我们一起来看一下。

汽车ECU设计

函数栈的原理:原文配图 2

一个菜鸟程序猿的成长之路。116篇原创内容

公众号本文共1900字,预估阅读时5分钟

什么是堆栈 ?

虽然我们平时总是堆栈的说着,但是堆和栈是两个不同的概念。简单而言,堆是由程序编写者自由分配的内存区域由编写者自己管理,如使用 malloc 和 free 来进行分配和释放,而栈是由系统自动分配和管理的,不需要编写者干预,程序中函数的局部变量,参数,调用上下文等,都是存放在栈上的,当函数运行结束,这些变量栈上占用的空间会被自动释放掉。

函数栈的原理:原文配图 1

使用栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

栈的结构

栈(Stack):是限制在表的一端进行插入和删除操作的线性表。又称为 LIFO(Last In First Out)或先进后出 FILO(First In Last Out)。栈顶(Top):允许进行插入,删除操作的一段,又称为表尾。栈尾(Bottom):固定端,又成为表头。栈就像一个梯子一样,只有后爬的人都清了之后,先爬上去的人才能下来。栈是函数的基础,可以说没有栈就没有函数的出现。栈的空间是向下增长的,遵循“先进后出”的原则。它只有两个操作,push:将数据压入栈;pop:将数据压出栈内并返回。

函数栈的原理:原文配图 3

有两个关键的寄存器记录着栈中地址的变化,分别是扩展基址指针寄存器 EBP( Extended base pointer ) 和 扩 展 堆 栈 指 针 寄 存 器 ESP ( Extended stack pointer)。EBP:始终指向栈帧的底部,高地址ESP:始终指向栈帧的顶部,低地址当我们执行入栈和出栈操作时,ESP 会自动记录栈顶地址。没想到吧,就是通过这两个小小的寄存器,就实现了C语言中所有的函数调用工作。有了这些知识,我们就可以来看一看函数栈是如何实现的!

函数栈帧

首先让我们先来思考一下,在函数的调用过程中,函数栈需要完成一些什么操作 ?作为一个称职的函数栈至少要能实现如下功能:

  1. 为函数准备好相关的实际参数

  2. 创立一段空间用于存储所需的临时变量

  3. 函数执行完成后,程序需要能回到原来指令的地址

  4. 函数执行完成后,相关的寄存器需要能回到之前的状态(恢复上文)在实际的过程中,栈帧(Stack Frame)充当了这样的角色。简单来讲,栈帧就是函数运行的环境。每个函数在被调用时都会在栈区形成一个叫栈帧的结构,这个结构中保存了函数参数、函数的局部变量 、函数执行完后返回到哪里等等一些数据。

函数栈的原理:原文配图 4

函数调用过程

当程序在调用另一个函数时,函数栈主要有两部分工作,分别是 调用 与 退出。

(1)当我们调用一个函数的时候

  1. 将函数实参按照一定的顺序压入栈中

  2. push 函数的返回地址

  3. push 当前 epb,令新的 epb = esp,这时候我们就开启了一个新的栈帧

  4. esp 向下拓宽栈帧空间(用于存储本地变量以及可能的其他函数调用)

  5. push 当前的寄存器(保存上文内容)

(2)当我们退出一个函数调用的时候

  1. pop 之前保存的寄存器,恢复之前的状态

  2. 令esp = epb,pop epb 恢复到调用之前的值

  3. ret 指令返回ret 指令会使得出栈一次,并将出栈的内容当作地址,将程序执行跳转到该地址。到此,函数的整个调用过程结束,相关的寄存器也恢复到了调用之前的状态。这下你或许能理解,使用没有被赋值的临时变量有多么危险,指不定是什么数值呢。

函数栈的原理:原文配图 5

栈缓冲区溢出

有了上述的知识,我们可以来讨论下栈溢出这个概念了。栈溢出是由于C语言系列没有内置检查机制来确保复制到缓冲区的数据不得大于缓冲区的大小,因此当这个数据足够大的时候,将会溢出缓冲区的范围。以如下代码为例:

  1 #include "stdio.h"
  2 #include "string.h"
  3 void main(int argc, char **argv) {
  4 char buf[4];
  5 char ch = 'A';
  6 strcpy(buf, argv[1]);//拷贝字符串
  7 printf("%c", ch); //期望打印出A
  8 }

当使用库函数 strcpy 来复制在Command line 输入的字符串到栈帧中预留的 buf 数组时,会一直 copy 到字符串末尾的 ‘\0’ 为止。当输入的字符串长度超过3时,字符串中的第4个字符就会把 ch 变量给替覆盖掉,导致后面的软件失效。

函数栈的原理:原文配图 6

当发生缓冲区溢出时,意味着函数栈帧的内容发生了意外的篡改,程序可能出现无法预知的行为。如果这段缓冲区溢出的内容是攻击者精心构造的代码呢?比如通过精心设计缓冲长度和内容,替换掉当前栈帧的返回地址,就可以跳过目标代码,达到绕过校验的目的。正是因为这种特性,导致C语言成为一种不太安全的语言。我们在使用字符串库函数的时候,应尽量选用带长度参数的函数作为首选,尽可能的在源头避免这类问题的发生!

函数栈的原理:原文配图 8

完

拓展阅读:

早在盘古开天之前 (Main的故事)

C语言 Memory Layout

历史精华文章集合:

【汽车ECU设计】汽车总线文章合集

【汽车ECU设计】Autosar文章合集

【汽车ECU设计】工作思考合集

【汽车ECU设计】第51~100期目录

【汽车ECU设计】第1~50期目录

函数栈的原理:原文配图 7

氨气“ 携手进步 共同成长 ”

编程 · 目录

上一篇 下一篇

编译警告 —— 软件质量的第一道关 RTOS 中的信号量和互斥量

阅读原文

← 返回文章列表