跳至主要內容

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

张威大约 10 分钟c/c++操作系统堆栈

栈空间

image-20240130155633380
image-20240130155633380

栈空间是从高地址向低地址扩充,堆地址是从低地址向高地址扩充。

堆栈是一种具有一定规则的,我们可以按照一定的规则进行添加和删除数据。它使用的是先进后出的原则。在x86等汇编集合中堆栈与弹栈的操作指令分别为:

  • PUSH:将目标内存推入栈顶。

  • POP:从栈顶中移除目标。

ESP和EBP

image-20240130155845449
image-20240130155845449

  • 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 #反汇编、与相关联的源代码交替并且以英特尔的框架显示🍗🍗🍗

image-20240130173737985
image-20240130173737985

问题1.sum函数调用完,如何知道回到main函数 ?

问题2.回到main函数,如何知道从哪一行开始?

  • call:函数调用指令

    1. 把下一行指令的地址(位于.text段)压栈(问题2)
    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函数的入口和退出:{ 会进行入栈操作,}进行出栈操作

img
img
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)之前会进行下述操作

img
img

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

main函数内

image-20240130182232318
image-20240130182232318

接下来是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的值压入堆栈

image-20240130182849062
image-20240130182849062

执行call sum (0F8108Ch) #执行call

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

image-20240130183016130
image-20240130183016130

第二步进入函数调用:sum

img
img

函数调用第一步: 将调用函数(main)函数的栈底指针ebp压栈

第二步:将新的栈底ebp指向原来的栈顶esp

第三步:将esp指向新的栈顶(开辟了函数的栈帧):大小:0cch

img
img

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

img
img

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

img
img

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

img
img
  1. mov esp,ebp 回退栈帧 将栈顶指针指向栈底

  2. pop ebp 栈顶出栈,并将出栈内容赋值给ebp,也是将main的栈底重新赋值给ebp

  3. ret

接着调用函数完毕,回到主函数: 利用了PC寄存器,使得程序知道退出sum后运行哪一条指令:

image-20240130203609288
image-20240130203609288
img
img

最后return 0,程序结束

栈空间大小

  • linuxulimit -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
image-20240130204306887
image-20240130204306887

栈溢出

出现栈内存溢出的常见原因有2个:

  1. 函数调用层次过深,每调用一次,函数的参数、局部变量等信息就压一次栈。
  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];
    }
   大多数情况下都会出现内存溢出的错误,

解决办法大致说来也有两种:

  1. 🍗