區別snprintf和sprintf

在項目完成階段,進行coverity scan時,常常會掃出類似提示,說使用sprintf, is Calling risky function,May result in a security violation。

實際是因爲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版本。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章