建立自己的函数调用帧

本文从最简单的打印“hello world!”的C程序开始,写出其汇编程序(在汇编中使用C库函数),讲解怎样建立自己的函数调用帧,接着使用jmp指令替代call完成函数的调转与返回。在linux内核中这种技巧被大量使用,最后举出内核中使用到的两个实例。

首先,下面的C程序完成的功能,相信大家学大多数语言,都是用来讲解的第一个示例:

//helloworld1.c

#include <stdio.h>

int main()
{
        printf("hello world!\n");
        return 0;
}

我们使用gcc进行编译生成可执行文件,结果如下所示:

[guohl@guohl]$ gcc -o helloworld1 helloworld1.c
[guohl@guohl]$ ./helloworld1
hello world!

 

将上述C语言函数改成汇编程序,当然printf与exit函数还是使用C库自带的函数,这样就是汇编与C的混合编程,修改后程序如下:

#helloworld2.s

.section .data
output:
        .asciz "hello world!\n"

.section .text
.globl _start
_start:
        pushl $output	#通过栈传递参数
        call printf	#调用C库的printf函数
        addl $4, %esp	#恢复栈指针
        pushl $0	#以下两行为exit(0)
        call exit

在这里开始调用printf与exit函数所使用到的栈帧是我们自己建立的,因为这两个函数的参数均是通过栈传递的,因此将参数入栈。从函数返回时,再恢复调用之前的栈帧。

使用as与ld分别进行汇编和链接,运行结果如下:

[guohl@guohl]$ as -o helloworld2.o helloworld2.s
[guohl@guohl]$ ld -dynamic-linker /lib/ld-linux.so.2 -o helloworld2 -lc helloworld2.o[guohl@guohl]$ ./helloworld2
hello world!

在此程序中call指令实际完成了两件事——将下一条指令的地址压栈和将当前程序指针指向调用函数的入口。这样接下来就开始执行调用函数的程序,当从函数中返回时,ret指令恢复将之前压栈的下一条指令恢复到程序指针寄存器。

下面我们将call指令完成的工作重写一遍,得到:

#helloworld3.s

.section .data
output:
        .asciz "hello world!\n"

.section .text
.globl _start
_start:
        pushl $output
        pushl $1f	#将标签为1处的地址压栈
        jmp printf	#jmp到printf函数入口处,不是call
1:
        addl $4, %esp
        pushl $0
        call exit

在这里由于使用的是jmp指令而不是call指令,因此如果没有第11行的压栈指令,当程序从printf函数返回时,ret会将栈顶的值弹出到程序指针寄存器(即ip)中,对于本实验就跳转到数据段output那里了,这样就会出现段错误。因此,我们需要人为将函数返回时应该跳转的地址压栈,对于本程序即标号为1的地址。

与前一个实验一样编译链接并执行,得到结果如下:

[guohl@guohl]$ as -o helloworld3.o helloworld3.s
[guohl@guohl]$ ld -dynamic-linker /lib/ld-linux.so.2 -o helloworld3 -lc helloworld3.o
[guohl@guohl]$ ./helloworld3
hello world!

 

也许你就疑惑了,明明我用一个call就可以搞定的事,你为什么要用push和jmp两条指令完成呢?试想一下,如果我们不希望函数返回时执行到call的下一条指令,而是执行我们指定的一段程序,那么怎么实现呢?这时,将那段程序的地址先压栈,再通过jmp而不是call到调用函数,这样从函数返回的时候,就能执行到我们指定的程序段了。

下面举出内核中一个例子,使用的就是这种技巧:

#define switch_to(prev, next, last)					\
do {									\
	/*								\
	 * Context-switching clobbers all registers, so we clobber	\
	 * them explicitly, via unused output variables.		\
	 * (EAX and EBP is not listed because EBP is saved/restored	\
	 * explicitly for wchan access and EAX is the return value of	\
	 * __switch_to())						\
	 */								\
	unsigned long ebx, ecx, edx, esi, edi;				\
									\
	asm volatile("pushfl\n\t"		/* save    flags */	\
		     "pushl %%ebp\n\t"		/* save    EBP   */	\
		     "movl %%esp,%[prev_sp]\n\t"	/* save    ESP   */ \
		     "movl %[next_sp],%%esp\n\t"	/* restore ESP   */ \
		     "movl $1f,%[prev_ip]\n\t"	/* save    EIP   */	\
		     "pushl %[next_ip]\n\t"	/* restore EIP   */	\
		     __switch_canary					\
		     "jmp __switch_to\n"	/* regparm call  */	\
		     "1:\t"						\
		     "popl %%ebp\n\t"		/* restore EBP   */	\
		     "popfl\n"			/* restore flags */	\
									\
		     /* output parameters */				\
		     : [prev_sp] "=m" (prev->thread.sp),		\
		       [prev_ip] "=m" (prev->thread.ip),		\
		       "=a" (last),					\
									\
		       /* clobbered output registers: */		\
		       "=b" (ebx), "=c" (ecx), "=d" (edx),		\
		       "=S" (esi), "=D" (edi)				\
		       							\
		       __switch_canary_oparam				\
									\
		       /* input parameters: */				\
		     : [next_sp]  "m" (next->thread.sp),		\
		       [next_ip]  "m" (next->thread.ip),		\
		       							\
		       /* regparm parameters for __switch_to(): */	\
		       [prev]     "a" (prev),				\
		       [next]     "d" (next)				\
									\
		       __switch_canary_iparam				\
									\
		     : /* reloaded segment registers */			\
			"memory");					\
} while (0)

这时进程切换的核心代码,切换的具体过程就不赘述,可以参考我的一个PPT,下载地址http://wenku.baidu.com/view/f9a17542b307e87101f6968d.html?st=1。重点看第17行和19行,第17行将希望即将切换进来的进程next的执行的ip压栈(而此ip是在next被切换出去之前执行siwtch_to在第16行所保存的,即标号为1的地址),在第19行调转到__switch_to函数,待到从__switch_to函数返回时,此时就可以恢复执行next进程从上一次切换出去的地方(即标号为1)继续执行。如果按照此情景,完全可以将17和19行换成一句“call __switch_to”语句;关键地方在于,如果是fork新建的一个进程,第一次调度,它之前并未执行switch_to的语句,因此切换到它的时候,并不能让它从标号为1的地方开始执行,而是应该让进程从sys_fork系统调用中返回,该地址在sys_fork->do_fork->copy_process->copy_thread 函数中进行赋值:

p->thread.ip = (unsigned long) ret_from_fork;

这样对于切换到新进程第17行压栈的将不是标号为1的地址,而是ret_from_fork的地址。可以看出,在这里,设计的非常巧妙!而且还是必须的!

posted @ 2012-11-26 21:35  hazir  阅读(1919)  评论(4编辑  收藏  举报

一个代码可以创造一个世界,也可以毁灭一个世界!