Gỡ Lỗi Thiết Lập Điểm Dừng Trong Mã Lua - nói dối e blog

Gỡ Lỗi Thiết Lập Điểm Dừng Trong Mã Lua

Lua 5.1 đi kèm với một thư viện debug mạnh mẽ, xuất khẩu toàn bộ các API liên quan đến gỡ lỗi từ C API. Khi sử dụng Lua như một ngôn ngữ độc lập, những công cụ này hoàn toàn đủ để xây dựng một hệ thống gỡ lỗi tiện lợi.

Khi nói đến phương pháp gỡ lỗi phổ biến nhất - thiết lập điểm dừng (breakpoint), chúng ta thường nghĩ đến việc sử dụng hook trong thư viện debug của Lua. Cách tiếp cận cổ điển là tạo một bảng ghi nhớ các vị trí điểm dừng, kích hoạt hook theo từng dòng mã, và kiểm tra xem dòng hiện tại có phải là điểm dừng hay không. Nếu đúng, chương trình sẽ tạm dừng để chờ tương tác gỡ lỗi.

Phương pháp này tuy hiệu quả nhưng lại tiêu tốn nhiều tài nguyên CPU. Vì mỗi khi chuyển sang dòng mã mới, hệ thống phải gọi lại một hàm xử lý. Đặc biệt khi hàm này được viết bằng Lua, hiệu suất sẽ càng giảm sút.

Bài viết này xin giới thiệu một cách tiếp cận khác: thiết lập điểm dừng cứng (hard breakpoint) trực tiếp trong mã nguồn. Phương pháp này đảm bảo hiệu suất chạy mã không bị ảnh hưởng khi không có điểm dừng nào được kích hoạt.

Tương tự như cách gỡ lỗi trong ngôn ngữ C, các công cụ debug thường chèn lệnh ngắt phần cứng (như int 3 trên x86) vào mã máy để tạm dừng thực thi. Khi cần tiếp tục chạy, chỉ cần thay thế lại mã gốc.

Mặc dù máy ảo Lua không hỗ trợ lệnh ngắt gỡ lỗi và cũng không có cơ chế chèn mã vào bytecode, nhưng lợi thế của ngôn ngữ thông dịch là có thể chỉnh sửa mã nguồn trực tiếp mà không cần biên dịch lại. Do đó, việc chèn các lệnh gỡ lỗi vào mã nguồn trở nên đơn giản. Sau khi hoàn tất gỡ lỗi, ta chỉ cần xóa các đoạn mã này đi.

Mình đã xây dựng một thư viện gỡ lỗi đơn giản theo nguyên lý này, hoàn toàn viết bằng Lua. Người dùng chỉ cần thêm dòng require "bp" là có thể sử dụng. Mã nguồn đã được chia sẻ trên wiki.

Cơ chế hoạt động

Mình phân loại điểm dừng thành hai dạng:

  1. Điểm dừng ẩn danh: Thiết lập bằng bp.bp()
  2. Điểm dừng có tên: Thiết lập bằng bp.bp "tên"

Khi gọi bp.bp() lần đầu, hệ thống sẽ tạo một điểm dừng mới và kích hoạt ngay lập tức. Mỗi điểm dừng được định danh duy nhất dựa trên địa chỉ đối tượng closure, cho phép phân biệt các instance khác nhau của cùng một hàm. Điều này mang lại sự linh hoạt cao hơn so với phương pháp dựa trên vị trí mã nguồn.

Ví dụ:

1
2
3
4
function foo(arg)
  bp.bp()  -- Thiết lập điểm dừng ẩn danh
  return arg
end

Khi closure bị thu gom rác (garbage collected), điểm dừng tương ứng cũng tự động bị xóa.

Đối với điểm dừng có tên, bạn có thể quản lý hàng loạt điểm dừng cùng lúc. Ví dụ:

1
2
bp.bp "init"  -- Điểm dừng tên "init"
bp.bp "init"  -- Một điểm dừng khác cũng tên "init"

Sử dụng bp.trigger(id, true/false) để bật/tắt điểm dừng theo ID. Gọi bp.list() để liệt kê tất cả điểm dừng, hoặc bp.list "tên" để lọc theo tên.

Công cụ hỗ trợ

Thư viện này đi kèm một số tiện ích:

  • In bảng (table) đệ quy: Hàm bp.print_r(tbl, limit=64) giúp in cấu trúc bảng, tự động xử lý tham chiếu vòng và giới hạn độ sâu để tránh tràn bộ nhớ.
  • Quản lý biến cục bộ/upvalue: Thay vì dùng debug.getlocaldebug.getupvalue phức tạp, hệ thống cung cấp hai bảng _L (biến cục bộ) và _U (upvalue) để truy cập và chỉnh sửa trực tiếp trong giao diện console.

Ví dụ thực tế

Chạy đoạn mã sau:

1
2
3
4
5
6
7
8
require "bp"

function foo(arg)
  bp.bp()  -- Thiết lập điểm dừng ẩn danh
  return arg
end

=foo(0)  -- Gọi hàm và in kết quả

Kết quả:

1
2
3
break point:  1    on   =stdin(2)
_L=   {arg=0,}
_U=   {}

Tại đây, bạn có thể sửa giá trị biến cục bộ:

1
2
_L.arg = 1
cont

Chương trình sẽ tiếp tục chạy và trả về giá trị 1 thay vì 0.

Hướng phát triển

Trong tương lai gần, mình dự định thêm tính năng chạy từng bước (step-by-step) để hoàn thiện các chức năng cơ bản tương đương với gdb.

Hệ thống này chứng minh rằng ngay cả một ngôn ngữ thông dịch như Lua cũng có thể xây dựng công cụ gỡ lỗi mạnh mẽ, linh hoạt mà không cần phụ thuộc vào C API hay công cụ bên ngoài.

0%