由 snprintf 引发的一个问题

所有代码在如下平台编译运行:

gcc 4.1.2
kernel 2.6

当使用32位编译如下代码时,会出现乱码:

long long n = 0x123456LL;
const char* s  = "helloworld";
char buff[512] = {0};
snprintf(buff, 512, "n=%d&s=%s\n", n, s);
printf("%s\n", buff);

使用 gcc -m32 ./main.c -o main.out 命令编译,输出的结果是 Segmentation fault
不过在编译的时候会报 warning,因为 %d 期待的是 int,而传进去的是 long long。当然,这里如果严格要求的话,把 %d 改为 %lld,就不会有错误。
所以,这个 bug 告诉我的第一个经验是: 一定要做到 warning free。很多 warning 看似是类型转换的问题,或者其他一些无足轻重的问题,但是warning本身就是编译器提示你这里 coding 的不规范,在某些特殊情况下会带来程序的 crash。要做到程序的健壮必须要一个 warning 都没有。

32位代码为什么会出现乱码?

因为这里使用了 C语言 变参,而C语言的变参传统实现方法如下(注意:这里只是讨论传统方法,而不是说所有编译器都是这样做的):
对于一个如下的可变参数函数:

void func(const char* fmt, ...) {
    // pass
}

int main(void) {
    long long n;
    const char* s = "helloworld";
    func("%d&%s", n, s);
    return 0;
}

首先调用者会将参数压栈,压栈顺序是从右向左,对于上面的示例,我们压栈之后的结构是:

可以看到,long long n 占了 8 个字节(绿色),s 指针占了 4 字节(黄色),fmt 指针占了 4 字节(蓝色)。使用从右向左压栈,这样的话就会把第一个固定参数 fmt 放在栈顶,那么 func 内部就可以解析 fmt 中的格式化占位符,然后去栈里向上去查找其他参数,这里就会出现问题,因为 n 占了 8 个字节,但是 fmt 中第一个占位符是 %d,那么 func 只会向上回溯 4 字节,拿到 n 的低 32 位,然后 func 查找到 %s,它会继续向上看 4 字节,这个时候就会找到 n 的高 32 位,因此解析错误,程序 crash。
这里我学到的是:C语言依靠从右向左压栈保证可以处理可变参数 (注意,这里限定了是在某种实现上,并且是32位。当然,经过验证,gcc 是这样的)

但是如果取消 m32 的时候,即使用 64 位编译,程序就不会 crash。

这又出现了一个问题,根据查资料,会发现 64 位因为增加了最少 8 个通用寄存器,因此对于前面几个参数采用寄存器传值的方式。(这里在 CSAPP 中有讲到,第3.11节)这里可以使用打印参数的方式来验证。如下代码:

void fixed_addr(int a, int b, int c) {
    printf("a=%p\n", &a);
    printf("b=%p\n", &b);
    printf("c=%p\n", &c);
}

int main(void) {
    int a = 0x02, b = 0x03, c = 0x04;
    fixed_addr(a,b,c);
    return 0;
}

使用 gdb 单步,在进入 fixed_addr 之前,寄存器状态如下:

在进入函数之后,寄存器状态如下:

可以看到,64位明显使用了寄存器传参。

当然,在把 int 改为复杂结构,比如结构体时,当不断增加结构体size时,就会出现打印的地址由 a > b > c 变为 c > b > a,这就是说寄存器的大小是有限的,当寄存器存不下参数时,就会使用栈传参数。
测试代码:

typedef struct st_t{
    int field1;
    int field2;
    int field3;
    int field4;
} st;

void fixed_addr(st a, st b, st c) {
    printf("a=%p\n", &a);
    printf("b=%p\n", &b);
    printf("c=%p\n", &c);
}

int main(void) {
    st a,b,c;
    fixed_addr(a,b,c);
    return 0;
}

当 st 中有 4 个整型时,采用的是寄存器传值,当变成 5 个整型时,采用的是栈传值。
这一步学到了: 64位会再允许的情况下采用寄存器传递参数进入被调用者

那么有没有什么通用方法来解决 32位变参的函数编写?

答案肯定是“有”,C标准库就提供了这种功能。
使用以下几个宏:

typedef  char *  va_list;
/*
   Storage alignment properties -- 堆栈按X对齐
*/
#define  _AUPBND        (sizeof (X) - 1) 
#define  _ADNBND        (sizeof (X) - 1)
 
/* Variable argument list macro definitions -- 变参函数内部实现需要用到的宏 */                  
#define _bnd(X, bnd)    (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_start(ap, A)  (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
#define va_arg(ap, T)   (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap)     (void) 0

_bnd 是按照 word 进行补齐,va_arg 是返回当前的参数地址,并且将 va_list 地址向前波动。当然,解析 fmt 固定参数的任务还是需要程序员自己完成。

一个示例函数:

#include <stdio.h>
#include <stdarg.h>

void vars_args_func(const char* fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    printf("%d\n", va_arg(ap, int));
    printf("%d\n", va_arg(ap, int));
    printf("%s\n", va_arg(ap, char*));
    va_end(ap);
}

int main(void)
{
    var_args_func("%d %d %s\n", 4, 5, "helloworld");
    return 0;
}

这一步就需要到了 C语言处理变参函数的一种做法。上面的实现是针对 32 位,64位另有实现,并且通能相同,因此采用 va_list 可以跨平台。
这一步学到了如何用C语言的方式处理变参
当然C++11支持了可变模板参数,比 C标准库 的这种方法还是更友好一些。

猜你喜欢

转载自www.cnblogs.com/daghlny/p/9351512.html