Các Hàm Hủy Thread Local Storage Ngăn Cản Việc Gỡ Bỏ Thư Viện Động Trong Hệ Thống Linux

Nhóm Cộng đồng BigGo
Các Hàm Hủy Thread Local Storage Ngăn Cản Việc Gỡ Bỏ Thư Viện Động Trong Hệ Thống Linux

Quản lý thư viện động trong các hệ thống Linux đã tiết lộ một hành vi bất ngờ có thể gây ra những khó khăn đáng kể trong việc gỡ lỗi cho các nhà phát triển. Khi các ứng dụng cố gắng gỡ bỏ các thư viện chia sẻ bằng cách sử dụng dlclose(), các thư viện có thể vẫn còn lại trong bộ nhớ mặc dù có vẻ như đã được đóng đúng cách, dẫn đến việc trạng thái được duy trì một cách bất ngờ và các lỗi khởi tạo.

Thủ Phạm Ẩn Giấu: Các Hàm Hủy Thread Local Storage

Nguyên nhân gốc rễ của hành vi này nằm ở các hàm hủy thread local storage (TLS) vẫn được đăng ký khi các thư viện được đóng. Khi một thư viện sử dụng các biến thread-local, nó đăng ký các hàm dọn dẹp được thiết kế để chạy khi các thread thoát. Tuy nhiên, nếu các hàm hủy này vẫn đang chờ xử lý khi dlclose() được gọi, bộ tải động từ chối gỡ bỏ thư viện hoàn toàn.

Điều này tạo ra một tình huống đặc biệt có vấn đề trong các môi trường đa ngôn ngữ. Các thư viện Rust thường xuyên sử dụng thread-local storage cho nhiều mục đích khác nhau, bao gồm cả hệ thống ghi log. Khi các thư viện này được tải động như là các phụ thuộc của các thư viện khác, các hàm hủy TLS của chúng có thể ngăn cản việc dọn dẹp đúng cách ngay cả sau khi thư viện chính có vẻ như đã được gỡ bỏ.

Thread local storage: Bộ nhớ duy nhất cho mỗi thread trong một chương trình, cho phép các thread khác nhau có các bản sao riêng của các biến.

Các Điều Kiện Ngăn Cản Việc Gỡ Bỏ Thư Viện

Điều Kiện Mô Tả Nguyên Nhân Phổ Biến
Số lượng tham chiếu > 1 Thư viện được sử dụng bởi nhiều thư viện khác Nhiều phụ thuộc
Cờ NODELETE được đặt Thư viện được đánh dấu là không thể gỡ bỏ Cờ linker -z nodelete hoặc RTLD_NODELETE
Ký hiệu STB_GNU_UNIQUE Chứa các ký hiệu với ràng buộc duy nhất Template C++, hàm inline
Destructor TLS đang chờ xử lý Các hàm dọn dẹp thread local storage đã được đăng ký Thread-local của Rust , biến thread_local của C++
Đang được sử dụng Mã hoặc dữ liệu của thư viện vẫn đang được truy cập Các lời gọi hàm hoặc tham chiếu dữ liệu đang tồn tại

Vượt Ra Ngoài Việc Đếm Tham Chiếu: Nhiều Rào Cản Gỡ Bỏ

Trong khi hầu hết các nhà phát triển hiểu rằng các thư viện sẽ không được gỡ bỏ nếu số lượng tham chiếu của chúng vượt quá một, bộ tải động Linux áp đặt một số hạn chế bổ sung. Các thư viện được đánh dấu với cờ NODELETE sẽ không bao giờ được gỡ bỏ, bất kể số lượng tham chiếu. Cờ này có thể được thiết lập một cách rõ ràng trong quá trình liên kết hoặc được áp dụng tự động cho các thư viện chứa các ký hiệu được đánh dấu là STB_GNU_UNIQUE.

Cộng đồng đã xác định rằng các thành phần thư viện chuẩn C++ thường rơi vào danh mục này. Nhiều template C++ và các hàm inline tạo ra các ký hiệu với ràng buộc duy nhất, điều này tự động đánh dấu toàn bộ thư viện là không thể gỡ bỏ. Điều này giải thích tại sao libstdc++.so thường vẫn còn trong bộ nhớ trong suốt thời gian tồn tại của chương trình một khi đã được tải.

STB_GNU_UNIQUE: Một loại ràng buộc ký hiệu đảm bảo chỉ có một phiên bản của ký hiệu tồn tại trên tất cả các thư viện được tải trong một tiến trình.

Các Chiến Lược Gỡ Lỗi và Giải Pháp Thay Thế

Các nhà phát triển gặp phải các vấn đề tương tự có thể tận dụng biến môi trường LD_DEBUG để theo dõi hành vi tải và gỡ bỏ thư viện. Công cụ gỡ lỗi này tiết lộ khi nào các thư viện được đánh dấu là NODELETE và cung cấp cái nhìn sâu sắc về quá trình ra quyết định của bộ tải động.

Cách tiếp cận duy nhất an toàn, nhất quán và đáng tin cậy là không đóng các DLL.

Một số chuyên gia ủng hộ các cách tiếp cận thay thế, chẳng hạn như chạy các thư viện trong các tiến trình riêng biệt giao tiếp qua giao tiếp liên tiến trình. Điều này cô lập trạng thái thư viện hoàn toàn và cho phép gỡ bỏ sạch sẽ bằng cách kết thúc tiến trình con. Mặc dù điều này tăng thêm độ phức tạp, nó loại bỏ hành vi không thể dự đoán được liên quan đến quản lý thư viện động.

Cuộc điều tra đã tiết lộ một tác dụng phụ thú vị: việc kích hoạt ghi log trong thư viện có vấn đề thực sự đã giải quyết vấn đề bằng cách đảm bảo cả hai thư viện vẫn được tải một cách nhất quán, duy trì trạng thái đồng bộ giữa chúng.

Công cụ Debug cho các vấn đề Dynamic Library

  • Biến môi trường LD_DEBUG: Theo dõi hành vi tải, tìm kiếm và gỡ bỏ thư viện
  • Lệnh nm: Kiểm tra các ký hiệu nhị phân để xác định các marker STB_GNU_UNIQUE (chữ 'u' viết thường)
  • Kiểm thử dlsym: Thử phân giải các ký hiệu sau dlclose để xác minh việc gỡ bỏ
  • Breakpoint GDB: Đặt breakpoint trong các hàm dlopen, dlclose, và _cxa_thread_atexit_impl
  • Ánh xạ bộ nhớ tiến trình: Giám sát /proc/[pid]/maps để quan sát sự hiện diện của thư viện trong không gian địa chỉ

Tác Động Đối Với Phát Triển Hiện Đại

Hành vi này làm nổi bật sự phức tạp của các hệ thống liên kết động hiện đại và những thách thức của việc kết hợp các ngôn ngữ lập trình khác nhau trong các tình huống thư viện chia sẻ. Sự tương tác giữa việc sử dụng thread-local storage của Rust và các yêu cầu ký hiệu duy nhất của C++ tạo ra các trường hợp đặc biệt có thể khó dự đoán và gỡ lỗi.

Hiểu được những hạn chế này là rất quan trọng đối với các nhà phát triển xây dựng hệ thống plugin hoặc các ứng dụng dựa vào việc tải và gỡ bỏ thư viện động. Thao tác tưởng chừng đơn giản của việc đóng một thư viện bao gồm nhiều kiểm tra và điều kiện có thể ngăn cản việc dọn dẹp mong đợi xảy ra.

Tham khảo: Why did dlclose not unload the library?