通過(guò)GDB學(xué)習(xí)C語(yǔ)言的講解
對(duì)于那些具有高級(jí)編程語(yǔ)言諸如: Ruby、Scheme、Haskell 等背景的人來(lái)說(shuō),學(xué)習(xí) C 語(yǔ)言是具有挑戰(zhàn)性的。除了糾結(jié)于 C 語(yǔ)言中像手動(dòng)內(nèi)存管理和指針等底層特性外,你必須在沒(méi)有 REPL ( Read-Eval-Print Loop ) 的條件下完成工作。一旦你已經(jīng)習(xí)慣于在 REPL 環(huán)境下進(jìn)行探索性的編程,必須進(jìn)行“編寫(xiě)-編譯-運(yùn)行”這樣循環(huán)實(shí)在有點(diǎn)令人生厭。
最近我發(fā)現(xiàn)其實(shí)可以用 GDB 來(lái)作為 C 語(yǔ)言的偽 REPL。我一直嘗試使用 GDB 作為學(xué)習(xí) C 語(yǔ)言的工具,而不僅僅是用來(lái)調(diào)試 C 程序,事實(shí)上這非常有趣。
這篇文章我的目的就是向你展示 GDB 是一個(gè)非常好的學(xué)習(xí) C 語(yǔ)言工具。下面我將會(huì)向你介紹一些我最喜歡的 GDB 命令,然后我會(huì)向你闡述怎樣使用 GDB 來(lái)理解 C 語(yǔ)言中一個(gè)出了名的復(fù)雜問(wèn)題:數(shù)組和指針的區(qū)別。
GDB 簡(jiǎn)介
從創(chuàng)建一個(gè)簡(jiǎn)單的 C 程序開(kāi)始,minimal.c:
int main() { int i = 1337; return 0; }
注意這個(gè)程序并沒(méi)有做任何事情,也沒(méi)有一條輸出指令。擁抱使用 GDB 學(xué)習(xí) C 語(yǔ)言的美麗新世界吧!
使用 -g 參數(shù)進(jìn)行編譯,這樣會(huì)生成一些有助于 debug,gdb 可以利用的信息,編譯后用 GDB 運(yùn)行起來(lái):
$ gcc -g minimal.c -o minimal $ gdb minimal
你現(xiàn)在應(yīng)該能看到明顯的 GDB 提示行。我之前告訴你這是一個(gè) REPL,下面我們就來(lái)試試:
(gdb) print 1 + 2 $1 = 3
多么神奇! print 是 GDB 的內(nèi)置命令,他能夠打印出一個(gè) C 語(yǔ)言命令的返回值。如果你不確定一個(gè) GDB 命令是做什么,嘗試在 GDB 提示下運(yùn)行命令 help。
然后是一個(gè)更有趣的例子:
(gbd) print (int) 2147483648 $2 = -2147483648
這里我先忽略為什么 2147483648 == -2147483648;我想要說(shuō)明的是即使是算術(shù)運(yùn)算在 C 語(yǔ)言中也是有很多坑的,GDB 能夠理解運(yùn)行 C 語(yǔ)言中的算術(shù)運(yùn)算。
現(xiàn)在讓我們?cè)谥骱瘮?shù)中設(shè)置一個(gè)斷點(diǎn)然后運(yùn)行程序:
(gdb) break main (gdb) run
現(xiàn)在程序在第 3 行處暫停,正好在 i 進(jìn)行初始化之前。有趣的是,盡管 i 還沒(méi)有被初始化,我們依然能夠使用 print 命令看到它的值。
(gdb) print i $3 = 32767
在C語(yǔ)言中,一個(gè)未被初始化的局部變量的值是沒(méi)有定義的,所以你用 GDB 打印出的值可能與這里的不一樣。
我們可以用 next 命令來(lái)執(zhí)行當(dāng)前斷點(diǎn)這一行:
(gdb) next (gdb) print i $4 = 1337
使用x 命令檢查內(nèi)存
在C語(yǔ)言中變量用來(lái)標(biāo)示一塊連續(xù)的內(nèi)存區(qū)間。一個(gè)變量的內(nèi)存區(qū)間由兩個(gè)數(shù)字決定:
這塊內(nèi)存第一個(gè)字節(jié)數(shù)的數(shù)值地址
內(nèi)存的大小,單位是字節(jié)。變量所占內(nèi)容的大小取決于變量的類(lèi)型。
C 語(yǔ)言中一個(gè)獨(dú)特的特性是你能夠直接訪(fǎng)問(wèn)變量所占的內(nèi)存。操作符 & 可以計(jì)算一個(gè)變量的地址,操作符 sizeof 計(jì)算變量所占內(nèi)存的大小。
你可以在 GDB 中測(cè)試以上兩個(gè)概念:
(gdb) print &i $5 = (int *) 0x7fff5fbff584 (gdb) print sizeof(i) $6 = 4
字面上看,i 所占內(nèi)存起始于地址 0x7fff5fbff5b4,占內(nèi)存 4 個(gè)字節(jié)。
我前面提到的變量在內(nèi)存中的大小取決于它的類(lèi)型,所以操作符 sizeof 能夠直接作用于類(lèi)型:
(gdb) print sizeof(int) $7 = 4 (gdb) print sizeof(double) $8 = 8
以上顯示意味著,至少在我的計(jì)算機(jī)上 int 變量占 4 個(gè)字節(jié)空間,double 變量占 8 個(gè)字節(jié)。
GDB 帶來(lái)了一個(gè)功能強(qiáng)大的工具,能夠直接檢測(cè)內(nèi)存:x 命令。x 命令從一個(gè)特定的地址開(kāi)始檢測(cè)內(nèi)存。結(jié)合一些結(jié)構(gòu)化的命令和這些已給的命令能精確控制你想檢測(cè)多少字節(jié),你想怎樣打印它們。當(dāng)你有疑問(wèn)時(shí),嘗試在 GDB 提示下運(yùn)行 help x。
& 操作符計(jì)算變量的地址,這意味著我們能將 &i 返回給 x,從而看到 i 值背后原始的字節(jié)。
(gdb) x/4xb &i 0x7fff5fbff584: 0x39 0x05 0x00 0x00
標(biāo)識(shí)參數(shù)表示我想要檢查 4 個(gè)值,格式是十六進(jìn)制,一次顯示一個(gè)字節(jié)。我選擇檢查 4 個(gè)字節(jié),是因?yàn)?i 在內(nèi)存中的大小是 4 字節(jié);逐字節(jié)打印出 i 在內(nèi)存中的表示。
在 Intel 機(jī)器上有一個(gè)坑應(yīng)當(dāng)記得,逐字節(jié)檢測(cè)時(shí)字節(jié)數(shù)是以“小端”順序保存:不像人類(lèi)一般使用的標(biāo)記方法,一個(gè)數(shù)字的低位在內(nèi)存中排在前面(個(gè)位數(shù)在十位數(shù)之前)。
為了讓這個(gè)問(wèn)題更加明顯,我們可以為 i 賦一個(gè)特別的值,然后重新檢測(cè)所占內(nèi)存。
(gdb) set var i = 0x12345678 (gdb) x/4xb &i 0x7fff5fbff584: 0x78 0x56 0x34 0x12
使用 ptype 檢查類(lèi)型
ptype 命令可能是我最喜愛(ài)的命令。它告訴你一個(gè) C 語(yǔ)言表達(dá)式的類(lèi)型。
(gdb) ptype i type = int (gdb) ptype &i type = int * (gdb) ptype main type = int (void)
C 語(yǔ)言中的類(lèi)型可以變得很復(fù)雜,但是好在 ptype 允許你交互式地查看他們。
指針和數(shù)組
數(shù)組在C語(yǔ)言中是非常難以捉摸的概念。這節(jié)的計(jì)劃是寫(xiě)出一個(gè)簡(jiǎn)單的程序,然后在 GDB 中運(yùn)行,直至它的意義變得清晰易懂。
編寫(xiě)如下的程序,array.c:
int main() { int a[] = {1,2,3}; return 0; }
使用 -g 作為命令行參數(shù)進(jìn)行編譯,在 GDB 中運(yùn)行,然后輸入 next,執(zhí)行初始化那一行
$ gcc -g arrays.c -o arrays $ gdb arrays (gdb) break main (gdb) run (gdb) next
在這里,你應(yīng)該能夠打印出 a 的內(nèi)容并檢查它的類(lèi)型:
(gdb) print a $1 = {1, 2, 3} (gdb) ptype a type = int [3]
現(xiàn)在我們的程序已經(jīng)在 GDB 中運(yùn)行起來(lái)了,我們應(yīng)該做的第一件事是使用 x 看看 a 在內(nèi)存中是什么樣子。
(gdb) x/12xb &a 0x7fff5fbff56c: 0x01 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x7fff5fbff574: 0x03 0x00 0x00 0x00
以上意思是 a 所占內(nèi)存開(kāi)始于地址 0x7fff5fbff5dc。起始的四個(gè)字節(jié)存儲(chǔ) a[0], 隨后的四個(gè)字節(jié)存儲(chǔ) a[1], 最后的四個(gè)字節(jié)存儲(chǔ) a[2]。事實(shí)上你可以通過(guò) sizeof 得到,a 在內(nèi)存中的大小是 12 字節(jié)。
(gdb) print sizeof(a) $2 = 12
現(xiàn)在,數(shù)組好像確實(shí)有個(gè)數(shù)組的樣子。他們有自己的數(shù)組類(lèi)型,在連續(xù)的內(nèi)存空間中存儲(chǔ)自己的成員。然而在某些情況下,數(shù)組表現(xiàn)得更像指針。例如,我們能在 a 上進(jìn)行指針運(yùn)算。
= preserve do :escaped (gdb) print a + 1 $3 = (int *) 0x7fff5fbff570
字面上看,a+1 是一個(gè)指向 int 的指針,占據(jù)地址 0x7fff5fbff570。這時(shí),你應(yīng)該反過(guò)來(lái)將指針傳遞給 x 命令,讓我們看看會(huì)發(fā)生什么:
= preserve do :escaped (gdb) x/4xb a + 1 0x7fff5fbff570: 0x02 0x00 0x00 0x00
注意 0x7fff5fbff570 比 0x7fff5fbff56c 大 4,后者是 a 在內(nèi)存地址中的第一個(gè)字節(jié)??紤]到 int 值占 4 字節(jié),這意味著 a+1 指向 a[1].
事實(shí)上,在 C 語(yǔ)言中數(shù)組索引是指針運(yùn)算的語(yǔ)法糖:a[i] 等于 *(a+i)。你可以在 GDB 中嘗試一下。
= 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
我們已經(jīng)看到在某些情況下,a 表現(xiàn)的像一個(gè)數(shù)組,在另一些情況下表現(xiàn)得像一個(gè)指向它首元素的指針。接下來(lái)會(huì)發(fā)生什么呢?
答案是當(dāng)一個(gè)數(shù)組名在 C 語(yǔ)言表達(dá)式中使用時(shí),它“退化”成指向這個(gè)數(shù)組首元素的指針。這個(gè)規(guī)則只有兩個(gè)例外:當(dāng)數(shù)組名傳遞給 sizeof 時(shí),當(dāng)數(shù)組名傳遞給操作數(shù) & 時(shí)。
事實(shí)上,a 在傳遞給操作數(shù) & 時(shí)并沒(méi)有“退化”成一個(gè)指針,這就帶來(lái)一個(gè)有趣的問(wèn)題:由“退化”變成的指針和 &a 存在區(qū)別嗎?
數(shù)值上講,他們都表示相同的地址:
= preserve do :escaped (gdb) x/4xb a 0x7fff5fbff56c: 0x01 0x00 0x00 0x00 (gdb) x/4xb &a 0x7fff5fbff56c: 0x01 0x00 0x00 0x00
然而,他們的類(lèi)型是不同的。我們已經(jīng)看到 a 退化的值是指向 a首元素的指針;這個(gè)必須是類(lèi)型 int *。對(duì)于類(lèi)型 &a,我們可以直接詢(xún)問(wèn) GDB:
= preserve do :escaped (gdb) ptype &a type = int (*)[3]
從顯示上看,&a 是一個(gè)指向 3 個(gè)整數(shù)數(shù)組的指針。這就說(shuō)明:當(dāng)傳遞給 & 時(shí),a 沒(méi)有退化,a 有了一個(gè)類(lèi)型,是 int[3]。
通過(guò)測(cè)試他們?cè)谥羔樳\(yùn)算時(shí)的表現(xiàn),你可以觀(guān)察到 a 的退化值和 &a 的明顯區(qū)別。
= preserve do :escaped (gdb) print a + 1 $10 = (int *) 0x7fff5fbff570 (gdb) print &a + 1 $11 = (int (*)[3]) 0x7fff5fbff578
注意到對(duì) a 增加 1 等于對(duì) a 的地址增加 4,與此同時(shí),對(duì) &a 增加 1 等于對(duì) a 的地址增加 12!
實(shí)際上 a 退化成的指針是 &a[0];
= preserve do :escaped (gdb) print &a[0] $11 = (int *) 0x7fff5fbff56c
結(jié)論
希望我已經(jīng)向你證明 GDB 是學(xué)習(xí) C 語(yǔ)言的一個(gè)靈巧而有富有探索性的環(huán)境。你能使用 print 打印表達(dá)式的值,使用 x 查看內(nèi)存中原始字節(jié),使用 ptype 配合類(lèi)型系統(tǒng)進(jìn)行問(wèn)題修補(bǔ)。
如果你想要進(jìn)一步對(duì)使用 GDB 學(xué)習(xí) C 語(yǔ)言進(jìn)行嘗試,我有一些建議如下:
1.用 gdb 通過(guò) Ksplice 指針挑戰(zhàn)。
2.研究結(jié)構(gòu)體是怎樣在內(nèi)存中存儲(chǔ)的? 他們與數(shù)組比較又有什么異同?
3.使用 GDB 的 disassemble 命令學(xué)習(xí)匯編語(yǔ)言!一個(gè)特別有趣的練習(xí)是研究函數(shù)調(diào)用棧是如何工作的。
4.試試 GDB 的 “ tui ”模式,這個(gè)模式在常規(guī) GDB 頂層提供一個(gè)圖像化的 ncurses 層(Ncurses 提供字符終端處理庫(kù),包括面板和菜單)。在 OS X 系統(tǒng)中,你可能需要用源代碼安裝 GDB。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。如果你想了解更多相關(guān)內(nèi)容請(qǐng)查看下面相關(guān)鏈接
- C語(yǔ)言項(xiàng)目小學(xué)生數(shù)學(xué)考試系統(tǒng)參考
- C語(yǔ)言項(xiàng)目爬樓梯的兩種實(shí)現(xiàn)方法參考
- C語(yǔ)言程序打豆豆(函數(shù)版)
- 劍指offer之C語(yǔ)言不修改數(shù)組找出重復(fù)的數(shù)字
- C語(yǔ)言測(cè)試n的階乘和x的n次方
- C語(yǔ)言數(shù)組a和&a的區(qū)別講解
- C語(yǔ)言實(shí)現(xiàn)詞法分析器
- 使用Python向C語(yǔ)言的鏈接庫(kù)傳遞數(shù)組、結(jié)構(gòu)體、指針類(lèi)型的數(shù)據(jù)
- 如何寫(xiě)出優(yōu)美的C語(yǔ)言代碼
- C語(yǔ)言項(xiàng)目全正整數(shù)后再計(jì)算的三種參考解答方法
相關(guān)文章
C++中bitset位圖介紹及模擬實(shí)現(xiàn)
位圖就是用每一位來(lái)存放某種狀態(tài),適用于海量數(shù)據(jù),本文就介紹一下C++中bitset位圖介紹及模擬實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-07-07C++面向行輸入之get()與getline()實(shí)例詳解
在c++里當(dāng)我們輸入一個(gè)字符串時(shí)習(xí)慣用cin,但是cin只能讀取一段不含空格的字符串,如果我們需要讀取一段包含空格的字符串時(shí),就需要用到getline()或get(),下面這篇文章主要給大家介紹了關(guān)于C++面向行輸入之get()與getline()的相關(guān)資料,需要的朋友可以參考下2021-10-10C++基于Floyd算法實(shí)現(xiàn)校園導(dǎo)航系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C++基于Floyd算法實(shí)現(xiàn)校園導(dǎo)航系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03opencv3/C++ 將圖片轉(zhuǎn)換為視頻的實(shí)例
今天小編就為大家分享一篇opencv3/C++ 將圖片轉(zhuǎn)換為視頻的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-12-12C語(yǔ)言實(shí)現(xiàn)銷(xiāo)售管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)銷(xiāo)售管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02詳解C++中如何將構(gòu)造函數(shù)或析構(gòu)函數(shù)的訪(fǎng)問(wèn)權(quán)限定為private
這篇文章主要介紹了詳解C++中如何將構(gòu)造函數(shù)或析構(gòu)函數(shù)的訪(fǎng)問(wèn)權(quán)限定為private的方法,文中還解釋了構(gòu)造函數(shù)與虛函數(shù)的區(qū)別,需要的朋友可以參考下2016-03-03