0%

浅谈栈与调用惯例

  最近看程序员自我修养-链接、装载与库,到了内存分布章节。虽说对于学习软件安全的人来说,栈的分布和函数调用规则已是老生常谈的事,但是,这次耐着性子看下去果然还是有新的收获。


  直接上图,Linux下经典内存结构布局(图片取自程序员自我修养):

stack0fLinux

其中箭头标明了一般情况下的增长方向,地址仅针对2.4.x内核版本。

  栈中保存函数调用时的一些信息,包括:

  • 函数返回地址和参数
  • 临时变量
  • 保存的上下文(某些需要保持不变的寄存器)

  在i386下,函数调用如下:

  • 将参数按一定次序放入栈中
  • 压入ret_addr
  • call指定函数

  对于被调用函数:

  • 压入ebp
  • esp->ebp
  • 压入要保存值的寄存器

调用惯例

  调用惯例目前我知道的如下:

  • cdecl: 调用方维护栈,参数从右至左入栈,特点在于调用方可以根据情况去动态调整参数个数
  • stdcall: 被调用方维护栈,参数从右至左入栈
  • fastcall: 被调用方维护栈,前两个参数(不大于DWORD)寄存器传参,剩下的从右至左入栈

这里顺便补上一句,对于x64系统,函数调用时参数传递方式如下图:

64位传参

函数返回值

  简单点讲,一般情况下eax传递返回值,当然对于超过4B的数据,eax存低位,ebx存高位。再大的数据,其返回值传递方式就很有意思了。拿一个结构体举例子,当返回这个结构体时,基本都知道,是返回一个指向结构体的指针,但是整个的过程却如下:

  • main在栈上额外开辟一片空间,作为临时对象temp
  • temp被作为隐藏参数传给函数
  • 函数将数据拷贝给temp,返回的eax作为指向temp的指针
  • main中将eax之乡的temp拷贝给n

返回值类型的尺寸太大导致在函数返回时,会开辟一段区域作为中转,返回值对象会被复制两次: D