snprintf和sprintf区别分析

忘是亡心i 2022-01-14 05:27 491阅读 1赞

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

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

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

snprintf函数的返回值

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

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

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

  1. len_3 = 25,tlist_3 = this is a

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

snprintf函数的字符串缓冲

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

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

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

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

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

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

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

使用_IO_vsnprintf函数实现:

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

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

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

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

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

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

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

  1. hong@ubuntu:~/test/test-example$ ldd snprintf_test
  2. linux-gate.so.1 => (0xb76f7000)
  3. libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7542000)
  4. /lib/ld-linux.so.2 (0xb76f8000)
  5. hong@ubuntu:~/test/test-example$ file /lib/i386-linux-gnu/libc.so.6
  6. /lib/i386-linux-gnu/libc.so.6: symbolic link to `libc-2.15.so'
  7. lzhong@ubuntu:~/test/test-example$ file /lib/i386-linux-gnu/libc-2.15.so
  8. /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. hong@ubuntu:~/test/test-example$ ldd --version
  2. ldd (Ubuntu EGLIBC 2.15-0ubuntu20) 2.15
  3. Copyright (C) 2012 Free Software Foundation, Inc.
  4. This is free software; see the source for copying conditions. There is NO
  5. warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  6. Written by Roland McGrath and Ulrich Drepper.

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

转载于:https://my.oschina.net/shelllife/blog/177279

发表评论

表情:
评论列表 (有 0 条评论,491人围观)

还没有评论,来说两句吧...

相关阅读