尾部调用优化

gcc实在是太聪明了,有时候就会聪明过头,尾部调用优化就是一个很好的例子。:)

尾部调用其实很好理解,就是在函数的最后调用另外一个函数,一种大的可能就是这个函数和调用函数参数差别很小,那么这时候,gcc会对这个尾部调用进行优化,如果可能,完全可以把调用那个函数时的入栈操作给直接优化掉。这就是尾部调用的优化。我们可以看看下面的例子:

[c]
extern int callee(int a, int b);

static int c;

int call(int a, int b)
{
int ret = callee(c, b);
return ret;
}
[/c]

用”gcc -fomit-frame-pointer -fno-inline -O2 -S tail_call.c”编译它,得到的汇编代码截取如下:

call:
        movl    c, %eax
        movl    %eax, 4(%esp)
        jmp     callee

很明显,入栈操作被优化掉了,成了直接操作call()的参数!这会有问题,如果call()的调用函数不想看到堆栈被改变的话。

在Linux内核中有个很好的例子,那就是系统调用!系统调用的入口(arch/x86/kernel/entry_32.S)是用汇编写成的,在进入一个系统调用前存放参数的寄存器会被入栈,系统调用完毕后又会被恢复,所以,如果系统调用也被做了尾部调用优化的话,那么系统调用前后寄存器的值就会发生变化!这就可能会破坏用户空间的代码!

Linux内核采取了相应的办法来解决这个问题,那就是宏asmlinkage_protect(),它的定义在arch/x86/include/asm/linkage.h。我们通过个具体的例子来看,比如open(2),其定义是这样的:

[c]
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode)
{
long ret;

    if (force_o_largefile())
            flags |= O_LARGEFILE;

    ret = do_sys_open(AT_FDCWD, filename, flags, mode);
    /* avoid REGPARM breakage on x86: */
    asmlinkage_protect(3, ret, filename, flags, mode);
    return ret;

}
[/c]

上面的asmlinkage_protect会被展开成:


asm volatile (“” : “=r” (ret) : “0” (ret), “g” (filename),
“g” (flags), “g” (mode));

这句不会直接生成任何汇编代码,但是它会强迫gcc把参数放到寄存器中,也就避免了上面展示的直接对栈进行操作。

为了完整,我们把最开始的那段代码修复一下:
[c]
extern int callee(int a, int b);

static int c;

int call(int a, int b)
{
int ret = callee(c, b);
asm(“”:”=r”(ret):”0”(ret), “g”(a), “g”(b));
return ret;
}
[/c]
再汇编就可以看到堆栈操作了:


call:
pushl %ebx
subl $8, %esp
movl c, %eax
movl 20(%esp), %ebx
movl %eax, (%esp)
movl %ebx, 4(%esp)
call callee
addl $8, %esp
popl %ebx
ret

这里展示的代码都很简单,实际中往往很复杂,情况不同gcc的优化操作也不同,比如,当上面的callee()是static的时候虽然也会被优化但不会直接操作堆栈(而是通过使用寄存器),因为gcc可以完整地看到两个函数,可以在“全局”进行优化。实际问题实际分析,但这个尾部调用优化带来的问题我们必须得小心才是。