最近看程序员自我修养-链接、装载与库,到了内存分布章节。虽说对于学习软件安全的人来说,栈的分布和函数调用规则已是老生常谈的事,但是,这次耐着性子看下去果然还是有新的收获。
栈
直接上图,Linux下经典内存结构布局(图片取自程序员自我修养):
其中箭头标明了一般情况下的增长方向,地址仅针对2.4.x内核版本。
栈中保存函数调用时的一些信息,包括:
- 函数返回地址和参数
- 临时变量
- 保存的上下文(某些需要保持不变的寄存器)
在i386下,函数调用如下:
- 将参数按一定次序放入栈中
- 压入ret_addr
- call指定函数
对于被调用函数:
- 压入ebp
- esp->ebp
- 压入要保存值的寄存器
调用惯例
调用惯例目前我知道的如下:
- cdecl: 调用方维护栈,参数从右至左入栈,特点在于调用方可以根据情况去动态调整参数个数
- stdcall: 被调用方维护栈,参数从右至左入栈
- fastcall: 被调用方维护栈,前两个参数(不大于DWORD)寄存器传参,剩下的从右至左入栈
这里顺便补上一句,对于x64系统,函数调用时参数传递方式如下图:
函数返回值
简单点讲,一般情况下eax传递返回值,当然对于超过4B的数据,eax存低位,ebx存高位。再大的数据,其返回值传递方式就很有意思了。拿一个结构体举例子,当返回这个结构体时,基本都知道,是返回一个指向结构体的指针,但是整个的过程却如下:
- main在栈上额外开辟一片空间,作为临时对象temp
- temp被作为隐藏参数传给函数
- 函数将数据拷贝给temp,返回的eax作为指向temp的指针
- main中将eax之乡的temp拷贝给n
返回值类型的尺寸太大导致在函数返回时,会开辟一段区域作为中转,返回值对象会被复制两次: D