3.4 C与汇编程序的相互调用
为了提高代码执行效率,内核源代码中有些地方直接使用了汇编语言编制。这就会涉及在两种语言编制的程序之间相互调用的问题。
函数调用包括从一块代码到另一块代码之间的双向数据传递和执行控制转移。数据传递通过函数参数和返回值来进行。另外,我们还需要在进入函数是为函数的局部变量分配空间,并在函数退出时回收者部分空间。Intel 8086 CPU为控制传递提供了简单的指令,而数据的传递和局部变量存储空间的分配与回收则通过栈操作来实现。
1.栈帧结构和控制转移权方式
大多数CPU的程序实现使用栈来支持函数调用操作。栈被用来传递函数参数、存储返回信息、临时保存寄存器原有值和存储局部数据。单个函数调用操作所使用的部分被称为栈帧(stack frame)结构。栈帧结构的两端由两个指针指定。寄存器ebp通常用作帧指针,esp用作栈指针。程序执行过程中,栈指针会随着数据的入栈和出栈而移动,因此函数中对大部分数据的访问都是基于帧指针ebp进行。
对于函数A调用函数B的情况,传递给B的参数包含在A的栈帧中。当A调用B时,函数A的返回地址被压入栈中,该位置也明确指明A栈帧的结束处。而B的栈帧从随后的栈部分开始。B函数同样也使用栈来保存不能放在寄存器中的局部变量值。例如:由于通常CPU的寄存器数量有限而不能存放函数的所有局部数据,或者有些局部变量是数组或结构,因此必须使用数组或结构引用来访问。还有就是C语言地址操作符“&”被应用到一个局部变量上时,我们就需要为该变量生成一个地址,即为该变量的地址指针分配一块空间。最后,B函数会使用栈来保存调用任何其他函数的参数。
栈是往小(低)地址方向扩展的,而esp指向当前栈顶处元素。通过使用push和pop指令我们可以把数据压入栈中或从栈中弹出。对于没有指定初始值的数据所需要的空间,我们可以通过把栈指针递减适当的值来做到。类似的可以回收已经分配的空间。
指令CALL和RET用来处理函数调用和返回操作。调用指令CALL的作用是把返回地址压入栈中并且跳转到被调用函数开始处执行。返回地址是程序中紧随调用指令CALL后面的一条指令的地址。因此当被调用函数返回是就从该位置继续执行。返回指令RET用于弹出栈顶处的地址并跳转到该地址处。在使用该指令之前,应该先正确处理栈中内容,使得当前栈指针所指内容正是先前CALL指令保存的返回地址。另外,若返回值是一个整数或者一个指针,那么寄存器eax将被默认用来传递返回值。
尽管某一个时刻只有一个函数在执行,但是我们还是需要确认在一个函数调用其他函数时,被调用者不会修改或覆盖调用者今后要用到的寄存器内容。因此Intel CPU采用了所有函数必须遵守的寄存器用法统一惯例。该惯例指明,寄存器eax、edx、ecx的内容必须由调用者自己负责保存。当函数A调用函数B时,B可以在不用保存这些寄存器的内容的情况下任意使用它们而不会毁坏函数A所需要的任何数据。另外,寄存器ebx、esi和edi的内容则必须由被调用者来保护。当被调用者需要使用到这些寄存器中的任何一个时,必须首先在栈中保存其内容,并在退出时恢复这些寄存器的内容。因为调用者A并不负责保存这些寄存器的内容,但可能在以后的操作中还需要用到原先的值。还有寄存器ebp和esp也必须遵守第二个惯例用法。