國內私募機構九鼎控股打造APP,來就送 20元現金領取地址:http://jdb.jiudingcapital.com/phone.html
內部邀請碼:C8E245J (不寫邀請碼,沒有現金送)
國內私募機構九鼎控股打造,九鼎投資是在全國股份轉讓系統掛牌的公眾公司,股票代碼為430719,為“中國PE第一股”,市值超1000億元。
------------------------------------------------------------------------------------------------------------------------------------------------------------------
Linux下C程序的編輯,編譯和運行以及調試
要使用的工具:
編輯:vim(vi)
編譯和運行:gcc
調試:gdb
安裝很簡單(以下是以在CentOS中安裝為例):
1 |
yum vim gcc gdb |
1.使用vim編輯源文件
首先,打開終端練下手:
1 |
vim hello.c |
(進入一般模式)
按下"i",進入編輯模式,在編輯模式下輸入:
1 |
#include <stdio.h> |
2 |
int main(){ |
3 |
printf ( "Hello, World!\n" ); |
4 |
return 0; |
5 |
} |
輸入完成,按"ESC"鍵,回到一般模式,然后按下":wq",即可保存並退出vim。
附注:
在一般模式下,按下":%!xxd"查看hello.c的16進制形式,回到文本格式按下":%!xxd -r"。
查看hello.c的二進制形式,按下":%!xxd -b",這是hello.c保存在磁盤上的存儲狀態。
至此,在vim已完成C源文件的編輯。
關於vim的使用,直接上網搜索vim,相關的文章是相當多的;或者參考vim的聯機幫助,在命令行上鍵入"man vim"即可。
2.編譯和運行
gcc命令的基本用法:
1 |
gcc[options] [filenames] |
其中,filenames為文件名;options為編譯選項
當不使用任何編譯選項編譯hello.c時,gcc將會自動編譯產生一個a.out的可執行文件:
1 |
[root@localhost c] # ls |
2 |
hello.c |
3 |
[root@localhost c] # gcc hello.c |
4 |
[root@localhost c] # ls |
5 |
a.out hello.c |
執行:
1 |
[root@localhost c] # ./a.out |
2 |
Hello, World! |
使用-o編譯選擇,可以為編譯后的文件指定一個名字:
1 |
[root@localhost c] # ls |
2 |
a.out hello.c |
3 |
[root@localhost c] # gcc hello.c -o hello |
4 |
[root@localhost c] # ls |
5 |
a.out hello hello.c |
執行:
1 |
[root@localhost c] # ./hello |
2 |
Hello, World! |
注意:使用-o選項時,-o后面必須跟一個文件名,即:-o outfile。
為了便於描述后面的選項,刪除hello和a.out可執行文件。
結合介紹gcc的編譯選項,分析hello.c的編譯和執行過程:
(1)預處理階段:使用-E選項,對輸入文件只做預處理不編譯。當使用這個選項時,預處理器的輸出被送到標准輸出而不是存儲到文件。如果想將預處理的輸出存儲到文件,可結合-o選項使用,使用如下:
1 |
[root@localhost c] # ls |
2 |
hello.c |
3 |
[root@localhost c] # gcc -E hello.c -o hello.i |
4 |
[root@localhost c] # ls |
5 |
hello.c hello.i |
使用less查看下hello.i:
1 |
[root@localhost c] # less hello.i |
(2)編譯階段:使用-S選項,將C程序編譯為匯編語言文件后停止編譯,gcc編譯產生匯編文件的默認后綴為.s。
1 |
[root@localhost c] # ls |
2 |
hello.c hello.i |
3 |
[root@localhost c] # gcc -S hello.c |
4 |
[root@localhost c] # ls |
5 |
hello.c hello.i hello.s |
在gcc -S hello.c處,使用C源文件編譯,也可以用gcc -S hello.i的預處理文件編譯,結果一樣。
使用-S編譯時,也可以和-o結合使用指定編譯產生的匯編語言文件的名字:
1 |
[root@localhost c] # ls |
2 |
hello.c hello.i hello.s |
3 |
[root@localhost c] # gcc -S hello.i -o hello_s.s |
4 |
[root@localhost c] # ls |
5 |
hello.c hello.i hello.s hello_s.s |
可使用less命令查看匯編代碼。
(3)匯編階段:使用-c選項,將C源文件或者匯編語言文件編譯成可重定向的目標文件(二進制形式),其默認后綴為.o。
1 |
[root@localhost c] # ls |
2 |
hello.c hello.i hello.s hello_s.s |
3 |
[root@localhost c] # gcc -c hello.s |
4 |
[root@localhost c] # ls |
5 |
hello.c hello.i hello.o hello.s hello_s.s |
也可以和-o結合使用指定編譯產生的目標文件的名字:
1 |
[root@localhost c] # gcc -c hello.s -o hello.o |
由於hello.o是二進制文件,使用less查看顯示為亂碼;
然后使用vim hello.o打開也顯示為亂碼,按下":%!xxd"查看其16進制形式,按下":%!xxd -r"退出 16進制查看模式,回到亂碼狀態。在退出vim時,若提示已經修改了文件,則使用":q!"強制退出。
(4)鏈接階段:鏈接器將可重定向的目標文件hello.o以及庫文件(如printf.o)執行並入操作,形成最終可執行的可執行目標文件。
1 |
[root@localhost c] # ls |
2 |
hello.c hello.i hello.o hello.s hello_s.s |
3 |
[root@localhost c] # gcc hello.o |
4 |
[root@localhost c] # ls |
5 |
a.out hello.c hello.i hello.o hello.s hello_s.s |
可使用-o選項,指定輸出文件(即可執行目標文件)的名字:
1 |
[root@localhost c] # gcc hello.o -o hello |
2 |
[root@localhost c] # ls |
3 |
a.out hello hello.c hello.i hello.o hello.s hello_s.s |
(5)執行階段:
1 |
[root@localhost c] # ./a.out |
2 |
Hello, World! |
3 |
[root@localhost c] # ./hello |
4 |
Hello, World! |
由此,看出前面使用的gcc hello.c -o hello命令,將hello.c直接編譯為可執行的目標文件,中間經過於處理器的預處理階段(源文件到預處理文件),編譯器的編譯階段(預處理文件到匯編文件),匯編器的匯編階段(匯編文件到可重定向的目標文件),鏈接器的鏈接階段(可重定向的目標文件到可執行的目標文件)。
還有其他的選項如下:
-Idir:dir是頭文件所在的目錄
-Ldir:dir是庫文件所在的目錄
-Wall:打印所有的警告信息
-Wl,options:options是傳遞給鏈接器的選項
編譯優化選項:-O和-O2
-O選項告訴GCC 對源代碼進行基本優化。這些優化在大多數情況下都會使程序執行的更快。-O2選項告訴GCC產生盡可能小和盡可能快的代碼。
-O2選項將使編譯的速度比使用-O時慢。但通常產生的代碼執行速度會更快。
除了-O和-O2優化選項外,還有一些低級選項用於產生更快的代碼。這些選項非常的特殊,而且最好只有當你完全理解這些選項將會對編譯后的代碼產生什么樣的效果時再去使用。這些選項的詳細描述,請參考GCC的聯機幫助,在命令行上鍵入"man gcc"即可。
調試選項:-g(使用詳情見第3部分)
-g選項告訴GCC產生能被GNU調試器使用的調試信息以便調試你的程序。
即:在生成的目標文件中添加調試信息,所謂調試信息就是源代碼和指令之間的對應關系,在gdb調試和objdump反匯編時要用到這些信息。
3.調試
雖然GCC提供了調試選項,但是本身不能用於調試。Linux 提供了一個名為gdb的GNU調試程序。gdb是一個用來調試C和C++程序的調試器。它使你能在程序運行時觀察程序的內部結構和內存的使用情況。以下是gdb所提供的一些功能:
a.它使你能監視你程序中變量的值;
b.它使你能設置斷點以使程序在指定的代碼行上停止執行;
c.它使你能一行行的執行你的代碼。
(1)啟動gdb
在命令行上鍵入"gdb"並按回車鍵就可以運行gdb了,如下:
1 |
[root@localhost c] # gdb |
2 |
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-60.el6_4.1) |
3 |
Copyright (C) 2010 Free Software Foundation, Inc. |
4 |
License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it. |
5 |
There is NO WARRANTY, to the extent permitted by law. Type "show copying" |
6 |
and "show warranty" for details. |
7 |
This GDB was configured as "x86_64-redhat-linux-gnu" . |
8 |
For bug reporting instructions, please see:<>. |
9 |
(gdb) |
當啟動gdb之后,即可在命令行上輸入命令進行相關的調試操作。
也可以以下面的方式來啟動gdb:
1 |
[root@localhost c] # gdb hello |
這種方式啟動gdb,直接將指定調試的程序文件裝載到調試環境中。也就是讓gdb裝入名稱為filename的可執行文件,從而准備調試。
為了能夠進行調試,當前調試的程序文件中必須包含調試信息。其中調試信息包含程序中的每個變量的類型和其在可執行文件里的地址映射以及源代碼的行號,gdb利用這些信息使源代碼和機器碼相關聯。因此在使用gcc編譯源程序的時候必須使用-g選項,以便將調試信息包含在可執行文件中。
例如:
1 |
[root@localhost c] # gcc -g hello.c -o hello |
gdb還提供了其他的啟動選項,請參考gdb的聯機幫助。在命令行上鍵入"man gdb"並回車即可。
(2)gdb基本命令
<1>單步執行和跟蹤函數調用
程序編輯如下:
01 |
#include <stdio.h> |
02 |
int add_range( int low, int high){ |
03 |
int i; |
04 |
int sum; |
05 |
for (i = low; i <= high; i++){ |
06 |
sum = sum + i; |
07 |
} |
08 |
return sum; |
09 |
} |
10 |
11 |
int main(){ |
12 |
int result[100]; |
13 |
result[0] = add_range(1, 10); |
14 |
result[1] = add_range(1, 100); |
15 |
printf ( "result[0] = %d\nresult[1] = %d\n" , result[0], result[1]); |
16 |
return 0; |
17 |
18 |
} |
編譯和運行如下:
1 |
[root@localhost gdb_demo] # vim test1.c |
2 |
[root@localhost gdb_demo] # gcc test1.c -o test1 |
3 |
[root@localhost gdb_demo] # ls |
4 |
test1 test1.c |
5 |
[root@localhost gdb_demo] # ./test1 |
6 |
result[0] = 55 |
7 |
result[1] = 5105 |
以上程序的結果中,顯然第二個結果是不正確的,有基礎的人會一眼看出問題處在哪里,呵呵,這里只是為了演示使用gdb調試而故意為之。當然在開發人員最好不要太過於依賴gdb才能找到錯誤。
在編譯時加上-g選項,生成的目標文件才能用gdb進行調試:
01 |
[root@localhost gdb_demo] # gcc test1.c -g -o test1 |
02 |
[root@localhost gdb_demo] # gdb test1 |
03 |
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-60.el6_4.1) |
04 |
Copyright (C) 2010 Free Software Foundation, Inc. |
05 |
License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it. |
06 |
There is NO WARRANTY, to the extent permitted by law. Type "show copying" |
07 |
and "show warranty" for details. |
08 |
This GDB was configured as "x86_64-redhat-linux-gnu" . |
09 |
For bug reporting instructions, please see:<>... |
10 |
Reading symbols from /root/code/c/gdb_demo/test1... done . |
11 |
(gdb) |
-g選項的作用是在目標文件中加入源代碼的信息,比如目標文件中的第幾條機器指令對應源代碼的第幾行,但並不是把整個源文件嵌入到目標文件中,所以在調試時目標文件必須保證gdb也能找到源文件。
gdb提供一個類是shell的命令行環境,上面的(gdb)就是提示符,在這個提示符下輸入help可以查看命令的類別:
01 |
(gdb) help |
02 |
List of classes of commands: |
03 |
aliases -- Aliases of other commands |
04 |
breakpoints -- Making program stop at certain points |
05 |
data -- Examining data |
06 |
files -- Specifying and examining files |
07 |
internals -- Maintenance commands |
08 |
obscure -- Obscure features |
09 |
running -- Running the program |
10 |
stack -- Examining the stack |
11 |
status -- Status inquiries |
12 |
support -- Support facilities |
13 |
tracepoints -- Tracing of program execution without stopping the program |
14 |
user-defined -- User-defined commands |
15 |
Type "help" followed by a class name for a list of commands in that class. |
16 |
Type "help all" for the list of all commands. |
17 |
Type "help" followed by command name for full documentation. |
18 |
Type "apropos word" to search for commands related to "word" . |
19 |
Command name abbreviations are allowed if unambiguous. |
可以進一步查看某一個類別中有哪些命令,例如查看files類別下有哪些命令可以用:
01 |
(gdb) help files |
02 |
Specifying and examining files. |
03 |
List of commands: |
04 |
add-symbol- file -- Load symbols from FILE |
05 |
add-symbol- file -from-memory -- Load the symbols out of memory from a dynamically loaded object file |
06 |
cd -- Set working directory to DIR for debugger and program being debugged |
07 |
core- file -- Use FILE as core dump for examining memory and registers |
08 |
directory -- Add directory DIR to beginning of search path for source files |
09 |
edit -- Edit specified file or function |
10 |
exec - file -- Use FILE as program for getting contents of pure memory |
11 |
file -- Use FILE as program to be debugged |
12 |
forward-search -- Search for regular expression (see regex(3)) from last line listed |
13 |
generate-core- file -- Save a core file with the current state of the debugged process |
14 |
list -- List specified function or line |
15 |
load -- Dynamically load FILE into the running program |
使用list命令從第一行開始列出源代碼:
01 |
(gdb) list 1 |
02 |
1 #include <stdio.h> |
03 |
2 |
04 |
3 int add_range(int low, int high){ |
05 |
4 int i; |
06 |
5 int sum ; |
07 |
6 for (i = low; i <= high; i++){ |
08 |
7 sum = sum + i; |
09 |
8 } |
10 |
9 return sum ; |
11 |
10 } |
12 |
(gdb) |
一次只列出10行,如果要從11行開始繼續列出源代碼可以輸入:
1 |
(gdb) list |
也可以什么都不輸入直接敲回車,gdb提供類一個方便的功能,在提示符下直接敲回車表示用適當的參數重復上一條命令。
1 |
(gdb) (直接回車) |
2 |
11 |
3 |
12 int main(){ |
4 |
13 int result[100]; |
5 |
14 result[0] = add_range(1, 10); |
6 |
15 result[1] = add_range(1, 100); |
7 |
16 printf ( "result[0] = %d\nresult[1] = %d\n" , result[0], result[1]); |
8 |
17 return 0; |
9 |
18 } |
gdb的很多常用命令有簡寫形式,例如list命令可以寫成l,要列出一個函數的源碼也可以用函數名做list的參數:
01 |
(gdb) l add_range |
02 |
1 #include <stdio.h> |
03 |
2 |
04 |
3 int add_range(int low, int high){ |
05 |
4 int i; |
06 |
5 int sum ; |
07 |
6 for (i = low; i <= high; i++){ |
08 |
7 sum = sum + i; |
09 |
8 } |
10 |
9 return sum ; |
11 |
10 } |
現在退出gdb的環境(quit或簡寫形式q):
1 |
(gdb) quit |
現在把源代碼改名或移動到別處,再用gdb調試目標文件,就列不出源代碼了:
01 |
[root@localhost gdb_demo] # ls |
02 |
test1 test1.c |
03 |
[root@localhost gdb_demo] # mv test1.c test.c |
04 |
[root@localhost gdb_demo] # ls |
05 |
test1 test .c |
06 |
[root@localhost gdb_demo] # gdb test1 |
07 |
...... |
08 |
(gdb) l |
09 |
5 test1.c: 沒有那個文件或目錄. |
10 |
in test1.c |
11 |
(gdb) |
可見gcc的-g選項並不是把源代碼嵌入到目標文件中的,在調試目標文件時也需要源文件。
現在把源代碼恢復原樣,繼續調試。首先使用start命令開始執行程序:
01 |
[root@localhost gdb_demo] # mv test.c test1.c |
02 |
[root@localhost gdb_demo] # gdb test1 |
03 |
...... |
04 |
(gdb) start |
05 |
Temporary breakpoint 1 at 0x4004f8: file test1.c, line 14. |
06 |
Starting program: /root/code/c/gdb_demo/test1 |
07 |
Temporary breakpoint 1, main () at test1.c:14 |
08 |
14 result[0] = add_range(1, 10); |
09 |
Missing separate debuginfos, use: debuginfo- install glibc-2.12-1.132.el6.x86_64 |
10 |
(gdb) |
這表示停在main函數中變量定義之后的第一條語句處等待我們發命令,gdb列出這條語句表示它還沒執行,並且馬上要執行。我們可以用next命令(簡寫為n)控制這些語句一條一條地執行:
1 |
(gdb) n |
2 |
15 result[1] = add_range(1, 100); |
3 |
(gdb) (直接回車) |
4 |
16 printf ( "result[0] = %d\nresult[1] = %d\n" , result[0], result[1]); |
5 |
(gdb) (直接回車) |
6 |
result[0] = 55 |
7 |
result[1] = 5105 |
8 |
17 return 0; |
用n命令依次執行兩行賦值語句和一行打印語句,在執行打印語句時結果立刻打印出來類,然后停在return語句之前等待我們發命令。
雖然我們完全控制了程序的執行,但仍然看不出哪里錯了,因為錯誤不再main函數中而是在add_range函數中,現在用start命令重新執行,這次用step命令(簡寫為s)進入函數中去執行:
01 |
(gdb) start |
02 |
The program being debugged has been started already. |
03 |
Start it from the beginning? (y or n) y |
04 |
Temporary breakpoint 3 at 0x4004f8: file test1.c, line 14. |
05 |
Starting program: /root/code/c/gdb_demo/test1 |
06 |
Temporary breakpoint 3, main () at test1.c:14 |
07 |
14 result[0] = add_range(1, 10); |
08 |
(gdb) s |
09 |
add_range (low=1, high=10) at test1.c:6 |
10 |
6 for (i = low; i <= high; i++){ |
這次進入add_range函數中,停在函數中定義變量之后的第一條語句處。
在函數中有幾種查看狀態的辦法,backtrace(簡寫為bt)可以查看函數調用的棧幀:
1 |
(gdb) bt |
2 |
#0 add_range (low=1, high=10) at test1.c:6 |
3 |
#1 0x0000000000400507 in main () at test1.c:14 |
可見當前的add_range函數是被main函數調用的,main函數中傳給add_range的參數是low=1, high=10。main函數的棧幀編號是1,add_range函數的棧幀編號是0。
現在可以使用info命令(簡寫為i)查看add_range局部變量的值:
1 |
(gdb) i locals |
2 |
i = 0 |
3 |
sum = 0 |
如果想查看main函數中的當前局部變量的值也可以做到的,先用frame命令(簡寫為f)選擇1號棧幀,然后再查看main中的局部變量:
01 |
(gdb) f 1 |
02 |
#1 0x0000000000400507 in main () at test1.c:14 |
03 |
14 result[0] = add_range(1, 10); |
04 |
(gdb) i locals |
05 |
result = {4195073, 0, -1646196904, 50, 4195016, 0, 0, 1, 2041, 1, -1654612390, |
06 |
50, 0, 0, -1652419360, 50, -7728, 32767, -7688, 32767, -1652420216, 50, |
07 |
-134227048, 32767, -163754450, 0, -1654612390, 50, 0, 0, -134227048, 32767, |
08 |
1, 0, 0, 0, 1, 50, -1652420216, 50, -7848, 32767, 750006344, 0, 6, 0, -7826, |
09 |
32767, 0, 0, -1652419360, 50, -7808, 32767, -1645672825, 50, -7784, 32767, 0, |
10 |
1, 0, 0, 4195073, 0, 191, 0, -7826, 32767, -7825, 32767, 1, 0, 0, 0, |
11 |
-1645672168, 50, 0, 0, 4195680, 0, 0, 0, 4195235, 0, -7512, 32767, 4195749, |
12 |
0, -1646199904, 50, 4195680, 0, 0, 0, 4195296, 0, -7536, 32767, 0, 0} |
注意到result數組中的很多元素具有雜亂無章的值,因為未經初始化的局部變量具有不確定的值。
到目前為止(即已經進入第一次的函數調用的函數體內),是正常的。
繼續用s或n往下走,然后用print(簡寫為p)打印變量sum的值。
01 |
(gdb) s |
02 |
7 sum = sum + i; |
03 |
(gdb) |
04 |
6 for (i = low; i <= high; i++){ |
05 |
(gdb) |
06 |
7 sum = sum + i; |
07 |
(gdb) |
08 |
6 for (i = low; i <= high; i++){ |
09 |
(gdb) p sum |
10 |
$1 = 3 |
注意:這里的$1表示gdb保存着這些中間結果,$后面的編號會自動增長,在命令中可以用$1、$2、$3等編號代替相應的值。
-----------------------------------------------------
以上的執行過程使用下面的方法可能看得更清楚(這里的步驟不是繼續跟着上面步驟,是在另一個終端中調試的):
01 |
(gdb) f 0 |
02 |
#0 add_range (low=1, high=10) at test1.c:7 |
03 |
7 sum = sum + i; |
04 |
(gdb) i locals |
05 |
i = 1 |
06 |
sum = 0 |
07 |
(gdb) s |
08 |
6 for (i = low; i <= high; i++){ |
09 |
(gdb) i locals |
10 |
i = 1 |
11 |
sum = 1 |
12 |
(gdb) s |
13 |
7 sum = sum + i; |
14 |
(gdb) i locals |
15 |
i = 2 |
16 |
sum = 1 |
17 |
(gdb) s |
18 |
6 for (i = low; i <= high; i++){ |
19 |
(gdb) i locals |
20 |
i = 2 |
21 |
sum = 3 |
-----------------------------------------------------
由此看出,第一次循環的結果是正確的,再往下單步調試已經沒有意義了,可以使用finish命令讓程序一直運行到從當前函數返回為止:
1 |
(gdb) finish |
2 |
Run till exit from #0 add_range (low=1, high=10) at test1.c:6 |
3 |
0x0000000000400507 in main () at test1.c:14 |
4 |
14 result[0] = add_range(1, 10); |
5 |
Value returned is $1 = 55 |
返回值是55,當前正准備執行賦值操作,用s命令執行賦值操作,然后查看result數組:
01 |
(gdb) s |
02 |
15 result[1] = add_range(1, 100); |
03 |
(gdb) print result |
04 |
$2 = {55, 0, -1646196904, 50, 4195016, 0, 0, 1, 2041, 1, -1654612390, 50, 0, 0, |
05 |
-1652419360, 50, -7728, 32767, -7688, 32767, -1652420216, 50, -134227048, |
06 |
32767, -163754450, 0, -1654612390, 50, 0, 0, -134227048, 32767, 1, 0, 0, 0, |
07 |
1, 50, -1652420216, 50, -7848, 32767, 750006344, 0, 6, 0, -7826, 32767, 0, 0, |
08 |
-1652419360, 50, -7808, 32767, -1645672825, 50, -7784, 32767, 0, 1, 0, 0, |
09 |
4195073, 0, 191, 0, -7826, 32767, -7825, 32767, 1, 0, 0, 0, -1645672168, 50, |
10 |
0, 0, 4195680, 0, 0, 0, 4195235, 0, -7512, 32767, 4195749, 0, -1646199904, |
11 |
50, 4195680, 0, 0, 0, 4195296, 0, -7536, 32767, 0, 0} |
第一個值是55確實賦值給類result數組的第0個元素。
使用s命令進入第二次add_range調用,進入之后首先查看參數和局部變量:
1 |
(gdb) s |
2 |
add_range (low=1, high=100) at test1.c:6 |
3 |
6 for (i = low; i <= high; i++){ |
4 |
(gdb) bt |
5 |
#0 add_range (low=1, high=100) at test1.c:6 |
6 |
#1 0x000000000040051c in main () at test1.c:15 |
7 |
(gdb) i locals |
8 |
i = 11 |
9 |
sum = 55 |
到這里,看出了問題:由於局部變量i和sum沒有初始化,所以具有不確定的值,又由於連次調用是連續的,i和sum正好取類上次調用時的值。i的初始值不是0不要緊,因為在for循環開始重新賦值了,但如果sum的處置不是0,累加得到的結果就錯了。
問題找到了,可以退出gdb修改源代碼了。然而我們不想浪費一次調試機會,可以在gdb中馬上把sum的初始值改為0,繼續運行,看看改了之后有沒有其他的bug:
01 |
(gdb) set var sum =0 |
02 |
(gdb) finish |
03 |
Run till exit from #0 add_range (low=1, high=100) at test1.c:6 |
04 |
0x000000000040051c in main () at test1.c:15 |
05 |
15 result[1] = add_range(1, 100); |
06 |
Value returned is $3 = 5050 |
07 |
(gdb) s |
08 |
16 printf ( "result[0] = %d\nresult[1] = %d\n" , result[0], result[1]); |
09 |
(gdb) s |
10 |
result[0] = 55 |
11 |
result[1] = 5050 |
12 |
17 return 0; |
這樣結果就對了。
修改變量的值除了用set命令之外也可以使用print命令,因為print命令后跟的是表達式,而我們知道賦值和函數調用都是表達式,所以還可以用print來修改變量的值,或者調用函數:
1 |
(gdb) print result[2]=88 |
2 |
$4 = 88 |
3 |
(gdb) p printf ( "result[2]=%d\n" , result[2]) |
4 |
result[2]=88 |
5 |
$5 = 13 |
我們知道:printf函數的返回值表示實際打印的字符數,所以$5的結果是13。
總結一下本節使用過的gdb命令:
01 |
list(l):列出源代碼,接着上次的位置往下列,每次列10行 |
02 |
list 行號:列出產品從第幾行開始的源代碼 |
03 |
list 函數名:列出某個函數的源代碼 |
04 |
start:開始執行程序,停在main函數第一行語句前面等待命令 |
05 |
next(n):執行下一列語句 |
06 |
step(s):執行下一行語句,如果有函數調用則進入到函數中 |
07 |
breaktrace(或bt):查看各級函數調用及參數 |
08 |
frame(f) 幀編號:選擇棧幀 |
09 |
info(i) locals:查看當前棧幀局部變量的值 |
10 |
finish:執行到當前函數返回,然后挺下來等待命令 |
11 |
print(p):打印表達式的值,通過表達式可以修改變量的值或者調用函數 |
12 |
set var:修改變量的值 |
13 |
quit:退出gdb |
<2>斷點
程序編輯如下:
01 |
# include <stdio.h> |
02 |
int main(){ |
03 |
int sum =0; |
04 |
int i = 0; |
05 |
char input[5]; |
06 |
|
07 |
while (1){ |
08 |
scanf ( "%s" , input); //在輸入字符后自動加'\0'形成字符串 |
09 |
for (i = 0; input[i] != '\0' ; i++){ |
10 |
sum = sum * 10 + input[i] - '0' ; //'1'-'0'=1,'\0'=0 |
11 |
} |
12 |
printf ( "input = %d\n" , sum); |
13 |
} |
14 |
return 0; |
15 |
} |
編譯和運行:
01 |
[root@localhost gdb_demo] # vim test2.c |
02 |
[root@localhost gdb_demo] # gcc test2.c -g -o test2 |
03 |
[root@localhost gdb_demo] # ls |
04 |
test1 test1.c test2 test2.c |
05 |
[root@localhost gdb_demo] # ./test2 |
06 |
123 |
07 |
input = 123 |
08 |
12345 |
09 |
input = 12345 |
10 |
12345678 |
11 |
input = -268647318 |
12 |
(Ctrl-C退出程序) |
從結果看出程序明顯是有問題的。
下面來調試:
1 |
[root@localhost gdb_demo] # gdb test2 |
2 |
...... |
3 |
(gdb) start |
4 |
Temporary breakpoint 1 at 0x40053c: file test2.c, line 4. |
5 |
Starting program: /root/code/c/gdb_demo/test2 |
6 |
Temporary breakpoint 1, main () at test2.c:4 |
7 |
4 int sum =0; |
8 |
Missing separate debuginfos, use: debuginfo- install glibc-2.12-1.132.el6.x86_64 |
9 |
(gdb) |
可見,start不會跳過賦值語句,通過第一次單步調試的例子,sum可列為重點觀察對象,可以使用display命令使得每次停下來的時候都顯示當前的sum值,繼續往下走:
01 |
(gdb) display sum |
02 |
1: sum = 0 |
03 |
(gdb) n |
04 |
5 int i = 0; |
05 |
1: sum = 0 |
06 |
(gdb) |
07 |
9 scanf( "%s" , input); |
08 |
1: sum = 0 |
09 |
(gdb) |
10 |
123 |
11 |
10 for (i = 0; input[i] != '\0' ; i++){ |
12 |
1: sum = 0 |
13 |
(gdb) |
這個循環應該是沒有問題的,因為循環開始sum的值是正確的。可以使用undisplay取消先前設置的那些變量的跟蹤。
如果不想一步一步跟蹤這個循環,可以使用break(簡寫為b)在第8行設置一個斷點(Breakpoint):
01 |
(gdb) l |
02 |
5 int i = 0; |
03 |
6 char input[5]; |
04 |
7 |
05 |
8 while (1){ |
06 |
9 scanf( "%s" , input); |
07 |
10 for (i = 0; input[i] != '\0' ; i++){ |
08 |
11 sum = sum * 10 + input[i] - '0' ; |
09 |
12 } |
10 |
13 printf ( "input = %d\n" , sum ); |
11 |
14 } |
12 |
(gdb) b 8 |
13 |
Breakpoint 2 at 0x40054a: file test2.c, line 8. |
break命令的參數也可以是函數名,表示在某一個函數開頭設置斷點。現在用continue命令(簡寫為c)連續運行而非單步運行,程序到達斷點會自動停下來,這樣就可以停在下一次循環的開頭:
1 |
(gdb) c |
2 |
Continuing. |
3 |
input = 123 |
4 |
Breakpoint 2, main () at test2.c:9 |
5 |
9 scanf( "%s" , input); |
6 |
1: sum = 123 |
然后輸入新的字符串准備轉換:
1 |
(gdb) n |
2 |
12345 |
3 |
10 for (i = 0; input[i] != '\0' ; i++){ |
4 |
1: sum = 123 |
此時問題已經暴露出來了,新的轉換應該是從0開始累加的,而現在sum卻是123,原因在於新的循環沒有把sum歸零。
可見斷點有助於快速跳過與問題無關的代碼,然后在有問題的代碼上慢慢走慢慢分析,“斷點加單步”是使用調試器的基本方法。至於應該在哪里設置斷點,怎么知道哪些代碼可以跳過而哪些代碼要慢慢走,也要通過對錯誤現象的分析和假設來確定。
一次調試可以設置多個斷點,用info命令(簡寫為i)可以查看已經設置的斷點:
1 |
(gdb) b 11 |
2 |
Breakpoint 3 at 0x40056c: file test2.c, line 11. |
3 |
(gdb) i breakpoints |
4 |
Num Type Disp Enb Address What |
5 |
2 breakpoint keep y 0x000000000040054a in main at test2.c:8 |
6 |
breakpoint already hit 2 times |
7 |
3 breakpoint keep y 0x000000000040056c in main at test2.c:11 |
每一個斷點都有一個編號,可以用編號指定刪除某個斷點:
1 |
(gdb) delete breakpoints 2 |
2 |
(gdb) i breakpoints |
3 |
Num Type Disp Enb Address What |
4 |
3 breakpoint keep y 0x000000000040056c in main at test2.c:11 |
有時候一個斷點不想用可以禁用而不必刪除,這樣以后想用的時候可以直接啟用,而不必重新從代碼里找應該在哪一行設置斷點:
01 |
(gdb) disable breakpoints 3 |
02 |
(gdb) i breakpoints |
03 |
Num Type Disp Enb Address What |
04 |
3 breakpoint keep n 0x000000000040056c in main at test2.c:11 |
05 |
(gdb) enable breakpoints 3 |
06 |
(gdb) i breakpoints |
07 |
Num Type Disp Enb Address What |
08 |
3 breakpoint keep y 0x000000000040056c in main at test2.c:11 |
09 |
(gdb) delete breakpoints |
10 |
Delete all breakpoints? (y or n) y |
11 |
(gdb) i breakpoints |
12 |
No breakpoints or watchpoints. |
gdb設置斷點功能非常靈活,還可以設置斷點在滿足某個條件時才激活,例如我們仍然在循環開頭設置斷點,但是僅當sum不等於0時才中斷,然后用run(簡寫為r)重新從程序開頭連續執行:
01 |
(gdb) break 10 if sum != 0 |
02 |
Breakpoint 5 at 0x400563: file test2.c, line 10. |
03 |
(gdb) i breakpoints |
04 |
Num Type Disp Enb Address What |
05 |
5 breakpoint keep y 0x0000000000400563 in main at test2.c:10 |
06 |
stop only if sum != 0 |
07 |
(gdb) r |
08 |
The program being debugged has been started already. |
09 |
Start it from the beginning? (y or n) y |
10 |
Starting program: /root/code/c/gdb_demo/test2 |
11 |
123 |
12 |
input = 123 |
13 |
123 |
14 |
Breakpoint 5, main () at test2.c:10 |
15 |
10 for (i = 0; input[i] != '\0' ; i++){ |
16 |
1: sum = 123 |
結果:第一次執行輸入之后沒有中斷,第二次卻中斷了,因為第二次循環開始sum的值為123。
總結一下本節使用到的gdb命令:
01 |
break (b) 行號:在某一行設置斷點 |
02 |
break 函數名:在某個函數開頭設置斷點 |
03 |
break ... if ...:設置條件斷點 |
04 |
continue (或c):從當前位置開始連續而非單步執行程序 |
05 |
delete breakpoints:刪除所有斷點 |
06 |
delete breakpoints n:刪除序號為n的斷點 |
07 |
disable breakpoints:禁用斷點 |
08 |
enable breakpoints:啟用斷點 |
09 |
info(或i) breakpoints:參看當前設置了哪些斷點 |
10 |
run(或r):從開始連續而非單步執行程序 |
11 |
display 變量名:跟蹤查看一個變量,每次停下來都顯示它的值 |
12 |
undisplay:取消對先前設置的那些變量的跟蹤 |
下面再看一個例子:
程序如下:
01 |
#include <stdio.h> |
02 |
int main(){ |
03 |
int i; |
04 |
char str[6] = "hello" ; |
05 |
char reverse_str[6] = "" ; |
06 |
printf ( "%s\n" , str); |
07 |
for (i = 0; i < 5; i ++){ |
08 |
reverse_str[5-i] = str[i]; |
09 |
} |
10 |
printf ( "%s\n" , reverse_str); |
11 |
return 0; |
12 |
} |
運行結果:
1 |
[root@localhost gdb_demo] # gcc test3.c -g -o test3 |
2 |
[root@localhost gdb_demo] # ./test3 |
3 |
hello |
其中,第二次打印空白,結果顯然是不正確的。
調試過程如下:
01 |
[root@localhost gdb_demo] # gdb test3 |
02 |
...... |
03 |
(gdb) l |
04 |
1 #include <stdio.h> |
05 |
2 |
06 |
3 int main(){ |
07 |
4 int i; |
08 |
5 char str[6] = "hello" ; |
09 |
6 char reverse_str[6] = "" ; |
10 |
7 |
11 |
8 printf ( "%s\n" , str); |
12 |
9 for (i = 0; i < 5; i ++){ |
13 |
10 reverse_str[5-i] = str[i]; |
14 |
(gdb) |
15 |
11 } |
16 |
12 printf ( "%s\n" , reverse_str); |
17 |
13 return 0; |
18 |
14 } |
19 |
(gdb) i breakpoints |
20 |
No breakpoints or watchpoints. |
21 |
(gdb) b 10 |
22 |
Breakpoint 1 at 0x4004fb: file test3.c, line 10. |
23 |
(gdb) i breakpoints |
24 |
Num Type Disp Enb Address What |
25 |
1 breakpoint keep y 0x00000000004004fb in main at test3.c:10 |
26 |
(gdb) r |
27 |
Starting program: /root/code/c/gdb_demo/test3 |
28 |
hello |
29 |
Breakpoint 1, main () at test3.c:10 |
30 |
10 reverse_str[5-i] = str[i]; |
31 |
Missing separate debuginfos, use: debuginfo- install glibc-2.12-1.132.el6.x86_64 |
32 |
(gdb) p reverse_str |
33 |
$1 = "\000\000\000\000\000" |
34 |
(gdb) c |
35 |
Continuing. |
36 |
Breakpoint 1, main () at test3.c:10 |
37 |
10 reverse_str[5-i] = str[i]; |
38 |
(gdb) p reverse_str |
39 |
$2 = "\000\000\000\000\000h" |
40 |
(gdb) c |
41 |
Continuing. |
42 |
Breakpoint 1, main () at test3.c:10 |
43 |
10 reverse_str[5-i] = str[i]; |
44 |
(gdb) p reverse_str |
45 |
$3 = "\000\000\000\000eh" |
46 |
(gdb) c |
47 |
Continuing. |
48 |
Breakpoint 1, main () at test3.c:10 |
49 |
10 reverse_str[5-i] = str[i]; |
50 |
(gdb) p reverse_str |
51 |
$4 = "\000\000\000leh" |
52 |
(gdb) c |
53 |
Continuing. |
54 |
Breakpoint 1, main () at test3.c:10 |
55 |
10 reverse_str[5-i] = str[i]; |
56 |
(gdb) p reverse_str |
57 |
$5 = "\000\000lleh" |
58 |
(gdb) c |
59 |
Continuing. |
60 |
Program exited normally. |
61 |
(gdb) |
由上面的觀察可知:
在於將str數組中的值賦值到reverse_str的時候,將str的第1個元素賦值給類reverse_str的第6個元素,而該循環只循環了5次(即str數組元素個數),從而導致reverse_str的第一個元素為'\000',所以輸出為空白。
修改如下:
01 |
#include <stdio.h> |
02 |
#include <string.h> |
03 |
int main(){ |
04 |
int i; |
05 |
char str[6] = "hello" ; |
06 |
char reverse_str[6] = "" ; |
07 |
printf ( "%s\n" , str); |
08 |
int len = strlen (str); |
09 |
for (i = 0; i <= len-1; i ++){ |
10 |
reverse_str[len-1-i] = str[i]; |
11 |
} |
12 |
printf ( "%s\n" , reverse_str); |
13 |
return 0; |
14 |
} |
再次運行就好了:
1 |
[root@localhost gdb_demo] # gcc test3.c -o test3 |
2 |
[root@localhost gdb_demo] # ./test3 |
3 |
hello |
4 |
olleh |
<3>觀察點(Watchpoint)
斷點是當程序執行到某一代碼行時中斷,而觀察點一般來觀察某個表達式(變量也是一種表達式)的值是否有變化了,如果有變化,馬上停住程序。
vim test4.c
01 |
# include <stdio.h> |
02 |
int main(){ |
03 |
int sum =0; |
04 |
int i; |
05 |
for (i = 1; i <= 10; i++){ |
06 |
sum = sum +i; |
07 |
} |
08 |
printf ( "sum = %d\n" , sum); |
09 |
return 0; |
10 |
} |
編譯運行:
1 |
[root@localhost gdb_demo] # gcc test4.c -g -o test4 |
2 |
[root@localhost gdb_demo] # ./test4 |
3 |
sum = 55 |
設置觀察點進行調試:
01 |
[root@localhost gdb_demo] # gdb test4 |
02 |
...... |
03 |
(gdb) start |
04 |
Temporary breakpoint 1 at 0x4004cc: file test4.c, line 4. |
05 |
Starting program: /root/code/c/gdb_demo/test4 |
06 |
Temporary breakpoint 1, main () at test4.c:4 |
07 |
4 int sum =0; |
08 |
Missing separate debuginfos, use: debuginfo- install glibc-2.12-1.132.el6.x86_64 |
09 |
(gdb) l |
10 |
1 # include <stdio.h> |
11 |
2 |
12 |
3 int main(){ |
13 |
4 int sum =0; |
14 |
5 int i; |
15 |
6 for (i = 1; i <= 10; i++){ |
16 |
7 sum = sum +i; |
17 |
8 } |
18 |
9 printf ( "sum = %d\n" , sum ); |
19 |
10 return 0; |
20 |
(gdb) |
21 |
11 } |
22 |
(gdb) watch sum |
23 |
Hardware watchpoint 2: sum |
24 |
(gdb) c |
25 |
Continuing. |
26 |
Hardware watchpoint 2: sum |
27 |
Old value = 0 |
28 |
New value = 1 |
29 |
main () at test4.c:6 |
30 |
6 for (i = 1; i <= 10; i++){ |
31 |
(gdb) c |
32 |
Continuing. |
33 |
Hardware watchpoint 2: sum |
34 |
Old value = 1 |
35 |
New value = 3 |
36 |
main () at test4.c:6 |
37 |
6 for (i = 1; i <= 10; i++){ |
38 |
(gdb) c |
39 |
Continuing. |
40 |
Hardware watchpoint 2: sum |
41 |
Old value = 3 |
42 |
New value = 6 |
43 |
main () at test4.c:6 |
44 |
6 for (i = 1; i <= 10; i++){ |
45 |
(gdb) c |
46 |
Continuing. |
47 |
Hardware watchpoint 2: sum |
48 |
Old value = 6 |
49 |
New value = 10 |
50 |
main () at test4.c:6 |
51 |
6 for (i = 1; i <= 10; i++){ |
52 |
(gdb) |
總結一下本節使用到的gdb命令:
1 |
watch :設置觀察點 |
2 |
info(或i) watchpoints:查看當前設置了哪些觀察點 |
GDB的補充:
輸出格式:
一般來說,GDB會根據變量的類型輸出變量的值。但你也可以自定義GDB的輸出的格式。
例如,你想輸出一個整數的十六進制,或是二進制來查看這個整型變量的中的位的情況。要
做到這樣,你可以使用GDB的數據顯示格式:
x 按十六進制格式顯示變量。
d 按十進制格式顯示變量。
u 按十六進制格式顯示無符號整型。
o 按八進制格式顯示變量。
t 按二進制格式顯示變量。
a 按十六進制格式顯示變量。
c 按字符格式顯示變量。
f 按浮點數格式顯示變量。
01 |
(gdb) p sum |
02 |
$1 = 10 |
03 |
(gdb) p/a sum |
04 |
$2 = 0xa |
05 |
(gdb) p/x sum |
06 |
$3 = 0xa |
07 |
(gdb) p/o sum |
08 |
$4 = 012 |
09 |
(gdb) p/t sum |
10 |
$5 = 1010 |
11 |
(gdb) p/f sum |
12 |
$6 = 1.40129846e-44 |
13 |
(gdb) p/c sum |
14 |
$7 = 10 '\n' |
查看內存:
你可以使用examine命令(簡寫是x)來查看內存地址中的值。x命令的語法如下所示:
x/
n、f、u是可選的參數。
n 是一個正整數,表示顯示內存的長度,也就是說從當前地址向后顯示幾個地址的內容。
f 表示顯示的格式,參見上面。如果地址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是i。
u 表示從當前地址往后請求的字節數,如果不指定的話,GDB默認是4個bytes。u參數可以用下面的字符來代替,b表示單字節,h表示雙字節,w表示四字節,g表示八字節。當我們指定了字節長度后,GDB會從指內存定的內存地址開始,讀寫指定字節,並把其當作
一個值取出來。
表示一個內存地址。
n/f/u三個參數可以一起使用。例如:
命令:x/3uh 0x54320 表示,從內存地址0x54320讀取內容,h表示以雙字節為一個單位,3表示三個單位,u表示按十六進制顯示。