Sự Cố Bug Do Liên Kết Kép Thư Viện Lua Và Bài Học Lần Thứ Ba - nói dối e blog

Sự Cố Bug Do Liên Kết Kép Thư Viện Lua Và Bài Học Lần Thứ Ba

Hôm nay đã mất gần 3 giờ đồng hồ để hỗ trợ đồng nghiệp xử lý một lỗi crash xảy ra trong Lua VM, kết quả là làm gián đoạn kế hoạch ban đầu và không hoàn thành được các công việc dự kiến trước Tết. Thực tế đây không phải là vấn đề quá nghiêm trọng - bản thân cũng đã từng gặp phải trước đây. Tôi tự nhận thấy sai sót của mình nằm ở việc khi nhìn thấy call stack lỗi ban đầu đã không lập tức kiểm tra mã nguồn liên quan đến Lua. Nếu làm như vậy, với kinh nghiệm xử lý lỗi tương tự trước đây, có thể nhanh chóng xác định nguyên nhân như phản xạ tự nhiên, thay vì mất cả nửa ngày trời vật lộn vô ích.

Giờ đây tiến độ tích hợp bị chậm lại, việc tiếp tục xử lý một mình sau Tết xem như dang dở. Xin ghi lại chi tiết bug này như một lời nhắc nhở bản thân: đến lần gặp thứ ba phải đủ kinh nghiệm để nhận diện ngay lập tức.

Nguyên nhân gốc rễ bắt nguồn từ việc một số thư viện mở rộng Lua vô tình liên kết thừa thêm một bản sao thư viện Lua khi biên dịch. Điều này dẫn đến tình trạng có nhiều phiên bản Lua library cùng tồn tại trong tiến trình, xảy ra do cấu hình sai trong file Makefile. Khi đã xác định được nguyên nhân thì cách giải quyết rất đơn giản: hoặc biên dịch Lua thành thư viện động .so, hoặc chỉ cho phép framework chính dùng tham số -E để liên kết tĩnh thư viện Lua. Vấn đề này xin không trình bày sâu ở đây.

Trải qua 10 năm gắn bó với Lua, đây không phải là lần đầu tôi giúp đồng nghiệp khác nhận ra lỗi liên kết thư viện này. Điều đáng quan tâm là sau sự cố, cơ chế crash của chương trình diễn ra như thế nào.

Trong mã nguồn Lua, gần như không sử dụng biến toàn cục. Nói cách khác, trạng thái API của Lua chỉ tồn tại thông qua tham số L mà không phụ thuộc vào thư viện. Đây là thiết kế rất tinh tế của Lua. Về mặt lý thuyết, với các API không có trạng thái bên ngoài (stateless), dù đoạn mã được liên kết nhiều lần trong cùng một tiến trình cũng không gây ra side effect nào. Tuy nhiên thực tế lại xảy ra điều ngược lại - tại sao vậy?

Bí mật nằm ở biến toàn cục tĩnh dummynode_ trong file ltable.c. Để hiểu rõ, cần biết rằng khi triển khai bảng (table) trong Lua, nhằm tối ưu hiệu suất, thay vì dùng con trỏ NULL khi phần hash rỗng, hệ thống sử dụng node giả này để giảm bớt các thao tác kiểm tra điều kiện. Biến dummynode được cấp phát tĩnh đặc biệt và không thể giải phóng bằng các hàm quản lý bộ nhớ thông thường. Lua dùng macro đặc biệt để kiểm tra node giả như sau:

1
#define isdummy(n)   ((n) == dummynode)

Khi có nhiều bản sao thư viện Lua được liên kết, sẽ tồn tại nhiều biến dummynode riêng biệt. Hệ quả là macro isdummy sẽ đưa ra kết quả sai lệch. Điều này dẫn đến lỗi crash khi thực hiện giải phóng bộ nhớ, biểu hiện dưới dạng segmentation fault trong hàm lua_Alloc.

Hai vị trí thường xuyên gặp lỗi là:

  1. Khi chèn phần tử hash đầu tiên vào bảng trống - tại dòng cuối cùng của hàm luaH_resize:
1
2
if (!isdummy(nold))
  luaM_freearray(L, nold, cast(size_t, twoto(oldhsize))); /* giải phóng mảng cũ */
  1. Khi đóng state Lua và giải phóng bảng - ở đầu hàm luaH_free:
1
2
if (!isdummy(t->node))
  luaM_freearray(L, t->node, cast(size_t, sizenode(t)));

Tôi liệt kê cụ thể mã nguồn và vị trí lỗi vì đây không phải lần đầu tiên phân tích điểm này. Nếu buổi chiều nay, khi mới nhận được thông tin traceback stack, tôi lập tức mở mã nguồn Lua và xem hàm luaH_resize, chắc chắn sẽ nhớ ra kinh nghiệm xử lý tương tự trước đây, từ đó tiết kiệm được hàng giờ trời mò mẫm vô ích.

Hy vọng qua sự cố này, khi có đồng nghiệp khác báo cáo vấn đề tương tự, tôi có thể nhanh chóng nhận diện bản chất và đưa ra giải pháp ngay lập tức.

Bổ sung ngày 19/4: Làm thế nào để phát hiện vấn đề này ngay lập tức khi xảy ra? Hãy thêm dòng luaL_checkversion(L) ở đầu mỗi thư viện C mở rộng cho Lua. Hàm này không chỉ kiểm tra việc liên kết lặp thư viện Lua mà còn phát hiện các lỗi tương thích phiên bản khác.

0%