Kỹ thuật debug trên Visual Studio

    Debug là một phần không thể thiếu trong phát triển phần mềm, cũng giống như thịt chó không thể thiếu mắm tôm, đi ị thì không thể không đái vậy. Và có nhiều khi thời gian chúng ta ngồi debug còn nhiều hơn thời gian code :). Debug là một công việc thú vị, ảo diệu nhưng đầy thách thức, đôi khi rất khó hiểu, gây cho chúng ta sự bực bội, điên tiết và chửi thề như chí phèo (kiểu như “cái đkm, đéo hiểu, ảo vc, cái nồi gì vậy…etc”). Chính vì vậy nên nếu chúng ta muốn giảm thời gian debug, giảm thiếu sự ức chế do debug gây ra thì chúng ta phải có kỹ năng, phải có phương pháp khi debug.

Trong phạm vi bài viết này Cpp•Developer sẽ chia sẻ với anh em một số kỹ năng debug trên Visual Studio.

1. Sử dụng Call Stack

Call Stack dùng để xem các lời gọi hàm hoặc thủ tục hiện có trong Stack. Để mở cửa sổ Call Stack thì khi đang debug, chọn menu Debug → Windows → Call Stack (xem hình dưới)

Mở của sổ Call Stack

Cửa sổ sẽ Call Stack hiển thị tên của từng hàm kèm theo danh sách tham số, dòng code đang chạy, tên của ngôn ngữ lập trình…

Thông tin trên Call Stack

Mũi tên màu vàng chỉ ra Stack Frame nơi con trỏ thực thi đang nằm. Theo mặc định thì các thông tin liên quan của Frame này sẽ hiển thị trong các cửa sổ DisassemblyLocalsWatch, và Autos.

2. Xem giá trị biến, biểu thức nhanh bằng chuột

Khi debug chúng ta luôn luôn phải chạy vào các hàm khả nghi để tìm hiểu xem cái quái gì đang xảy ra, lướt qua Call Stack để xem một số giá trị mờ ám bắt nguồn từ đâu … Những lúc như vậy, việc thêm biến/biểu thức vào danh sách theo dõi (watch) hoặc xem qua danh sách locals có thể mất khá nhiều thời gian. Rất may là Visual Studio có tính năng hỗ trợ để mọi thứ trở nên dễ dàng hơn. Nếu bạn chỏ chuột vào một biến mà bạn quan tâm thì giá trị của biến đó hoặc giá trị của tất cả các trường của nó (đối với class/struct) sẽ được show ra giúp bạn có thể kiểm tra một cách nhanh chóng và thuận tiện.

Di chuột vào biến testStruct để xem giá trị các trường của nó

3. Thay đổi giá trị biến trực tiếp khi đang chạy (on-the-fly)

Debugger (công cụ debug) không phải chỉ dùng để điều tra và tìm lỗi khi đã có lỗi xảy ra rồi. Nhiều lỗi có thể được ngăn chặn ngay từ lúc code bằng cách chạy thử qua hàm mới được viết và kiểm tra xem nó có hoạt động như mong đợi hay không. Đôi khi, bạn muốn biết hàm có chạy đúng nếu một điều kiện nào đó là “true” hoặc “false” hay không ? Hoặc hàm có chạy đúng nếu một biến nào đó nhận một giá trị cụ thể nào đó hay không ?

Hầu hết những trường hợp như vậy bạn có thể test được luôn trong lúc debug mà không cần sửa code và chạy lại. Chỉ cần di chuột vào biến, nhấp đúp vào giá trị, nhập vào giá trị mà bạn muốn, enter và sau đó tiếp tục debug.

4. Chạy dòng lệnh được chỉ định

Một trường hợp thường hay gặp khi debug là phân tích lý do tại sao một hàm lại trả về lỗi bằng cách chạy các hàm từng step một. Và thật là thốn nếu chúng ta phát hiện ra rằng một hàm vừa call một hàm khác và hàm đó là hàm trả về lỗi ?

Khi ta vừa phát hiện ra hàm functionA trả về false nhưng lại lỡ chạy qua hàm đó mất rồi.

Thốn vãi ! Chả nhẽ lại chạy lại và debug lại để phi vào cái hàm vừa trả về lỗi xem nó làm cái lồi gì mà lỗi ?

Vâng, rất may là Visual Studio thấu hiểu anh em chúng ta và cung cấp cho chúng ta phương pháp dễ chịu hơn rất nhiều. Chúng ta chỉ cần kéo mũi tên màu vàng vào dòng code mà mình muốn chạy (kéo luôn vào cái dòng gọi cái hàm ngu ngu vừa trả về lỗi chứ còn gì nữa), sau đó F11 để phi vào hàm đó mà check. Nuột phải không anh em ?

Kéo mũi tên màu vàng vào dòng gọi hàm functionA và F11 để debug vào trong hàm

5. Sửa code và build không cần restart lại debug

Khi đang debug một chương trình phức tạp, bug khù khoằm ? Chúng ta đã tìm ra nguyên nhân ở đâu, nhưng không muốn mất thời gian stop chương trình, build lại và chạy lại (vì để tái hiện lại hiện trường là khá phức tạp).

Không sao, chỉ cần fix bug tại chỗ và tiếp tục debug. Visual Studio sẽ build chương trình, áp dụng code mới và tiếp tục debug mà không cần phải khởi động lại.

Tuy nhiên để làm việc này thì cần một số điều kiện sau:

  • Thứ nhất, chương trình phải được cấu hình như sau:

Chuột phải vào project trên Solution Explorer → Properties (hoặc bấm Alt + Enter)

Tiếp theo, chọn C/C++ → General → Debug Information Format, chọn “Program Database for Edit And Continue (/ZI)”

  • Thứ hai, các thay đổi phải là local bên trên trong hàm. Nếu bạn thay đổi khai báo hàm, thêm các phương thức hoặc class mới, bạn sẽ phải khởi động lại chương trình, build lại để apply những thay đổi mới sau đó debug lại.

6. Sử dụng Watch Window

Watch Window là cửa sổ sử dụng để theo dõi sự thay đổi giá trị của các biến (local hay global đều được hết). Để mở Watch Window có 2 cách

  • Cách 1: Bấm tổ hợp phím Ctrl + D + W và sau đó bấm một số nằm trong dải từ 1 đến 4 (vì có tối đa 4 của sổ Watch)
  • Cách 2: chọn menu Debug → Windows → Watch → Watch 1 hoặc 2, 3, 4 (xem hình dưới)

→ Watch Window sẽ hiện ra như hình dưới

Để theo dõi một biến ở Watch Window bạn có 2 cách

  • Cách 1: Click chuột phải vào biến trên editor → chọn “Add Watch”
  • Cách 2: Nhập tên biến vào cột “Name” trên Watch Window

Ngoài ra, thông tin có thể xem được từ Watch Window không giới hạn ở các biến bình thường. Bạn có thể nhập tên của một số biến đặc biệt để theo dõi, ví dụ:

  • esp: giá trị hiện tại của stack pointer
  • err: mã lỗi của hàm cuối cùng được gọi

7. Sử dụng Threads Window

Threads Window khá hữu ích khi debug các ứng dụng đa luồng (multi-threading). Nó cho chúng ta thông tin về các Threads đang chạy, điểm Breakpoint đang dừng đang được thực thi trên Thread nào (đánh dấu bằng mũi tên màu vàng)

Để mở Threads Window có 2 cách

  • Cách 1: Bấm tổ hợp phím Ctrl + Alt + H
  • Cách 2: Chọn menu Debug → Windows → Threads (xem hình dưới)

→ Threads Window sẽ hiện ra như hình dưới

8. Conditional Breakpoints

Nếu bạn muốn dừng chương trình tại một line code nào đó nhưng bạn không muốn cứ chạy qua đấy là dừng mà cần phải có điều kiện nào đó mới dừng thì bạn có thể làm điều đó bằng cách tạo Conditional Breakpoint.

Giả sử có vòng lặp đơn giản như sau, i chạy từ 0 đến 99

Nếu ta muốn trình debug dừng lại tại dòng printf(“i = %d\n”, i); chỉ khi nào i đang có giá trị là 5, ta làm như sau:

Đặt Breakpoint tại dòng đó → click chuột phải vào breakpoint (màu đỏ) → chon Conditions…

Nhập “i == 5” như hình dưới

Chạy debug và chương trình sẽ dừng tại Breakpoint khi có giá trị 5

9. Sử dụng Memory Window

Có những bug liên quan đến căn chỉnh bộ nhớ, buffer overflow đòi hỏi chúng ta phải theo dõi giá trị của memory một cách chi tiết và sát sao mới có thể phát hiện ra. Trong những tình huống như vậy chúng ta sẽ cần đến Memory Window.

Để mở Memory Window có 2 cách

  • Cách 1: Bấm tổ hợp phím Ctrl + Alt + M → sau đó bấm một số nằm trong dải từ 1 đến 4 (Visual Studio hỗ trợ tối đa 4 Memory Windows)
  • Cách 2: Chọn menu Debug → Windows → Memory → Memory 1 hoặc 2, 3, 4 (xem hình dưới)

→ Memory Window sẽ hiện ra như hình dưới

Nếu muốn vùng nhớ nào đó hiển thị lên trên cùng của Memory Window cho tiện theo dõi thì có thể nhập địa chỉ của vùng nhớ đó vào ô “Address”, xem hình dưới

10. Data Breakpoints

Trong các chương trình phức tạp, đôi khi chúng ta gặp trường hợp giá trị của một biến hay một vùng nhớ nào đó bị thay đổi nhưng có quá nhiều đoạn code truy cập đến thay đổi biến/vùng nhớ đó. Chúng ta sẽ rất khó khăn để debug theo cách thông thường vì méo biết đặt debug vào đâu.

Trong những trường hợp như vậy thì Data Breakpoints là sự lựa chọn hoàn hảo. Mục đích của Data Breakpoints là làm cho trình debug dừng lại khi một vùng nhớ tại một địa chỉ cụ thể (do chúng ta chỉ định) có sự thay đổi.

Ví dụ có chương trình sau →

Có một biến toàn cục gTest kiểu int (4 bytes). Có 2 hàm cùng thay đổi giá trị của gTest là functionA và functionB, 2 hàm này được chạy trên 2 threads khác nhau. Bây giờ tôi muốn chương trình sẽ tạm dừng khi gTest bị thay đổi, tôi sẽ làm như sau

  • Bước 1: Để tổng quát, tôi sẽ không đặt Breakpoint vào cụ thể vào functionA hay functionB mà đặt debug vào điểm entry point của chương trình → dòng bắt đầu hàm main – line 18 → F5 để chạy debug chương trình

  • Bước 2: Khi chương trình dừng tại điểm bắt đầu hàm main, việc cần làm bây giờ là lấy địa chỉ của biến gTest. Để lấy địa chỉ của gTest thì có thể dùng Watch Window (gõ vào &gTest)nhưng tiện thể ở đây hướng dẫn anh em một cách khác, là dùng Immediate Window. Cách mở Immediate Window thì cũng đơn giản như Watch Window, Threads Window,… anh em có thể tự triển được rồi. Khi đã mở được Immediate Window thì gõ vào &gTest (xem hình dưới)

  • Bước 3: Tạo Data Breakpoint, chọn Debug → New Breakpoint → Data Breakpoint…

→ Paste địa chỉ của gTest vào ô Address, chọn số byte của vùng nhớ (ở đây là 4) → OK

  • Bước 4: F5 để tiếp tục debug chương trình. Khi gTest bị thay đổi giá trị chương trình sẽ tạm dừng và Visual Studio sẽ thông báo như hình dưới

→ Bấm OK, vào phi vào Call Stack xem hàm nào thay đổi giá trị của gTest

→ Nhìn vào Call Stack thì thấy hàm functionA chính là thủ phạm. Việc bây giờ khá đơn giản, phi vào functionA xem nó đang chạy ở dòng nào.

Đây chỉ là một ví dụ đơn giản để demo cách sử dụng Data Breakpoint. Các bạn hãy áp dụng linh hoạt vào những trường hợp cụ thể mà mình gặp phải, sẽ rất hữu ích đấy.

— Author: Phạm Minh Tuấn —

Solution Architect tại FPT Software