--2012年11月22日22:06:23
为了提高代码执行效率,代码中有些地方直接使用汇编语言编制。这就会涉及到两种语言的程序间相互调用的问题。本文首先说明C语言函数的相互调用机制,然后使用示例来说明C与汇编之间的调用方法。【C函数相互调用机制】函数调用操作包括从一块代码到另一块代码之间的双向数据传递和执行控制转移。数据传递通过函数参数和返回值来进行。另外,还需要在进入函数时为函数的局部变量分配存储空间,并且在退出函数时收回这部分空间。Intel80x86CPU为控制传递提供了简单的指令,而数据的传递和局部变量存储空间的分配和回收则通过栈操作来实现。1、栈帧结构和控制权转移方式大多数CPU上的程序实现使用栈来支持函数调用操作。栈被用来传递函数参数、存储返回信息、临时保存寄存器原有值以备恢复以及用来存储局部数据。单个函数调用操作所使用的栈部分被称为栈帧(Stackframe)结构,如下图所示。栈帧结构的两端由两个指针来指定。寄存器ebp通常用作帧指针(framepointer),而esp则用作栈指针(stackpointer)。在函数执行过程中,栈指针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将被默认用来传递返回值。尽管某一时刻只有一个函数在执行,但还是需要确定在一个函数调用其它函数时,被调用者不会修改或覆盖掉调用者今后要用到的寄存器内容。因此IntelCPU采用了所有函数必须遵守的寄存器用法统一惯例。该惯例指明,寄存器eax、edx、ecx的内容必须由调用者自己负责保存。寄存器ebx、esi、edi、ebp、esp的内容则必须由被调用者B来保护。当被调用者需要使用这些寄存器中的任意一个时,必须首先在栈中保存其内容,并在退出时恢复这些寄存器内容。2、函数调用举例上面函数调用时的栈结构如下:用gcc1.40编译生成的汇编代码如下(汇编语法为AT&T语法,这里删除了几行与讨论无关的伪指令):从上面的分析可知,C语言在调用函数时是在堆栈上临时存放被调函数的值(压栈顺序为从右至左,即C调用约定(__cdecl)。更多的关于函数调用时的入栈顺序可以参考:http://blog.sina.com.cn/s/blog_54f82cc2010133mn.html),C语言是传值类语言,没有直接的方法可用来在被调用函数中修改调用者变量的值。因此为了达到修改的目的就需要向函数传递变量的指针(即变量的地址)。3、main()也是一个函数上面提到的C程序的主程序main()也是一个函数。因为gcc在编译连接时它将被作为crt0.s汇编程序的函数被调用。crt0.s是一个桩(stub)程序,名称中的“crt”是“Crun-time”的缩写。该程序的目标文件将被连接到每个用户执行程序的开始部分,主要用于设置一些初始化全局变量等。Linux-0.11中crt0.s汇编程序见如下所示。其中建立并初始化全局变量_environ供程序中其他模块使用。现在的gcc编译器(2.x)已经把这个crt0扩展成几个模块:crt1.o、crti.o、crtbegin.o、crtend.o和crtn.o。【在汇编中调用C函数】从汇编程序中调用C语言函数的方法实际上在上面已经给出。在上面C语言例子对应的汇编程序代码中,可以看出汇编程序语句是如何调用swap()函数的。现在对调用方法作一总结。在汇编程序调用一个C函数时,程序需要按照从右至左的顺序将函数参数压栈,见下图。然后执行CALL指令去执行被调函数。在被调函数返回后,汇编程序需要再把先前压栈的函数参数清除掉。在执行CALL指令时,CPU会把CALL指令下一条指令的地址压入栈中(见图中EIP)。如果调用还涉及到代码特权级变化,那么CPU还会进行堆栈切换,并且把当前堆栈指针、段描述符和调用参数压入新堆栈中。也可以根本不用CALL指令而采用JMP指令来同样达到调用函数的目的。方法是在参数入栈之后人工的把下一条要执行的指令地址压入栈中,然后直接使用JMP指令跳转到被调函数开始处去执行。此后当函数执行完成时就会执行RET指令把人工压栈的下一条指令地址弹出,作为函数返回地址。【在C程序中调用汇编程序】从C程序中调用汇编程序函数的方法与汇编程序调用C函数的原理相同。调用方法的着重点仍然是对函数参数在栈中位置的确定。当然,如果调用的汇编语言程序比较短,那么可以直接在C程序中使用内嵌汇编语句来实现。下面以一个示例来说明C调用汇编的方法。下面是包含两个函数的汇编程序:调用这两个汇编函数的C程序如下:若有问题,欢迎联系:e9999e@163.com本文参考了:《Linux内核完全注释》---linux0.11版内核---赵炯
因篇幅问题不能全部显示,请点此查看更多更全内容