__attribute__ regparm

最早是在linux内核代码看到regparm这个函数属性的,因为搞系统调用机制的时候看到linux系统调用前面都加了asmlinkage,感到奇怪就查了下:

#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))

gcc在x86-Function-Attributes对regparm的解释如下:

regparm (number)
On x86-32 targets, the regparm attribute causes the compiler to pass arguments number one to number if they are of integral type in registers EAX, EDX, and ECX instead of on the stack. Functions that take a variable number of arguments continue to be passed all of their arguments on the stack.
Beware that on some ELF systems this attribute is unsuitable for global functions in shared libraries with lazy binding (which is the default). Lazy binding sends the first call via resolving code in the loader, which might assume EAX, EDX and ECX can be clobbered, as per the standard calling conventions. Solaris 8 is affected by this. Systems with the GNU C Library version 2.1 or higher and FreeBSD are believed to be safe since the loaders there save EAX, EDX and ECX. (Lazy binding can be disabled with the linker or the loader if desired, to avoid the problem.)

大意是x86-32体系结构下,编译器会把0到number个参数顺次写入eax, edx, ecx和栈,就是说number取值范围是0-3,再多的参数就放进栈来传递了。number为0的时候全放栈里去。
写个代码测试下:

int q = 5;

int t1 = 1;
int t2 = 2;
int t3 = 3;
int t4 = 4;

#define REGPARM3 __attribute((regparm(3)))
#define REGPARM0 __attribute((regparm(0)))

void REGPARM0 p1(int a)
{
    q = a + 1;
}

void REGPARM3 p2(int a, int b, int c, int d)
{
    q = a + b + c + d + 1;
}

void p3(int a, int b, int c, int d)
{
    q = a + b + c + d + 1;
}

int main(void)
{
    p1(t1);
    p2(t1, t2, t3, t4);
    p3(t1, t2, t3, t4);

    return 0;
}

很简单的代码,让p1()参数通过栈传递;p2()先传到寄存器,不够再放栈上传递;p3()不指定传递方式。把c代码编成汇编代码,看看这几个函数的参数传递细节:

main:
.LFB3:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    pushl   %ebx
    subl    $16, %esp
    .cfi_offset 3, -12
    movl    t1, %eax
    movl    %eax, (%esp) // 放栈上
    call    p1 // 调用p1()
    movl    t4, %ebx // 第4个参数暂时放到ebx
    movl    t3, %ecx // 第3个参数放ecx
    movl    t2, %edx // 第2个参数放edx
    movl    t1, %eax // 第1个参数放eax
    movl    %ebx, (%esp) // 把第4个参数放栈上
    call    p2 // 调用p2()
    movl    t4, %ebx
    movl    t3, %ecx
    movl    t2, %edx
    movl    t1, %eax
    movl    %ebx, 12(%esp) // 第4个参数入栈
    movl    %ecx, 8(%esp) // 第3个参数入栈
    movl    %edx, 4(%esp) // 第2个参数入栈
    movl    %eax, (%esp) // 第1个参数入栈
    call    p3 // 调用p3()
    movl    $0, %eax
    addl    $16, %esp
    popl    %ebx
    .cfi_restore 3
    popl    %ebp
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret

简单明了。缺省情况下,传参是通过栈传递的,符合一般情况。
但是!
凡事都有个但是是吧。
该来的总会来的……
情况是,自己通过指令int 0x2e陷入实现系统调用机制,参数不是跟linux的做法那样是通过寄存器传递的,而是从用户空间栈拷贝到系统空间栈,然后在内核入口函数(汇编代码)里调用自己写的示例系统调用。当时写的示例系统调用有两个,f1有1个参数(系统调用号),f2有3个参数。f1和f2都是无修饰的,但是f1能正常工作,但是f2会导致程序崩溃(killed,segment fault),有时候导致系统崩溃……
后来突然想到linux的系统调用加了个asmlinkage修饰,怀疑是因为这个原因。然后就给f1,f2加__attribute__((regparm(0))修饰,果然work……
事后诸葛了一下,原来f1的参数是系统调用号,而系统调用号按约定放在eax里,编译器从eax里找参数赶巧找到了。(尽管装着系统调用号的eax也被在调用栈里,但是编译器没去找==)而f2就悲催了,编译器去寄存器里找,结果找到一堆无意义的数据作为参数,导致访问到了不该访问的系统内存空间。
这样看,内核函数不加__attribute__((regparm(0))限定的话,编译器默认是从寄存器找参数,而不是栈,跟用户空间函数是不一样的。猜想内核这么做是处于参数传递效率考虑,毕竟从寄存器取数比从内存取数块。但是对于系统调用,则是把用户空间通过寄存器传递到内核的参数再压入系统空间栈,再指示编译器从栈上找参数的,或许是跟eax存放系统调用号的约定和内核入口函数的一些行为有关。
over.

参考:
1. __attribute__((regparm(n)))
http://blog.csdn.net/tongsean/article/details/8434022
2. x86 Function Attributes
https://gcc.gnu.org/onlinedocs/gcc/x86-Function-Attributes.html#x86-Function-Attributes
3. Linux Kernel: What does asmlinkage mean in the definition of system calls?
https://www.quora.com/Linux-Kernel/Linux-Kernel-What-does-asmlinkage-mean-in-the-definition-of-system-calls

你可能感兴趣的:(__attribute__ regparm)