Chinaunix首页 | 论坛 | 博客
  • 博客访问: 221641
  • 博文数量: 93
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 542
  • 用 户 组: 普通用户
  • 注册时间: 2014-12-09 16:59
文章分类

全部博文(93)

文章存档

2016年(27)

2015年(66)

我的朋友

分类: C/C++

2015-08-31 19:53:18

    上一次我们分析了函数调用的实现过程(http://blog.chinaunix.net/uid-30057524-id-5175844.html),重点分析了其中栈的变化情况,我们知道函数默认的调用规则(cdecl)是使用栈来传递参数的,参数被从右至左压入栈,并且是连续存放的,所以我们可以根据固定参数(至少要有一个固定参数)的地址和可变参数的类型计算出可变参数的地址,这就是变参函数实现的基本原理。下面给出实例分析:

//编译环境为Windows下的VC6.0
#include <stdio.h>

typedef char * va_list_c; 

#define va_start_c(ap,v) ( ap = (va_list_c)&v + sizeof(v) )
//va_start_c宏根据固定参数的地址计算出第一个可变参数的地址
 
#define va_arg_c(ap,type) \
( *(type *)((ap += sizeof(type)) - sizeof(type)) ) 
//va_arg_c宏根据上一个可变参数的地址计算出下一个可变参数的地址

#define va_end_c(ap) ( ap = (va_list_c)0 ) 
//可变参数全部取出后令指针指向空地址

//注:在标准头文件stdarg.h中包含了上述宏(命名上有区别,应去掉_c),可包含该头文件后直接使用

int sum(int n, ...)
{
    int i = 0;
    int result = 0;
    va_list_c arg = NULL;

    va_start_c(arg, n);
    //宏展开后:arg = ( arg = (char *)&n + sizeof(n) ),arg指向第一个可变参数的地址

    for(i = 0; i < n; i++)
    {
        result += va_arg_c(arg, int);
        //宏展开后:( *(int *)((arg += sizeof(int)) - sizeof(int)) ),上式实现了两个功能,将arg指向地址的值取出来累加至变量result,
        //并将arg指向下一个可变参数的地址
    }

    return result;
}


int main()
{
    printf("%d\n", sum(3, 1, 9, 4));    //这里第一个参数代表后面可变参数的个数
    printf("%d\n", sum(2, 6, 3));
    return 0;
}

输出结果:
14
9

输出结果正确,但这个实现却有Bug。下面在同样的环境下将sun函数参数的类型换为short int

#include

typedef char * va_list_c; 
typedef short int sint;

#define va_start_c(ap,v) ( ap = (va_list_c)&v + sizeof(v) ) 
#define va_arg_c(ap,type) \
( *(type *)((ap += sizeof(type)) - sizeof(type)) ) 
#define va_end_c(ap) ( ap = (va_list_c)0 ) 

sint sum(sint n, ...)
{
    sint i = 0;
    sint result = 0;
    va_list_c arg = NULL;


    va_start_c(arg, n); 


    for(i = 0; i < n; i++)
    {
        result += va_arg_c(arg, sint); 
    }


    return result;
}


int main()
{
    printf("%d\n", sum(3, (sint)1, (sint)9, (sint)4));
    printf("%d\n", sum(2, (sint)6, (sint)3));
    return 0;
}

输出结果:
1
6

输出结果错误,这是因为在Windows下VC6.0编译环境中,栈的默认对齐是四字节对齐,所以在此环境下即使短整形只占两个字节,但由于栈需要四字节对齐实际上相当于给短整形分配了四个字节,其中两个字节被浪费。这时程序需要有如下改动:

typedef char * va_list_c; 
typedef short int sint;

#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
//宏_INTSIZEOF(n)使计算出的n的长度为四的整数倍,可解决栈的字节对齐问题

#define va_start_c(ap,v) ( ap = (va_list_c)&v + _INTSIZEOF(v) ) 
#define va_arg_c(ap,type) \
( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) ) 
#define va_end_c(ap) ( ap = (va_list_c)0 ) 

程序其他部分不变。
输出结果:
14
9

输出结果正确。下面来说说栈在给临时变量分配空间时的规则问题,这里面有些问题需要我们特别注意。

不同的编译环境,栈在给临时变量分配空间时的规则是不同的。在Windows下VC6.0编译环境中,栈的默认对齐是四字节对齐,如在此环境下两个字符型变量(不管是作为局部变量还是函数参数)在栈中要占8个字节;

在Linux下GCC编译环境中,栈在给局部变量分配空间时的默认对齐是一字节对齐,而函数的参数入栈时默认对齐是四字节对齐,两个字符局部变量占2个字节,两个字符变量作为函数参数入栈时要占8个字节;

Linux下GCC编译环境中还有一点需要特别指出:在Linux下GCC编译环境中,当入栈的函数固定参数类型不是四字节数类型的时候,栈会给此参数重新分配空间,造成函数固定参数和可变参数在栈中的位置不连续,固定参数之间的位置顺序也将与入栈时不同,这将使得无法实现变参函数,在Windows下VC6.0中没有这种情况。下面看一个实例分析:

C代码(在Linux下GCC环境中编译运行):

#include

typedef char * va_list_c; 
typedef short int sint;

#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start_c(ap,v) ( ap = (va_list_c)&v + _INTSIZEOF(v) ) 
#define va_arg_c(ap,type) \
( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) ) 
#define va_end_c(ap) ( ap = (va_list_c)0 ) 

sint sum(sint n, ...)
{
    sint i = 0;
    sint result = 0;
    va_list_c arg = NULL;

    va_start_c(arg, n); 

    for(i = 0; i < n; i++)
    {
        result += va_arg_c(arg, sint); 
    }

    return result;
}

int main()
{
    sint a = 1;
    sint b = 9;
    sint c = 4;

    printf("%d\n", sum((sint)3, a, b, c));
    printf("%d\n", sum((sint)2, (sint)6, (sint)3));
    return 0;
}

输出结果:
-16246
31300

输出结果错误。我们来分析下对应的汇编代码,看看错在哪里。执行gcc -S -o main.s main.c得到如下汇编代码:

.file "va4.c"
.text
.globl sum
.type sum, @function
sum:
pushl %ebp
movl %esp, %ebp
subl $20, %esp
movl 8(%ebp), %eax
movw %ax, -20(%ebp)
movw $0, -8(%ebp)
movw $0, -6(%ebp)
movl $0, -4(%ebp)
leal -20(%ebp), %eax
addl $4, %eax
movl %eax, -4(%ebp)
movw $0, -8(%ebp)
jmp .L2
.L3:
addl $4, -4(%ebp)
movl -4(%ebp), %eax
subl $4, %eax
movzwl (%eax), %eax
movl %eax, %edx
movzwl -6(%ebp), %eax
leal (%edx,%eax), %eax
movw %ax, -6(%ebp)
addw $1, -8(%ebp)
.L2:
movzwl -20(%ebp), %eax
cmpw %ax, -8(%ebp)
jl .L3
movzwl -6(%ebp), %eax
leave
ret
.size sum, .-sum
.section .rodata
.LC0:
.string "%d\n"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
movw $1, 26(%esp)
movw $9, 28(%esp)
movw $4, 30(%esp)
movswl 30(%esp), %ecx
movswl 28(%esp), %edx
movswl 26(%esp), %eax
movl %ecx, 12(%esp)
movl %edx, 8(%esp)
movl %eax, 4(%esp)
movl $3, (%esp)
call sum
movswl %ax, %edx
movl $.LC0, %eax
movl %edx, 4(%esp)
movl %eax, (%esp)
call printf
movl $3, 8(%esp)
movl $6, 4(%esp)
movl $2, (%esp)
call sum
movswl %ax, %edx
movl $.LC0, %eax
movl %edx, 4(%esp)
movl %eax, (%esp)
call printf
movl $0, %eax
leave
ret
.size main, .-main
.ident "GCC: (GNU) 4.4.2 20091027 (Red Hat 4.4.2-7)"
.section .note.GNU-stack,"",@progbits

看main函数中的这段汇编代码:

movw $1, 26(%esp)
movw $9, 28(%esp)
movw $4, 30(%esp)    //以上三句表明一个短整形局部变量在栈中占2个字节
movswl 30(%esp), %ecx
movswl 28(%esp), %edx
movswl 26(%esp), %eax
movl %ecx, 12(%esp)
movl %edx, 8(%esp)
movl %eax, 4(%esp)    //以上三句表明一个短整形变量作为函数入栈时占四个字节

上面这段汇编代码可以看出前面提到的Linux下GCC中栈的字节对齐规则。

再看sum函数中的这段汇编代码:

movl 8(%ebp), %eax    //将第一个固定参数赋给寄存器eax
movw %ax, -20(%ebp)    //在-20(%ebp)位置存放第一个固定参数,相当于给第一个固定参数重新分配了空间
movw $0, -8(%ebp)
movw $0, -6(%ebp)
movl $0, -4(%ebp)
leal -20(%ebp), %eax    //以下三句可以看出在计算可变参数地址时,使用的是重新分配的空间地址,而可变参数地址未变,所以出错
addl $4, %eax
movl %eax, -4(%ebp)
    
前面已经说明这种错误是由于函数固定参数类型不是四字节数类型造成的,所以只要将参数n的类型换为int型即可。

将参数n换为int型后编译运行,输出结果:
14
9

输出结果正确。















阅读(1084) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~