用 gdb 學 C 語言

我寫作本文的目的是,是向大家展示 gdb 這一學習 C 語言的強大工具。

我將介紹一些我最常用的 gdb 命令,同時還將會演示如何用 gdb 來理解 C 語言中最令人頭疼的內容:指針和數組的區別。

gdb 簡介

我們用這個 minimal.c 的小程序來開始:

int main(){   int i = 1337;   return 0;}

我們的程序簡單到連一個 printf 都沒使用。

來吧,拿出勇氣來,用 gdb 來探索 C 語言的新世界吧!

用 -g 選項編譯,以生成帶調試信息的可執行文件,導入 gdb :

$ gcc -g minimal.c -o minimal$ gdb minimal

現在,如果你看到的是一堆 gdb 提示信息,說明你已經進入 gdb 了。接下來:

(gdb) print 1 + 2$1 = 3

Amazing!print 是 gdb 用於處理 C 表達式的內建命令。如果你對 gdb 的某個命令不清楚, 試試help 命令查看 gdb 的幫助。接下來,看一個更有趣的例子:

(gdb) print (int) 2147483648$2 = -2147483648

至於爲什麼 2147483648 = -2147483648 ,我且不做解釋。這裏要表達的意思是,在 C語言中,即使是算數,也充滿着各種陷阱。還好 gdb 能看透這種伎倆。

現在我們在 main 函數設置斷點並運行:

(gdb) break main(gdb) run

好了,現在程序暫停在第3行,變量 i 初始化之前。趁 i 還未初始化,我們用 print 命令來顯示它的值:

(gdb) print i$3 = 32767

在 C 語言中,本地變量在未初始化前,它的值是不確定的,所以在你的電腦上 gdb 顯示的結果可能跟我的不一樣!我們用 next 命令執行當前這行代碼:

(gdb) next(gdb) print i$4 = 1337

內存查看命令 x

C 語言爲變量分配連續的內存空間。一個變量的內存空間由兩方面決定:

1.內存塊的起始字節地址。2.內存塊的 byte 大小。變量內存大小是由變量類型決定的。

C語言鮮明的特徵之一便是,可以直接訪問內存空間。取地址操作符 & 用於獲取變量的地址,而 sizeof 操作符用於計算變量佔用的內存空間大小。

嘗試在 gdb 運行下面的命令:

(gdb) print &i$5 = (int *) 0x7fff5fbff584(gdb) print sizeof(i)$6 = 4

輸出結果表明,變量 i 的內存起始地址爲 0x7fff5fbff584 ,並且佔用4 byte 空間。我在上面曾說過,變量佔用內存的大小由它的類型決定,實際上 sizeof 運算符也可以直接對變量類型進行計算:

(gdb) print sizeof(int)$7 = 4(gdb) print sizeof(double)$8 = 8

這說明,在我的機器上, int 類型變量佔4字節,而 double 類型變量佔8字節。

x 命令,是 gdb 直接查看內存的強大工具。 x 命令查看指定位置起始的內存段,並有一系列格式化命令精確控制查看的字節數以及顯示方式。

如有疑問,請在 gdb 中查看幫助 help x 。

由於 & 操作符用於計算變量地址,因此可以把 &i 的結果作爲參數傳入 x ,這樣,我們便可以窺探到變量 i 在內存的原始字節形式啦:

(gdb) x/4xb &i0x7fff5fbff584: 0x39    0x05    0x00    0x00

4xb 意思是,我要查看4個數值,格式化爲 he x (十六進制), 按1 byte一次顯示。我只選擇顯示4字節,是因爲變量 i 大小爲4個字節。上面的輸出顯示, i 是逐字節存放在內存中的。

在直接查看內存字節時,這裏有個細節需要記住:在 Intel 的機器上,字節內容是按照"little-edian" 小端的方式存放的。與人的直覺相反,小端方式是數值的最低有效位放在內存的最前面。

爲了更加直觀,舉個例子來說明:

(gdb) set var i = 0x12345678(gdb) x/4xb &i0x7fff5fbff584: 0x78 0x56 0x34 0x12

類型查看命令 ptype

ptype 是我最常用的命令,它可以返回 C 表達式的類型。

(gdb) ptype itype = int(gdb) ptype &itype = int *(gdb) ptype maintype = int (void)

C 語言中,有非常複雜的數據類型,不過,利用 ptype 命令,便可以以交互的方式來查看,實在是方便的很~

指針與數組

數組是 C 語言中相當靈活的內容。我寫的這部分就是想通過 gdb 剖析這個簡單的程序,進而弄懂數組的含義。名爲 arrays.c 的程序:

int main(){    int a[] = {1,2,3};    return 0;}

用 -g 選項編譯,裝入 gdb 中運行, next 直到運行完數組初始化:

$ gcc -g arrays.c -o arrays$ gdb arrays(gdb) break main(gdb) run(gdb) next

看下經過初始化化後 a 中的內容:

(gdb) print a$1 = {1, 2, 3}(gdb) ptype atype = int [3]

好了,程序正確地運行了。接下來我們首先要做的是用 x 命令看看 a 在內存中到底是怎樣一種存在:

(gdb) x/12xb &a0x7fff5fbff56c: 0x01  0x00  0x00  0x00  0x02  0x00  0x00  0x000x7fff5fbff574: 0x03  0x00  0x00  0x00

上面的結果顯示,a 這塊內存起始地址爲 0x7fff5fbff56c ,第一個4 byte 存儲在 a[0] ,下一個4 byte 存放在 a[1] ,最後4 byte 放在 a[2] 。當然,也可以通過 sizeof 得知 a 所佔內存大小:

(gdb) print sizeof(a)$2 = 12

從這點上來看,數組確實有其作爲數組的特徵:有他們自己像數組的類型以及存放數組於連續的內存空間。然而,從特定的角度看,數組的行爲又與指針極爲相似:

= preserve do  :escaped    (gdb) print a + 1    $3 = (int *) 0x7fff5fbff570

這顯示,a + 1 是一個指向 0x7fff5fbff570 的 int 指針。到這裏,本能地用 x 命令看下這地址的內容:

= preserve do  :escaped    (gdb) x/4xb a + 1    0x7fff5fbff570: 0x02  0x00  0x00  0x00

我們注意到 0x7fff5fbff570 比 a 的起始地址 0x7fff5fbff56c 大4個字節。因爲給定 int 類型佔4byte ,所以 a + 1 應該是指向 a[1] 。

事實上,數組的下標運算是指針的語法糖:a[i] 等同於 *(a + i) 。看下面的嘗試:

= preserve do  :escaped    (gdb) print a[0]    $4 = 1    (gdb) print *(a + 0)    $5 = 1    (gdb) print a[1]    $6 = 2    (gdb) print *(a + 1)    $7 = 2    (gdb) print a[2]    $8 = 3    (gdb) print *(a + 2)    $9 = 3

由上面種種可知,在一些環境下 a 類似於數組,而在另外一些條件下它又像指向數組第一個元素的指針。這究竟是怎麼一回事呢?

這個問題的答案是:當數組名用在 C 表達式中時,它便“退化"爲指向數組第一個元素的地址。不過,對於這條規則,有2個例外:

•數組名作爲 sizeof 參數時;•數組名用於 & 操作符時。

這就讓人疑惑了,難道同爲指針的 a 和 &a 竟是不相同的嗎?

就數值而言,它們都表示同一地址:

= preserve do  :escaped    (gdb) x/4xb a    0x7fff5fbff56c: 0x01  0x00  0x00  0x00    (gdb) x/4xb &a    0x7fff5fbff56c: 0x01  0x00  0x00  0x00

然而,它們本身的類型卻是不相同的。

此前,我們已經知道 a 是指向數組第一元素的指針,所以是 int * 類型。

至於 &a 的話,我們直接用 gdb 查看一下:

= preserve do  :escaped    (gdb) ptype &a    type = int (*)[3]

很明顯, &a 是指向帶有3個整數的數組的指針,亦即 int [3] ,顯然與 a 不一樣嘛。在下面的指針運算中,你可以更加明顯的看到 a 與 &a 的差異:

= preserve do  :escaped    (gdb) print a + 1    $10 = (int *) 0x7fff5fbff570    (gdb) print &a + 1    $11 = (int (*)[3]) 0x7fff5fbff578

a + 1 只是在 a 的地址上加4,而 &a + 1 卻增加了12!指針 a 實際上是與 &a[0] 等同的。

總結

希望我上述的內容已經讓你心動:gdb 的確是一個深入探索 C 語言不可多得的學習環境! 

心動不如行動,趕緊用起來吧,在實踐中不斷提高!

參考

1.GNU Debugger - Wikipedia[1] ;2.Learning C with gdb[2] ;3.Expert C Programming: Deep C Secrets[3] .

References

[1] GNU Debugger - Wikipedia: https://en.wikipedia.org/wiki/GNU_Debugger
[2] Learning C with gdb: https://www.recurse.com/blog/5-learning-c-with-gdb
[3] Expert C Programming: Deep C Secrets: http://www.electroons.com/8051/ebooks/expert%20C%20programming.pdf

 

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