gdb教學一基本操作

本篇文章修改參考這篇文章製作範例和教學,首先我們先寫一個有bug的程式factorial.c

基本操作

編譯程式給GDB

如果要給GDB除錯,一定要加上-g選項

1
gcc -g <any other flags e.g. -Wall> -o <file> <file.c>

開啟GDB session

在終端機輸入gdb和執行檔名稱即可開啟gdb session

1
2
3
4
5
6
7
8
9
gdb <program_name>
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
...
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from factorial...
(gdb)

run指令

用run指令執行程式。

1
(gdb) run

如果有程式有接收參數,可以直接放在run後面

1
(gdb) run <arg1> <arg2> ... <arg n>

如果要用檔案作為輸入,可以用<

1
(gdb) run < <data>

start指令

用start指令執行程式,並且在main函式的第一行停下來。

1
(gdb) start

如果有程式有接收參數,可以直接放在run後面

1
(gdb) start <arg1> <arg2> ... <arg n>

如果要用檔案作為輸入,可以用<

1
(gdb) start < <data>

quit指令

離開GDB可以用quit指令

1
(gdb) quit

範例

以下面程式為範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//This program calculates and prints out the factorials of 5 and 17


#include <stdio.h>
#include <stdlib.h>

int factorial(int n);

int main(void) {

int n = 5;
int f = factorial(n);
printf("The factorial of %d is %d.\n", n, f);
n = 17;
f = factorial(n);
printf("The factorial of %d is %d.\n", n, f);

return 0;

}
//A factorial is calculated by n! = n * (n - 1) * (n - 2) * ... * 1
//E.g. 5! = 5 * 4 * 3 * 2 * 1 = 120
int factorial(int n) {
int f = 1;
int i = 1;
while (i <= n) {
f = f * i;
i++;
}
return f;
}

用以下指令編譯

1
gcc -Wall -g -o factorial factorial.c

接下來開啟GDB session

1
gdb factorial

在GDB session中執行程式,會發現程式輸出錯誤的值

1
(gdb) run

如果要結束GDB session指令可以用quit。

1
(gdb) quit

中斷點

break指令

如果要放置中斷點在特定檔案的某一行,可以用break <filename>:<line number>指令

1
(gdb) break <filename>:<line number>

你也可以直接指定要放在哪一個function(gdb) break <filename>:<function>

1
(gdb) break <filename>:<function>

列出全部中斷點

要列出全部的中斷點可以用以下指令

1
(gdb) info break

delete指令

如果要刪除中斷點可以先用info break所有中斷點的號碼再刪除

1
(gdb) delete <breakpoint number>

範例

接著前面的例子,我們懷疑程式的第15行有錯誤,因此在GDB session中把中斷點放在第15行並且執行程式

1
2
(gdb) break 15
(gdb) run

我們應該會看到類似以下的輸出,你可以看到GDB輸出我們的第15行程式,代表現在程式停在第15行。
注意!!這時候程式並還沒有執行第15行。GDB告訴我們的是他下一行將要執行的程式。

1
2
3
4
5
6
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
The factorial of 5 is 120.

Breakpoint 1, main () at factorial.c:15
15 f = factorial(n);

查看資料

print指令

印出變數

1
(gdb) print <variable_name>

info locals指令

印出當前所有的local variable,除了輸入函式的參數以外。

1
(gdb) info locals

info args指令

印出輸入給函式的參數

1
(gdb) info args

範例

接續前面的範例,們想查看程式運作到中斷點時變數的值。首先查看f和n的值

1
2
3
4
(gdb) print f
$1 = 120
(gdb) print n
$2 = 17

我們也可以用info locals來查看所有區域變數。

1
2
3
(gdb) info locals
n = 17
f = 120

查看程式運作

當程式在中斷點停下後,利用step, next and continue可以控制程式運作以便我們逐行觀察程式運作的情形。

step指令

執行下一行程式,如果下一行程式是呼叫function,gdb就會進到function裡面。

1
(gdb) step

next指令

執行下一行程式,與step不同的是,如果下一行是function,則將function執行完而不進入function。

1
(gdb) next

continue指令

執行程式直到碰到下一個中斷點

1
(gdb) continue

where指令

印出function call stack

1
(gdb) where

list指令

印出目前所在的行數以及前後各兩行。

1
(gdb) list

範例

接續前面的範例。我們想要查看我們的factorial函式如何運作的。因此用step指令進入factorial函式。

1
2
3
(gdb) step
factorial (n=17) at factorial.c:24
24 int f = 1;

接下來我們想一步一步看看factorial函式如和運作的

1
2
3
4
5
6
7
8
(gdb) next
25 int i = 1;
(gdb) next
26 while (i <= n) {
(gdb) next
27 f = f * i;
(gdb) next
28 i++;

除了不斷輸入重複的指令,你也可以直接按Enter,GDB會重複你上一個指令。
接下來我們預期執行到這裡,if應該要等於1

1
2
3
4
(gdb) print i
$2 = 1
(gdb) print f
$3 = 1

如果我們想查看目前在程式哪一行,可以用where指令來印出call stack

1
2
3
(gdb) where
#0 factorial (n=17) at factorial.c:28
#1 0x0000555555555196 in main () at factorial.c:15

如果要印出目前行數前後的程式可以用list

1
2
3
4
5
6
7
8
(gdb) list
23 int factorial(int n) {
24 int f = 1;
25 int i = 1;
26 while (i <= n) {
27 f = f * i;
28 i++;
29 }

我們也可以用continue指令和中斷點來加速除錯,首先先下一個中斷點在第28行。

1
2
(gdb) break 28
Breakpoint 2 at 0x5555555551e1: file factorial.c, line 28.

接下來用continue指令直接跳到這個中斷點

1
2
3
4
5
(gdb) continue
Continuing.

Breakpoint 2, factorial (n=17) at factorial.c:28
28 i++;

然後依次印出所有區域變數

1
info locals

我們不斷重複這個動作,可以發現前面都還運作正常,直到i=13時,答案開始出出錯了,如果繼續執行會發現答案越來越小,甚至變成負的。這個錯誤原因是int這個資料型態無法儲存這麼大的值,我們必須使用更大的資料型態才能儲存。

Call Stack

call Stack是由stack frames所組成。stack frames是用來儲存呼叫函式的時候函式的區域變數。如果函式內又呼叫另一個函式,新的一個stack frames會被放當前函式的stack frames的上面。當一個函式完成後,就會移除一個stack frames。

where指令

印出call stack並且包含檔名和行數。

up指令

往上移動stack一層frame

1
2
(gdb) up
(gdb) up <n_frames>

down指令

往下移動stack一層frame

1
2
(gdb) down
(gdb) down <n_frames>

frame指令

移動到指定的frame

1
(gdb) frame <frame_number>

範例

如果錯誤出現在函式庫的程式碼,這時候用call stack來debug就會很有用,我們可以利用call stack來尋找我們的程式在什麼地方出錯。用下面範例corrupted_linked_list.c來講解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//Makes a linked list of length 7 and prints it out
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>


struct node {
int data;
struct node *next;
};

struct node *create_node(int data);
struct node *create_list(int length);
void print_list(struct node *list);

int main(void){
struct node *list1 = create_list(7);
print_list(list1);

return 0;
}

struct node *create_node(int data){
struct node *new = malloc(sizeof(struct node));
assert(new != NULL);
new->data = data;
new->next = NULL;
return new;
}

struct node *create_list(int length) {

struct node *head = NULL;
if (length > 0) {
head = create_node(0);
int i = 1;
struct node *curr = head;
while (i < length) {
curr->next = create_node(i);
curr = curr->next;
i++;
}
}
return head;
}

void print_list(struct node *list){
struct node *curr = list;

while (curr != NULL) {
printf("%d->", curr->data);

curr == curr->next;
}
printf("X\n");
}

{:file=’corrupted_linked_list.c’}

首先我們編譯並且執行程式,我們會發現程式進入無窮迴圈,於是我們強制程式停下來。

1
2
3
4
$ gcc -g -o corrupted_linked_list corrupted_linked_list.c
./corrupted_linked_list
0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->
**ctrl + c**

我們預期程式的輸出應該像下面這樣

1
2
./corrupted_linked_list
0->1->2->3->4->5->6->X

為了要了解程式到底錯在哪裡,我們在GDB session裡面執行程式。並且中斷程式

1
2
3
4
5
6
7
$ gdb corrupted_linked_list
(gdb) run
0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0
**ctrl + c**
Program received signal SIGINT, Interrupt.
0x00007fffff1272c0 in __write_nocancel () at ../sysdeps/unix/syscall-template.S:84
84 ../sysdeps/unix/syscall-template.S: No such file or directory.

中斷後我們可以用where指令看一下目前所在的位置,輸出會類似如下

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) where
#0 0x00007fffff1272c0 in __write_nocancel () at ../sysdeps/unix/syscall-template.S:84
#1 0x00007fffff0a8bff in _IO_new_file_write (f=0x7fffff3f5620 <_IO_2_1_stdout_>, data=0x6020f0, n=512) at fileops.c:1263
#2 0x00007fffff0aa409 in new_do_write (to_do=512,
data=0x6020f0 "0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0-"..., fp=0x7fffff3f5620 <_IO_2_1_stdout_>) at fileops.c:518
#3 _IO_new_do_write (fp=0x7fffff3f5620 <_IO_2_1_stdout_>,
data=0x6020f0 "0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0->0-"..., to_do=512) at fileops.c:494
#4 0x00007fffff0a947d in _IO_new_file_xsputn (f=0x7fffff3f5620 <_IO_2_1_stdout_>, data=<optimised out>, n=2) at fileops.c:1331
#5 0x00007fffff07d92d in _IO_vfprintf_internal (s=0x7fffff3f5620 <_IO_2_1_stdout_>, format=<optimised out>, ap=ap@entry=0x7ffffffedf08) at vfprintf.c:1663
#6 0x00007fffff085899 in __printf (format=<optimised out>) at printf.c:33
#7 0x000000000040071b in print_list (list=0x602010) at corrupted_linked_list.c:50
#8 0x0000000000400628 in main () at corrupted_linked_list.c:17

可以看到程式被中斷在標準函式庫的程式,不過我們想看一看輸入函式庫的參數是什麼。因此我們可以用up指令從frame 0 移動到 frame 1。或者我們直接用frame指令移動到我們要的地方

1
2
3
(gdb) frame 7
#7 0x000000000040071b in print_list (list=0x602010) at corrupted_linked_list.c:50
50 printf("%d->", curr->data);

首先我們先看一下區域變數

1
2
(gdb) info locals
curr = 0x602010

可以用ptype指令查看curr的型態,可以發現他是一個node struct的指針。

1
2
3
4
5
(gdb) ptype curr
type = struct node {
int data;
struct node *next;
} *

我們dereference查看一下內容

1
2
(gdb) print *curr
$1 = {data = 0, next = 0x602030}

也可以查看其他的內容

1
2
3
4
5
(gdb) print *(curr->next)
$2 = {data = 1, next = 0x602050}
(gdb) print *(curr->next->next)
$3 = {data = 2, next = 0x602070}
(gdb)

Core Dumps

程式當機當下程式的狀態對於除錯十分有幫助,我們可以利用core dump檔來記錄這些狀態。對於一些不定時發生的錯誤,這些除錯資訊就十分珍貴了。

Core Dumps設定

首先查看Core Dumps記錄功能有沒有被開啟,如果回傳0代表沒有打開Core Dumps記錄功能

1
ulimit -c

用以下指令設定打開Core Dumps記錄功能

1
ulimit -c unlimited

通常Core Dumps檔產生的位置會記路在/proc/sys/kernel/core_pattern這個設定,用以下指令查看Core Dumps檔的位置。

產生Core Dump

當Core Dumps紀錄功能打開後,如果程式遇到Segmentation fault的錯誤,就會產生Core Dump檔。

用GDB查看Core Dump檔

要查看Core Dump檔可以用以下指令。注意當我們利用Core Dump檔來除錯的時候,程式實際上並沒有在運作,所以step, nextcontinue 這些指令這時候是沒有功能的。

1
gdb <binary-file> <core-dump-file>

範例

利用下面範例broken_linked_list.c將說明如何使用Core Dump除錯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//Makes a linked list of length 7 and prints it out
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>


struct node {
int data;
struct node *next;
};

struct node *create_node(int data);
struct node *create_list(int length);
void print_list(struct node *list, int length);

int main(void){
int length1 = 7;
struct node *list1 = create_list(length1);
print_list(list1, length1);

return 0;
}

struct node *create_node(int data){
struct node *new = malloc(sizeof(struct node));
assert(new != NULL);
new->data = data;
new->next = NULL;
return new;
}

struct node *create_list(int length) {

struct node *head = NULL;
if (length > 0) {
head = create_node(0);
int i = 1;
struct node *curr = head;
while (i < length) {
curr->next = create_node(i);
curr = curr->next;
i++;
}
}
return head;
}

void print_list(struct node *list, int length){
struct node *curr = list;
int i = 0;
while (i <= length) {
printf("%d->", curr->data);
curr = curr->next;
i++;
}
printf("X\n");
}

{: file=’broken_linked_list.c’}
編譯並且執行程式,可以看到程式出現Segmentation fault (core dumped)錯誤

1
2
3
$ gcc -g -o broken_linked_list broken_linked_list.c
$ ./broken_linked_list
Segmentation fault (core dumped)

接下來讀取Core Dump檔

1
gdb broken_linked_list core

GDB將會顯示程式出錯的位置

1
2
3
4
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x000055be9593e283 in print_list (list=0x55be96c20260, length=7)
at broken_linked_list.c:51
51 printf("%d->", curr->data);

從這裡可以知道我們在第51行正在嘗試存取一個非法的記憶體位置。因此我們可以推測可能是curr的位址不正確或者是data是不可以讀取的位址。於是先嘗試印出curr的值

1
2
(gdb) print curr
$1 = (struct node *) 0x0

可以看到curr是一個NULL址針。接下來我們在印出當前狀況的區域變數

1
2
3
(gdb) info locals
curr = 0x0
i = 7

可以看到i是7,也就是現在是第8次執行for迴圈,但是我們的Linking list只有7個節點,而且依照我們的建立Linking list的方式,第8個節點會是NULL址針,所以程式會出錯。
我們在檢查一下Linking list本身是否有問題。

1
2
(gdb) print *list
$2 = {data = 0, next = 0x55be96c20280}

可以看到Linking list的位址沒問題,因此可以更加確定問題就是迴圈多執行了一次。我們可以用list指令看一下程式現在所在的位置。

1
2
3
4
5
6
7
8
9
10
11
(gdb) list
46
47 void print_list(struct node *list, int length){
48 struct node *curr = list;
49 int i = 0;
50 while (i <= length) {
51 printf("%d->", curr->data);
52 curr = curr->next;
53 i++;
54 }
55 printf("X\n");

GDB Init File

如果單純使用GDB的指令,有些變數就會變得難以查看,例如如果想要查看linked list的所有成員就會變得很麻煩。而GDB提供讓使用者自定義指令讓我們可以容易議處想要的結果。

GDB User Initialization File

GEB啟動時會載入User Initialization File所記錄的指令,你可以建立一個新的User Initialization File,他的檔名是.gdbinit,放置的位置是home directory

1
~/.gdbinit

建立之後在檔案內加入下面指令,如此一來就可以在每一個專案下各自建立專屬的initialization file .gdbinit

GDB

專案自己的initialization file位在專案的跟目錄下,使用者可以自訂義指令或是GDB啟動的行為。

1
~/<file_path>/.gdbinit

基礎用法

例如你的專案想要每次使用GDB的時候都會放一個breakpoint在某一個function,你就可以在.gdbinit寫入下面這行。

1
break <function_name>

GDB腳本語法

定義命令

你可以用以下命令定義一個自訂義命令

1
2
3
define <command>
<code>
end

增加命令說明

你可以用以下命令為自訂義命令增加註解

1
2
3
document <command>
<information about the command>
end

如果要查看命令說明可以用以下命令

1
2
(gdb) help <command>
<information about the command>

命令參數

如果需要傳入參數給自訂義命令可以用以下方式

1
(gdb) <command> <arg0> <arg1> <arg2> ...

如果要在指令內使用參數可以用以下方式

1
2
3
4
5
$argc
$arg0
$arg1
$arg2
...

方便的內建變數

可以用以下方式定義

1
$<variable_name>

設定變數

用以下指令設定變數

1
set $<variable_name> = <value_or_expression>

if 聲明

用以下方式可以做if聲明

1
2
3
4
5
if <condition>
<code>
else
<code>
end

while loop

1
2
3
while <condition>
<code>
end

Printing

GDB的print語法跟c十分相似

1
printf "<format string>", <arg0>, <arg1>, ...

使用自訂義命令

使用者自訂義命令的用法跟內建指令的用法相同。

1
(gdb) <command> <arg0> <arg1> <arg2> ...

範例:除錯Linked lists

下面將以除錯Linked list作為範例linked_list.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//Makes a linked list of length 7 and prints it out
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>


struct node {
int data;
struct node *next;
};

struct node *create_node(int data);
struct node *create_list(int length);
void print_list(struct node *list);

int main(void){
struct node *list1 = create_list(7);
print_list(list1);

return 0;
}

struct node *create_node(int data){
struct node *new = malloc(sizeof(struct node));
assert(new != NULL);
new->data = data;
new->next = NULL;
return new;
}

struct node *create_list(int length) {

struct node *head = NULL;
if (length > 0) {
head = create_node(0);
int i = 1;
struct node *curr = head;
while (i < length) {
curr->next = create_node(i);
curr = curr->next;
i++;
}
}
return head;
}

void print_list(struct node *list){
struct node *curr = list;

while (curr != NULL) {
printf("%d->", curr->data);
curr = curr->next;
}
printf("X\n");
}

{:file=’linked_list.c’}

首先先把下面這行加入~/.gdbinit

1
set auto-load safe-path /

接下來撰寫自訂義命令,在專案根目錄也建立一個.gdbinit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
define p_generic_list
set var $n = $arg0
while $n
print *($n)
set var $n = $n->next
end
end

document p_generic_list
p_generic_list LIST_HEAD_POINTER
Print all the fields of the nodes in the linked list pointed to by LIST_HEAD_POINTER. Assumes there is a next field in the struct.
end



define indentby
printf "\n"
set $i_$arg0 = $arg0
while $i_$arg0 > 10
set $i_$arg0 = $i_$arg0 - 1
printf "%c", ' '
end
end

{:file=’.gdbinit’}

在命令裡面,我們建立一個變數來儲存第一個參數($arg0),也就是linked list的指針

1
set var $n = $arg0

接下來印出 linked list的內容。

1
print *($n)

接下來把把linked list的指針指向下一個元素

1
set var $n = $n->next

運行結果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ gcc -g -o linked_list linked_list.c
$ gdb -q ./linked_list
Reading symbols from ./linked_list...
(gdb) br 18
Breakpoint 1 at 0x11c3: file linked_list.c, line 18.
(gdb) r
Starting program: /home/steven/tmp/gcc_practice/linked_list
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main () at linked_list.c:18
18 print_list(list1);
(gdb) p_generic_list list1
$1 = {data = 0, next = 0x5555555592c0}
$2 = {data = 1, next = 0x5555555592e0}
$3 = {data = 2, next = 0x555555559300}
$4 = {data = 3, next = 0x555555559320}
$5 = {data = 4, next = 0x555555559340}
$6 = {data = 5, next = 0x555555559360}
$7 = {data = 6, next = 0x0}
(gdb)

watch and display

watch可以監看一個變數,每當這個變數數值改變的時候就暫停程式。display 則是每當程式停止的時候顯示變數。

watch

使用以下指令設定想要監看的變數

1
(gdb) watch <variable_name>

查看Watchpoints

查看Watchpoints的方式跟查看breakpoints的方式一樣

1
(gdb) info breakpoints

移除Watchpoints

使用以下指令移除Watchpoints

1
(gdb) disable <watchpoint_number>

display

使用以下指令設定display

1
(gdb) display expression

查看所有display

查看所有display

1
(gdb) info display

移除display

用以下指令移除display

1
(gdb) delete display <display_number>

範例

以下將以計算階乘的程式factorial.c來做示範。程式在計算階層

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//This program calculates and prints out the factorials of 5 and 17


#include <stdio.h>
#include <stdlib.h>

int factorial(int n);

int main(void) {

int n = 5;
int f = factorial(n);
printf("The factorial of %d is %d.\n", n, f);
n = 17;
f = factorial(n);
printf("The factorial of %d is %d.\n", n, f);

return 0;

}
//A factorial is calculated by n! = n * (n - 1) * (n - 2) * ... * 1
//E.g. 5! = 5 * 4 * 3 * 2 * 1 = 120
int factorial(int n) {
int f = 1;
int i = 1;
while (i <= n) {
f = f * i;
i++;
}
return f;
}

接著編譯程式並且用gdb讀取

1
2
3
$ gcc -g -o factorial factorial.c
$ gdb factorial
Reading symbols from factorial...done.

首先先設定中斷點,可以看到n=5的時候晟是正常運作,讓嘗試繼續執行讓n=17

1
2
3
4
5
6
7
8
9
10
11
(gdb) br factorial
Breakpoint 1 at 0x11a5: file factorial.c, line 24.
(gdb) r
Starting program: ~/factorial
Breakpoint 1, factorial (n=5) at factorial.c:24
24 int f = 1;
(gdb) c
Continuing.
The factorial of 5 is 120.
Breakpoint 1, factorial (n=17) at factorial.c:24
24 int f = 1;

接下來設定watch和display,我們希望i初始化之後再設定watch和display,

1
2
3
4
(gdb) n
25 int i = 1;
(gdb) n
26 while (i <= n) {

然後設定watch和display

1
2
3
4
(gdb) watch f
Hardware watchpoint 2: f
(gdb) display i
1: i = 1

然後我們就可以觀察程式計算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
(gdb) c
Continuing.

Hardware watchpoint 2: f

Old value = 1
New value = 2
factorial (n=17) at factorial.c:28
28 i++;
1: i = 2
(gdb) c
Continuing.

Hardware watchpoint 2: f

Old value = 2
New value = 6
factorial (n=17) at factorial.c:28
28 i++;
1: i = 3
(gdb) c
Continuing.

Hardware watchpoint 2: f

Old value = 6
New value = 24
factorial (n=17) at factorial.c:28
28 i++;
1: i = 4
(gdb) c
Continuing.

Hardware watchpoint 2: f

Old value = 24
New value = 120
factorial (n=17) at factorial.c:28
28 i++;
1: i = 5
(gdb) c
Continuing.

Hardware watchpoint 2: f

Old value = 120
New value = 720
factorial (n=17) at factorial.c:28
28 i++;
1: i = 6
(gdb) c
Continuing.

Hardware watchpoint 2: f

Old value = 720
New value = 5040
factorial (n=17) at factorial.c:28
28 i++;
1: i = 7
(gdb) c

Continuing.

Hardware watchpoint 2: f

Old value = 5040
New value = 40320
factorial (n=17) at factorial.c:28
28 i++;
1: i = 8
(gdb) c
Continuing.

Hardware watchpoint 2: f

Old value = 40320
New value = 362880
factorial (n=17) at factorial.c:28
28 i++;
1: i = 9
(gdb) c
Continuing.

Hardware watchpoint 2: f

Old value = 362880
New value = 3628800
factorial (n=17) at factorial.c:28
28 i++;
1: i = 10
(gdb) c
Continuing.

Hardware watchpoint 2: f

Old value = 3628800
New value = 39916800
factorial (n=17) at factorial.c:28
28 i++;
1: i = 11
(gdb) c
Continuing.

Hardware watchpoint 2: f

Old value = 39916800
New value = 479001600
factorial (n=17) at factorial.c:28
28 i++;
1: i = 12
(gdb) c
Continuing.

Hardware watchpoint 2: f

Old value = 479001600
New value = 1932053504
factorial (n=17) at factorial.c:28
28 i++;
1: i = 13

我們可以觀察到當n=13的時候程式就開始出錯。

條件式Breakpoints

利用conditional breakpoints可以讓程式達到特定條件的時候才停下來。

設定條件式Breakpoints的方法

  1. 建立一個中斷點
    如同前面所教的方法先建立一個中斷點。

    1
    2
    (gdb) break <file_name> : <line_number>
    (gdb) break <function_name>
  2. 查看所有中斷點
    下面指令可以查看目前已經設定過的中斷點。

    1
    (gdb) info breakpoints
  3. 設定條件
    首先我們必須要知道中斷點的編號,並且用以下指令設定條件

    1
    (gdb) condition <breakpoint_number> condition
  4. 移除中斷點的停止條件
    如果想要移除中斷點的停止條件,可以用以下指令

    1
    (gdb) condition <breakpoint_number>

範例

下面將繼續沿用階層計算程式factorial.c來示範conditional breakpoints

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//This program calculates and prints out the factorials of 5 and 17


#include <stdio.h>
#include <stdlib.h>

int factorial(int n);

int main(void) {

int n = 5;
int f = factorial(n);
printf("The factorial of %d is %d.\n", n, f);
n = 17;
f = factorial(n);
printf("The factorial of %d is %d.\n", n, f);

return 0;

}
//A factorial is calculated by n! = n * (n - 1) * (n - 2) * ... * 1
//E.g. 5! = 5 * 4 * 3 * 2 * 1 = 120
int factorial(int n) {
int f = 1;
int i = 1;
while (i <= n) {
f = f * i;
i++;
}
return f;
}

{:file=’factorial.c’}

編譯程式並啟動GDB

1
2
3
$ gcc -g -o factorial factorial.c
$ gdb factorial
Reading symbols from factorial...done.

設定conditional breakpoints

我們已經知道程式在i <= 5 之前都正常運作,所以我們不必在確認i <= 5 之前的輸出結果。因此我們設定的條件是 i > 5。

1
2
3
4
5
$ gdb factorial
Reading symbols from factorial...done.
(gdb) br 28
Breakpoint 1 at 0x11bf: file factorial.c, line 28.
(gdb) condition 1 i > 5

開始除錯

接下來就可以執行程式並且觀察不同i之下的輸出變化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
(gdb) r
Starting program: ~/factorial
The factorial of 5 is 120.

Breakpoint 1, factorial (n=17) at factorial.c:28
28 i++;
(gdb) info locals
f = 720
i = 6
(gdb) c
Continuing.

Breakpoint 1, factorial (n=17) at factorial.c:28
28 i++;
(gdb) info locals
f = 5040
i = 7
(gdb) c
Continuing.

Breakpoint 1, factorial (n=17) at factorial.c:28
28 i++;
(gdb) info locals
f = 40320
i = 8
(gdb) c
Continuing.

Breakpoint 1, factorial (n=17) at factorial.c:28
28 i++;
(gdb) info locals
f = 362880
i = 9
(gdb) c
Continuing.

Breakpoint 1, factorial (n=17) at factorial.c:28
28 i++;
(gdb) info locals
f = 3628800
i = 10
(gdb) c
Continuing.

Breakpoint 1, factorial (n=17) at factorial.c:28
28 i++;
(gdb) info locals
f = 39916800
i = 11
(gdb) c
Continuing.

Breakpoint 1, factorial (n=17) at factorial.c:28
28 i++;
(gdb) info locals
f = 479001600
i = 12
(gdb) c
Continuing.

Breakpoint 1, factorial (n=17) at factorial.c:28
28 i++;
(gdb) info locals
f = 1932053504
i = 13
(gdb)

我們可以發現i=13之後數值就開始出現異常了。

除錯shared library

gcc印出所使用到的shared library

1
gcc -Wl,-t your_program.c -o your_program > ld_output.txt

gdb查看被載入的shared library

1
info share

每當有新的shared library被載入的時候就暫停

set stop-on-solib-events 1

https://jasonblog.github.io/note/gdb/li_yong_gdb_jin_xing_shared_library_de_chu_cuo.html

參考:
https://www.cse.unsw.edu.au/~learn/debugging/modules/gdb_basic_use/

Linux安裝prebuild函式庫以OpenCV為例

我的部落格文章轉錄–Linux環境撰寫Shared Library有詳細介紹如何製作和安裝Shared Library,如果想要了解更多Shared Library安裝和製作方式可以參考這篇。
{: .prompt-tip }

在這篇我們想直接使用OpenCV預編譯的函式庫,省去自己編譯函式庫的時間,首先我們先到官網提到的Third-party packages的System packages in popular Linux distributions,找到自己的Linux distributions,在這裡我們是使用Ubuntu22,而我們要下載的是development files for opencv也就是libopencv-dev_4.5.4+dfsg-9ubuntu4_amd64.deb這個連結,在Install Howto的地方可以看到安裝指令。

如果你不想弄亂你的環境,建議你可以用Vagrant建立一台測試環境來測試一下安裝後的結果,或是做一些實驗。
安裝Vagrant的方法在安裝vagrant製作測試環境
{: .prompt-tip }

1
2
sudo apt-get update
sudo apt-get install libopencv-dev

Files的地方可以看到他幫我們裝了什麼東西以及他們被安裝的位置。在Requires的地方可以看到他還幫我們安裝了哪些相依套件。
在這裡我們比較一下libopencv-core-devlibopencv-core4.5d這兩個函式庫。在這兩個package的Files的地方可以發現libopencv-core-dev幫我們在/usr/include/opencv4多裝了很多標頭檔(*.hpp),因為如果要在我們自己的C++中使用OpenCV,必須要include OpenCV的標頭檔,而dev套件已經幫我們幫把標頭檔都放在/usr/include/opencv4讓我們可以引用了。而在編寫OpenCV的C++專案的時候,要記得把這個include資料夾放到你的專案裡。

pkg-config幫我們列出全部的標頭檔位置和opencv的名稱

標頭檔路徑只需要加上g++選項-I/usr/include/opencv4就可以了,不過如果要把所有用到的library都手動寫出來實在很麻煩,這時候pkg-config可以幫我們把全部的opencv library全部列出來,我們可以試看看在終端機輸入下面指令pkg-config --libs --cflags opencv4(如果安裝的是opencv 2.x或3.x要輸入pkg-config --libs --cflags opencv),終端機的回應應該會長的像下面這樣。

1
2
$ pkg-config --libs --cflags opencv4
-I/usr/include/opencv4 -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_ml -lopencv_videostab -lopencv_videoio -lopencv_viz -lopencv_wechat_qrcode -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core

g++的指令是長這樣

1
/usr/bin/g++ -g main.cpp -o main `pkg-config --libs --cflags opencv4`

你可以到linux下設定vscode-cmake-gcc-gdb來開發c-專案查看如何用VSCode連接和編譯OpenCV library。

參考:
https://blog.gtwang.org/programming/ubuntu-linux-install-opencv-cpp-python-hello-world-tutorial/
https://pkgs.org/search/?q=opencv
https://ubuntu.pkgs.org/22.04/ubuntu-universe-amd64/libopencv-dev_4.5.4+dfsg-9ubuntu4_amd64.deb.html

文章轉錄--Linux環境撰寫Shared Library

撰寫範例程式

1
bool isPalindrome(char* word);

{: file=”pal.h” }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "pal.h"
#include <string.h>

bool isPalindrome(char* word)
{
bool ret = true;

char *p = word;
int len = strlen(word);
char *q = &word[len-1];

for (int i = 0 ; i < len ; ++i, ++p, --q)
{
if (*p != *q)
{
ret = false;
}
}

return ret;
}

{: file=”pal.cpp” }

編譯shared library

在終端機輸入以下GCC指令,注意這裡我們有-c選項,這告訴GCC不要進行linking stage,如果沒加GCC就會報錯,因為一個應用程式一定會有main()函式,但是Library不需要main()函式。這行指令將會產生一個pal.o

1
g++ -fPIC -c -Wall pal.cpp

接下來我們要用pal.o檔和以下指令產生真正的library。ld是linker program,通常會被g++呼叫。-shared告訴ld製作一個shared object,以pal.o為輸入輸出名為libpal.so。在Linux通常shared libraries的副檔名都是.so。

1
ld -shared pal.o -o libpal.so

使用shared library

首先建立一個main.cpp來呼叫我們的library的isPalindrome函式。在程式中我們include函式庫的標頭檔pal.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "pal.h"
#include <iostream>

using namespace std;

int main()
{
while (1)
{
char buffer[64] = {0};
cin >> buffer;

if (isPalindrome(buffer))
{
cout << "Word is a palindrome" << endl;
}
else
{
cout << "Word is not a palindrome" << endl;
}
}

return 0;
}

{: file=”main.cpp” }

接下來我們可能直接用g++ -Wall main.cpp -lpal這行指令連接我們的shared library,c但是如果這麼做我們會得到下面錯誤訊息。這是因為ld在預設的搜尋路徑下找不到libpal.so

1
2
/usr/bin/ld: cannot find -lpal: No such file or directory
collect2: error: ld returned 1 exit status

其中一種解法是用g++告入ld函式庫的位置

1
g++ -Wall -L<libpal.so的路徑> -Wl,-rpath=<libpal.so的路徑> main.cpp -lpal

例如

1
g++ -Wall -L/home/faye -Wl,-rpath=/home/faye/ main.cpp -lpal

其中:

  • -Wall是用來檢查所有編譯警告的
  • L式shared library的路徑,讓ld知道要去那裡尋找
  • -Wl是一連串用逗號分隔的linker指令,在這裡有
    • -rpath表示library 的路徑會被嵌入到主程式的執行檔,因此loader在執行主程式的時候可以找到library

-L-rpath的區別是:

  • -L是給linker用的
  • -rpath是被嵌入到執行檔給loader看的

最後g++會幫我們產生a.out執行檔,執行方式和結果如下

1
2
3
4
5
$ ./a.out 
ada
Word is a palindrome
team
Word is not a palindrome

安裝自己開發的Shared Libraries

用剛剛的方式連接Shared Libraries的方法有個缺點,也就是你的編譯指令直接給其他人的話可能會出錯,因為Shared Libraries在每個人的電腦上的位置可能都不一樣。因此rpath-L選項的路徑也會不一樣。而我們的解決方法之一就是LD_LIBRARY_PATH

LD_LIBRARY_PATH

如果我們直接在終端機輸入指令echo $LD_LIBRARY_PATHLD_LIBRARY_PATH的值,他應該會是空的,除非你以前曾經設定過他。要是用LD_LIBRARY_PATH我們只需要以下指令

1
export LD_LIBRARY_PATH=<Shared Library所在資料夾>:$LD_LIBRARY_PATH

例如

1
export LD_LIBRARY_PATH=/home/faye:$LD_LIBRARY_PATH

這時在輸入一次echo $LD_LIBRARY_PATH應該就會輸出/home/faye:。而這時候我們編譯main.cpp的時候就算沒有-rpath選項編譯出來的執行檔也不會找不到libpal.so

1
g++ -Wall -L/home/faye/sotest main.cpp -lpal

你可以先嘗試看看在還沒設定LD_LIBRARY_PATH之前或是利用unset LD_LIBRARY_PATH指令清空LD_LIBRARY_PATH變數,所編譯出來的執行檔會出現什麼問題。
你應該會發現編譯的過程沒有任何錯誤訊息,但是一旦你執行編譯出來的執行檔a.out就會跳出錯誤訊息./a.out: error while loading shared libraries: libpal.so: cannot open shared object file: No such file or directory,這表示執行檔loader找不到libpal.so
{: .prompt-tip }

不過LD_LIBRARY_PATH其實只適合在開發階段拿來測試library用,因為它不需要root權限,但是函式庫發布給大家使用的時候,要求每個人都去設定LD_LIBRARY_PATH並不是個好方法。

ldconfig : 安裝Shared Libraries正統方法

首先我們先清除上一個教學的LD_LIBRARY_PATH設定,可以用unset LD_LIBRARY_PATH指令來清除。
接下來我們必須把我們的函式庫複製到/usr/lib資料夾。複製函式庫到/usr/lib必須擁有root權限,因此我們用sudo來幫助我們。

1
sudo mv <函式庫所在資料夾>/libpal.so /usr/lib

例如

1
sudo mv /home/faye/libpal.so /usr/lib

接下來更新系統中儲存可用libraries的cache。

1
sudo ldconfig

你可以用下面指令來確定cache已經被更新了而且系統可以找到你的函式庫

1
ldconfig -p | grep libpal

系統應該會回應你

1
libpal.so (libc6,x86-64) => /lib/libpal.so

接下來你就可以用下面指令編譯我們的執行檔了,而且這次我們不需要-rpath,也不需要-L選項!!因為現在我們的函式庫已經位於系統預設的函式庫搜尋路徑下了。

1
g++ -Wall main.cpp -lpal

參考:
https://www.fayewilliams.com/2015/07/07/creating-a-shared-library/
https://www.fayewilliams.com/2015/07/14/installing-and-accessing-shared-libraries/

相關文章:
Linux安裝prebuild函式庫以OpenCV為例