pwn 入门

过程

过程是软件中一种很重要的抽象。它提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能。然后,可以在程序中不同的地方调用这个函数,。设计良好的软件用过程作为抽象机制,隐藏某个行为的具体实现,同时又提供清晰简洁的接口定义,说明要计算的是哪些值,过程会对程序状态产生什么样的影响。不同的编程语言中,过程的形式多种多样:函数(function)、方法(method)、子例程(subroutine)、处理函数(handler)等等,但是它们有一些共有的特性。
要提供对过程的机器级的支持,必须要处理许多不同的属性。为了讨论方便,假设过程 p 调用 过程 Q,Q 执行完侯返回到 P。这些动作包括下面一个或多个机制:

  1. 传递控制:在进入过程 Q 的时候,程序计数器必须设置 Q 为代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。
  2. 传递数据:p 必须能够想 Q 提供一个或多个参数,Q 必须能够向 P 返回一个值。

运行时栈

C 语言过程调用机制的一个关键特性(大多数其他语言也是如此)在于使用了栈数据结构提供的后进先出的内存管理原则。在过程 p 中调用过程 Q 的例子中,可以看到当 Q 在执行时,P 以及所有在向上追溯到 P 的调用链中的过程,都是暂时被挂起的。当 Q 运行时,它只需要为局部变量分配新的存储空间,或者设置到另一个过程的调用。另一方面,当 Q 返回时,任何它所分配的局部变量存储空间都可以被释放。因此,程序可以用栈来管理它的过程所需要的存储空间,栈和程序寄存器存放着传递控制和数据、分配内存所需要的信息。当 P 调用 Q 时,控制和数据信息田添加到栈尾。当 P 返回时,这些信息会释放掉。
x86-64 的栈向低地址方向增长,而栈指针 %rsp 指向栈顶元素。可以用 pushq 和 popq 指令将数据存入栈中或是从栈中取出。将栈指针减小一个适当的量可以为没有指定初始值的数据在栈上分配空间。类似地,可以通过增加栈指针来释放空间。
当 x86-64 过程需要的存储空间超出了寄存器能够存放的大小时,就会在栈上分配空间。这个部分称为过程的栈帧。

栈底
...            较早的帧
...            调用函数 P 的帧
参数 n         调用函数 P 的帧   
...            调用函数 P 的帧
参数 7         调用函数 P 的帧
返回地址        调用函数 P 的帧
被保存的寄存器   正在执行的函数 Q 的帧
局部变量        正在执行的函数 Q 的帧
参数构造区      正在执行的函数 Q 的帧
%rsp -> 栈顶    正在执行的函数 Q 的帧

上图给出了运行时栈的通用结构,包括把它划分为栈帧。当前正在执行的过程的帧总是在栈顶。当过程 P 调用过程 Q 时,会把返回地址压入栈中,指明当 Q 返回时,要从 P 程序的哪个位置继续执行。我们把这个返回地址当作 P 的栈帧的一部分,因为它存放的是与 P 相关的状态。Q 的代码或扩展当前栈的边界,分配它的栈帧所需的空间。在这个空间中,它可以保存寄存器的值,分配局部变量空间,为它调用的过程设置参数。大多数过程的栈帧都是定长的,在过程的开始就分配好了。通过寄存器,过程 P 可以传递最多 6 个整数值,但是如果 Q 需要更多的参数,P 可以在调用 Q 之前在自己的栈帧里存储好这些参数。
为了提高空间和时间效率,x86-64 过程只分配自己所需要的栈帧部分。例如,许多过程有 6 个或者更少的参数,那么所有的参数都可以通过寄存器传递。实际上,许多函数甚至根本不需要要栈帧。当所有的局部变量都可以保存在寄存器中,而且该函数不会调用任何其他函数时,就可以这样处理。

转移控制

将控制从函数 P 转移到函数 Q 只需要简单地把程序计数器(PC)设置为 Q 的代码的起始位置。不过,当稍后从 Q 返回的时候,处理器必须记录好它需要继续 P 的执行的代码位置。在 x86-64 机器中,这个信息使用指令 call Q 调用过程 Q 来记录的。该指令会把地址 A 压入栈中,并将 PC 设置为 Q 的起始地址。压入的地址 A 被称为返回地址,是紧跟在 call 指令后面的那条指令的地址。对应的指令 ret 会从栈中弹出地址 A,并把 PC 设置为 A。

数据传送

当调用一个过程时,除了要把控制传递给它并在过程返回时再传回来之外,过程调用还可能包括把数据作为参数传递,而从过程返回还有可能包括返回一个值。x86-64 中,大部分过程间的数据传送是通过寄存器实现的。例如,我们已经看到无数的函数示例,参数在寄存器 %rdi、%rsi 和其他寄存器中传递。当过程 P 调用过程 Q 时,P 的代码必须首先把参数复制到适当的寄存器中。类似地,当 Q 返回 P 时, P 的代码可以访问寄存器 %rax 中的返回值。

栈上的局部存储

有些时候,局部数据必须存放在内存中,常见的情况包括:

  1. 寄存器不足够存放所有的本地数据。
  2. 对一个局部变量使用地址运算符‘&’,因此必须能够为它产生一个地址。
  3. 某些局部变量是数据或结构,因此必须能够通过数据或结构引用被访问到。

一般来说,过程通过减少栈指针在栈上分配空间。分配的结果作为栈帧的一部分,标号为“局部变量”。

寄存器中的局部存储空间

寄存器组是唯一被所有过程共享的资源。
虽然在给定时刻只有一个过程是活动的,我们仍然必须确保当一个过程调用另一个过程时,被调用者不会覆盖调用者稍后会使用的寄存器值。为此, x86-64 采用了一组统一的寄存器使用惯例,所有的过程都必须遵循。
根据惯例,寄存器 %rbx、%rbp 和 %12~%15 被划分为被调用者保存寄存器。当过程 P 调用过程 Q 时,Q 必须保存这些寄存器的值,保证它们的值在 Q 返回到 P 时与 Q 被调用时候时一样的。过程 Q 保存一个寄存器的值不变,要么就时根本不去改变它,要么就是把原始值压入栈中,改变寄存器的值,然后在返回钱从栈中弹出旧值。压入寄存器的值会在栈帧中创建标号为 “被保存的寄存器” 的一部分。有了这条惯例,P 的代码就能安全地把值存在被调用者保存寄存器中,调用 Q,然后继续使用寄存器中的值,不用担心被破坏。
所有其他的寄存器,除了栈指针 %rsp,都分类为调用者保存寄存器。这就意味着任何函数都能修改它们。可以这样理解“调用者保存”这个名字:过程 P 在某个此类寄存器中有局部数据,然后调用过程 Q。因为 Q 可以随意修改这个寄存器,所以在调用之前首先保存好这个数据时 P 的责任。

递归过程

前面已经描述了寄存器和栈的惯例使得 x86-64 过程能够递归完成地调用自身。每个过程调用在栈中都有它自己的私有空间,因此多个未完成调用的局部变量不会相互影响。此外,栈的原则很自然地就提供了了适当的策略,当过程调用时分配局部存储,当返回时释放存储。

栈溢出

这里 ctfwiki 有做介绍

内容来源

深入理解计算机系统

posted @ 2021-01-31 16:07  PwnKi  阅读(521)  评论(0编辑  收藏  举报