2014/2/23

【C/C++】修飾子小細節大臭蟲

這裡要探討的是修飾子可能導致的臭蟲,很容易埋入的臭蟲而且難以找出,直接看下面這段程式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <linux/types.h>

int main (int argc, char *argv[])
{
 __u16 b;
 __u8 a[3];
 
 b = 102;
 
 if (argc <= 1) {
  printf ("Please provide 1 integer as arguments\n");
  return -1;
 }
 
 if (sscanf (argv[1], "%d", &a[0]) != 1) {
  printf ("Wrong argument: %s\n", argv[1]);
  return -1;
 }
 
 printf ("a[0] = %d, b = %d\n", a[0], b);
}

先看執行結果:

1
2
$ ./test 10
a[0] = 10, b = 0

從程式描述可知預期輸出結果應該是b = 102
但問題在哪呢?如果有試著編譯上述程式碼還會發現編譯時,編譯器早發覺不對勁而提出警告了:

1
2
3
$ gcc -o test test.c 
test.c: In function ‘main’:
test.c:16:2: warning: format ‘%d’ expects argument of type ‘int *’, but argument 3 has type ‘__u8 *’ [-Wformat]

根據編譯器提出的警告,可以有兩種修正方法:
第一種,讓編譯器開心但會徹底把臭蟲埋入程式且難以發現:
既然編譯器預期%d修飾符應該給對應一個int *類別變數,那就改成

1
2
3
4
if (sscanf (argv[1], "%d", (int *)&a[0]) != 1) {
 printf ("Wrong argument: %s\n", argv[1]);
 return -1;
}

改完後看來編譯器滿意了,沒有丟出任何警告執行應該就沒問題吧,結果發現結局一樣,這時不禁開始懷疑sscanf這個函數是不是有bug阿?

翻閱sscanf手冊的Conversions區段有以下描述:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Conversions
  The following type modifier characters can appear in a conversion spec‐
  ification:

  h      Indicates that the conversion will be one of d, i, o, u,  x,  X,
         or  n  and  the  next  pointer  is  a  pointer to a short int or
         unsigned short int (rather than int).

  hh     As for h, but the next pointer is a pointer to a signed char  or
         unsigned char.

還沒看出端倪嗎?沒關係,再看看下面增加幾行印出變數位址的程式內容:

 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
#include <stdio.h>
#include <linux/types.h>

int main (int argc, char *argv[])
{
 __u16 b;
 __u8 a[3];
 
 b = 102;
 
 if (argc <= 1) {
  printf ("Please provide 1 integer as arguments\n");
  return -1;
 }
 
 if (sscanf (argv[1], "%d", (int *)&a[0]) != 1) {
  printf ("Wrong argument: %s\n", argv[1]);
  return -1;
 }
 
 printf ("a[0] = %d, b = %d\n", a[0], b);
 printf ("Address of a[0]: %p\n", &a[0]);
 printf ("Address of b: %p\n", &b);
 printf ("Size of int: %d\n", sizeof (int));
}

執行結果如下:

1
2
3
4
5
$ ./test 10
a[0] = 10, b = 0
Address of a[0]: 0xbfc058cb
Address of b: 0xbfc058ce
Size of int: 4

發現變數b距離a[0]只有3 bytes,且得知在此系統環境下一個int長度是4 bytes,此時是否恍然大悟?原來sscanf使用%d修飾字要求把argv[1]字串解析成數值為int並且存入a[0],但a[0]實際只有1 bytes呢!多出來的3 bytes就把記憶體後面連續的空間也改蓋掉了,此時變數b的內容就給填為0了。

第二種修正方法:
回憶剛剛貼的手冊說明內容,既然後面承接的變數長度為8 bits,則應該將程式內容改為:

1
2
3
4
if (sscanf (argv[1], "%hhd", &a[0]) != 1) {
 printf ("Wrong argument: %s\n", argv[1]);
 return -1;
}

得到結果也符合預期且編譯器也不再哇哇叫,皆大歡喜!

Keywords: C/C++、Variable Alignment

沒有留言:

張貼留言