C语言代码和汇编代码相互调用
C语言代码和汇编代码相互调用
- 为了提高代码执行效率,Linux内核代码中有很多地方直接使用了汇编语言机制,这就涉及到两种语言的程序之间相互调用问题;
C函数调用机制
- 在 Linux 内核程序 boot/head.s 执行完基本的初始化操作后,就会跳转到 init/main.c 中去执行 C语言程序;
- 那么 head.s 是如何把执行控制权转交给 init/main.c 的呢?
- 函数调用操作包括从一块代码到另一块代码之间的双向数据传递和执行控制转移;
- 数据传递通过函数参数和返回值来进行;
- 另外,我们还需要在进入函数时,为函数的局部变量分配存储空间,并且在退出函数时收回这部分空间;
- Intel 80x86 CPU 为控制传递提供了简单的指令,而数据的传递和局部变量存储空间的分配与回收则通过栈操作来实现;
栈帧结构和控制移权方式
- 大多数 CPU 上的程序实现使用栈来支持函数调用操作,栈被用来传递函数参数、存储返回信息、临时保存寄存器原有值以备恢复和用来存储局部数据;
- 单个函数调用操作所使用的栈部分被称为栈帧(stack frame)结构,其一般结构如下图所示:
- 栈帧结构的两端由两个指针来指定,寄存器 ebp 通常用作帧指针(frame pointer),而 esp 则用作栈指针(stack pointer),在函数执行过程中,栈指针 esp 会随着数据的入栈和出栈而移动,因此函数中对大部分数据的访问都基于栈指针 ebp 进行;
- 对于函数 A 调用函数 B 的情况,传递给 B 的参数包含在 A 的栈帧中,当A调用B时,函数A的返回地址(调用返回后继续执行的指令地址)被压入栈中,栈中该位置明确指明了A栈帧的结束处;
- 而B的栈帧则从随后的栈部分开始,即图中保存帧指针(ebp)的地方开始,再随后则用于存放任何保存的寄存器值和函数的临时值;
- B函数同样也使用栈来保存不能存放在寄存器中的局部变量值,例如由于通常CPU的寄存器数量有限而不能够存放函数的所有局部数据,或者有些局部变量时数组或结构,因此必须使用数组或结构引用来访问;
- 另外,C语言的地址操作符 & 别应用到一个局部变量上时,我们就需要为该变量生成一个地址,即为变量的地址指针分配一空间,最后,B函数会使用栈来保存调用任何其他函数的参数;
- 栈是王低地址方向扩展的,而 esp 指向当前栈顶处的元素,通过使用 push 和 pop 指令,我们可以把数据压入栈中或从栈中弹出,对于没有指定初始值的数据所需要的存储空间,我们可以通过把栈指针递减到适当的值来做到,类似的,通过增加栈指针值也可以回收栈中已经分配的空间;
在汇编程序中调用C函数
- 在汇编程序中调用C函数时,程序首先按照逆向顺序把函数压入栈中,即函数最后(最右边的)一个参数先入栈,而最左边的第1个参数在最后调用指令之前入栈,如下图所示:
- 然后执行 CALL 指令去执行被调用的函数,在调用函数返回后,程序需要把先前压入栈中的函数参数清除掉;
- 在执行 CALL 指令时,CPU会把 CALL 指令的下一条指令的地址压入栈中(见图中的 EIP),如果调用还涉及代码特权级变化,那么CPU会进行堆栈切换,并且把当前堆栈指针、段描述符和调用参数压入新堆栈中;
- Linux 内核中只使用 中断门 和 陷阱门 方式处理特权级变化时的调用情况,并没有使用 CALL 指令来处理特权级变化的情况;
- 汇编程序中调用C函数比较自由,只要是在栈中适当位置的内容就都可以作为参数供供C函数使用;