author: bilala
0x00 前言
栈是一个先入后出的结构,如果还不了解的话得先自己百度看看。
本篇在示例中详细解析了汇编层面的指令执行过程,且画了图帮助理解,所以理论部分看的不是很懂的话也不要紧,看示例时候跟一跟就能明白了。
0x01 函数的调用
众所周知,当一个子函数要被调用时,他就会入栈,即push son_fun
,执行完成后就会pop
出栈。这里还要强调的一点是栈从高地址往低地址生长,接下来看一张图
调用者的帧就是父函数的栈帧,第一个省略号(最上边的)就是正常执行时的栈,当我们在调用子函数时,才开始下面的这些操作,也就是从“参数n”开始都和调用子函数有关,操作如下:
- 父函数将子函数的参数从后向前压栈 ,也就是图中的参数n到参数1
- 将返回地址压栈保存,对应图中“返回地址”。当子函数调用结束后,我们要告诉程序接下来要回到哪里继续执行命令,这个哪里就是返回地址的地址值,这也是栈溢出实现的关键地方
- 跳转到子函数起始地址开始执行
- 子函数将父函数栈帧的起始地址入栈,对应着图中的“上一栈帧的%rbp”。注意此时的%rbp指着“调用者栈帧”的栈底,此时的%rsp指向“上一栈帧的%rbp”。
- 将%rbp的值设置为%rsp的值,也就是将%rbp也指向了“上一栈帧的%rbp”
- 开始执行子函数里的相关内容
上述过程中,保存返回地址和跳转到子函数处执行由 call 一条指令完成,在call 指令执行完成时,已经进入了子程序中,因而将上一栈帧%rbp 压栈的操作,需要由子程序来完成。
汇编层面的指令如下:
... # 参数压栈
call FUNC # 将返回地址压栈,并跳转到子函数 FUNC 处执行
... # 函数调用的返回位置
FUNC: # 子函数入口
push %rbp # 保存旧的帧指针,相当于创建新的栈帧
mov %rsp, %rbp # 让 %rbp 指向新栈帧的起始位置
sub $N, %rsp # 在新栈帧中预留一些空位,供子程序使用,用 (%rsp+K) 或 (%rbp-K) 的形式引用空位
0x02 函数的返回
函数返回时,其返回值存储在%rax中。获取返回值后,只需将栈结构恢复到进入子函数前的结构即可。
操作如下:
- 将%rsp指向%rbp,即两个指针指向同个位置:子函数栈帧的栈底
- 弹出此栈。%rbp回到上一栈帧的%rbp,%rsp往高地址移一个位置,即指向了“返回地址”处
x86-64 架构中提供了 leave 指令来实现上述两步操作的功能。执行leave后,栈帧如下
leave对应的汇编指令如下:
movq %rbp, %rsp # 使 %rsp 和 %rbp 指向同一位置,即子栈帧的起始处
popq %rbp # 将栈中保存的父栈帧的 %rbp 的值赋值给 %rbp,并且 %rsp 上移一个位置指向父栈帧的结尾处
调用leave后一般都会跟一个ret指令,作用就是从当前 %rsp 指向的位置(即栈顶)弹出数据,并跳转到此数据代表的地址处,在图中就是跳转到“参数1”处,这时程序会从返回地址处的地方开始继续执行,而这些父栈帧尾部存储的调用参数由编译器自动释放。
0x03 函数调用示例
先写一个.c文件
#include <stdio.h>
#include <stdlib.h>
int add(int a,int b){
int res = a+b;
return res;
}
int main()
{
int data1;
int data2;
int result;
data1 = 111;
data2 = 222;
result = add(data1, data2);
printf("result: %d", result);
return 0;
}
利用gcc -fno-stack-protector -no-pie test.c -o test
编译成可执行文件
然后在gdb中打下断点b main
,然后r
运行,在main处停下
因为main本身也是一个函数,程序在运行到这个函数前做了很多初始化操作,所以各个通用寄存器中都已经有值了。
在disasm中箭头指向的就是下一条要执行的汇编指令,可以看到RIP寄存器中存储了这条指令的地址,此时的栈是这样的
画个图就是这样
然后回看刚刚那条 0x401144 sub rsp, 0x10
的指令
意思都知道是rsp的值减掉0x10,作用是为当前的栈帧开辟一段新空间。
按下s
执行这条指令,可以看到
这里可以留意一下这三个指针寄存器的变化,顺便回顾一下这三个寄存器的作用,此时图如下
开辟完空间后,下两条命令就是将两个int整数的常量保存到内存中,0x6f=111
, 0xde=222
运行掉这两条mov
命令后,我们再来看栈的变化
此时我们需要运行的两个数就已经被放到了内存中,可以使用x /8wx 0x7fffffffe2f0
命令查看
剩下的4个mov就是将这个值移来移去了,不重要,我们一路s
命令跟到call
指令
此时的下一步就要进入到子函数add
了,我们继续按下s命令,查看栈的变化
根据我们刚刚讲的,第一步先将返回地址入栈(因为add函数中的参数都是常量了,所以跳过参数入栈这一步),也就是call的下一条命令的地址0x401165
,此时图为
这时候我们继续s往下走,执行push命令,也就是将main栈帧的rbp入栈
图为:
然后再下一步把rbp指向当前的rsp,s查看结果
图为:
之后几条命令就是add子函数的内容了,这里不作详细解析,就是最终结果存储到的eax中
再一路跟进到pop处,这里就是涉及到函数的返回了,刚刚的函数调用过程和我们理论分析时一模一样,再看看返回函数会怎么做
add函数由于不需要空间,所以整个函数过程中,mov、add这些执行时,rbp和rsp指的位置都没变,都还在0x7fffffffe2e0
处
在pop还未执行时停住
pop执行后,rbp的值会变成当前所在地址的值,也就是上一栈帧的rbp,然后rsp也会往高地址回一个位置
按下s看看栈是否如上述一般变化
完全没毛病,此时图为
然后ret
指令,就是rsp往高地址回一个位置,程序回到返回地址的值处继续走,图为
pwndbg中也可以看到,程序又回到了main函数的地方,子函数调用结束
Comments | NOTHING