由 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标准库 的这种方法还是更友好一些。

原文地址:https://www.cnblogs.com/daghlny/p/9351512.html

时间: 2024-11-05 22:36:34

由 snprintf 引发的一个问题的相关文章

js json 与字符串 转换过程由于书写不统一规范引发的一个问题

对于两个字符串: 字符串1:{title:{},tooltip:{trigger:"axis"},legend:{data:["新关注人数"]},calculable:true,xAxis:[{type:"category",boundaryGap:false,data:["2016-01-01","2016-01-02","2016-01-03","2016-01-04&qu

C 局部变量引发的一个BUG

代码片断如下: unsigned char status; status = 0x01; // local zone { unsigned char status; status = 0x00; } printf("status = %d\n", status); 析:如果定义一个全局变量A,在局部区域又定义了一次A,那么在局部区域对A修改后,当退出该局部区域后, A的值还是未进入局部区时的值.如下 BUG描述:status本用于判断设备运行状态的,结果局部区也定义了status,且把

“static”引发的一个错误

昨天晚上,舍友发来一个程序,先把代码贴上: #include<stdio.h>#define N 20short bufferA[N]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20};short bufferB[N]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20};int dotp(short a[N],short b[N]){  int y; int i; for(i=0;i&

debug版本的DLL调用release版本的DLL引发的一个问题

stl的常用结构有 vector.list.map等. 今天碰到需要在不同dll间传递这些类型的参数,以void*作为转换参数. 比如 DLL2 的接口 add(void*pVoid); 1.在DLL1中调用该接口, struct st_headerTerminalRes{ st_headerTerminalRes(){id=0;} int id; int type;//restype 1=mc 2=camera int resId; int headerId;};typedef vector<

关于Manjaro与Ubuntu双系统并存引发的一个boot问题

事情发生在写下这篇博客的半小时前.笔者的电脑本身是Manjaro+win10双系统并存,因为一些原因要安装ubuntu. 装完ubuntu用了一阵子,想切回manjaro,于是遇到了这个问题. 看到kernel panic的时候把我吓了一跳,怎么装个系统还能搞到连根文件系统都无法挂载了……仔细想想完全没道理,看到下面出现好几次kernel_init,猜测是boot的姿势不对. 马上强行reboot,并选择manjaro的fallback-initramfs选项,果然能进系统.说明boot出现了问

5672端口引发的一个大坑

目前项目使用ActiveMQ 某日,领导要求使用RabbitMQ,于是乎,装ERLang.RabbitMQ,按照网上流程走一遍, 发现死活都无法启动RabbitMQ!提示 Failed to start Ranch listener {acceptor,{0,0,0,0,0,0,0,0},5672} in ranch_tcp:listen([{port,5672},{ip,{0,0,0,0,0,0,0,0}},inet6,{backlog,128},{nodelay,true},{linger,

C# 一个简单的秒表引发的窗体卡死问题

一个秒表程序也是我的一个心病,因为一直想写这样的一个东西,但是总往GUI那边想,所以就比较怵,可能是上学的时候学MFC搞出的后遗症吧,不过当我今天想好用Win Form(话说还是第一次写win form)写这么一个东西的时候,居然so easy. 所以说,做不了不可怕,怕的是你不去做,因为你不去做,你就永远不知道你能不能做它.事实证明,大部分你犹豫能不能做的事情,实际上你都能搞定. 虽然成功实现了一个秒表的简单功能,即开始计时和停止.但是却引发了一个关于win form和C#线程的问题. 下面一

CLR via C#中的一个多线程例子

parallel的For和ForEach方法有一些重载版本允许传递三个委托 1.任务局部初始化委托(localInit),未参与工作的每一个任务都调用一次委托,在任务被要求处理前调用. 2.主体委托(body),为参与工作的各个线程锁处理的酶一项都调用一次该委托. 3.任务局部终结委托(localFinially),为参与工作的每一个任务都调用一次委托.这个委托实在任务处理好派遣给它的所有工作项之后调用的.即使主体委托代码引发的  一个未处理的异常,也会调用它. 演示如何利用三个委托,计算目录中

远程触发SYSRQ获取最新的dmesg信息-一个几乎没有什么用的方案

本文在前半部分叙述一个听起来十分吸引人且合理的故事,然后紧接着告诉你这个美好的故事事实上几乎不会发生,最后来个总结.在接下来的一篇文章中,我提出一个比较自我的方案. 第一部分:美好的故事 在xtables-addons中,有一个特别有意思的小模块,那就是xt_SYSRQ,它作为一个iptables的target加载进内核,可以在远程为本机发送sysrq命令,这个功能可谓强大.在去年的项目中中,我已经将其部署到了实际的产品中,然而今日再看,发现还是有些美中不足,确实需要改进: 1.原版的xt_SY