指令角度理解堆栈调用过程
栈空间

栈空间是从高地址向低地址扩充,堆地址是从低地址向高地址扩充。
堆栈是一种具有一定规则的,我们可以按照一定的规则进行添加和删除数据。它使用的是先进后出的原则。在x86等汇编集合中堆栈与弹栈的操作指令分别为:
PUSH
:将目标内存推入栈顶。POP
:从栈顶中移除目标。
ESP和EBP

esp
:当前函数栈顶指针;来标记栈的底部,他随着栈的变化而变化ebp
:当前函数栈底指针(虚拟内存空间中);通过固定的地址与偏移量来寻找在栈参数与变量
ESP是可变的,随着栈的生产而逐渐变小(因为栈向低地址扩充,栈顶寄存器数值不断变小),而EBP寄存器是固定的,。
pop ebp;出栈 栈扩大4byte 因为ebp为32位
push ebp;入栈,栈减少4byte
add esp, 0Ch;表示栈减小12byte
sub esp, 0Ch;表示栈扩大12byte
🍗🍗🍗示例
#include <iostream>
int sum(int a, int b)
{
int temp = 0;
temp = a + b;
return temp;
}
int main()
{
int a = 10;// mov dword ptr[ebp - 4], 0Ah
int b = 20;// mov dword ptr[ebp - 8], 14h
int ret = sum(a, b);//取a,b的值,放入寄存器,压入sum函数的栈(esp从main函数栈顶,上移两个int的位(分别放形参a,b)变成sum函数的栈顶)
return 0;
}
打断点,调试,查看反汇编:
g++ main.cc -m32 -g -o main.o #-m32指定编译为32位程序
gdb main.o
(gdb) b main
(gdb) start
(gdb) set disassembly-flavor intel #在windows下使用习惯了intel汇编,在Linux下看的难受,在gdb下使用
(gdb) disassemble /mr #/m 显示相关联的源代码;/r 显示具体值
可能会报错如下
$g++ -m32 -g -o main.o main.cc In file included from main.cc:6:0: /usr/include/c++/7/iostream:38:10: fatal error: bits/c++config.h: No such file or directory #include <bits/c++config.h> ^~~~~~~~~~~~~~~~~~ compilation terminated.
只需要安装缺少的库即可
sudo apt-get install g++-multilib
(gdb) disassemble /m sum
Dump of assembler code for function sum(int, int):
9 {
0x565555dd <+0>: push ebp
0x565555de <+1>: mov ebp,esp
0x565555e0 <+3>: sub esp,0x10
0x565555e3 <+6>: call 0x565556b7 <__x86.get_pc_thunk.ax>
0x565555e8 <+11>: add eax,0x19e8
10 int temp = 0;
0x565555ed <+16>: mov DWORD PTR [ebp-0x4],0x0
11 temp = a + b;
0x565555f4 <+23>: mov edx,DWORD PTR [ebp+0x8]
0x565555f7 <+26>: mov eax,DWORD PTR [ebp+0xc]
0x565555fa <+29>: add eax,edx
0x565555fc <+31>: mov DWORD PTR [ebp-0x4],eax
12 return temp;
0x565555ff <+34>: mov eax,DWORD PTR [ebp-0x4]
13 }
0x56555602 <+37>: leave
0x56555603 <+38>: ret
End of assembler dump.
(gdb) disassemble /m main
Dump of assembler code for function main():
16 {
0x56555604 <+0>: push ebp
0x56555605 <+1>: mov ebp,esp
0x56555607 <+3>: sub esp,0x10
0x5655560a <+6>: call 0x565556b7 <__x86.get_pc_thunk.ax>
0x5655560f <+11>: add eax,0x19c1
17 int a = 10;
=> 0x56555614 <+16>: mov DWORD PTR [ebp-0xc],0xa
18 int b = 20;
0x5655561b <+23>: mov DWORD PTR [ebp-0x8],0x14
19 int ret = sum(a, b);
0x56555622 <+30>: push DWORD PTR [ebp-0x8]
0x56555625 <+33>: push DWORD PTR [ebp-0xc]
0x56555628 <+36>: call 0x565555dd <sum(int, int)>
0x5655562d <+41>: add esp,0x8
0x56555630 <+44>: mov DWORD PTR [ebp-0x4],eax
20
21 return 0;
0x56555633 <+47>: mov eax,0x0
22 }
0x56555638 <+52>: leave
0x56555639 <+53>: ret
End of assembler dump.
g++ -g -c main main.cc
#前提编译时 加-g
objdump -d main.o
#反汇编
objdump -M intel -S main.o
#反汇编、与相关联的源代码交替并且以英特尔的框架显示🍗🍗🍗

问题1.sum函数调用完,如何知道回到main函数 ?
问题2.回到main函数,如何知道从哪一行开始?
call
:函数调用指令- 把下一行指令的地址(位于.text段)压栈(问题2)
- 进入调用函数(sum)
进入sum函数之后,把esp的位置压栈(问题1),然后esp从main函数,上移到ebp位置(esp=ebp),并为sum函数开辟栈帧,有的编译器(windows)会为开辟的栈帧中初始化为0xCCCCCCCC(如果此类编译器如果允许访问未初始化的值,那么打印出来可能就是此值)
sum函数后ebp回到esp位置(
mov ebp esp
),开辟的栈空间返回给系统;把栈的值出栈,给esp(,即回到main函数栈底)再出栈(将出栈内容,call的1所存入的值,放入CPU的PC寄存器),形参的地址归还给系统
在main函数的入口和退出:{ 会进行入栈操作,}进行出栈操作

9 {
0x565555dd <+0>: push ebp
0x565555de <+1>: mov ebp,esp
0x565555e0 <+3>: sub esp,0x10
上面两句话的意思是将ebp推入栈中,之后让esp等于ebp
为什么这么做呢?因为ebp作为一个用于寻址的固定值是有时间周期的。只有在某个函数执行过程中才是固定的,在函数调用与函数执行完毕后会发生改变。
在函数调用之前,将调用者的函数(caller)的ebp存入栈,以便于在执行完毕后恢复现场是还原ebp的值。下一步,必须为它的局部变量分配空间,同时,也必须为它可能用到的一些临时变量分配空间。
sub esp, 0E4h;
之后会根据情况看是否保存某些特定的寄存器(EBX,ESI和EDI)
之后ebp的值会保持固定。此后局部变量和临时存储都可以通过基准指针EBP加偏移量找到了
在函数执行完毕,控制流返回到调用者的函数(caller)之前会进行下述操作

所谓有始有终,这是会还原上面保存的寄存器值(edi esi ebx),之后还原esp的值(上一个函数调用之前的esp被保存在固定的ebp中)与ebp值。这一过程被称为还原现场之后通过ret返回上一个函数
main函数内

接下来是int ret = sum(a,b)
:
17 int a = 10;
=> 0x56555614 <+16>: mov DWORD PTR [ebp-0xc],0xa
18 int b = 20;
0x5655561b <+23>: mov DWORD PTR [ebp-0x8],0x14
19 int ret = sum(a, b);
0x56555622 <+30>: push DWORD PTR [ebp-0x8] #b
0x56555625 <+33>: push DWORD PTR [ebp-0xc] #a
0x56555628 <+36>: call 0x565555dd <sum(int, int)>
0x5655562d <+41>: add esp,0x8
0x56555630 <+44>: mov DWORD PTR [ebp-0x4],eax
函数调用参数的压栈顺序:参数由右向左压入堆栈。
先将b的值压入堆栈,再将a的值压入堆栈

call sum (0F8108Ch) #执行call
:
执行call函数首先会将下一行执行的地址入栈:假设下一行指令的地址位0x08124458

第二步进入函数调用:sum

函数调用第一步: 将调用函数(main)函数的栈底指针ebp压栈
第二步:将新的栈底ebp指向原来的栈顶esp
第三步:将esp指向新的栈顶(开辟了函数的栈帧):大小:0cch

temp = a + b;
由于a,b的值之前入栈,可以通过ebp+12字节找到b的值,ebp+8字节找到a的值,最后将运算结果赋值给temp

接着运行return temp;
: mov eax,dword ptr [temp]

接着是函数的右括号“}”

mov esp,ebp
回退栈帧 将栈顶指针指向栈底pop ebp
栈顶出栈,并将出栈内容赋值给ebp,也是将main的栈底重新赋值给ebpret
接着调用函数完毕,回到主函数: 利用了PC寄存器
,使得程序知道退出sum后运行哪一条指令:


最后return 0
,程序结束
栈空间大小
- linux
ulimit -s 16384
将把默认栈大小设置为16384 KB(或16MB)(以 kbytes 为单位)。- 这个设置仅对,当你重新启动终端时,它会恢复为系统默认值。
- 如果你希望更改,你可能需要编辑
/etc/security/limits.conf
文件,并添加相应的条目。
zw 20:41:13 ~
$ulimit -s
8192
zw 20:41:19 ~
$ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7558
max locked memory (kbytes, -l) 65536
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7558
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited

栈溢出
出现栈内存溢出的常见原因有2个:
- 函数调用层次过深,每调用一次,函数的参数、局部变量等信息就压一次栈。
- 局部静态变量体积太大
第一种情况不太常见,因为,所以只要不出现无限制的调用都应该是没有问题的,起码深度几十层我想是没问题的。检查是否是此原因的方法为,在引起溢出的那个函数处设一个断点,然后执行程序使其停在断点处, 然后按下快捷键Alt+7调出call stack窗口,在窗口中可以看到函数调用的层次关系。
第二种情况比较常见 在函数里定义了一个局部变量,是一个类对象,该类中有一个大数组
即如果函数这样写:
void test_stack_overflow()
{
char* chdata = new[2*1024*1024];
delete []chdata;
}
是不会出现这个错误的,而这样写则不行:
void test_stack_overflow()
{
char chdata[2*1024*1024];
}
大多数情况下都会出现内存溢出的错误,
解决办法大致说来也有两种:
- 🍗