【逆向工程】一文搞懂:栈与调用约定

【逆向工程】一文搞懂:栈与调用约定

本文将会用一个实际例子结合IDA pro工具对程序运行中栈的特点进行分析。

字节序(Endianness)

在进行进一步学习之前 ,我们先了解一些基本概念。在计算机中,不同数据类型占用不同大小字节,例如一个int类型占用4字节,一个char类型仅占用1字节。可以想象一下,一个字节就是一个小盒子,里面存放了8个bit的数据。对于一个int类型,要连续的4个盒子才能表达出完整的信息,而对于char类型,只需要一个盒子。

计算机只认识0与1的二进制数据,但对于我们人类来说,只使用二进制表示数据会显得冗长,因此,我们通常喜欢使用十六进制(hex)表示数据。例如,若使用二进制表示十进制数256,就要写成000100000000,而十六进制表示的话只用写100

占用多个字节的数据要存放在内存中,计算机就要决定低位字节和高位字节在内存中的排序方式。(也就是说计算机要决定堆放这些盒子的顺序)

常见的排序方式有两种

小端序(Little Endian)

规则:位字节存放在地址处,位字节存放在地址处

例如:整数 0x12345678 在内存中是 78 56 34 12

应用范围:Intel x86、AMD 架构都采用小端序,在使用IDA pro进行逆向分析时,查看程序的hex表示,也都是小端序。

大端序(Big Endian)

规则:位字节存放在地址处,位字节存放在地址处

例如:整数 0x12345678 在内存中是 12 34 56 78

应用:网络传输协议(TCP/IP)通常使用大端序

实例演示

创建一个.c文件并编译为.exe文件,随后在IDA pro中反编译。

#include <stdio.h>

int main() {
  int a = 0x12345678;
  printf("%d", a);
  return 0;
}

在这里插入图片描述

进程与线程

进程(Process)

进程是程序的一次运行实例,是操作系统分配资源(如内存、文件句柄等)的基本单位

  • 每个进程有自己独立的内存空间系统资源

  • 一个进程崩溃不会直接影响其他进程。

通俗来说,你打开的每一个软件(例如,QQ,Edge浏览器等等)都算是独立的进程

线程(Thread)

线程是进程中的最小执行单元,它运行在进程的资源环境中,是CPU调度的基本单位。

  • 同一进程内的多个线程共享内存空间,但每个线程有自己的栈和寄存器状态

多个线程在一个进程中完成任务

为什么需要栈

调用函数时,程序需要保存开始调用函数时的环境,以便函数执行完毕后能够正确返回,继续程序执行的逻辑。

现在,我们来看一个嵌套调用的情况。

int function_3(int a,int b)
{
    return a+b;
}
int function_2(int a,int b)
{
    return function_3(a,b);
}

int function_1(int a,int b)
{
    return function_2(a,b);
}

当我调用函数function_1,它需要执行function_2function_2需要执行function_3,在函数function_3中执行a+b的逻辑,并开始返回。返回的时候,需要先返回到function_2,紧接着是function_1

也就是说,function_1是第一个被调用的,但function_1也是最后一个返回的。我们需要管理这个过程,就需要一个先进后出的逻辑。因此,在程序中使用来管理函数调用相关工作。

当一个进程创建时,操作系统会为它分配一套独立的虚拟地址空间,其中就包括栈空间,每个进程的栈空间是独立的,互不干扰。

栈的作用

规范来说,栈的作用有如下

  1. 保存函数调用信息(包括返回地址、参数、局部变量)
  2. 调用函数时传递参数

栈的特征

  1. 先进后出(LIFO):最后压入栈的数据最先被弹出。
  2. 从高地址向低地址增长(在x86架构中):这是一种约定,方便硬件实现。

这一小节,主要介绍了为什么需要使用栈,下一小节将介绍栈的工作机制(重点)

栈帧

定义

当一个函数被调用时,系统会在栈上为它划出一片独立的区域,用来保存它执行所需的全部信息,这个区域就叫作 栈帧(Stack Frame)

栈帧结构

push ebp
mov  ebp, esp
....
mov esp, ebp
pop ebp
ret

这就是栈帧技术管理函数调用。

在函数调用过程中,CPU 需要知道“栈顶在哪里”“当前函数的栈范围有多大”,这两件事分别由两个寄存器负责:

  • ESP(Stack Pointer,栈顶指针)
    始终指向当前栈顶的位置,也就是下一次 push(入栈) 或 pop(出栈) 操作的目标位置
    栈是向低地址方向增长的,因此每次入栈(push)都会让 ESP 减小,出栈(pop)时 ESP 增大
  • EBP(Base Pointer,基址指针)
    用来标记当前函数栈帧的起始位置
    它相对稳定,不会像 ESP 那样频繁变化,因此函数中的局部变量、参数都能通过固定的偏移量相对 EBP 来访问。

栈的工作机制

编写一个简单的文件

#include <stdio.h>

__attribute__((noinline)) int add(int a, int b) { return a + b; }

int main() {
  int x = add(2, 3);
  printf("%d", x);
  return 0;
}

gcc -fno-omit-frame-pointer -O0 -g test.c -o test编译时关闭优化,保留完整的栈帧结构

在这里插入图片描述

这个就是add函数的栈帧结构

在一个进程中,每当我们调用一个函数时,就会在这个进程中的栈空间里开辟一个新的栈帧。

push rbp
mov rbp,rsp

这两条语句就是起到了建立新栈帧空间的作用

push rbp时,做了两件事。

第一件事,rsp=rsp-8,这是为保存数据挪出空间,因为栈是向低地址增长的,因此要减8。

第二件事,就是把rbp的值写入栈中。

注意,这里并不是把当前函数的返回地址写入,返回地址的写入是执行call指令时,CPU自动就会将返回地址压入栈中。

这里其实隐含了一种“递归”的思想,我们来重新梳理一下,当我们开始执行任何一个函数时(当前是执行了add函数),就会重新建立一个新的栈帧,push rbp里的rbp就是上一个栈帧结构的基址,我们要在上一个栈帧结构的基础上建立一个新的栈帧。也就是说,我们不关心上一个栈帧到底是什么,只需要得到它的基址就行。

mov rsp,rbp
pop rbp
retn

这三条语句的效果就显而易见了,先将函数栈帧的起始地址返回到rsp中,此时rsp就指向了栈帧起始地址。

pop rbp也干了两件事

第一件事,先弹出rbp的值

第二件事,rsp向上移动8个字节

现在,调用者的栈环境完全恢复,RBP、RSP 都回到了函数调用前的状态。

retn是函数返回指令,由 CPU 自动完成以下动作:

  1. 从当前栈顶([rsp])取出 返回地址(就是调用时 call 自动压入的那一条);

  2. 把这个地址加载到 指令指针寄存器(RIP)
    让程序跳转回调用者的下一条指令处继续执行

  3. 同时 rsp 再向上移动 8 字节(弹出返回地址)。

回到文章最初那个例子

int function_3(int a,int b)
{
    return a+b;
}
int function_2(int a,int b)
{
    return function_3(a,b);
}

int function_1(int a,int b)
{
    return function_2(a,b);
}

当调用function_1时,它会在栈空间上开辟一个新的栈帧空间,而这个栈帧空间的起始地址就是上一个函数的基址(保底是main()函数,也可能是其他函数,不重要),然后function_1调用了function_2,就会继续开辟一个新的栈帧空间,此时起始地址就是function_1的基址,如此进行下去。这样最终返回的时候,就能保证按顺序返回。

函数调用约定

调用者与被调用者

  • 调用者:调用函数的一方
  • 被调用者:被调用的函数

例如,在main()函数中调用printf()函数,调用者就是main()函数,被调用者就是printf()函数。

细心的你也许注意到了前文反汇编图片中,add函数前面有一个cdecl,这其实是一种函数调用约定,规定函数调用时如何传递参数。这类约定有三种类型,分别是cdecl stacall fastcall

cdecl(C Declaration)

C 语言默认调用约定(大多数编译器默认)。

  • 参数传递:通过从右到左依次压入。
  • 栈清理者调用者(Caller) 负责清理参数。

stdcall

Windows API 中的常见约定。

  • 参数传递:同样通过栈从右到左压入。
  • 栈清理者被调用者(Callee) 清理参数。

fastcall

用于提高性能的调用方式。

  • 参数传递:前两个参数通过寄存器传递(通常是 ECX, EDX),其余通过栈。
  • 栈清理者:一般仍由被调用者清理。

至此,你已了解了栈的基本工作原理如果想更直观地理解,可以访问我的 GitHub和B站,我使用 Manim 制作了栈帧动画演示。觉得有帮助的话,欢迎给我一个 Star!✨

Github:https://github.com/wasim404/manim_animation.git

谢谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值