免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 1316 | 回复: 0
打印 上一主题 下一主题

关于prevent_tail_call(尾部调用) [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2008-01-15 17:28 |只看该作者 |倒序浏览

                看代码时发现系统调用函数里的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
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP