snprintf和sprintf區別分析


目錄[-]

今天在項目中使用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版本。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM