GDB調試技巧:調試複雜的宏定義

C語言中的宏定義,有著各種各樣的好處和壞處,可謂讓人有愛有恨。在大型的工程項目中,為了簡潔,為了封裝,宏的應用必不可少。但是在調試問題時,因為宏定義是被預定義處理的,所以不會有任何的編譯符號和調試信息。這樣給調試宏定義時,帶來了很大的困難。對於開發人員來說,除了直接肉眼去看宏定義,自己來展開宏定義去確定問題,是否還有其它手段來調試宏定義嗎?

本文介紹兩種調試宏定義的小技巧:

第一個方法是通過gcc -E 產生預編譯後的源代碼,即源代碼經過預編譯後的結果,所有的預編譯動作都已完成。如頭文件的插入,宏定義的展開。 如下面的代碼:

#include <stdlib.h>
#include <stdio.h>

#define MACRO1(x) (++(x))
#define MACRO2(x) (MACRO1(x)+100)
#define MACRO3(x) (MACRO2(x)+200)


int main(void)
{
    int a = 0;
    int b = 0;

    b = MACRO3(a);

    printf("%d\n", b);

    return 0;
}

這裡的MACRO3嵌套調用了MACRO2,MACRO1。在真正的代碼中,這種用法很常見,不過這處的宏定義很簡單,即使是嵌套調用也很容易看出。此處只是一個示意。 Ok,使用gcc -E test.c > test.e,得到預編譯後的代碼: /* 前面是1800+行的頭文件代碼,此處省略 */

int main(void)
{
    int a = 0;
    int b = 0;

    b = (((++(a))+100)+200);

    printf("%d\n", b);

    return 0;
}

這裡可以清晰的看到b = (((++(a))+100)+200);這個就比剛才的宏定義要清楚的多。

但是從這個例子也可以看到這個方法的侷限性。

  1. 由於預編譯處理會執行所有的預處理代碼,包括頭文件的插入,這導致最後的代碼行數太多。
  2. 得到的了一個新的代碼文件。這樣的話,在大型工程中,如果需要調試多個文件中的宏定義,需要我們一個一個的預編譯,太麻煩了。

下面看看第二個方法,這個方法要比第一種方法方便得多。 我們都知道為了調試程序,需要使用-g選項,它的作用就是將調試信息加入到最後的二進制可執行文件中。但是你可知道-g 也通-o一樣,是分級別的。當不指定級別的時候,其level為2。為了調試宏定義,我們可以使用更高的級別-g3。 下面為我使用-g3編譯上面的代碼,然後進行調試:

Breakpoint 1, main () at test.c:11
11 int a = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.i686
(gdb) n
12 int b = 0;
(gdb)
14 b = MACRO3(a);
(gdb)
16 printf("%d\n", b);
(gdb) macro expand MACRO3(a)
expands to: (((++(a))+100)+200)
(gdb) macro expand MACRO3(0)
expands to: (((++(0))+100)+200)
(gdb) macro exp MACRO3(0)
expands to: (((++(0))+100)+200)
(gdb)

在調試的過程中,可以使用macro expand/exp 來展開宏定義。從上面的調試過程中,可以直接看到宏定義展開後的結果。並且我們還可以給宏傳入任何的一個值,如:

(gdb) macro exp MACRO3(3)
expands to: (((++(3))+100)+200)
(gdb)

第二個方法無疑比第一個方法要方便簡單得多。我們只需要在全局的Makefile中添加新的編譯參數-g3,就可以支持整個工程代碼中所有的宏的調試。當然這個方法也有一個缺點,就是g3的調試信息會比默認的g2的調試信息要大——自然嘛,不然gdb如何知道怎樣展開宏定義呢。


书籍推荐