今天在項目中使用snprintf時遇到一個比較迷惑的問題,追根溯源了一下,在此對sprintf和snprintf進行一下對比分析。 linux
由於sprintf可能致使緩衝區溢出問題而不被推薦使用,因此在項目中我一直優先選擇使用snprintf函數,雖然會稍微麻煩那麼一點點。這裏就是sprintf和snprintf最主要的區別:snprintf經過提供緩衝區的可用大小傳入參數來保證緩衝區的不溢出,若是超出緩衝區大小則進行截斷。可是對於snprintf函數,還有一些細微的差異須要注意。 shell
sprintf函數返回的是實際輸出到字符串緩衝中的字符個數,包括null結束符。而snprintf函數返回的是應該輸出到字符串緩衝的字符個數,因此snprintf的返回值可能大於給定的可用緩衝大小以及最終獲得的字符串長度。看代碼最清楚不過了: ubuntu
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);上述代碼段的輸出結果以下:
len_3 = 25,tlist_3 = this is a因此在使用snprintf函數的返回值時,須要當心慎重,避免人爲形成的緩衝區溢出,否則得不償失。
int sprintf(char *str, const char *format, ...); int snprintf(char *str, size_t size, const char *format, ...);上面的函數原型你們都很是熟悉,我一直覺得snprintf除了多一個緩衝區大小參數外,表現行爲都和sprintf一致,直到今天趕上的bug。在此以前我把下面的代碼段的兩個輸出視爲一致。
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);可實際上獲得的輸出結果倒是:
tlist_1: g1; tlist_2: a1;b1;c1;d1;e1;f1;g1;知其然就應該知其因此然,這是良好的求知態度,因此果斷翻glibc的源代碼去,不憑空想固然。下面用代碼說話,這就是開源的好處之一。首先看snprintf的實現:
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函數實現:
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的實現:
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函數實現:
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掉了: 函數
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用法。 ui
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版本。