《链接、装载与库》——运行库

栈和函数调用惯例

栈是很重要的一种数据结构,调用函数,创建局部变量都会压栈,函数究竟是怎么调用的呢,通常是将函数参数压栈,将下一条指令地址压栈,然后跳转到函数体内执行,函数体内大概是这样:

1
2
3
4
5
6
7
8
9
10
11
push ebp        // 保存ebp
mov ebp, esp // 重新设置ebp
sub esp x // 分配栈上临时空间
[push reg] // 保存寄存器
//
... // 实际函数内容
//
[pop reg] // 恢复寄存器
mov esp, ebp // 回收栈临时空间
pop ebp // 恢复ebp
ret // 函数返回

但,函数调用还有一些问题:被调用函数是怎么知道函数参数的位置的?这些函数参数由谁负责清理?这需要一些约定,而这个约定就是函数调用惯例。通常来说,函数调用惯例包括下面几个方面:

  • 函数参数的传递顺序和方式;
  • 栈的维护方式;
  • 名字修饰的方法;

几种常见的调用惯例:

调用惯例 出栈方 参数传递 名字修饰
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,进行系统调用;
  • 根据寄存器里的系统调用号和系统调用参数,进行系统调用;