Debugging là gì: quy trình, kỹ thuật và công cụ tìm lỗi

Debugging là gì: quy trình, kỹ thuật và công cụ tìm lỗi phần mềm

Debugging là quá trình phát hiện, cô lập và khắc phục lỗi trong phần mềm. Từ khi Grace Hopper ghi lại dòng “First actual case of a bug being found” sau khi tìm thấy một con nhiễu mắc trong relay của máy Mark II, từ “bug” đã trở thành thuật ngữ chỉ bất kỳ lỗi nào trong mã nguồn. Ngày nay debugging không dừng lại ở việc đọc nhật ký lỗi, mà là cả một chuỗi kỹ thuật gồm: tái hiện lỗi, thu hẹp đầu vào, đặt breakpoint, dò theo call stack, sửa mã rồi viết kiểm thử phòng ngừa tái phát.

Bài viết này đi sâu vào quy trình chuẩn và những công cụ mà lập trình viên dùng hằng ngày, dẫn chứng từ tài liệu Python, GDB, Mozilla và Node.js.

Quy trình debug truyền thống

Trước khi đụng vào công cụ, lập trình viên thường làm ba việc: tái hiện lỗi, thu gọn đầu vào tối đa, rồi mới chạy debugger. Theo trang Wikipedia về debugging, bước thu gọn đầu vào đặc biệt quan trọng. Nếu lỗi chỉ xuất hiện với một tệp vài nghìn dòng, chạy debugger trên toàn bộ tệp sẽ lãng thời gian và làm nhiễu tín hiệu. Khi rút đầu vào xuống còn vài dòng vẫn tái hiện được lỗi, khu vực cần tập trung gần như lộ ra.

Khả năng tái hiện lỗi là nền tảng của mọi việc còn lại. Một lỗi không tái hiện được gần như xác suất 1 thì mọi bản vá đều là may mắn trơ trọn. Vì vậy nhiều đội phát triển coi việc viết một trường hợp tái hiện tự động là yêu cầu bắt buộc trước khi bắt đầu sửa, và bắt buộc chạy lại toàn bộ bộ kiểm thử sau mỗi lần sửa để chắc rằng lỗi mới không lọt vào bản phát hành.

Breakpoint: dừng chương trình đúng chỗ cần xem

Breakpoint là điểm trong mã nguồn mà khi luồng thực thi đi vào đó, debugger sẽ dừng lại và đưa ra dấu nhắc tương tác để kiểm tra biến, xem call stack hoặc sửa giá trị ngay tại chỗ đứng. Hầu hết debugger hiện đại đều hỗ trợ điều kiện dừng, giới hạn số lần dừng, và logpoint ghi giá trị mà không ngắt luồng.

Trong Python debugger (pdb), các lệnh nền tảng gồm:

  • s(tep) dừng ở dòng kế tiếp có thể, kể cả bên trong hàm được gọi.
  • n(ext) chạy tới dòng kế tiếp trong hàm hiện tại, hàm con chạy gần hết tốc độ.
  • c(ont(inue)) chạy tiếp cho tới khi gặp breakpoint kế tiếp hoặc chương trình kết thúc.
  • b(reak) đặt breakpoint theo tên tệp kèm số dòng, theo tên hàm, hoặc kèm điều kiện.
  • tbreak đặt breakpoint tạm thời, tự xoá ngay sau lần dừng đầu tiên.

Điểm tiện lợi nhất của pdb là có thể nhúng trực tiếp vào mã nguồn bằng import pdb; pdb.set_trace(), hoặc từ Python 3.7 chỉ cần gọi breakpoint(). Không cần sửa cấu hình chạy nào, trình thông dịch sẽ tự chuyển sang chế độ debugger khi gặp dòng đó.

Trên Linux, trang hướng dẫn GDB cho thấy các lệnh tương ứng: break đặt điểm dừng theo tệp, dòng hoặc tên hàm; next qua lời gọi hàm mà không đi vào bên trong; step đi vào bên trong hàm; c chạy tiếp; bt in call stack; và print hiện giá trị một biểu thức. GDB cũng nhận điều kiện dừng bằng break … if bieuc_thuc.

hình chụp màn hình NetBeans IDE đang dừng tại breakpoint, hiển thị biến cục bộ và call stack khi gỡ lỗi chương trình Java

Trong trình soạn thảo, breakpoint thường hiện dưới dạng vòng tròn đỏ nằm ở lề số dòng, cạnh với bảng điều khiển chứa các biến cục bộ và khung gọi hàm. Chính bảng điều khiển đó cho phép so sánh giá trị của một biến qua nhiều vòng lặp, nhanh hơn nhiều so với việc chèn câu lệnh in thủ công.

Các kiểu breakpoint nâng cao

Ngoài điểm dừng đơn giản, các môi trường phát triển còn hỗ trợ nhiều biến thể để bám sát từng tình huống:

  • Function breakpoint dừng mỗi khi hàm được gọi, bất kể gọi từ đâu. Trong Visual Studio Code, tạo bằng nút cộng trong mục BREAKPOINTS rồi nhập tên hàm.
  • Data breakpoint hay watchpoint dừng khi một biến bị đọc, ghi hoặc đổi giá trị. Đây là công cụ đáng giá nhất khi truy tìm lỗi ghi đè ngoài dự kiến.
  • Logpoint ghi một dòng thông báo ra bảng điều khiển mà không dừng luồng thực thi. Hợp với các vòng lặp chạy hàng nghìn lần mà chỉ cần ghi vài dòng đáng chú ý.
  • Breakpoint có điều kiện chỉ dừng khi biểu thức trả về đúng, ví dụ chỉ dừng khi bộ đếm chạm tới một giá trị cụ thể.

Mô tả này lấy từ tài liệu chính thức: hướng dẫn debugging của Visual Studio Code nêu rõ function breakpoint, data breakpoint và logpoint, trong đó logpoint ghi ra Debug Console mà không dừng thực thi. Ở phía trình duyệt, tài liệu Mozilla mô tả breakpoint có điều kiện được tạo bằng tổ hợp phím tương ứng và hiển thị khác màu so với điểm dừng thường.

Call stack: bản đồ đường đi của lỗi

Khi một ngoại lệ thoát ra, phần lớn ngôn ngữ in ra call stack, tức danh sách các hàm đã được gọi từ điểm bắt đầu tới chỗ lỗi xảy ra. Đọc call stack giúp nhảy thẳng tới nơi có khả năng chứa lỗi thay vì lần theo từng dòng từ đầu chương trình.

Trong pdb, lệnh where hoặc bt in ra chuỗi khung gọi hiện tại. GDB có bt với vai trò tương tự. Từ call stack, lập trình viên thường nhận ra ngay lỗi đến từ đọc tệp sai đường dẫn, từ hàm của thư viện bên thứ ba, hay từ một phép chia cho số không.

Khi lỗi không tái hiện: profiler và công cụ khác

Nếu chương trình chạy đúng hết nhưng chậm bất thường, đó không còn là debugging theo nghĩa dừng ở lỗi sai nữa mà là trường hợp của phân tích hiệu năng. Công cụ profiler đo thời gian thực thi của từng hàm rồi hiển thị dưới dạng biểu đồ lửa, trong đó bề rộng mỗi khối tương ứng thời gian đã dùng. Nhìn vào đó, hàm chiếm tỷ lệ lớn bất thường gần như luôn là thủ phạm.

ảnh chụp biểu đồ lửa flame graph hiển thị thời gian thực thi của từng hàm khi phân tích hiệu năng phần mềm

Ảnh trên là một flame graph thu được từ dự án MediaWiki. Từng khối là một hàm, bề rộng thể hiện tỷ lệ thời gian, và màu sắc phân biệt các giai đoạn thực thi. Loại biểu đồ này giúp tìm ra nút thắt hiệu năng nhanh hơn nhiều so với việc đo thủ công từng đoạn mã.

Ngoài profiler, các công cụ khác cũng hỗ trợ gỡ lỗi theo hướng khác. Trình dò lỗi tĩnh kiểm tra mã nguồn mà không cần chạy chương trình, phát hiện biến dùng trước khi gán, so sánh kiểu dữ liệu sai hoặc tài nguyên không được giải phóng. Trình phân tích bộ nhớ theo dõi phần còn lại của các lần cấp phát, từ đó chỉ ra chỗ rò rỉ. Còn công cụ kiểm tra tự động xác nhận lỗi cũ không tái phát sau mỗi lần sửa.

Debugger cho từng ngôn ngữ

Debugging không giới hạn trong IDE. Mỗi hệ sinh thái có bộ công cụ riêng, và biết công cụ nào dành cho ngôn ngữ nào giúp tiết kiệm hàng giờ mỗi lần gặp lỗi:

  • Python có pdb tích hợp sẵn, cùng ipdb và giao diện toàn màn hình pudb.
  • JavaScript và Node.js dùng Chrome DevTools, cộng với cờ --inspect để bật giao thức kiểm tra. Theo hướng dẫn chính thức của Node.js, tiến trình lắng nghe mặc định trên cổng 9229 và mỗi tiến trình có một mã định danh riêng mà công cụ phải biết để nối vào.
  • C và C++ dùng GDB trên Linux, LLDB trong bộ công cụ Clang, và trình gỡ lỗi tích hợp của Visual Studio trên Windows.
  • Java dùng giao thức JDWP, thường thông qua trình gỡ lỗi có sẵn trong Eclipse hay IntelliJ.

Có một lưu ý áp dụng cho mọi công cụ: debugger làm chậm chương trình vì phải chèn thêm mã theo dõi trạng thái. Vì vậy bước dừng theo từng câu lệnh chỉ hữu ích trong môi trường phát triển hoặc khi chạy bộ kiểm thử, không dùng trên bản phát hành thật.

Kinh nghiệm thực tế khi gỡ lỗi

Nhiều lỗi khó không nằm ở logic mà ở chỗ dữ liệu vượt khỏi giả định ban đầu: danh sách rỗng, chuỗi rỗng, số phân giải bằng không, ký tự đa byte làm lệch độ dài chuỗi. Thêm tiền điều kiện kiểm tra ở biên của hàm, kèm thông báo lỗi nói rõ giá trị nhận vào, thường tiết kiệm thời gian hơn nhiều so với lần theo từng vòng lặp.

Một thói quen đáng giá là sau mỗi lần sửa xong, viết thêm một kiểm thử tái hiện đúng lỗi đó. Kiểm thử đó sẽ bảo vệ bạn khỏi việc vá nhầm chỗ rồi tái phát ở một module khác, và trở thành tài liệu sống về nguyên nhân gốc của lỗi.

Kết luận

Debugging là quy trình có quy tắc chứ không phải trò mò: tái hiện lỗi, thu gọn đầu vào, chọn công cụ phù hợp, dừng đúng chỗ, sửa mã, rồi viết kiểm thử. Ba trụ cột là breakpoint để dừng đúng nơi, call stack để biết mình đến từ đâu, và bộ công cụ riêng cho từng ngôn ngữ. Nắm rõ từng thành phần giúp thời gian tìm lỗi rút ngắn từ hàng giờ xuống vài phút, đồng thời tạo ra phần mềm ổn định hơn ngay từ những lần chạy đầu tiên.

Nếu muốn thử ngay, hãy chọn một công cụ quen thuộc: trong Python, gõ breakpoint() trước dòng đáng ngờ rồi chạy lại tập lệnh; trên Linux, chạy gdb ./ten_chuong_trinh rồi đặt break main; trong Node.js, bật node --inspect và nối bằng Chrome DevTools. Mỗi công cụ đều có tài liệu chi tiết và cộng đồng lớn, nên bạn không phải tự mò một mình khi gặp khó khăn.

Tôi là một lập trình viên IOS. Code chính là IOS nhưng thỉnnh thoảng vẫn đá sang Android hoặc web. Mặc dù không quá thông thạo nhưng tôi sẽ chia sẻ những kiến thức mà mình đã tìm hiểu, áp dụng qua.

Bài viết liên quan

Terraform là gì: công cụ Infrastructure as Code của HashiCorp

Terraform là gì: công cụ Infrastructure as Code của HashiCorp Terraform là một công cụ Infrastructure as Code (IaC) do HashiCorp phát triển, cho phép người dùng khai báo và…

Xem thêm

Fortran là gì: ngôn ngữ lập trình cho tính toán khoa học

Fortran là gì: ngôn ngữ lập trình cho tính toán khoa học Fortran là gì? Đây là ngôn ngữ lập trình cao cấp được thiết kế riêng cho các bài…

Xem thêm

Đệ quy là gì: cách lập trình bằng cách gọi lại chính hàm

Đệ quy là gì: cách lập trình bằng cách gọi lại chính hàm Đệ quy (recursion) là kỹ thuật lập trình trong đó một hàm tự gọi lại chính nó…

Xem thêm
0 0 đánh giá
Article Rating
Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất