目录[-]

今天在项目中使用snprintf时遇到一个比较迷惑的问题,追根溯源了一下,在此对sprintf和snprintf进行一下对比分析。

因为sprintf可能导致缓冲区溢出问题而不被推荐使用,所以在项目中我一直优先选择使用snprintf函数,虽然会稍微麻烦那么一点点。这里就是sprintf和snprintf最主要的区别:snprintf通过提供缓冲区的可用大小传入参数来保证缓冲区的不溢出,如果超出缓冲区大小则进行截断。但是对于snprintf函数,还有一些细微的差别需要注意。

snprintf函数的返回值

sprintf函数返回的是实际输出到字符串缓冲中的字符个数,包括null结束符。而snprintf函数返回的是应该输出到字符串缓冲的字符个数,所以snprintf的返回值可能大于给定的可用缓冲大小以及最终得到的字符串长度。看代码最清楚不过了:

1
2
3
4
5
char tlist_3[10] = {0};
    int len_3 = 0;
 
    len_3 = snprintf(tlist_3,10,"this is a overflow test!\n");
    printf("len_3 = %d,tlist_3 = %s\n",len_3,tlist_3);

上述代码段的输出结果如下:

1
len_3 = 25,tlist_3 = this is a

所以在使用snprintf函数的返回值时,需要小心慎重,避免人为造成的缓冲区溢出,不然得不偿失。

snprintf函数的字符串缓冲

1
2
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);

上面的函数原型大家都非常熟悉,我一直以为snprintf除了多一个缓冲区大小参数外,表现行为都和sprintf一致,直到今天遇上的bug。在此之前我把下面的代码段的两个输出视为一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char tlist_1[1024] = {0},tlist_2[1024]={0};
    char fname[7][8] = {"a1","b1","c1","d1","e1","f1","g1"};
    int i = 0, len_1,len_2 = 0;
 
    len_1 = snprintf(tlist_1,1024,"%s;",fname[0]);
    len_2 = snprintf(tlist_2,1024,"%s;",fname[0]);
 
    for(i=1;i<7;i++)
    {
        len_1 = snprintf(tlist_1,1024,"%s%s;",tlist_1,fname[i]);
        len_2 = sprintf(tlist_2,"%s%s;",tlist_2,fname[i]);
    }
 
    printf("tlist_1: %s\n",tlist_1);
    printf("tlist_2: %s\n",tlist_2);

可实际上得到的输出结果却是:

1
2
tlist_1: g1;
tlist_2: a1;b1;c1;d1;e1;f1;g1;

知其然就应该知其所以然,这是良好的求知态度,所以果断翻glibc的源代码去,不凭空想当然。下面用代码说话,这就是开源的好处之一。首先看snprintf的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
glibc-2.18/stdio-common/snprintf.c:
 18 #include <stdarg.h>
 19 #include <stdio.h>
 20 #include <libioP.h>
 21 #define __vsnprintf(s, l, f, a) _IO_vsnprintf (s, l, f, a)
 22
 23 /* Write formatted output into S, according to the format
 24    string FORMAT, writing no more than MAXLEN characters.  */
 25 /* VARARGS3 */
 26 int
 27 __snprintf (char *s, size_t maxlen, const char *format, ...)
 28 {
 29   va_list arg;
 30   int done;
 31
 32   va_start (arg, format);
 33   done = __vsnprintf (s, maxlen, format, arg);
 34   va_end (arg);
 35
 36   return done;
 37 }
 38 ldbl_weak_alias (__snprintf, snprintf)

使用_IO_vsnprintf函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
glibc-2.18/libio/vsnprintf.c:
 94 int
 95 _IO_vsnprintf (string, maxlen, format, args)
 96      char *string;
 97      _IO_size_t maxlen;
 98      const char *format;
 99      _IO_va_list args;
100 {
101   _IO_strnfile sf;
102   int ret;
103 #ifdef _IO_MTSAFE_IO
104   sf.f._sbf._f._lock = NULL;
105 #endif
106
107   /* We need to handle the special case where MAXLEN is 0.  Use the
108      overflow buffer right from the start.  */
109   if (maxlen == 0)
110     {
111       string = sf.overflow_buf;
112       maxlen = sizeof (sf.overflow_buf);
113     }
114
115   _IO_no_init (&sf.f._sbf._f, _IO_USER_LOCK, -1, NULL, NULL);
116   _IO_JUMPS (&sf.f._sbf) = &_IO_strn_jumps;
117   string[0] = '\0';
118   _IO_str_init_static_internal (&sf.f, string, maxlen - 1, string);
119   ret = _IO_vfprintf (&sf.f._sbf._f, format, args);
120
121   if (sf.f._sbf._f._IO_buf_base != sf.overflow_buf)
122     *sf.f._sbf._f._IO_write_ptr = '\0';
123   return ret;
124 }

关键点出来了,源文件第117行string[0] = '\0';把字符串缓冲先清空后才进行实际的输出操作。那sprintf是不是就没有清空这个操作呢,继续代码比较中,sprintf的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
glibc-2.18/stdio-common/snprintf.c:
 18 #include <stdarg.h>
 19 #include <stdio.h>
 20 #include <libioP.h>
 21 #define vsprintf(s, f, a) _IO_vsprintf (s, f, a)
 22
 23 /* Write formatted output into S, according to the format string FORMAT.  */
 24 /* VARARGS2 */
 25 int
 26 __sprintf (char *s, const char *format, ...)
 27 {
 28   va_list arg;
 29   int done;
 30
 31   va_start (arg, format);
 32   done = vsprintf (s, format, arg);
 33   va_end (arg);
 34
 35   return done;
 36 }
 37 ldbl_hidden_def (__sprintf, sprintf)
 38 ldbl_strong_alias (__sprintf, sprintf)
 39 ldbl_strong_alias (__sprintf, _IO_sprintf)

使用_IO_vsprintf而不是_IO_vsnprintf函数,_IO_vsprintf函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
glibc-2.18/libio/iovsprintf.c:
 27 #include "libioP.h"
 28 #include "strfile.h"
 29
 30 int
 31 __IO_vsprintf (char *string, const char *format, _IO_va_list args)
 32 {
 33   _IO_strfile sf;
 34   int ret;
 35
 36 #ifdef _IO_MTSAFE_IO
 37   sf._sbf._f._lock = NULL;
 38 #endif
 39   _IO_no_init (&sf._sbf._f, _IO_USER_LOCK, -1, NULL, NULL);
 40   _IO_JUMPS (&sf._sbf) = &_IO_str_jumps;
 41   _IO_str_init_static_internal (&sf, string, -1, string);
 42   ret = _IO_vfprintf (&sf._sbf._f, format, args);
 43   _IO_putc_unlocked ('\0', &sf._sbf._f);
 44   return ret;
 45 }
 46 ldbl_hidden_def (__IO_vsprintf, _IO_vsprintf)
 47
 48 ldbl_strong_alias (__IO_vsprintf, _IO_vsprintf)
 49 ldbl_weak_alias (__IO_vsprintf, vsprintf)

在40行到42行之间没有进行字符串缓冲的清空操作,一切了然。

一开始是打算使用gdb调试跟踪进入snprintf函数探个究竟的,可是调试时发现用step和stepi都进不到snprintf函数里面去,看了一下链接的动态库,原来libc库已经stripped掉了:

1
2
3
4
5
6
7
8
hong@ubuntu:~/test/test-example$ ldd snprintf_test
        linux-gate.so.1 =>  (0xb76f7000)
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7542000)
        /lib/ld-linux.so.2 (0xb76f8000)
hong@ubuntu:~/test/test-example$ file /lib/i386-linux-gnu/libc.so.6
/lib/i386-linux-gnu/libc.so.6: symbolic link to `libc-2.15.so'
lzhong@ubuntu:~/test/test-example$ file /lib/i386-linux-gnu/libc-2.15.so
/lib/i386-linux-gnu/libc-2.15.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), BuildID[sha1]=0x7a6dfa392663d14bfb03df1f104a0db8604eec6e, for GNU/Linux 2.6.24, stripped

所以只能去找 ftp://ftp.gnu.org/gnu/glibc官网啃源代码了。

在找glibc源码时,我想知道系统当前使用的glibc版本,一时不知道怎么查,Google一下大多数都是Redhat上的rpm查法,不适用于Ubuntn,而用dpkg和aptitude show都查不到glibc package,后来才找到ldd用法。

1
2
3
4
5
6
hong@ubuntu:~/test/test-example$ ldd --version
ldd (Ubuntu EGLIBC 2.15-0ubuntu20) 2.15
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

现在才发现Ubuntn用的是好像是EGLIBC,而不是标准的glibc库。其实上面ldd snprintf_test查看应用程序的链接库的方法可以更快速地知道程序链接的glibc版本。

snprintf和sprintf区别分析的更多相关文章

  1. C++中关于[]静态数组和new分配的动态数组的区别分析

    这篇文章主要介绍了C++中关于[]静态数组和new分配的动态数组的区别分析,很重要的概念,需要的朋友可以参考下 本文以实例分析了C++语言中关于[]静态数组和new分配的动态数组的区别,可以帮助大家加 ...

  2. Java中Comparable和Comparator接口区别分析

    Java中Comparable和Comparator接口区别分析 来源:码农网 | 时间:2015-03-16 10:25:20 | 阅读数:8902 [导读] 本文要来详细分析一下Java中Comp ...

  3. Oracle nvchar2和varchar2区别分析

    Oracle nvchar2和varchar2区别分析: [注意]VARCHAR2是Oracle提供的特定数据类型,Oracle可以保证VARCHAR2在任何版本中该数据类型都可以向上和向下兼容.VA ...

  4. jQuery中的.bind()、.live()和.delegate()之间区别分析

    jQuery中的.bind()..live()和.delegate()之间区别分析,学习jquery的朋友可以参考下.   DOM树   首先,可视化一个HMTL文档的DOM树是很有帮助的.一个简单的 ...

  5. jQuery中的bind() live() delegate()之间区别分析

    jQuery中的bind() live() delegate()之间区别分析 首先,你得要了解我们的事件冒泡(事件传播)的概念,我先看一张图 1.bind方式 $('a').bind('click', ...

  6. addEventListener()及attachEvent()区别分析

    Javascript 的addEventListener()及attachEvent()区别分析 Mozilla中: addEventListener的使用方式: target.addEventLis ...

  7. C# Parse和Convert的区别分析

    原文:C# Parse和Convert的区别分析 大家都知道在进行类型转换的时候有连个方法供我们使用就是Convert.to和*.Parse,但是疑问就是什么时候用C 什么时候用P 通俗的解释大家都知 ...

  8. jquery中attr和prop的区别分析

    这篇文章主要介绍了jquery中attr和prop的区别分析的相关资料,需要的朋友可以参考下 在高版本的jquery引入prop方法后,什么时候该用prop?什么时候用attr?它们两个之间有什么区别 ...

  9. ql语句中left join和inner join中的on与where的区别分析

    sql语句中left join和inner join中的on与where的区别分析   关于SQL SERVER的表联接查询INNER JOIN .LEFT JOIN和RIGHT JOIN,经常会用到 ...

随机推荐

  1. vue 实例化使用模板

    var vm = new Vue({ el:"", data:{ }, methods:{ } })

  2. Spring 使用注解对事务控制详解与实例

    1.什么是事务 一荣俱荣,一损俱损,很多复杂的操作我们可以把它看成是一个整体,要么同时成功,要么同时失败. 事务的四个特征ACID: 原子性(Atomic):表示组成一个事务的多个数据库的操作的不可分 ...

  3. JAVA POI替换EXCEL模板中自定义标签(XLSX版本)满足替换多个SHEET中自定义标签

    个人说明:为了简单实现导出数据较少的EXCEL(根据自定义书签模板) 一.替换Excel表格标签方法```/** * 替换Excel模板文件内容 * @param map * 需要替换的标签建筑队形式 ...

  4. xshell行号显示

    xshell显示行号: 输入命令: vim ~/.vimrc 输入: set nu 之后在打开文件 就可以 看到行号显示.

  5. Qt版本中国象棋开发(四)

    内容:走法产生 中国象棋基础搜索AI, 极大值,极小值剪枝搜索, 静态估值函数 理论基础: (一)人机博弈走法产生: 先遍历某一方的所有棋子,再遍历整个棋盘,得到每个棋子的所有走棋情况(效率不高,可以 ...

  6. 分别针对Customers表与Order表的通用查询操作

    1.针对customers表通用的查询操作 CustomerForQuery package com.aff.PreparedStatement; import java.lang.reflect.F ...

  7. PowerPC-MPC56xx 启动模式

    https://mp.weixin.qq.com/s/aU4sg7780T3_5tJeApFYOQ   参考芯片参考手册第5章:Chapter 5 Microcontroller Boot   The ...

  8. Java实现 蓝桥杯 历届试题 核桃的数量

    历届试题 核桃的数量 时间限制:1.0s 内存限制:256.0MB 问题描述 小张是软件项目经理,他带领3个开发组.工期紧,今天都在加班呢.为鼓舞士气,小张打算给每个组发一袋核桃(据传言能补脑).他的 ...

  9. Java实现 LeetCode 447 回旋镖的数量

    447. 回旋镖的数量 给定平面上 n 对不同的点,"回旋镖" 是由点表示的元组 (i, j, k) ,其中 i 和 j 之间的距离和 i 和 k 之间的距离相等(需要考虑元组的顺 ...

  10. Java实现 LeetCode 65 有效数字

    65. 有效数字 验证给定的字符串是否可以解释为十进制数字. 例如: "0" => true " 0.1 " => true "abc&q ...