栈帧学习笔记

前置知识

磁盘中的ELF与内存中的ELF

比较重要的是stack,shared libraries和heap三项

其中stack是栈,他的元素保存方法是从高地址到低地址保存
heap是堆,他的元素保存方法是从低地址到高地址保存
shared libraries是共享函数库,好像pwn手用的比较多,我这个re手就直接把他跳过吧

大端序与小端序

小端序存储即数据的高位字节保存在内存的高地址中,低位字节则保存在内存的低地址中。大端序反之。

现在我们主要使用的格式是小端序存储,此处重点分析小端序存储。

这是个很直观的小端序存储方式。我们再举一个在IDA里经常能观察到的例子。
比如我们看到这样的初始化
v 7 [ 0 ] = 1734437990 v7[0] = 1734437990 v7[0]=1734437990;
v 7 [ 1 ] = 1801545339 v7[1] = 1801545339 v7[1]=1801545339;
v 7 [ 2 ] = 1818648421 v7[2] = 1818648421 v7[2]=1818648421;
v 7 [ 3 ] = 2099341153 v7[3] = 2099341153 v7[3]=2099341153;
以v7[0]为例,该数字在内存中会表示为十六进制储存,即 0 x 67616 c 66 0x67616c66 0x67616c66
在地址中应2个字节2个字节存储,即 67 61 6c 66
那么由于是小端序储存,现在倒过来看这4个数字,即66 6c 61 67
转换成10进制即102 108 97 103
再转化为ascii码,即f l a g
以此类推,v7所储存的整数在转化为字符型后,所得的字符串为flag{fake_flag}

基本寄存器类型

首先是寄存器们的结构

rax: 8Bytes // r开头是64位的
eax: 4Bytes // e开头是32位的
ax:  2Bytes // 没有开头的是16位的
ah:  1Bytes // h是寄存器高位的那一半
al:  1Bytes // l是寄存器低位的那一半

然后是一些常见寄存器的功能

  • e i p eip eip: 存放当前执行的指令的地址
  • e s p esp esp: 存放当前栈帧的栈顶地址
  • e b p ebp ebp: 存放当前栈帧的栈底地址
  • e a x eax eax:通用寄存器,存放函数的返回值

和栈相关的重要汇编指令

  1. LEA REG, SRC:把源操作数的有效地址存放到指定的寄存器中
  • LEA EBX, ASC:把ASC的地址存放到EBX寄存器中
  • LEA EAX, 6[ESI]:把ESI+6的32位地址存放到EAX寄存器中
  1. PUSH VALUE:把目标值压栈,同时SP指针-1字长
  2. POP DEST: 将栈顶的值弹出至目的存储位置,同时SP指针+1字长
  3. LEAVE:在函数调用的末尾(即函数return的时候)恢复父函数(上一个函数)的栈帧指令
  • 可以理解为两句汇编的整合:
    • MOV ESP EBP // 相当于把栈空间初始化了,虽然保存的数据没删除,但是后面可以直接对其覆盖操作,没有影响
    • POP EBP // 恢复栈帧
  1. RET:在函数返回时,控制程序执行流返回父函数的指令
    *可以理解为 POP EIP(这条指令只是帮助理解,实际上并不能对EIP进行直接的操作)

函数调用栈

重点来了

基本概念

函数调用栈(stack)是指程序运行时内存中一段连续的区域,这段区域用来保存函数运行时的状态信息,包括函数的参数和局部变量等。

根据栈的特性,在发生函数调用时,调用函数(我们之后统称为"Caller")的状态被保存在调用栈内,被调用函数(我们之后统称为"Callee")的状态被压入调用栈的栈顶。

在函数调用结束,即Callee函数return时,栈顶函数(Callee)的状态被弹出,栈顶恢复到Caller的状态。

这里需要注意:由于函数调用栈在内存中从高地址向低地址变化,所以栈顶对应的内存地址在压栈时变小,退栈时变大。

操作流程

首先明确一点,函数状态主要涉及到三个寄存器: e s p esp esp e b p ebp ebp e i p eip eip。上面提到过, e s p esp esp 用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。 e b p ebp ebp 用来存储当前函数状态的栈底(或者说基地址),在函数运行时不变,可以用来索引确定函数参数或局部变量的位置。 e i p eip eip 用来存储即将执行的程序指令的地址,cpu 依照 e i p eip eip 的存储内容读取指令并执行, e i p eip eip 随之指向相邻的下一条指令,如此反复,程序就得以连续执行指令。

下面来看看函数调用时,栈的状态以及寄存器的变化。记住一点,这个过程的核心任务是将Caller的状态保存,并创建Callee的状态。

  1. 首先将Callee的参数按照逆序依次入栈(还记得小端序吗?),当然,如果Callee没有参数,则忽略这一步骤。注意一点,这些参数是作为Caller而非Callee的函数状态而保存起来的。之后入栈的数据则会作为Callee的函数状态保存。

  1. 然后,将Caller进行调用的下一条指令地址作为返回地址入栈。这样,Caller的 e i p eip eip信息得以保存。

  1. 再将当前 e b p ebp ebp的值入栈,并将 e b p ebp ebp的值更新为当前栈顶地址。这样,Caller的 e b p ebp ebp信息得以保存(以便后续找到Caller的栈底从而恢复Caller的函数状态),同时,对 e b p ebp ebp的更新相当于为Callee开辟了新的栈空间( e s p esp esp e b p ebp ebp指向同一地址,可以理解为当前栈(指Callee的栈)为空)

  2. 之后将Callee的局部变量等数据入栈

  3. 这之后的入栈过程中, e s p esp esp的值不断减小,对应着栈从高地址向低地址变化。入栈的数据包括调用参数、返回地址、Caller的栈底以及局部变量。之前提到过,除调用参数外的数据共同组成Callee的函数状态。在发生调用时,程序还会将Callee的指令地址存到 eip 寄存器内,这样程序就可以依次执行被调用函数的指令了。

    现在,我们应该很好理解函数调用结束后的变化了。这个阶段的任务是丢弃Callee的状态并复原Caller的状态。

  4. 首先,Callee的局部变量被弹出,栈顶指向Callee的栈底

  1. 然后,将储存的Caller的 e b p ebp ebp(栈底地址)弹出,并存储到当前的 e b p ebp ebp内,这样,Caller的栈底信息得以恢复,然后栈顶指向返回地址

  2. 将返回地址弹出,并存储到 e i p eip eip中,这样Caller的指令信息得以恢复。

    至此,Caller的函数状态已全部恢复。

一次形象的实践

这个是我们的源代码

int callee(int a, int b, int c) {
    return a + b + c;
}

int caller(void) {
    int ret;
    ret = callee(1, 2, 3);
    ret += 4;
    return ret;
}

这个是对应的汇编(尝试下amd64的格式)

00000012 <caller>:
  12:   55              push   %ebp
  13:   89 e5           mov    %esp,%ebp
  15:   83 ec 10        sub    $0x10,%esp
  18:   6a 03           push   $0x3
  1a:   6a 02           push   $0x2
  1c:   6a 01           push   $0x1
  1e:   e8 fc ff ff ff  call   1f <caller+0xd>
  23:   83 c4 0c        add    $0xc,%esp
  26:   89 45 fc        mov    %eax,-0x4(%ebp)
  29:   83 45 fc 04     addl   $0x4,-0x4(%ebp)
  2d:   8b 45 fc        mov    -0x4(%ebp),%eax
  30:   c9              leave
  31:   c3              ret

00000000 <callee>:
   0:   55          push   %ebp
   1:   89 e5       mov    %esp,%ebp
   3:   8b 55 08    mov    0x8(%ebp),%edx
   6:   8b 45 0c    mov    0xc(%ebp),%eax
   9:   01 c2       add    %eax,%edx
   b:   8b 45 10    mov    0x10(%ebp),%eax
   e:   01 d0       add    %edx,%eax
  10:   5d          pop    %ebp
  11:   c3          ret

我们一步一步执行上述汇编,看看栈是怎么变化的。

注意,以下步骤是遵循_cdecl调用约定的

  1. 把调用Caller函数的函数的 e b p ebp ebp入栈

  1. 把当前的 e b p ebp ebp的值置为 e s p esp esp,相当于开辟一个新的栈

  1. 把局部变量入栈,并且 e s p esp esp地址偏移16位,剩下12位空间可能编译器拿去干其他事情了?此处我们就认为他是未使用的空间吧(反正编译出来就是这样的。。有无懂哥教一下这个未使用空间具体是干啥用的。。)

  1. 把传参入栈

  1. 调用Callee,注意call在执行时会把返回地址直接入栈

  1. 同caller

  1. 这些指令可以对照源代码理解,需要注意的是,此处的0x8 0xc 0x10是偏移量,按照此偏移量可以找到以前的传参,eax edx均为通用寄存器,此处eax保存了和的值

  1. 弹栈, e b p ebp ebp恢复

  1. 执行ret,恢复 e i p eip eip(还记得ret可以看做pop eip吗)

  1. 注意,现在已经返回到Caller了,继续跟着 e i p eip eip执行,此处让 e s p esp esp偏移12位,相当于在栈中清除了传递的参数

  1. 此处是源码中的ret += 4;,很好理解

  1. leave可以理解为两句汇编的整合:MOV ESP EBPPOP EBP


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值