抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

栈作为重要的数据结构,主要用于管理程序的执行过程中的函数调用和局部变量。它通过后进先出的原则存储和管理数据,包括函数的参数、返回地址、局部变量等。

重要概念

栈在数据结构中是一种运算受限的线性表,即我们只能对表尾进行操作,称之为栈顶,相对的,另一端称为栈底。它按照先进后出的原则存储数据,先进入的数据保存在栈底,后来的数据保存在栈顶。栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,存储了函数调用时所需要使用的信息。

EBP

栈底指针

ESP

栈顶指针

call

汇编语言,调用函数时需要使用call来调用

retn

函数调用结束后返回。当retn指令执行时是将esp指向地址的值弹出到EIP寄存器中,表示返回到调用函数的下一条指令。

开辟栈帧

1
2
3
4
5
6
7
8
9
10
;栈帧结构
PUSH EBP ;函数开始(使用EBP前先把已有值保存到栈中)
MOV EBP, ESP ;保存当前ESP到EBP中(保存返回地址)

... ;函数体
;无论ESP值如何变化,EBP都保持不变,可以安全访问函数的局部变量、参数

MOV ESP, EBP ;将函数的起始(返回)地址给到ESP
POP EBP ;函数返回前弹出保存在栈中的值
RETN ;函数终止,返回到调用前的下一条指令

函数调用约定

在栈帧的开辟中涉及到函数的调用。例如有一个函数

1
int test(int a,int b);

编写代码时我们肯定理解这个函数如何传参,传几个参数。但是CPU怎么才能知道这个函数有几个参数,参数的值是什么,为了解决这些问题,计算机提供了一种被称为栈的数据结构来支持参数传递。

但是还有问题没有解决,当函数传递参数时,按照什么样的顺序传参,是从左到右还是从右到左,调用结束后如何清除堆栈,由调用者清除还是被调用者清除以及寄存器如何使用。这时就提出了函数调用约定来解决这个问题

stdcall

声明的语法为:

1
int __stdcall function(int a,int b);

stdcall的调用约定意味着:

  • 参数从右向左压入堆栈
  • 函数自身清理堆栈
  • 函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸

cdecl

是C和C++默认的调用约定

声明的语法:

1
2
int function (int a ,int b); //不加修饰就是C调用约定 
int __cdecl function(int a,int b);//明确指出C调用约定

cdecl调用约定意味着:

  • 参数从右向左压入堆栈
  • 调用者负责清理堆栈
  • C调用约定允许函数的参数的个数是不固定的,这也是C语言的一大特色。
  • 仅在函数名前加上一个下划线前缀,格式为_functionname。

fastcall

声明语法:

1
int fastcall function(int a,int b);

fastcall调用约定意味着:

  • 函数的第一个和第二个DWORD参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过从右向左的顺序压栈
  • 函数自身清理堆栈
  • 函数名修改规则同stdcall:函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。

thiscall

C++类成员函数缺省的调用约定

thiscall是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。由于成员函数调用还有一个this指针,因此必须特殊处理,thiscall意味着:

  • 参数从右向左入栈
  • 如果参数个数不确定,this指针在所有参数压栈后被压入堆栈。如果参数个数确定,this指针通过ecx传递给被调用者。
  • 如果参数个数不确定,调用者清理堆栈,否则函数自己清理堆栈

对于参数个数固定情况下,类似于stdcall,不定时则类似cdecl

naked call

一个很少见的调用约定,一般用于实模式驱动程序设计