Chapter.九 C函数调用栈,栈溢出,Pwn入门(1 / 4)
东方阅读网【www.dfmsc.com】第一时间更新《从零开始的CTFer生活》最新章节。
「栈」(St)是一种受限的线性表,其只允许在线性表指定的一端(称之为栈顶)进行数据的插入操作与删除操作,因此通常数据的插入操作被形象地称呼为“将数据压入栈中”(p),而相应的数据删除的操作则也被称为“将数据从栈上弹出”(pp)——虽然在筱懿明刚开始学英文的时候,p(推)与pll(拉)这两个词是相互配对的,但是现在问他 p 的反义词是什么,他脑海里浮现出的第一个答案则毫不犹豫地会是“pp”。
栈这一数据结构有着诸多的应用场景,例如括号匹配、逆波兰表达式的解析等,不过这一次筱懿明所要面对的并不是以往刷算法题时所遇到的那一个栈,而是在「函数调用过程」当中的一个基础结构——
「栈帧」(t fre)。
在介绍这个概念之前我们得先引入一个新的概念:「程序运行时栈」——当然,这玩意如果要展开来讲的话需要涉及到相当多的计组、OS等知识,至少在当前阶段我们的主人公筱懿明是无法直接理解好在一个进程运行的背后的所有原理的,但是至少在经历了那么多天的从互联网上获取知识的过程之后,目前他可以理解的是:
①我们常说的“运行内存”指的是「Dyi Rd Mery」,也就是我们常说的“内存条”所实际呈现出来的东西,这是一种是与 CP 直接交换数据的内部存储器。由于其需要通电才能保持数据的存储,因此 DRAM 属于一种「易失性存储器」( ery)。
②程序都是在运行时被动态地装载到内存当中的,一个运行中的程序实体被称之为一个「进程」(pre),每个进程各自「独立地」占有内存中的一小块区域,称之为一个进程的「地址空间」(ddre pe)。
③进程的地址空间按照类型的不同分为不同的「段」(eget),例如「text段」用来存放进程运行时需要用到的代码,「dt段」用来存放已经初始化过后的全局变量,「b段」用来存放未初始化的全局变量。
④一个可以被操作系统载入到内存当中进行运行的文件称之为「可执行文件」(exetble file),可执行文件有两种主流格式:「PE」(Prtble Exetble)与「ELF」(Exetble d Lible Frt)。其中在类 NIX 系统上运行的程序主要是 ELF 格式的。
⑤ELF格式文件主要提供了两种视图:「链接视图」与「执行视图」,而我们常说的可执行文件通常是执行视图的 ELF 文件,其由三部分组成——一个ELF Heder +一个Prgre Heder Tble +多个 Seget,在不同的 Seget(段)当中存放着不同类型的数据,例如「.text段」用来存放进程运行时需要用到的代码,「.dt段」用来存放已经初始化过后的全局变量,「.b段」用来存放未初始化的全局变量。当操作系统想要将一个可执行 ELF 文件加载到内存当中运行时,其首先会读取 ELF Heder,获取到 Prgre Heder Tble 的信息,再根据 Prgre Heder Tble 来逐个读取文件中的各个不同类型的段,为各个段分配内存并将各个段的数据从文件中拷贝到内存当中,最后从 ELF Heder 当中所指定的程序入口点开始运行。
⑥进程运行时有两个段是不存在于 ELF 文件当中的——「栈」(St)与「堆」(Hep),这两个段应当由操作系统完成开辟的过程,其中「栈」是操作系统在运行新进程时都会自动分配的一块内存,用来存放函数运行时的信息、临时变量等数据,而「堆」则需要程序手动通过 br 与 br 这两个系统调用完成开辟的过程,堆内存常被用作于进行动态的内存分配的工作。堆通常由低地址向高地址进行增长,而栈通常由高地址向低地址进行增长。
⑦x86架构下有两个与栈相关的寄存器——「p」(t piter)与「bp」(be piter),其中 p 寄存器永远指向「栈顶」,而 bp 寄存器则指向「栈基址」——也就是「栈底」。在 64 位指令集中,这两个寄存器同样被扩展为 64 位的寄存器:rp 与 rbp。
以上便是筱懿明目前所能理解的关于进程运行的几乎全部的知识了,这还是他这些天各种在互联网上查找资料才大致形成的一个模糊的框架,但这已经足够他去理解「栈帧」这一概念。
「栈帧」其实是一个逻辑上的概念,通常而言,我们将程序运行时「p寄存器」与「bp寄存器」所包含起来的一块位于「栈段」上的内存区域称之为一个「栈帧」,其用来存放当前所运行的函数所需要的一切信息——函数内的临时变量、返回地址等。
每个函数有着其独立的栈帧,存放着属于该函数自己的数据,例如我们有如下 C 代码:
------
vid f(vid)
it ter_iple_vl;
// e vrible
// d etig
retr ;
vid _iple_f(vid)
it _iple_vl;
// e vrible
// d etig
f();
// d etig ele
------
当我们在运行 _iple_f()这个函数时,其栈应当形如如下形式(这里我们先不管 rbp 寄存器所指向的是什么数据,后面再解析):
------
|一些其他变量|←rp
|_iple_vl|
|一些其他变量|
|一些其他数据|←rbp
------
当我们运行到调用函数 f()的指令时,程序首先会将下一条指令的地址压入到栈上:
------
|下一条指令的返回地址|←rp
|一些其他变量|
|_iple_vl|
|一些其他变量|
|一些其他数据|←rbp
------
接下来会将当前的 rbp 的值压入栈中:
------
|原来的 rbp 的值|←rp
|下一条指令的返回地址|
东方阅读网【www.dfmsc.com】第一时间更新《从零开始的CTFer生活》最新章节。
本章未完,点击下一页继续阅读。