我寫作本文的目的是,是向大家展示 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 &i
0x7fff5fbff584: 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 &i
0x7fff5fbff584: 0x78 0x56 0x34 0x12
類型查看命令 ptype
ptype
是我最常用的命令,它可以返回 C 表達式的類型。
(gdb) ptype i
type = int
(gdb) ptype &i
type = int *
(gdb) ptype main
type = 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 a
type = int [3]
好了,程序正確地運行了。接下來我們首先要做的是用 x
命令看看 a
在內存中到底是怎樣一種存在:
(gdb) x/12xb &a
0x7fff5fbff56c: 0x01 0x00 0x00 0x00 0x02 0x00 0x00 0x00
0x7fff5fbff574: 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