Debug #1 – GDB - B4050N

LỜI TỰA

Tiếp theo phần trước, ở phần này, chủ đề chính của chúng ta là làm quen với các công cụ debug. Mục tiêu của bài viết là cung cấp những thao tác làm việc cơ bản với gdb trên giao diện dòng lệnh và so sánh chúng với giao diện gỡ lỗi đồ họa trên Eclipse. Đây cũng là phần chuẩn bị để các bạn có thể hình dung tốt hơn diễn biến của những phần demo trong các bài viết tiếp theo. Ngoài ra, mình cũng sẽ giới thiệu sơ qua về các option yêu cầu khi biên dịch chương trình cho mục đích debug. Đương nhiên, minh họa cho phần này vẫn sẽ sử dụng trình biên dịch quen thuộc trong bộ công cụ GCC.

Cùng điểm qua thứ tự các nội dung: 1. Cài đặt công cụ trên môi trường Linux 2. Các thao tác cơ bản khi thực hiện gỡ lỗi 3. Tùy chọn bổ sung khi biên dịch chương trình cho mục đích debug 4. Tóm lược

Sau đây là chi tiết:

1. Cài đặt công cụ trên môi trường Linux

Vẫn là môi trường làm việc quen thuộc như trong các bài viết trước. Nói là phải cài đặt nhưng thực tế thì cũng chẳng cần làm gì to tát. Đơn giản là vì những công cụ này đa phần là đã có sẵn trên các bản phân phối Linux, hoặc là bạn chỉ cần lên internet, download chúng về và sử dụng được ngay.

1.1. GNU Compiler Collection

GCC – hay đầy đủ hơn là GNU Compiler Collection là một hệ thống các trình biên dịch được tạo ra từ dự án GNU, hỗ trợ rất nhiều các ngôn ngữ lập trình. GCC là thành phần cốt lõi của GNU Toolchain, một tập hợp lớn các công cụ lập trình trong dự án GNU, đóng vai trò quan trọng trong phát triển Linux và phần mềm cho hệ thống nhúng. Đây cũng chính là bộ trình biên dịch tiêu chuẩn trên các hệ điều hành Unix-like. Ngoài ra, GCC cũng được port sang nhiều nền tảng khác (Cygwin hay MinGW trên Windows chẳng hạn).

Gần như hầu hết các bản phân phối Linux hiện này đều đã được cài đặt sẵn ít nhất 1 phiên bản của GCC. Để kiểm tra xem đã cài đặt GCC hay chưa, bạn có thể dùng lệnh

gcc --version

Nếu bạn đã sở hữu GCC trên máy tính, kết quả thu được có dạng như sau:

gcc-version

Trong hình trên, mình đang sử dụng GCC phiên bản 5.4.0. trên Ubuntu. Ngoài ra, để các bạn cũng nên cài đặt thêm 2 gói phần mềm build-essential và binutils. Đặc biệt, một số công cụ trong bin-utils sẽ hữu ích trong quá trình gỡ lỗi. Trên Ubuntu:

sudo apt install build-essential binutils

Tương tự, với gdb, ta cũng kiểm tra tương tự.

gdb --version

Dưới đây là kết quả ví dụ, mình đang sử dụng gdb 7.11.1:

gdb-version

1.2. ARM-None-EABI-GCC

Toolchain quen thuộc này mình đã có giới thiệu ở bài Tools #2 – Debug trên console. Đây là bộ công cụ được thiết kế dựa theo GCC dành cho kiến trúc ARM. Theo quy ước thì cách đặt tên của 1 toolchain có dạng Arch-[Vendor]-[OS]-ABI:

  • Arch: kiến trúc đích, ví dụ: arm, i686, x86_64 …
  • Vendor: phía nhà cung cấp toolchain, ví dụ: linaro, fsl …
  • OS: hệ điều hành đích mà chương trình triển khai, ví dụ: linux, freebsd …
  • ABI: Application Binary Interface, ví dụ: eabi, gnueabi …

Như vậy, ARM-None-EABI-GCC có nghĩa là bộ tập hợp trình biên dịch GNU cho kiến trúc ARM biên dịch với giao diện Embedded ABI (eabi), không có vendor (khuyết tên), và không triển khai trên hệ điều hành (none – hay bare metal).

Về các thức cài đặt ARM-None-EABI-GCC, các bạn xem tại mục 2.2 – GNU ARM Embedded Toolchain, bài viết Tools #2 – Debug trên console.

1.3. Eclipse

Nếu chỉ đơn giản là lập trình ứng dụng trên các máy tính thông thường, bạn có thể download bất kỳ phiên bản Eclipse nào mà bạn muốn. Các phiên bản mới của IDE này đã hỗ trợ các trình đơn cài đặt, rất thuận tiện kể cả khi sử dụng hệ điều hành Windows hay Linux. Với các phiên bản cũ hơn, các bạn sẽ download về tệp tin nén tương ứng, tiến hành giải nén và có thể cần thêm đường dẫn vào biến môi trường tùy theo mục đích sử dụng của bạn.

Vì mình có ý định sử dụng giao diện debug của Eclipse để minh họa phần gỡ lỗi trên vi điều khiển ARM Cortex-M3 (ARMv7-m), ta nên chọn phiên bản Mars 2. Các bạn có thể download ở đây. Mình sử dụng phiên bản này vì nó hoạt động khá ổn định với GNU MCU Eclipse plugins. Plugins này cung cấp khá nhiều thứ hay ho: hỗ trợ khởi tạo project C/C++ cho các vi điều khiển ARM Cortex-M (đặc biệt là các dòng sản phẩm ST), hỗ trợ thiết lập phiên debug với arm-none-eabi-gdb, hỗ trợ JLinkGDBServer, dự án OpenOCD (nếu bạn muốn sử dụng TI ICDI, STLink…). Cách tải và cài đặt plugins này được chia sẻ trên website của dự án, khá chi tiết nên mình sẽ không viết lại ở đây nữa.

2. Các thao tác cơ bản khi thực hiện gỡ lỗi

Chúng ta sẽ cùng xem qua một số thao tác cơ bản khi bạn thực hiện gỡ lỗi. Phần này, mình sẽ tiến hành minh họa trên cả giao diện lệnh và đồ họa, để các bạn thuận tiện trong quá trình theo dõi. Các thao tác chia theo nhóm, câu lệnh trong gdb và thao tác trên giao diện Eclipse của một chức năng tương đương. Để phần trình bày bớt phức tạp, mình sẽ chỉ thực hiện các thao tác diễn ra khi debug. Phần kết nối trong với máy chủ gỡ lỗi khi debug vi điều khiển, các bạn có thể xem lại trong bài Tools #2 – Debug trên console hoặc bài viết kế tiếp. Mình sẽ cố gắng sắp xếp thời gian ghi lại video phần demo.

Trước tiên, chúng ta cần phải làm quen với giao diện debug.

2.1. Giao diện tổng quát GDB và Eclipse Debug perspective

Chắc cũng chẳng cần mô tả các bạn cũng biết giao diện của gdb hoặc arm-none-eabi-gdb sẽ trông ra sao… nền đen chữ trắng, đơn giản mà hiệu quả.

gdb-start

Đơn giản, bạn chỉ cần gõ gdb vào Terminal và mọi thứ sẽ hiện lên giống như hình trên đây. Tương tự, bên phía Eclipse, bạn sẽ vào giao diện debug thông qua biểu tượng hình con bọ trên thanh điều khiển debug-icon hoặc nhấp phải chuột vào tên project đang thực hiện và truy cập thông qua lựa chọn Debug As trong trình đơn ngữ cảnh. Lưu ý, là bạn cần cấu hình trước. Phần cấu hình này mình sẽ trình bày ở bài sau, khi chạy minh họa.

Debug-perspective
Giao diện gỡ lỗi trên Eclipse

Nhìn qua, các bạn có thể thấy mọi thứ hơi rắc rối. Mình sẽ mô tả về các khu vực, các tab chúng ta thường xuyên thao tác và câu lệnh gdb thực hiện chức năng tương đương của tab đó trên giao diện lệnh.

2.1.1. Call Stack và thẻ Debug

Đầu tiên là call stack, hay còn gọi là execution stack, program stack, control stack, run-time stack hay machine stack. Nó lưu trữ thông tin về các lời gọi thủ tục/hàm đang hoạt động trong quá trình chạy ứng dụng. Call stack giúp lập trình viên biết thủ tục/hàm nào đang được gọi (nằm ở đỉnh ngăn xếp) và danh sách + thứ tứ các thủ tục/hàm đang chờ thực thi sau khi thủ tục/hàm hiện tại kết thúc. Mô hình của Call stack, đúng như cái tên của nó, được tổ chức dưới dạng ngăn xếp.

Một ví dụ, từ hàm main(), bạn gọi đến hàm A(), hàm A() đang thực thi lại gọi tới hàm B(), hàm B() đang thực thi lại gọi tới hàm C(). Như vậy, nếu trong quá trình debug, vị trí của con trỏ chương trình dừng lại ở đâu đó trong hàm C(), thứ tự của Call stack sẽ là C() – B() – A() – main(). Nghĩa là, C() đang được thực thi, là đỉnh của ngăn xếp. Sau khi C() kết thúc và trả kết quả/điều khiển, B() sẽ thực thi phần mã còn lại kể từ sau vị trí gọi C(). Tương tự, B() kết thúc sẽ trả điều khiển/kết quả để thực hiện tiếp A(). Và sau khi A() thực thi xong, con trỏ chương trình sẽ trở về hàm main(), ở vị trí kế tiếp lệnh gọi đến A().

Vẫn ví dụ ấy, nhưng sau khi C() thực hiện xong, trả kết quả/điều khiển, B() tiếp tục gọi tới D(). Lúc này, Call stack sẽ có thứ tự D() – B() – A() – main(). Hoặc nếu C() đang thực thi lại gọi E(), Call stack sẽ có thứ tự E() – C() – B() – A() – main(). Nên nhớ, Call stack là một khái niệm khác biệt so với ngăn xếp bộ nhớ vật lý (vùng nhớ stack trong bộ nhớ máy tính). Đừng nhầm lẫn 2 khái niệm này.

Call stack được hiển thị trong thẻ Debug trên giao diện gỡ lỗi của Eclipse. Như các bạn thấy trên hình:

debug-tab
Thẻ Debug

Trong hình có thể thấy, Call stack hiện tại trong chương trình gồm hai hàm nonInlineFunc() và main(). Đối với giao diện console, GDB cung cấp cho chúng ta lệnh backtrace (có thể viết tắt là bt):

backtrace-cmd
backtrace

Trên thẻ Debug, các bạn còn thấy thứ tự luồng mà các hàm đang hoạt động. Trong giao diện console, lệnh info threads tương đương với chức năng này:

threads-cmd
info threads

2.1.2. Thẻ Console

Thẻ Console sẽ hiển thị nội dung tùy theo ngữ cảnh được chọn trong thẻ Debug. Nếu bạn trỏ vào các dòng Thread hoặc các nhánh bên trong các cụm đó, bạn có thể sử dụng các thao tác trên thanh công cụ như Step, Over, Return, Resume, v.v… Lúc này, nội dung trong thẻ Console sẽ là kết quả thực thi chương trình của bạn, tính tới trước vị trí của con trỏ lệnh hiện tại.

Debug-Perspective3.png

Nếu bạn chọn qua dòng gdb, như hình bên dưới là gdb(7.11.1), tương ứng với phiên bản gdb đang cài đặt trên máy, nội dung trong thẻ Console sẽ thay đổi giống với giao diện lệnh khi làm việc trên Terminal.Ở ngữ cảnh này, các thao tác trên thanh công cụ sẽ không thể sử dụng được. Bạn chỉ có thể nhập các lệnh điều khiển vào đây và trên giao diện sẽ là kết quả của lệnh bạn vừa đưa vào.

Debug-perspective2

2.1.3. Các thẻ Source code

Mã nguồn trong từng tệp tin được hiển thị trong các thẻ với tên tương ứng, như trong giao diện lập trình. Tại đây, bạn có thể thấy mũi tên tượng trưng cho con trỏ lệnh, trỏ vào câu lệnh sẽ thực thi tiếp theo. Lệnh sẽ được thực hiện cũng được highlight để bạn dễ quan sát hơn. Trên giao diện lệnh bạn sử dụng list để có thể theo dõi vị trí và nội dung câu lệnh đang thực thi. Mặc định, sau khi gọi list, trên màn hình sẽ in ra 10 dòng mã, và vị trí của con trỏ lệnh sẽ ở dòng thứ 6

source-tab2.png
source code

Ngoài ra, các bạn thấy có các dấu tròn ở vị trí bên trái số thứ tự dòng code, cùng cột với mũi tên con trỏ lệnh. Đây là các chính breakpoint. Trên giao diện đồ họa, bạn có thể đặt breakpoint trực tiếp bằng cách click đúp vào số thứ tự dòng code mà bạn muốn chương trình sẽ dừng lại ở đó. Để gỡ bỏ breakpoint, bạn cũng sử dụng thao tác tương tự.

Để đặt breakpoint trên giao diện lệnh, bạn có thể sử dụng lệnh break (viết tắt là b). Nếu chỉ đơn giản là break, breakpoint sẽ được đặt tại vị trí hiện tại của con trỏ lệnh. Bạn có thể đặt breakpoint tại bất kỳ vị trí nào trong chương trình bằng cách sử dụng break kết hợp với thứ tự dòng code với mã C/C++ hoặc địa chỉ của lệnh assembly.

break-cmd
set breakpoint

Để xóa breakpoint, bạn có thể sử dụng lệnh delete breakpoints , trong đó là chỉ số breakpoint, ví dụ delete breakpoints 5.

2.1.4. Thẻ Disassembly

Thẻ này hiển thị nội dung mã assembly tương ứng của chương trình. Trong thẻ Disassembly, bạn có thể thực hiện các thao tác tương tự như trên các thẻ mã nguồn C/C++.

disassembly

Bạn có thể sử dụng lệnh disassemble trên giao diện lệnh nếu có nhu cầu xem hợp ngữ.

disassemble-cmd.png
disassemble

2.1.5. Các thẻ thông tin

Có 3 thẻ chúng ta sẽ quan tâm là Variables, Breakpoints và Registers. Thẻ Varialbes hiển thị thông tin các biến cục bộ của hàm đang thực thi trong ngữ cảnh hiện tại (hàm nằm ở đỉnh của Call stack). Thẻ Breakpoints chứa danh sách các breakpoint đang được đặt trong quá trình gỡ lỗi. Thẻ Registers chứa danh sách và nội dung các thanh ghi trên vi xử lý.

variables-tab breakpoint-tab registers-tab

Dĩ nhiên, vẫn còn một số thẻ khác, nhưng có lẽ chúng chưa quá quan trọng nên mình sẽ không trình bày ở đây. Bạn nào tò mò tìm hiểu nhớ ghi chép chia sẻ cho anh em bạn bè.

2.1.6. Memory Browser

Thẻ này cho phép bạn duyệt nội dung bộ nhớ. Bạn sẽ nhập vào địa chỉ bắt đầu của vùng nhớ muốn duyệt, thông tin của vùng nhớ sẽ xuất hiện ở dạng hexadecimal kèm địa chỉ.

memory-browser-tab

2.1.7. Chức năng trên thanh công cụ

Mình sẽ trình bày qua về các chức năng hay sử dụng trên thanh công cụ. Từ trái qua phải:

DebugToolbar

1. Skip all breakpoint: bỏ qua tất cả các breakpoint, tương đương disable breakpoints trên console.

2. Resume: thực thi chương trình cho tới khi gặp breakpoint tiếp theo, tương đương continue trên console.

3. Suspend: tạm dừng, thường dùng khi gặp một khối lệnh thực thi quá lâu hay vòng lặp, bạn muốn tạm dừng để kiểm tra giá trị biến, tương đương tổ hợp CTRL + C trên console.

4. Terminate: dùng khi bạn muốn kết thúc phiên debug, tương đương quit trên console.

5. Disconnect: dùng khi bạn muốn ngắt kết nối tới chương trình debug, tương đương disconnect trên console.

6. Step Into: thực thi câu lệnh kế tiếp, nếu lệnh đó là một hàm, đi tới nội dung bên trong hàm. Tương đương step trên console.

7. Step Over: thực thi qua câu lệnh kế tiếp, nếu lệnh đó là một hàm, thực thi xuyên suốt từ đầu tới cuối hàm. Tương đương next trên console.

8. Step Return: khi đang dừng lại trong một hàm con, thực thi đến khi thoát ra khỏi hàm và trả về kết quả. Tương đương return trên console.

9. Instruction Stepping Mode: nếu được chọn, các lệnh thao tác 6 và 7 được thực hiện trên mã assembly tương ứng thay cho hàm C/C++. Các lệnh stepi và nexti cho kết quả tương tư trên console.

2.2. Tổng hợp với câu lệnh trên GDB

Ở trên đây, chúng ta đã dạo qua một lượt giao diện và các thao tác tương ứng với giữa gdb và Eclipse Debug perspective. Phần này, mình sẽ hệ thống lại các câu lệnh gdb theo từng nhóm chức năng để dễ quản lý hơn.

2.2.1. Các lệnh về breakpoint

b [file]:[line] Đặt breakpoint tại dòng cụ thể trong một file mã nguồn.
b [file]:[func] Đặt breakpoint tại vị trí bắt đầu của hàm được chỉ định trong một file mã nguồn.
b *[address] Đặt breakpoint tại địa chỉ được chỉ định, dùng với hợp ngữ.
break Đặt breakpoint ở vị trí hiện tại.
info break Danh sách các breakpoint đang đặt trong chương trình
clear Xóa breakpoint ở lệnh kế tiếp
clear [file]:[line] Xóa breakpoint tại dòng cụ thể trong một file mã nguồn.
clear [file]:[func] Xóa breakpoint tại vị trí bắt đầu của hàm được chỉ định trong một file mã nguồn.
delete [n] Xóa breakpoint [tùy chọn n là thứ tự của breakpoint trong chương trình mà bạn muốn xóa]
enable [n] Kích hoạt breakpoint [tùy chọn n là thứ tự của breakpoint trong chương trình mà bạn chọn]
disable [n] Tạm hủy breakpoint [tùy chọn n là thứ tự của breakpoint trong chương trình mà bạn chọn]
ignore [n] [count] Bỏ qua breakpoint n một số count lần

2.2.2. Các lệnh điều khiển thực thi

continue Tiếp tục thực thi chương trình cho tới khi gặp một breakpoint bất kỳ
c
Ctrl + c Tạm dừng quá trình thực thi chương trình
step Đi tới dòng lệnh kế tiếp, hoặc đi vào lời gọi hàm (step into)
s
next Đi tới dòng lệnh kế tiếp, qua lời gọi hàm (step over)
n
stepi Đi tới lệnh máy kế tiếp
si
nexti Đi tới lệnh máy kế tiếp, nếu là lệnh gọi hàm, thực thi cho tới khi hàm đó kết thúc.
ni
until [location] Thực thi cho khi tới lệnh kế tiếp, hoặc tới vị trí location cụ thể
finish Thực thi tới cuối thủ tục/hàm cho tới trước khi trả stack (return)
return Thực thi tới cuối thủ tục/hàm, trả về giá trị và pop stack

2.2.3. Biến và thao tác với biến

set var = expr Gán giá trị cho biến trong chương trình C/C++
set $var = expr Gán giá trị cho biến trong GDB

2.2.4. Khởi chạy chương trình

run [arglist] Bắt đầu chương trình với danh sách đối số. Danh sách đối số có thể để trống
run Bắt đầu chương trình với danh sách đối số đã có trước đó
kill Hủy chương trình đang chạy
set args Tạo danh sách đối số rỗng
set args [arglist] Tạo danh sách đối số cụ thể cho lần chạy kế tiếp
show args Hiển thị danh sách đối số

Các bạn lưu ý, lệnh run có được thực thi hay không phụ thuộc vào môi trường gỡ lỗi đích. Nếu môi trường bạn đang làm việc không hỗ trợ, như ví dụ gỡ lỗi trên vi điều khiển với arm-none-eabi-gdb, sẽ có một thông báo xuất hiện với nội dung như sau:

The "remote" target does not support "run". Try "help target" or "continue".

2.2.5. Hiển thị thông tin

info source Tên file mã nguồn hiện tại
info sources Danh sách các tệp mã nguồn
list Hiển thị 10 dòng mã đang thực thi
list – Hiển thị 10 dòng mã đã thực thi ngay trước đó
display [/f] expr Tự động hiển thị giá trị của expr mỗi khi chương trình dừng lại
display Hiển thị tất cả giá trị có trong danh sách các expression
undisplay n Xóa vị trí n khỏi danh sách các expression
disable disp n Vô hiệu hóa expression thứ n
enable disp n Cho phép expression thứ n
info display Hiển thị danh sách các expression đang được cho phép
print [/f] [expr] In giá trị của expr theo định dạng f. f có thể là: – u: thập phân không dấu – d: thập phân có dấu – x: thập lục phân – o: octal – b: nhị phân – c: ký tự – f: dấu phẩy động
x [/Nuf ] expr Khảo sát bộ nhớ có địa chỉ expr. N là số phần tử được hiển thị. u là độ rộng phần tử nhớ: – b: 1 byte – h: 2 byte (halfword) – w: 4 byte (word) – g: 8 byte f là định dạng in ra, được trình bày trong lệnh print cộng thêm 2 tùy chọn sau: – s: xâu kết thúc bằng null – i: lệnh máy
backtrace Hiển thị các frame trong call stack
frame [n] Chọn qua frame thứ n trong call stack. Nếu không có n, hiển thị mã nguồn frame hiện tại.
info frame [addr] Thông tin về frame, hoặc frame tại địa chỉ addr
info reg [rn] Thông tin toàn tệp thanh ghi hoặc cụ thể thanh ghi có tên rn
info all-reg [rn] Thông tin toàn tệp thanh ghi hoặc cụ thể thanh ghi có tên rn, kể cả các thanh ghi dấu phẩy động

Trong đó, nếu sử dụng các lệnh print và set (ở mục 2.2.3) thao tác với biến hoặc con trỏ trong chương trình C/C++, xuất hiện một số ngữ cảnh tương đối giống với lập trình: có thể bạn muốn biết địa chỉ của biến hay giá trị nơi con trỏ trỏ tới. Lúc này, cú pháp vẫn giốn như khi bạn làm việc với ngôn ngữ C/C++:

  • Nếu muốn xem địa chỉ của biến var, sử dụng print &var.
  • Nếu muốn xem giá trị con trỏ ptr đang trỏ đến, sử dụng print *ptr.
  • Sử dụng set *(data-type *) addr = value, nếu bạn muốn gán giá trị cho một ô nhớ có địa chỉ cho trước. Cú pháp này rất hữu dụng, còn vì sao hữu dụng thì các bạn hãy đoán xem?

2.2.6. Thao tác file

file [file] Sử dụng tệp được chỉ định ở cả dạng symbol (chỉ dẫn gỡ lỗi) và executable (thực thi)
exec-file [file] Sử dụng tệp được chỉ định ở dạng executable (thực thi)
symbol-file [file] Sử dụng tệp được chỉ định ở dạng symbol (chỉ dẫngỡ lỗi)
load Chuyển debug symbol đến hệ thống debug từ xa thông qua quá trình tải về hoặc thiết lập liên kết động.

Lệnh load sẵn có hay không phụ thuộc vào cấu hình gỡ lỗi thiết lập trong gdb, thường thì xuất hiện nếu bạn debug chương trình trên một máy tính ở xa, hoặc trên một mạch debug bên ngoài như khi làm việc với vi điều khiển. Nếu bạn cố tình sử dụng load khi câu lệnh này không sẵn có, một thông báo như sau sẽ xuất hiện:

You can't do that when your target is …

Hãy lưu ý kiểm tra trước khi tiến hành viết script tự động.

2.2.7. Kết nối GDB Server và debug từ xa/cục bộ

Phần này thường gặp khi bạn phải debug ở xa nơi cài đặt chương trình hoặc debug trên board mạch bên ngoài.

Muốn debug từ xa, cần chắc chắn rằng máy chủ debug đã được thiết lập. Với GDBServer, bạn thực thi theo cú pháp:

gdbserver :port prog [arg...]

Trên JLinkGDBServer, mình đã trình bày ở bài Tools #2 – Debug trên console. Một số lệnh sẽ sử dụng trong quá trình debug từ xa.

target remote ip:port Kết nối đến gdbserver
disconnect Ngắt kết nối
monitor help Thông tin về các lệnh monitor sẵn có. Lệnh monitor không giống nhau giữa các hệ thống.

Nếu chỉ debug cục bộ (debug trực tiếp trên máy tính của bản thân), bạn chỉ cần gọi gdb kèm theo tệp tin chương trình thực thi mà bạn muốn debug là đủ, ví dụ:

gdb demo.exe

Trên đây là những thao tác theo mình là cần thiết khi gỡ lỗi với GDB trên giao diện lệnh. Nếu các bạn muốn có một trải nghiệm tốt hơn mà vẫn chỉ sử dụng GDB, hãy tìm hiểu về tùy chọn -tui (text ui) cùng 2 lệnh focus và layout trong chế độ này. Mình sẽ không trình bày các thao tác trên giao diện đồ họa, mà sẽ để dành phần này trong video demo. Nhìn chung, trên giao diện đồ họa, các thao tác này khá đơn giản và đã thiết kế, hỗ trợ đầy đủ.

3. Tùy chọn bổ sung khi biên dịch chương trình cho mục đích debug

Những ai đã và đang làm trong lĩnh vực phần mềm chắc chẳng lạ gì điều này. Nhưng đa số bạn sinh viên sẽ không để tâm đến nó. Các bạn có bao giờ tự hỏi, tại sao build project xong lại vào tệp thực thi ở thư mục Debug khi sử dụng VS, QT Creator, Android Studio hay Eclipse chưa? Hoặc, bạn có đặt ra thắc mắc tương tự khi nhìn thấy những hình ảnh dưới đây?

toolbar build configuration

qtcreator4 beta highres debug dialog.png

Nếu bạn chưa biết, cũng không có gì là lạ, vì chủ yếu khi mới học lập trình, các bạn thường chỉ quan tâm code biên dịch xong chạy được là được, còn chương trình sau khi build lưu tại đâu thì vào đó lấy ra thôi. Nhắc lại câu chuyện ở bài viết trước. Xét cho cùng, thói quen này hình thành là bởi chủ quan cá nhân thường chỉ chú trọng vào 1 vấn đề mà bạn thích, ở đây là viết mã, mà ít quan tâm (thậm chí là không thèm để tâm) đến những vấn đề kéo theo phía sau có tầm quan trọng không hề thua kém. Phải nhìn nhận rõ vấn đề, thầy cô, người hướng dẫn chắc chắn đã đề cập tới nó, trừ khi bạn là người tự học, song, thời gian trên lớp không đủ để có thể giải quyết mọi vấn đề. Vậy nếu có người đã chỉ ra cho bạn, mà bạn vẫn mắc sai lầm, lỗi đó là ở bạn.

Thôi, gạt qua một bên câu chuyên muôn thuở ở trên, quay lại vấn đề chính. Nếu đang sử dụng một IDE bất kỳ, chắc chắn bạn đã từng dạo qua một của sổ tùy chọn có tên Build Configurations hay Project Configurations. Đây chính là nơi bạn đưa ra quyết định cho mục đích sử dụng của tệp tin chương trình sau biên dịch. Mục đích ở đây không phải là hướng dẫn cách cài đặt hay sử dụng. Mục đích ấy có liên quan đến nghiệp vụ của một lập trình viên, cụ thể là vấn để quản lý phiên bản. Dành cho những bạn chưa biết, một người lập trình khi tham gia phát triển dự án chắc chắn sẽ rơi vào 2 hoàn cảnh sau. Thứ nhất, quá trình phát triển đang diễn ra, hoạt động kiểm thử tích cực, suốt ngày lo toan tìm lỗi, fix bug. Thứ hai, hoạt động kiểm thử đã đi vào giai đoạn kết thúc, quá trình chuyển giao cho khách hàng bắt đầu.

Ở hoàn cảnh đầu tiên, chúng ta hay gặp nó ở các pha đầu trong vòng đời phát triển sản phẩm, lỗi còn nhiều, fix còn dài. Lập trình viên lúc này sẽ chọn cấu hình debug để build chương trình với khả năng gỡ lỗi thuận tiện hết mức có thể. Lúc đó, cấu hình debug sẽ giúp chèn các dấu hiệu chỉ thị vào chương trình. Các dấu hiệu này giúp debugger nắm bắt được tốt hơn các hoạt động của phần mềm, nhưng cũng làm gia tăng kích thước của tệp tinh sản phẩm.

Hoàn cảnh sau đó là khi đã hoàn thiện, chương trình sẽ được chuyển giao cho khách hàng. Mọi người sẽ muốn tối thiểu hoá kích thước của chương trình. Lập trình viên sẽ thiết lập tùy chọn release, chương trình sẽ được build mà không kèm theo (hoặc hạn chế) các dấu hiệu chỉ thị. Bớt chúng đi, kích thước mã nguồn sẽ giảm kha khá. Hãy so sánh 2 hình ảnh dưới đây:

DebugOpts ReleaseOpts

Bạn có thể thấy rõ sự khác biệt. Cùng một mã nguồn, trong tệp cấu hình của ảnh phía bên trái, dòng CXXFLAGS : -g -ggdb -gdwarf-2 –coverage -pg là các tùy chọn thêm của g++ phục vụ mục đích gỡ lỗi và kiểm thử khi macro DEBUG = 1. Chương trình build hoàn tất với kích thước 36.5 kB. Ảnh bên tay phải, DEBUG = 0, trong CXXFLAGS không có các tùy chọn debug và chương trình build hoàn tất với kích thước 8.8 kB.

Để hỗ trợ cho các hoạt động này, đây là lý do vì sao các IDE thường có 2 thiết lập cấu hình sẵn là debug và release, cũng như chia thư mục đầu ra theo 2 cấu hình này để quản lý tốt hơn.

Nếu bạn không sử dụng IDE, hoặc IDE đó không hỗ trợ sẵn các cấu hình build, bạn có thể tự xây dựng cấu hình build cho riêng mình. Lưu ý, cấu hình build sẽ không phụ thuộc vào IDE mà phụ thuộc vào compiler mà IDE đang sử dụng. Ví dụ đơn giản: trên Linux, Eclipse và QT Creator là 2 IDE khác nhau, nhưng theo cài đặt mặc định thì chúng đều sử dụng GCC để biên dịch chương trình C/C++. Vậy thì cấu hình build ở đây sẽ phải tuân theo các tùy chọn mà GCC cung cấp cho ngôn ngữ lập trình được sử dụng để viết mã nguồn. Hiện tại, để phục vụ mục đích học tập cũng lên blog lảm nhảm, mình thường sử dụng 2 bộ công cụ biên dịch là GCC và CLang. Các tùy chọn tối ưu cho gỡ lỗi của GCC, các bạn tham khảo chi tiết tại đây. Đối với CLang, chi tiết tại đây.

Các bạn cũng cần lưu ý, khi sử dụng các tùy chọn debug trên trình biên dịch, đồng thời cũng nên tắt các tùy chọn tối ưu mã nguồn (Optimization Options). Điều này sẽ giảm kích thước tệp thực thi. Nghe hơi ngược đời, đáng lẽ là tăng mức tối ưu hóa thì kích thước phải giảm chứ? Cái đó đúng nếu bạn sử dụng khi release. Nhưng nếu đã cấu hình debug, việc tối ưu càng nhiều sẽ làm cho quá trình chèn symbol phức tạp hơn. Trong khi tối ưu không làm giảm nhiều dung lượng, việc chèn nhiều symbol hơn sau khi tối ưu sẽ làm kích thước chương trình tăng lên. Với GCC các bạn đưa vào tùy chọn -O0, hoặc nếu vẫn muốn tối ưu bạn có thể chọn -Og. Trên CLang, các bạn vẫn có thể sử dụng -O0, song không có tùy chọn -Og.

4. Tóm lược

Hi vọng phần trình bày ở trên đã giúp các bạn nắm được phần nào cách thức sử dụng GDB cũng như làm quen với giao diện gỡ lỗi trên Eclipse. Dù chọn gỡ lỗi trên giao diện đồ họa hay dòng lệnh, mục tiêu cuối cùng của chúng ta vẫn là giúp sản phẩm cuối cùng trở nên hoàn thiện, trải nghiệm tốt hơn và ít lỗi hơn. Cũng cần phải lưu ý một vài vẫn đề liên quan đến cấu hình biên dịch để tăng cường trải nghiệm gỡ lỗi. Và cuối cùng, dù cho giao diện đồ họa rất trực quan, dễ thao tác, nhưng sức mạnh của GDB và text ui nằm ở khả năng tự động hóa và tái sử dụng kịch bản kiểm tra. Hãy tận dụng tốt thế mạnh của cả hai.

Tạm biệt.

Chia sẻ:

  • Tweet
Thích Đang tải...

Từ khóa » Debug Bằng Gdb