什么是栈
- 简单来说 , 栈 是一种 LIFO(Last In Frist Out , 后进先出) 形式的数据结构 。栈一般是从高地址向低地址增长 , 并且栈支持 push(入栈) 和 pop(出栈) 两个操作 。如下图所示:
文章插图
- push 操作先将 栈顶(sp指针) 向下移动一个位置 , 然后将数据写入到新的栈顶;而 pop 操作会从 栈顶 读取数据 , 并且将 栈顶(sp指针) 向上移动一个位置 。
- 例如 , 将 0x100 压入栈 , 过程如下图所示:
文章插图
?
- 我们再来看看 出栈 操作 , 如下图所示:
文章插图
?
更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取.
文章插图
Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂
栈帧
- 栈帧 , 也就是 Sack Frame , 其本质就是一种栈 , 只是这种栈专门用于保存函数调用过程中的各种信息(参数 , 返回地址 , 本地变量等) 。
- 栈帧 有 栈顶 和 栈底 之分 , 其中栈顶的地址最低 , 栈底的地址最高 。SP(栈指针) 就是一直指向栈顶的 。在 x86 的 32 位 CPU 中 , 我们用 %ebp 寄存器指向栈底 , 也就是基址指针;用 %esp 寄存器指向栈顶 , 也就是栈指针 。下面是一个栈帧的示意图:
文章插图
?
- 一般来说 , 我们将 %ebp 到 %esp 之间区域当做栈帧 。并不是整个栈空间只有一个栈帧 , 每调用一个函数 , 就会生成一个新的栈帧 。
- 在函数调用过程中 , 我们将调用函数的函数称为:调用者(caller) , 将被调用的函数称为:被调用者(callee) 。在这个过程中:
- 调用者 需要知道在哪里获取 被调用者 返回的值(一般存放到 %eax 寄存器) 。
- 被调用者 需要知道传入的参数在哪里和调用完后的返回地址在哪里 。
- 我们需要保证在 被调用者 返回后 , %ebp 和 %esp 寄存器的值应该和调用前一致 。
- 现在 , 我们来看看函数调用时 , 栈帧是如何变化的 。
- 我们以一个函数调用的实例来解说 , 代码如下:
// stack.cint add_func(int a, int b){int c, d;c = a;d = b;return c + d;}int main(int argc, char *argv[]){int total;total = add_func(1, 2);return 0;}
- 我们使用命令 gcc -S -m32 stack.c 来编译上面的代码 , 获取的汇编代码如下所示(去掉一些无关紧要的信息):
add_func:pushl%ebp// 保存ebp寄存器到栈movl%esp, %ebp// 把ebp进程设置为esp的值subl$16, %esp// 为局部变量申请空间movl8(%ebp), %eax// 把参数a保存到eax寄存器中movl%eax, -8(%ebp)// 把eax寄存器的值保存到局部变量c中(c = a)movl12(%ebp), %eax// 把参数b保存到eax寄存器中movl%eax, -4(%ebp)// 把eax寄存器到值保存到局部变量d中(d = b)movl-8(%ebp), %edx// 把d的值保存到edx寄存器中movl-4(%ebp), %eax// 把c的值保存到eax寄存器中addl%edx, %eax// 将eax寄存器与edx寄存器的值相加 , 保存到eax中(返回值)leaveret// 函数返回...
- 可能汇编代码比较难看懂 , 我们用下面的插图来说明这个调用过程:
文章插图
?
【如何读懂栈溢出攻击,从这五点入手】