- 论坛徽章:
- 0
|
看代码时发现系统调用函数里的prevent_tail_call,虽然代码及其简单,但是看了好久才想明白怎么回事。
先看看什么是tail call 用例子来说明
int fun2(int, int);
int fun1(int a, int b)
{
return fun2(a,b);
}
先用不优化的gcc编译,看看生成什么
#cc -S tailcall.c -fomit-frame-pointer
#cat tailcall.s
.file "tailcall.c"
.text
.globl fun1
.type fun1, @function
fun1:
subl $12, %esp//保留栈空间
movl 20(%esp), %eax//fun1的参数b
movl %eax, 4(%esp)//b-->fun2第二个参数
movl 16(%esp), %eax//fun1的参数a
movl %eax, (%esp)//a-->fun1的第一个参数
call fun2
addl $12, %esp//恢复栈
ret
.size fun1, .-fun1
.ident "GCC: (GNU) 4.1.2 (Ubuntu 4.1.2-0ubuntu4)"
.section .note.GNU-stack,"",@progbits
可以看出来,调用函数的过程大概就是参数入栈,调用call指令,正常情况下这没有问题。但是看看优化的代码
#cc -S tailcall.c -fomit-frame-pointer -O2
#cat tailcall.s
.file "tailcall.c"
.text
.p2align 4,,15
.globl fun1
.type fun1, @function
fun1:
jmp fun2
.size fun1, .-fun1
.ident "GCC: (GNU) 4.1.2 (Ubuntu 4.1.2-0ubuntu4)"
.section .note.GNU-stack,"",@progbits
少了很多指令
实际上fun1被调用时,称之为fun1_caller函数,它的代码如下
...
pushl arg2;
pushl arg1;
call fun1;
...
当call指令执行之后栈的情况为:
------------
| ... |
------------
| arg2 |
------------
| arg1 |
------------
| retaddr |<-----------ESP_fun1_caller
------------
看看未优化的代码是怎么做
------------
| ... |
------------
| arg2 |
------------
| arg1 |
------------
|ret_fun1_call|<-----------ESP_fun1_caller
------------
| 未用 |//ESP减少12字节,而实际只是用了8个字节
------------
| arg2 |被复制了一份
------------
| arg1 |被复制一份,见如上未优化代码,复制参数之后就调用fun2函数
------------
| ret_fun1 |<-----------ESP_fun1_caller-12 == ESP_fun1
------------
很容易看出来,arg1,arg2可以不用复制,复制是多余的,看优化后的代码对应的栈,正是如此
------------
| ... |
------------
| arg2 |
------------
| arg1 |
------------
|ret_fun1_call|<-----------ESP_fun1_caller
------------
在这里就jmp 到 fun2而不是call fun2,然后fun2的ret指令返回之后根本不用先返回到fun1,直接返回到调用fun1_caller
看优化后的指令条数少了很多,而且都是操作内存的,比较费时间,通过直接利用fun1_caller的栈,直接jmp到fun2减少了指令周期个数和访存次数以及栈的大小也节省了,达到了优化的目的
假如fun1中调用了fun2之后还做别的事情,fun2就不能直接返回到fun1_caller中了(否则fun1中的后面指令就没法执行了),所以,在尾部的fun2才能优化,这就是tail call 的优化。
再看一种情况
int fun2(int, int);
static int c;
int fun1(int a, int b)
{
return fun2(c,b);//c作为第一个参数而不是a
};
直接看优化后的代码
.file "tailcall.c"
.text
.p2align 4,,15
.globl fun1
.type fun1, @function
fun1:
movl c, %eax
movl %eax, 4(%esp)
jmp fun2
.size fun1, .-fun1
.local c
.comm c,4,4
.ident "GCC: (GNU) 4.1.2 (Ubuntu 4.1.2-0ubuntu4)"
.section .note.GNU-stack,"",@progbits
直接看fun1_caller的栈的情况:
------------
| ... |
------------
| arg2 |
------------
| c |<-----------ESP_fun1_caller+4 //movl %eax, 4(%esp)
------------
| retaddr |<-----------ESP_fun1_caller
------------
这里jmp 到 fun2
fun1还是使用fun1_caller的栈,只是本来栈中保存着的arg1值被修改为a了,为什么能修改呢,因为gcc认为arg2往下的内容都属于fun1(虽然是fun1_caller放进去的,因为它调用fun1),所以可以随意的改。
如果没有被优化,就应该是如下的了。
------------
| ... |
------------
| arg2 |
------------
| arg1 |
------------
|ret_fun1_call|<-----------ESP_fun1_caller
------------
| 未用 |//ESP减少12字节,而实际只是用了8个字节
------------
| arg2 |被复制了一份
------------
| c |
------------
| ret_fun1 |<-----------ESP_fun1_caller-12 == ESP_fun1
可以看出,在tail call被优化的时候,fun1_caller放入栈中的参数是会被无条件修改的。通常情况,这没错,因为fun1_caller的代码如下
...
push1 arg2;
push1 arg1;
call fun1;
add $8, %esp
修改不修改对fun1_caller函数都无所谓。但是这对linux系统调用的入口是不适用的。
看简化后的系统调用入口:
pushl %es;
pushl %ds;
pushl %eax;
pushl %ebp;
pushl %edi;
pushl %esi;
pushl %edx;
pushl %ecx;
pushl %ebx;
call *sys_call_table(,%eax,4);其中%eax保存系统调用号,而%ebx-%ebp分别为可能的参数,系统调用因为同一代码,所以没有专门的pushl参数入栈,也不知道具体的%eax对应系统调用到底有几个参数,“有就在栈中”,这样比如sys_open它就能找到自己的参数。
上面这段代码得到的寄存器都是用户态的时候放入的
看libc中的代码
#define _syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \
type5,arg5,type6,arg6) \
type name (type1 arg1,type2 arg2,type3 arg3,type4 arg4,type5 arg5,type6 arg6) \
{ \
long __res; \
__asm__ volatile ("push %%ebp ; movl %%eax,%%ebp ; movl %1,%%eax ; int $0x80 ; pop %%ebp" \
: "=a" (__res) \
: "i" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
"d" ((long)(arg3)),"S" ((long)(arg4)),"D" ((long)(arg5)), \
"0" ((long)(arg6))); \
__syscall_return(type,__res); \
}
这些寄存器的值被放到栈中,作为系统调用的参数,然后,在返回的时候有一段相关的代码pop出来到相应的寄存器中,这样用户态的寄存器就被恢复了
这样,通过上面说的arg1被改成c了,arg1在这里正好是对应ebx的值,这样,如果arg1改为c了,那么恢复的时候popl %ebx的时候就得到c了,而这不是原来的arg1,所以是错的。 就是说,系统调用的参数_不属于_系统调用函数,也就是系统调用函数是不允许修改的,这和gcc认为参数属于函数是相反的。这样的系统调用就不允许_tail_call_优化。
这就是为什么很多系统调用中都有一代码,举个例子:
asmlinkage long sys_ftruncate(unsigned int fd, unsigned long length)
{
long ret = do_sys_ftruncate(fd, length, 1);
/* avoid REGPARM breakage on x86: */
prevent_tail_call(ret);//就是这行代码
return ret;
}
它在x86上被定义为:
#define prevent_tail_call(ret) __asm__ ("" : "=r" (ret) : "0" (ret))
asmlinkage就是被定义为从栈上取参数,因为内核默认是 regram(3)
其实被优化之后,prevent_tail_call根本不生成代码了,但是它能强制gcc不对do_sys_ftruncate进行尾部调用优化(因为对它的调用已经不是tail call了,虽然__asm__对应的指令序列为空)。
虽然内核代码默认是用__attrbute__((regparm(3)))声明的而不是用栈来传递参数,但是发生的情况是类似的。
这样,系统调用在x86中的tail call优化就没有了,而且代码中都有prevent_tail_call,看着难受。有kernel开发者请求gcc提供指示“栈不属于函数,不要修改”的选项,参考:
http://gcc.gnu.org/bugzilla/show_bug.cgi?id=27234
还有一些资料
http://lkml.org/lkml/2005/7/11/287
http://www.webservertalk.com/archive242-2006-4-1471029.html
http://www.sidhe.org/~dan/blog/archives/000211.html
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u1/43233/showart_464991.html |
|