Vấn Đề Nhỏ Với Glx Trên Freebsd
Gần đây mình khá bận rộn, thường xuyên làm việc liên tục hơn 10 tiếng mỗi ngày. Vì vậy tâm trạng cũng bất ổn, nhiều khi một vấn đề lẽ ra chỉ mất 30 phút xử lý lại kéo dài hàng giờ đồng hồ. Ví dụ như sự cố mình gặp phải hôm qua.
Do phần code底层 (low-level code) được mình độc lực refactor toàn diện (với nguyên tắc không thay đổi kiến trúc tầng trung gian), tổng cộng sửa đổi hơn 10,000 dòng mã trải trên 100 file C, trong đó viết lại hoàn toàn hàng ngàn dòng. Sự thay đổi quy mô lớn như vậy dễ dẫn đến những lỗi hy hữu rất đau đầu. Code mới chạy hoàn hảo trên Windows và Ubuntu, nhưng kỳ lạ thay lại gây ra lỗi core dump khi thoát chương trình trên Freebsd.
Thực ra mình chưa có nhiều kinh nghiệm sử dụng các tính năng nâng cao của gdb, cộng thêm mấy năm gần đây toàn viết code mà không debug (theo quan điểm cá nhân, thà nâng cao năng lực thiết kế và viết code chất lượng cao ngay từ đầu còn hơn mất thời gian xử lý bug. Ngăn chặn bug xuất hiện hoặc cô lập chúng trong phạm vi hẹp còn quan trọng hơn nhiều so với việc sửa chữa về sau). Sau khi thử debug sơ bộ, mình quyết định dùng phương pháp phân tích tĩnh bằng tư duy con người. Cuối cùng xác định nguyên nhân nằm ở khâu đóng gói so một cách chủ động (gọi hàm dlclose).
Theo lập luận thông thường, nếu chương trình lỗi sau dlclose, khả năng cao là do các module khác vẫn còn tham chiếu tới đoạn code hoặc dữ liệu tĩnh trong đó. Theo biểu hiện thực tế, tiến trình crash ngay sau khi hàm main trong C trả về. Vì toàn bộ hệ thống được viết bằng C, nên ta có thể loại trừ các vấn đề liên quan đến C++ như destructor phức tạp. Quy luật logic cho thấy đây là lỗi xảy ra trong quá trình unload các so được tự động nạp vào tiến trình.
Suy luận hợp lý này về sau đã được chứng minh là chính xác. Nhìn tưởng đơn giản nhưng không hiểu sao hôm qua mình lại mất 4 tiếng đồng hồ mới đi đến kết luận này. Quá trình debug cụ thể là như vậy. Mình dùng phương pháp chia đôi mã nguồn để loại trừ từng phần code, cố gắng thu gọn chương trình test xuống mức tối thiểu mà vẫn tái hiện được lỗi. Dù biết đây là phương pháp không cần nhiều tư duy nhưng đành chấp nhận vì đầu óc đang không ổn định. Những phương pháp ngốc nghếch thường hiệu quả nhưng đòi hỏi nhiều công sức. Cuối cùng phát hiện ra vấn đề liên quan đến API XCreateBitmapFromData của X Window mà mình dùng để ẩn con trỏ chuột sau khi tạo cửa sổ X. Nếu không gọi API này thì mọi thứ vận hành bình thường, dù rằng bản thân API này thực thi hoàn toàn thành công.
Trong trạng thái mệt mỏi, mình mải mê điều tra lỗ hổng nhỏ này. Vừa nghi ngờ tham số đầu vào của API, vừa hoài nghi cơ chế hoạt động của XLib. Rồi lại phát hiện thêm vài API X khác cũng gây ra hiện tượng tương tự - đều thành công khi gọi nhưng crash khi kết thúc chương trình. Trong đó có hai trường hợp đặc trưng: một là crash không dấu hiệu báo trước, hai là nếu thay đổi thứ tự gọi hàm sẽ nhận được lỗi từ X Server sớm hơn. Nhóm hiện tượng thứ hai càng củng cố thêm ảo tưởng ban đầu của mình.
Điều bất ngờ là ngay cả khi giữ nguyên code, chỉ thay đổi tham số liên kết biên dịch cũng gây ra lỗi. Nếu chương trình chỉ liên kết với thư viện GL thì mọi thứ ổn định, nhưng chỉ cần thêm bất kỳ thư viện OpenGL nào khác như GLU hay GLEW dù không dùng hàm nào từ chúng trong mã test rút gọn, lỗi vẫn xảy ra. Tất nhiên mã test vẫn phải sử dụng một API của GLX nên bắt buộc phải liên kết với GL.
Hiện tại vẫn chưa xác định chính xác vị trí bug gây crash. Tạm thời mình quyết định nghỉ ngơi đầy đủ rồi quay lại xử lý sau. May mắn là toàn bộ mã nguồn liên quan đều có sẵn trên FreeBSD, desktop của mình cũng được build từ source gốc, nên sẽ dành thời gian nghiên cứu kỹ vào thứ Hai tuần sau.
Dù thế giới lập trình thường có mối quan hệ nhân quả rõ ràng hơn thế giới thực, nhưng thỉnh thoảng cũng gặp phải những vấn đề phức tạp. Mức độ nan giải của chúng phụ thuộc nhiều vào độ hiểu biết của bạn về các ngõ ngách hệ thống. Có thể bạn thấy bế tắc nhưng một người khác nắm rõ vấn đề sẽ nhìn ra ngay. Giống như việc virus hoành hành trên Windows, nhiều người dùng nhiễm mã độc mà không hay biết, nhưng các chuyên gia Windows lại có thể phát hiện bất thường chỉ qua những dấu hiệu rất nhỏ.
Quá trình liên kết (linking) thực chất là một quy trình phức tạp, ngay cả lập trình viên giàu kinh nghiệm cũng dễ mắc sai lầm. Mình từng giúp đồng nghiệp xử lý một lỗi liên quan đến Lua, nguyên nhân chính là do liên kết sai khiến hai bản copy của lua core tồn tại song song trong cùng một tiến trình, dẫn đến việc biến tĩnh bị nhân đôi. Những lỗi kiểu này thường ẩn nấp rất lâu rồi mới bộc phát dưới dạng kỳ lạ, vượt quá khả năng debug thông thường.
À nhân tiện, mình cho rằng vấn đề này thực ra đã tồn tại từ trước, chỉ là lần refactor này mới lộ rõ ra mà thôi. Vì trước đây toàn bộ quá trình liên kết và nạp module nhị phân đều do mình tự xử lý, còn lần này muốn đơn giản hóa nên dựa nhiều hơn vào cơ chế sẵn có của hệ thống.
Bổ sung ngày 25/3: Nguyên nhân gốc rễ liên quan đến vấn đề unload libstdc++