栈和函数调用惯例
栈是很重要的一种数据结构,调用函数,创建局部变量都会压栈,函数究竟是怎么调用的呢,通常是将函数参数压栈,将下一条指令地址压栈,然后跳转到函数体内执行,函数体内大概是这样:
1 | push ebp // 保存ebp |
但,函数调用还有一些问题:被调用函数是怎么知道函数参数的位置的?这些函数参数由谁负责清理?这需要一些约定,而这个约定就是函数调用惯例。通常来说,函数调用惯例包括下面几个方面:
- 函数参数的传递顺序和方式;
- 栈的维护方式;
- 名字修饰的方法;
几种常见的调用惯例:
调用惯例 | 出栈方 | 参数传递 | 名字修饰 |
---|---|---|---|
cdecl | 函数调用方 | 从右至左 | 下划线+函数名 |
stdcall | 函数本身 | 从右至左 | 下划线+函数名+@+参数字节数 |
fastcall | 函数本身 | 前两个不大于DWORD的参数放入寄存器从右至左 | @+函数名+@+参数字节数 |
pascal | 函数本身 | 从左至右 | 比较复杂 |
thiscall | 函数本身 | 从右至左,this指针当做第一参数或放入寄存器 | C++名字修饰比较复杂 |
还剩一个问题,函数返回值是怎么传递的呢?大致如下:
- 对于5-8字节的对象,用eax+edx传递(32位);
- 较大的对象,先在栈上开辟一片临时空间,将临时空间的地址传入函数,函数将返回值放在临时空间里,之后从临时空间里拷贝出返回值;
## 堆与内存管理
Linux下有两种堆空间分配方式:
- brk():设置进程数据段的结束地址,如果扩大数据段,扩大部分就可以被用作堆空间;
- mmap():mmap将一个文件映射到虚拟地址空间里,如果不用文件,使用MAP_ANONYMOUS,这可以用这块匿名空间当堆空间;
堆分配算法:
- 空闲链表;
- 位图;
- 对象池;
入口函数
程序真的从main函数开始吗?当程序进入main函数中时,栈、堆、argc、argv、stdout等一系列东西都初始化好了,这些正是入口函数做的。从操作系统角度来看程序的运行:
- 根据ELF文件头找到入口地址,可能进行动态链接,之后控制权交给程序入口,即入口函数;
- 入口函数对程序运行环境进行初始化,包括堆、栈、I/O、线程、全局变量、环境变量、命令行参数等;
- 调用main函数,执行函数本体;
- main函数执行完后,返回到入口函数,入口函数进行清理工作;
- 调用exit(_exit)结束进程;
## 运行库
任何一个程序,想要执行,背后都需要很多代码来支撑,这不仅包括入口函数,还有其依赖的一系列函数,以及标准库,这么一个代码集合就称之为运行库。
C中变长参数的实现:全靠cdecl调用惯例,从栈上获取变长的参数,当然,长度就要靠程序员了。
系统调用
系统调用是一种软中断,中断号是0x80,调用方式如下:
- eax里填系统调用号,ebx, ecx, edx, esi, edi, ebp里存放至多六个系统调用参数;
- int 0x80,触发中断,陷入内核;
- 进行栈切换,因为用户态和内核态使用的栈不同,实际行为是保存用户栈esp和ss的值,并设置esp和ss为内核栈的值;
- 查找中断向量表,找到中断号对应的中断处理程序,这里就是0x80,进行系统调用;
- 根据寄存器里的系统调用号和系统调用参数,进行系统调用;