linux 調試工具 - gdb

前言

GDB 是 Linux 下非常好用且強大的調試工具。GDB 可以調試 C、C++、Go、java、 objective-c、PHP 等語言。對於一名 Linux 下工作的 c/c++ 程序員,GDB 是必不可少的工具,本篇以 C 語言來調試。

GDB 簡介

UNIX 及 UNIX-like 下的調試工具。雖然它是命令行模式的調試工具,但是它的功能強大到你無法想象,能夠讓用戶在程序運行時觀察程序的內部結構和內存的使用情況。

一般來說,GDB 主要幫助你完成下面四個方面的功能:

1、按照自定義的方式啓動運行需要調試的程序。2、可以使用指定位置和條件表達式的方式來設置斷點。3、程序暫停時的值的監視。4、動態改變程序的執行環境。

基本命令的操作

GDB 中的命令很多,但我們只需掌握其中十個左右的命令,就大致可以完成日常的基本的程序調試工作。

gdb 命令擁有較多內部命令。在 gdb 命令提示符 “(gdb)” 下輸入 “help” 可以查看所有內部命令及使用說明。

判斷文件是否帶有調試信息

要調試 C/C++ 的程序,首先在編譯時,要使用 gdb 調試程序,在使用 gcc 編譯源代碼時必須加上 “-g” 參數。保留調試信息,否則不能使用 GDB 進行調試。

有一種情況,有一個編譯好的二進制文件,你不確定是不是帶有 - g 參數,帶有 GDB 調試,這個時候你可以使用如下的命令驗證:

如果沒有調試信息,則會出現:

Reading symbols from /home/minger/share/tencent/gdb/main…(no debugging symbols found)…done.

/home/minger/share/tencent/gdb/main 是程序的路徑。

如果帶有調試功能,下面會提示:

Reading symbols from /home/minger/share/tencent/gdb/main…done.

說明可以進行 GDB 調試。

還有使用命令 readlef 查看可執行文件是否帶有調試功能

readelf -S main|grep debug

如果有 debug 說明有調試功能,如果沒有 debug。說明沒有帶有調試功能,則不能被調試。

開始進入正題,GDB 啓動調試。

調試方式啓動運行無參程序

以下是 linux 下 GDB 調試的一個實例,先給出一個示例用的小程序,C 語言代碼:

main.c

#include <stdio.h>

void Print(int i){

	printf("hello,程序猿編碼 %d\n", i);
}

int main(int argc, char const *argv[]){
	
	int i = 0;
	for (i = 1; i < 3; i++){
		Print(i);
	}
	return 0;
}

編譯:

gcc -g main.c -o main

下面 “gdb” 命令啓動 GDB,將首先顯示 GDB 說明,不管它:

上面最後一行 “(gdb)” 爲 GDB 內部命令引導符,等待用戶輸入 GDB 命令。

下面使用 “file” 命令載入被調試程序 main(這裏的 main 即前面 gcc 編譯輸出的可執行文件):

如果最後一行提示 Reading symbols from /home/minger/share/tencent/gdb/main…done. 表示已經加載成功。

下面使用 “r” 命令執行(Run)被調試文件,因爲尚未設置任何斷點,將直接執行到程序結束:

調試啓動帶參程序

假設有以下程序,啓動時需要帶參數:

#include <stdio.h>

int main(int argc, char const *argv[]){

	if (1 >= argc){
		printf("usage:hello name\n");
		return 0;
	}
	
	printf("hello,程序猿編碼 %s\n", argv[1]);

	return 0;
}

編譯:

gcc -g test.c -o test

這種情況如何啓動調試呢?只需要 r 的時候帶上參數即可。

調試 core 文件

Core Dump:Core 的意思是內存,Dump 的意思是扔出來,堆出來(段錯誤)。開發和使用 Unix 程序時,有時程序莫名其妙的 down 了,卻沒有任何的提示 (有時候會提示 core dumped),這時候可以查看一下有沒有形如 core. 進程號的文件生成,這個文件便是操作系統把程序 down 掉時的內存內容扔出來生成的, 它可以做爲調試程序的參考,能夠很大程序幫助我們定位問題。那怎麼生成 Core 文件呢?

生成 Core 方法

產生 coredump 的條件,首先需要確認當前會話的 ulimit –c,若爲 0,則不會產生對應的 coredump,需要進行修改和設置。

即便程序 core dump 了也不會有 core 文件留下。我們需要讓 core 文件能夠產生, 設置 core 大小爲無限:

ulimit -c unlimied

更改 core dump 生成路徑

因爲 core dump 默認會生成在程序的工作目錄,但是有些程序存在切換目錄的情況,導致 core dump 生成的路徑沒有規律,

所以最好是自己建立一個文件夾,存放生成的 core 文件。

我建立一個 /data/coredump 文件夾,在根目錄 data 裏的 coredump 文件夾。

調用如下命令:

echo /data/coredump/core.%e.%p> /proc/sys/kernel/core_pattern

將更改 core 文件生成路徑,自動放在這個 / data/coredump 文件夾裏。

%e 表示程序名, %p 表示進程 id

測試代碼:

/*
#include <stdio.h>

int main(int argc, char const *argv[]){

	if (1 >= argc){
		printf("usage:hello name\n");
		return 0;
	}
	
	printf("hello,程序猿編碼 %s\n", argv[1]);

	return 0;
}


*/

#include <stdio.h>

int main(int argc, char const *argv[])
{
	int i = 0;
	
	scanf("%d",i);
	printf("hello,程序猿編碼 %d\n",i );
	return 0;
}

編譯運行:

運行後結果顯示段錯誤,該程序在主函數內部 scanf 的時候回崩潰,i 前面應該加上 &。

這個時候,進入 / data/coredump 文件夾可以查看生成的 core

然後用 gdb 調試該 core,命令爲 gdb core.test.3591 , 顯示如下

program terminated with signal 11 告訴我們信號中斷了我們的程序,發生了段錯誤。

這個時候可以敲命令 backtrace(bt) 查看函數的調用的棧幀和層級關係。

這個一堆問號很多人遇到過,網上有些人說是沒加載符號表,有人說是標準 glibc 版本不一致,不糾結這個問題。

可以通過如下命令調試:

gdb 可執行程序 exe 進入 gdb 環境後 core-file core 的名字 敲命令 bt 可以查看準確信息。

gdb 可執行程序

進入 gdb 環境後,core-file core 的名字

敲 bt 命令,這是 gdb 查看 back trace 的命令,查看函數的調用的棧幀和層級關係。

可以看到最近的棧中存儲的是調用了 IO 操作,可以看到 main 函數的 26 行出錯。

到此爲止,就是 core 文件配置生成和調試方法。

總結

至此,我們 GDB 啓動調試方式完畢,和 core 文件配置生成和調試方法。後續接着講斷點設置、單步調試等。

斷點設置與查看源碼

前言

上篇 GDB 啓動調試我們講到了 GDB 啓動調試的多種方式,在 Linux 環境軟件開發中,GDB 是主要的調試工具,用來調試 C 和 C++ 程序。這一篇主要講 GDB 的斷點設置與查看源碼。

爲什麼要設置斷點呢?

當我們想查看變量內容,堆棧情況等等,可以指定斷點。程序執行到斷點處會暫停執行。break 命令用來設置斷點,縮寫形式爲 b。設置斷點後,以便我們更詳細的跟蹤斷點附近程序的執行情況。

設置斷點有很多方式。下面我們舉例說明下常用的幾種方式。

通過行號設置斷點

格式:

break [行號]

break 行號,斷點設置在該行開始處,注意:該行代碼未被執行

如果你的程序是用 c 或者 c++ 寫的,那麼你可以使用 “文件名: 行號” 的形式設置斷點。示例如下:

//test.c
#include <stdio.h>
 
void judge_sd(int num){

	if ((num & 1) == 0){
		printf("%d is even\n",num);
		return;
	}else{
		printf("%d is odd\n",num);
		return;
	}
}
 
int main(int argc, char const *argv[]){
	

	judge_sd(0);
	judge_sd(1);
	judge_sd(4);

	return 0;
}

編譯:

gcc -g test.c -o test

gdb test

break 文件名 : 行號,適用於有多個源文件的情況。

示例中的 (gdb) b test.c:18 是設置了斷點。斷點的位置是 test.c 文件的 18 行。使用 r 命令執行腳本時,當運行到 18 行時就會暫停。注意:該行代碼未被執行

通過函數設置斷點

格式:

break [函數名]

break 函數名,斷點設置在該函數的開始處,斷點所在行未被執行:

同樣可以將斷點設置在函數處:

b judge_sd

設置條件斷點

如果按上面的方法設置斷點後,每次執行到斷點位置都會暫停。有時候非常討厭。我們只想在指定條件下才暫停。這時候根據條件設置斷點就有了用武之地。設置條件斷點的形式,就是在設置斷點的基本形式後面增加 if 條件。示例如下:

break test.c:6 if num>0

當在 num>0 時,程序將會在第 6 行斷住。

查看斷點

語法:

info breakpoints

可以使用 info breakpoints 查看斷點的情況。包含都設置了那些斷點,斷點被命中的次數等信息。示例如下:

它將會列出所有已設置的斷點,每一個斷點都有一個標號,用來代表這個斷點。

刪除斷點

語法:

delete breakpoint

對於無用的斷點我們可以刪除。刪除的命令格式爲 delete breakpoint 斷點編號。info breakpoint 命令顯示結果中的 num 列就是編號。刪除斷點的示例如下:

查看源碼

斷點設置完後,當程序運行到斷點處就會暫停。暫停的時候,我們可以查看斷點附近的代碼。查看代碼的子命令是 list,縮寫形式爲 l。

指定行號查看代碼

語法:

list first,last

例如,要列出 6 到 21 行之間的源碼:

列出指定文件的源碼

前面執行 l 命令時,默認列出 test.c 的源碼,如果想要看指定文件的源碼呢?可以

list 【文件名加行號或函數名】

總結

本文介紹了 GDB 調試中的斷點設置、源碼查看。斷點設置可以便於我們後期觀察變量,堆棧等信息,爲進一步的定位與調試做準備。源碼查看可以通過指定行號或者方法名來查看相關代碼。

單步調試與查看變量

前言

前面兩篇已經對 GDB 啓動調試, GDB 調試斷點設置與查看源碼我們已經瞭解了 GDB 基本的啓動調試,設置斷點,查看源碼等,如果這些內容你還不知道,建議先回顧一下前面的內容。

斷點附近的代碼你瞭解後,這時候你就可以使用單步執行一條一條語句的去執行。可以隨時查看執行後的結果。接下來你可能會想知道程序運行的一些情況,就需要查看變量的值。下面介紹單步調試與設置變量。

單步調試

居然是調試代碼,還是老規矩,先上代碼:

//test.c
#include <stdio.h>
 
void judge_sd(int num){

	if ((num & 1) == 0){
		printf("%d is even\n",num);
		return;
	}else{
		printf("%d is odd\n",num);
		return;
	}
}
 
int main(int argc, char const *argv[]){
	

	judge_sd(0);
	judge_sd(1);
	judge_sd(4);

	return 0;
}

編譯:

gcc -g test.c -o test

程序的功能比較簡單,這裏不多做解釋。斷點附近的代碼你瞭解後,這時候你就可以使用單步執行一條一條語句的去執行。可以隨時查看執行後的結果。單步執行有兩個命令,分別是 step 和 next。我們可能打了多處斷點,或者斷點打在循環內,這個時候,可以使用 continue 命令。這三個命令的區別在於:

1、next命令(可簡寫爲n)用於在程序斷住後,繼續執行下一條語句。
2、step命令(可簡寫爲s),它可以單步跟蹤到函數內部。
3、continue命令(可簡寫爲c)或者fg,它會繼續執行程序,直到再次遇到斷點處。

單步進入 - step

step 一條語句一條語句的執行。它有一個別名,s。它可以單步跟蹤到函數內部。

先用 list(可簡寫爲 l)將源碼列出來,例如:

先啓動調試,然後把源碼列出來。

從上面的過程可以看到,在 5 行設置斷點,運行程序,可見,step 命令進入到了被調用函數中 judge_sd。使用 step 命令也會在這個方法中一行一行的單步執行。但是如果沒有該函數源碼,需要跳過該函數執行,可使用 finish 命令,繼續後面的執行。

單步執行 - next

next 命令示例:

next 命令(可簡寫爲 n)用於在程序斷住後,繼續執行下一條語句。上面的信息在 5 行處打斷點,然後運行到 6 行,然後輸入 運行 n 2,則會單步執行兩行。可見,使用 next 命令只會在本方法中單步執行。

繼續執行到下一個斷點 - continue

我們可能打了多處斷點,或者斷點打在循環內,這個時候,想跳過這個斷點,甚至跳過多次斷點繼續執行該怎麼做呢?可以使用 continue 命令。它的作用就是從暫停處繼續執行。命令的簡寫形式爲 c。繼續執行過程中遇到斷點或者觀察點變化依然會暫停。示例代碼如下:

跳過執行–skip

根據上面的信息可以看到,使用 skip 之後,將不會進入 judge_sd 函數。好處就是 skip 可以在 step 時跳過一些不想關注的函數或者某個文件。

如果想刪除 skip,使用 skip delete [num] 。

查看變量

現在你已經會設置斷點,查看斷點附近的代碼,並可以單步執行和繼續執行。接下來你可能會想知道程序運行的一些情況,如查看變量的值。print 命令正好滿足了你的需求。以幫助我們進一步定位問題。

格式:

print[變量名]

print(可簡寫爲 p)打印變量內容。示例代碼如下:

//test.c
#include <stdio.h>
#include <stdlib.h> //malloc,free,rand

int main(int argc, char const *argv[])
{
	
	int input;
	int i ;

	printf("Please enter the length of the string:");

    scanf("%d",&input);

    char *buf = (char *) malloc(input + 1);//字符最後包含'\0'
    if (buf == NULL)
    {
    	printf("malloc failed!\n");
    	return -1;
    }

	//隨機生成字符串

	for ( i = 0; i < input; i++)
	{
		buf[i] = rand()%26 +'a';
	}

	buf[i] = '\0';

	printf("A randomly generated string:%s\n",buf);
	free(buf);

	return 0;
}

編譯:

gcc -g test.c -o test

先用 list(可簡寫爲 l)將源碼列出來,例如:

print 命令的簡寫形式爲 p,使用它打印出變量的值。

打印出的變量 i 的值爲 80。

當然,多個函數或者多個文件會有同一個變量名,這個時候可以在前面加上文件名或者函數名來區分:

p 'testfile.c'::i
p 'sum'::i

在看看指針。

注意到了沒有,如果使用上面的方式打印指針指向的內容,那麼打印出來的只是指針地址而已。那怎麼打印出指針指向的內容呢?

需要解引用,如下:

僅僅使用 * 只能打印第一個值,如果要打印多個值,後面跟上 @並加上要打印的長度。或者 @後面跟上變量值:如下:

另外值得一提的是,$ 可表示上一個變量,在調試鏈表時時經常會用到的,它有 next 成員代表下一個節點,則可使用下面方式不斷打印鏈表內容,舉例:

p *linkNode  #這裏顯示linkNode節點內容
p *$.next #這裏顯示linkNode節點下一個節點的內容

設置變量

使用 print 命令查看了變量的值,如果感覺這個值不符合預期,想修改下這個值,再看下執行效果。這種情況下,我們該怎麼辦呢?通常情況下,我們會修改代碼,再重新執行代碼。使用 gdb 的 set 命令,一切將變得更簡單。

set 命令可以直接修改變量的值。

設置觀察點

設置觀察點的作用就是:當被觀察的變量發生變化後,程序就會暫停執行,並把變量的原值 (Old) 和新值 (New) 都會顯示出來。設置觀察點的命令是 watch。

watch num

這個時候,讓程序繼續運行,如果 num 的值發生變化,則會打印相關內容,如:

Hardware watchpoint 3: num Old value = 1 New value = 10

總結

通過上面的例子演示,我相信讀者已經對於通過 GDB 調試 C/C++ 程序有了基本的理解,如果你想獲取更多的調試技巧請參考官方網站的 GDB 調試手冊,還有 GDB 官方網站的手冊。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/PXSbb8ruPLyCu1vgyqMxVw