Binary Linux không chạy: glibc hay musl?
Lỗi “GLIBC_2.34 not found” là do máy build đặt glibc tối thiểu. Xem binary thực sự cần gì và chọn 1 trong 4 cách ship portable.
Vì sao binary Linux không chạy trên server cũ
Khó tạo một binary Linux portable vì glibc, thư viện GNU C, chỉ cam kết tương thích theo một chiều. Binary cũ vẫn chạy trên glibc mới. Binary mới không chạy trên glibc cũ. Máy bạn dùng để compile sẽ đặt ra phiên bản glibc tối thiểu cho mọi máy mà bạn deploy.
Lỗi sẽ nêu chính xác phiên bản mà binary yêu cầu:
./mytool: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./mytool)Không có gì bị hỏng và cũng không có gì được cấu hình sai. Dynamic loader đọc một version tag được ghi bên trong binary, tìm tag đó trong libc của hệ thống, không tìm thấy nên từ chối khởi động chương trình. Tải lại file và chạy chmod +x cũng không thay đổi gì, vì yêu cầu này được ghi trực tiếp trong file. Bạn phải thay đổi cách build binary hoặc thay đổi thứ bạn ship.
glibc symbol versioning thực sự làm gì
Mỗi function mà glibc export đều có một version tag. printf bên trong libc.so.6 thực chất là printf@@GLIBC_2.2.5. Khi glibc thay đổi behavior hoặc ABI (application binary interface) của một function, nó không thay thế function cũ. Nó giữ code cũ dưới tag cũ và thêm code mới dưới tag mới, nên một libc.so.6 có thể chứa nhiều version của cùng một symbol cùng lúc. Một binary từ năm 2009 vẫn tìm thấy tag của năm 2009, nên forward compatibility hoạt động rất tốt.
Bước link ghi lại những gì nó đã sử dụng. Binary của bạn có một section .gnu.version_r, về bản chất ghi rằng: "Tôi cần GLIBC_2.38 từ libc.so.6". libc cũ hơn chưa từng có tag đó, nên loader dừng trước khi main chạy. Không có fallback, vì fallback sẽ khiến chương trình âm thầm nhận một function khác với function mà nó được compile dựa trên.
Trigger phổ biến nhất từ năm 2021 là glibc 2.34. Bản release đó gộp libpthread và libdl vào libc, đồng thời chuyển __libc_start_main, function khởi động mọi chương trình C, sang tag GLIBC_2.34. Vì vậy, một chương trình hello-world được compile trên bất kỳ distribution nào có glibc 2.34 hoặc mới hơn sẽ yêu cầu GLIBC_2.34, dù source không gọi bất kỳ thứ gì hiện đại. Đây là lý do vấn đề xuất hiện đột ngột với những người có code riêng không thay đổi trong nhiều năm.
Phiên bản glibc nào được distro của tôi phát hành?
The data behind this chart
[
{
"distro": "CentOS 7",
"glibc": 2.17
},
{
"distro": "RHEL 8",
"glibc": 2.28
},
{
"distro": "Ubuntu 20.04",
"glibc": 2.31
},
{
"distro": "Debian 11",
"glibc": 2.31
},
{
"distro": "RHEL 9",
"glibc": 2.34
},
{
"distro": "Ubuntu 22.04",
"glibc": 2.35
},
{
"distro": "Debian 12",
"glibc": 2.36
},
{
"distro": "Ubuntu 24.04",
"glibc": 2.39
},
{
"distro": "Debian 13",
"glibc": 2.41
}
]Đó là các phiên bản mà từng distribution phát hành trong repository riêng, theo thông tin do các distribution công bố và còn hiệu lực vào tháng 8 năm 2026. CentOS 7 đã hết vòng đời từ lâu, nhưng vẫn có trong danh sách vì các server kế thừa vẫn còn chạy nó. Trên bất kỳ máy nào bạn quản trị, hãy kiểm tra bằng ldd --version; lệnh này in bản release của glibc ở dòng đầu tiên. Bạn cũng có thể dùng getconf GNU_LIBC_VERSION.
Hãy đọc 9 dòng đó như một bậc thang. Build trên Debian 13 với glibc 2.41 thì kết quả sẽ không chạy được trên phiên bản nào cũ hơn Debian 13. Build trên Ubuntu 20.04 với glibc 2.31 thì cùng source đó chạy được trên mọi dòng nằm phía trên, bao gồm cả máy Debian 13. Server cũ nhất mà bạn phải hỗ trợ là build host duy nhất cần quan tâm. Vì vậy, distribution bạn chuẩn hóa sẽ xác định ABI floor cho mọi thứ bạn compile cho hệ thống đó. Bạn nên quyết định việc này trước khi fleet hình thành, cùng với các trade-off khác khi chọn OS để chạy trên VPS.
Cách tìm phiên bản glibc tối thiểu mà một binary yêu cầu
Đọc thông tin đó trực tiếp từ file. Cách này áp dụng được cho mọi binary, kể cả binary do vendor cung cấp mà không có ghi chú build.
objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5Dòng cuối cùng là phiên bản tối thiểu. Nếu thiếu objdump, hãy cài package binutils. Nếu không thể cài thêm gì trên máy đó, strings -a ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5 vẫn cho kết quả gần đúng, vì các tag được lưu dưới dạng chuỗi thuần văn bản bên trong file.
readelf -V ./mytool hiển thị cùng yêu cầu đó theo dạng có cấu trúc. Tìm block có tiêu đề Version needs section '.gnu.version_r'. Block này liệt kê một dòng Name: GLIBC_x.y cho mỗi tag, được nhóm theo library phải cung cấp tag đó.
Để biết function nào xác định phiên bản tối thiểu, hãy grep tag đó: objdump -T ./mytool | grep GLIBC_2.38. Tag này thường chỉ tương ứng với một symbol, và đôi khi là symbol bạn có thể tránh dùng. Nếu objdump -T không in ra gì, binary được link tĩnh, nên không có dynamic symbol table và không có yêu cầu về version để in ra.
Thông tin file và readelf -d cho biết gì về một binary người khác đưa cho bạn
file cho biết architecture và kiểu liên kết trên cùng một dòng.
file ./mytoolMột bản build glibc thông thường có dạng như sau:
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=..., for GNU/Linux 3.2.0, not strippedBản build static ghi statically linked và không chỉ định interpreter. Bản build musl chỉ định một interpreter khác:
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, ...Interpreter là trường quan trọng nhất. Đây là dynamic loader mà kernel chạy trước chương trình của bạn. Path chính xác đó phải tồn tại trên máy đích. /lib/ld-musl-x86_64.so.1 không có sẵn trên server Debian hoặc Ubuntu, trừ khi có người đã cài musl trên đó.
readelf -d ./mytool liệt kê các shared library mà binary yêu cầu theo tên:
Dynamic section at offset 0x2d58 contains 27 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libssl.so.3]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/../lib]Mỗi mục NEEDED là một yêu cầu bắt buộc, được đối chiếu theo soname. libssl.so.3 là OpenSSL 3. Vì vậy binary đó sẽ không load trên server chỉ có libssl.so.1.1. Soname là lý do: soname khác nhau có nghĩa là ABI không tương thích một cách có chủ đích. RUNPATH cho loader biết nơi cần tìm trước. $ORIGIN được thay bằng directory chứa binary. Đây là cách một bundle self-contained tìm thấy library của chính nó.
Không chạy ldd trên binary mà bạn không tin cậy. Trên glibc, ldd có thể resolve dependency bằng cách chạy chương trình dưới loader. Vì vậy file độc hại có thể thực thi code. readelf và objdump chỉ đọc các byte. Trước tất cả các bước này, hãy xác nhận download là file mà project thực sự đã publish. Xác minh download bằng checksum mà project đã công bố là bước duy nhất cho biết các byte bạn đang giữ đến từ đâu.
Các lỗi bạn thực sự sẽ gặp và ý nghĩa của từng lỗi
Loader báo một phiên bản GLIBC không thể tìm thấy. Binary được biên dịch với glibc mới hơn phiên bản trên target. Không có cách cài đặt nào trên target có thể sửa lỗi này một cách an toàn. Hãy chọn một trong 4 tùy chọn dưới đây.
Shell báo No such file or directory cho một file mà bạn vẫn nhìn thấy rõ. Thành phần bị thiếu là interpreter, không phải binary. execve trả về ENOENT khi đường dẫn loader trong ELF header bị thiếu, còn shell chỉ có thể in ra thông báo duy nhất mà nó biết. Chạy file và đọc đường dẫn interpreter. Binary liên kết với musl trên server chỉ có glibc sẽ gây ra đúng triệu chứng này.
cannot execute binary file: Exec format error có nghĩa là sai kiến trúc: binary x86-64 trên server arm64 hoặc ngược lại. Dòng đầu tiên trong output của file cho biết binary bạn đang có dùng kiến trúc nào.
error while loading shared libraries: libssl.so.3: cannot open shared object file có nghĩa là thư viện NEEDED bị thiếu hoặc có mặt với soname khác. Cài distribution package tương ứng. Tên package khác nhau giữa các họ distribution, vì vậy hãy chuyển đổi trước khi sao chép một dòng apt từ README; các lệnh tương đương của dnf và apt cung cấp cách ánh xạ đó.
Một Segmentation fault đơn lẻ từ bản build musl nhưng binary vẫn chạy bình thường dưới glibc. Thông thường nguyên nhân là kích thước thread stack, được đề cập trong tùy chọn 2.
Bốn cách phát hành binary Linux portable
Mỗi lựa chọn đều có chi phí thực tế. Hãy chọn dựa trên cách chương trình hoạt động khi chạy, không phải dựa trên lựa chọn nghe có vẻ gọn nhất.
Lựa chọn 1: build trên bản distro cũ nhất mà bạn hỗ trợ
Đây là câu trả lời ít hấp dẫn nhất và thường là câu trả lời đúng. Hãy compile trong container image của bản phân phối cũ nhất mà bạn cam kết hỗ trợ. Linker chỉ có thể ghi các tag mà glibc cũ đó thực sự có, vì vậy mức tối thiểu sẽ là bản release đó, trong khi binary vẫn là dynamic binary thông thường và giữ nguyên toàn bộ hành vi của glibc.
docker run --rm -v "$PWD:/src" -w /src ubuntu:20.04 sh -c 'apt-get update && apt-get install -y build-essential && make'Output sẽ chạy trên glibc 2.31 và mọi phiên bản cao hơn. Đừng chỉ giả định; hãy xác minh. Chạy one-liner objdump -T ở phần trước trên kết quả và kiểm tra xem tag cao nhất có đúng với giá trị bạn mong đợi không.
Cái giá phải trả là toolchain. Base image cũ cũng đi kèm compiler cũ, gây vấn đề khi code của bạn cần chuẩn C++ mới. Go và Rust phần lớn tránh được vấn đề này vì toolchain của chúng được cài độc lập vào container, không phụ thuộc vào các package của distro. Với C và C++, bạn có thể thêm compiler mới hơn từ toolchain channel của chính distro, hoặc dùng các image manylinux, vốn được tạo ra chính để kết hợp glibc rất cũ với GCC hiện tại. C compiler đi kèm của Zig cũng có thể target trực tiếp một glibc được chọn, như trong zig cc -target x86_64-linux-gnu.2.28. Cách này cho cùng mức tối thiểu mà không cần duy trì một image cũ.
Tùy chọn 2: liên kết tĩnh với musl
musl là một thư viện C nhỏ được thiết kế để hỗ trợ liên kết tĩnh. Binary musl tĩnh chứa libc của chính nó, không khai báo interpreter và chạy trên mọi Linux kernel đúng kiến trúc. Đây là cách hầu hết các bản tải xuống của tool chỉ gồm một file được build.
sudo apt install -y musl-tools
musl-gcc -static -O2 hello.c -o hello
file ./hellofile bây giờ phải hiển thị statically linked và không có trường interpreter. Với Rust, thêm target rồi build cho target đó:
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-muslGo không cần các bước này. Với CGO_ENABLED=0 go build, kết quả đã là static và hoàn toàn không link libc.
Tra cứu NSS sẽ thay đổi. glibc phân giải user và hostname thông qua NSS (name service switch), cơ chế này load các module libnss_* bằng dlopen trong khi chương trình đang chạy. glibc được link tĩnh không làm được việc đó, và linker sẽ cảnh báo bằng nội dung sau:
warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linkingmusl tránh cảnh báo này vì không triển khai NSS. Nó có resolver riêng và đọc trực tiếp /etc/resolv.conf và /etc/hosts. Trên một VPS thông thường, cách này hoạt động tốt và đơn giản hơn. Trên host lấy account hoặc hostname từ LDAP hoặc SSSD, binary của bạn sẽ không thấy chúng, trong khi mọi chương trình khác trên máy vẫn thấy. Resolver của musl cũng mới hơn resolver của glibc: cơ chế fallback sang TCP cho DNS reply lớn hơn 512 byte được bổ sung trong musl 1.2.4 vào năm 2023, nên các bản build với musl cũ hơn sẽ cắt ngắn các answer lớn.
dlopen không hoạt động. Trong binary musl tĩnh, dlopen chỉ là stub và luôn fail. Mọi thứ load code lúc runtime đều không dùng được: hệ thống plugin, PAM module, GPU driver và các module character-set iconv của glibc. Nếu chương trình cần dlopen, bạn không thể dùng liên kết tĩnh và phải chọn một trong 3 phương án còn lại.
Bạn phải tự xử lý các security update. Binary được link động sẽ nhận bản fix của libc ngay khi server chạy package upgrade. Binary tĩnh thì không bao giờ tự nhận. Khi một mục CVE (common vulnerabilities and exposures) xuất hiện đối với libc của bạn hoặc đối với OpenSSL tĩnh đã bundle, bạn phải build lại và phân phối lại. Mọi bản copy đã deploy vẫn dễ bị tấn công cho đến khi có người thay file. Hãy lưu lại những gì bạn đã link vào binary, vì không có thông tin nào trên target cho administrator biết binary chỉ gồm một file đó đang chứa thư viện từ 2 năm trước.
License sẽ thay đổi. glibc dùng LGPL, và liên kết tĩnh kích hoạt nghĩa vụ relink của LGPL: bạn phải cung cấp cho người nhận những gì họ cần để relink chương trình với một glibc khác. musl dùng MIT và không có điều kiện này. Đây là lý do chính khiến các project phát hành binary chỉ gồm một file chọn musl thay vì glibc tĩnh.
2 khác biệt runtime có thể gây ra các crash khó hiểu. Stack mặc định của thread trong musl là 128 KiB, trong khi glibc dùng 8 MiB. Vì vậy, code đặt một buffer lớn trên thread stack có thể segfault mà không có message hoặc log line nào. Đặt kích thước rõ ràng bằng pthread_attr_setstacksize hoặc chuyển buffer sang heap. Allocator của musl cũng được thiết kế cho kích thước nhỏ và hành vi có thể dự đoán, thay vì cho nhiều thread cùng allocate; vì vậy các chương trình nhiều thread và nặng về allocation có thể chạy chậm hơn rõ rệt. Hãy benchmark workload của chính bạn thay vì dựa vào danh tiếng của một trong hai thư viện.
Tùy chọn 3: đóng gói loader và các thư viện
Đặt các thư viện cạnh binary cùng với loader tương ứng, rồi khởi động chương trình thông qua loader đó.
./ld-linux-x86-64.so.2 --library-path ./lib ./mytoolĐể thiết lập cố định, ghi các đường dẫn vào file bằng patchelf:
patchelf --set-interpreter "$PWD/lib/ld-linux-x86-64.so.2" --set-rpath '$ORIGIN/lib' ./mytoolMột quy tắc quyết định cách này có hoạt động hay không: loader và libc.so.6 phải đến từ cùng một bản build glibc. Chúng là một cặp tương ứng. Nếu dùng loader của host với libc được đóng gói, chương trình sẽ crash trong lúc khởi động thay vì hiển thị lỗi dễ đọc. Hãy đóng gói cả hai hoặc không đóng gói cái nào.
AppImage là một gói theo mẫu này. Payload nằm trong một filesystem image squashfs và một runtime nhỏ sẽ mount image đó. Có một đặc điểm thường bị bỏ sót: AppImage không đóng gói glibc. Vì vậy, AppImage được build trên một distribution hiện tại vẫn gặp cùng lỗi version trên server cũ. Hướng dẫn của AppImage là build trên base cũ nhất mà bạn hỗ trợ. Do đó, đây là format phân phối được xây dựng trên option 1, không phải giải pháp thay thế cho option 1.
Trên headless server, AppImage cần FUSE (filesystem in userspace) để mount payload. Image VPS tối giản thường không có FUSE. Lỗi sẽ nêu tên libfuse.so.2. Bạn có thể bỏ qua hoàn toàn bước mount bằng ./App.AppImage --appimage-extract-and-run. Cách này giải nén vào một thư mục tạm thời rồi chạy chương trình từ đó.
Tùy chọn 4: phát hành container image
Đưa toàn bộ userland đi cùng chương trình. Image có sẵn libc riêng, nên glibc của host không còn quan trọng; chỉ kernel và architecture cần tương thích. Đây là tùy chọn ít phức tạp nhất và loại bỏ hoàn toàn nhóm vấn đề này. Vì vậy, rất nhiều server software được phân phối theo cách này. Chạy Docker trên VPS là cách thông thường để sử dụng image.
Kernel của host vẫn đặt ra giới hạn. glibc mới hơn sử dụng các system call mới hơn, và một container host cũ có thể chặn chúng: glibc 2.34 trở lên sử dụng clone3, nhưng các seccomp profile mặc định cũ sẽ từ chối system call này. Triệu chứng là chương trình fail ngay lập tức với Operation not permitted mà không có thông báo nào nhắc đến glibc. Nâng cấp container runtime trên host sẽ khắc phục lỗi, vì việc chặn nằm trong syscall filter của runtime, không phải trong kernel.
Các chi phí là những yêu cầu thông thường. Máy đích cần có container runtime và quyền sử dụng runtime đó. Dung lượng tải xuống tăng từ một file lên hàng chục hoặc hàng trăm megabyte. Bạn phải quản lý base image và lịch patch của image, nên công việc bảo mật từng tránh được ở tùy chọn 2 quay trở lại dưới dạng rebuild image. Với một service chạy lâu dài, đây là sự đánh đổi hợp lý. Với một command-line tool chỉ được chạy một lần, thì không.
Bạn nên chọn phương án nào?
- Công cụ nội bộ dành cho các server bạn kiểm soát và đều chạy cùng một distribution: build động trên distribution đó rồi dừng ở đó.
- Một file duy nhất để người lạ tải xuống và chạy: dùng static musl nếu chương trình không cần
dlopenvà không cần các tra cứu được NSS hỗ trợ. - Chương trình có plugin hoặc cần GPU: tiếp tục dùng build động, build trên một base cũ và dùng phương án 3 để phân phối.
- Service chạy lâu dài trên máy đã có sẵn runtime: phân phối image.
Dù chọn phương án nào, hãy ghi lại và kiểm tra. glibc trên build host giờ là một phần của quy trình release. Một build machine được nâng cấp từ bản LTS này lên bản LTS tiếp theo sẽ âm thầm nâng mức yêu cầu tối thiểu, khiến những người dùng vẫn chạy được vào tháng trước bị lỗi. Pin build image bằng tag và kiểm tra tag GLIBC_ cao nhất trong output như một build step. Khi đó, bước kiểm tra sẽ fail trong pipeline của bạn thay vì fail trên terminal của người khác.
FAQ
Vì sao server báo "version GLIBC_2.38 not found"?
Binary được biên dịch trên một máy có glibc mới hơn server của bạn. Cơ chế versioning symbol của glibc chỉ cung cấp khả năng tương thích ngược: binary cũ chạy được trên glibc mới, nhưng binary mới không chạy được trên glibc cũ, vì loader cần đúng version tag được ghi trong file còn libc cũ chưa từng có tag đó. Không có thứ gì bạn cài thêm trên server có thể sửa lỗi này một cách an toàn. Hãy build lại trên base cũ hơn, cung cấp bản build static dùng musl, đóng gói các library cùng loader tương ứng, hoặc cung cấp container image.
Làm sao biết một binary cần phiên bản glibc nào?
Đọc các version tag trong file bằng objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5. Dòng cuối cùng là phiên bản glibc thấp nhất có thể load binary đó. readelf -V ./mytool hiển thị cùng các yêu cầu trong section .gnu.version_r, được nhóm theo library phải cung cấp chúng. Nếu objdump -T không in gì, binary là static và hoàn toàn không yêu cầu glibc. So sánh kết quả với phiên bản của server, lấy bằng ldd --version.
Binary dùng musl có chậm hơn binary dùng glibc không?
Điều này phụ thuộc vào workload, và câu trả lời chính xác là phải đo. Binary static dùng musl khởi động nhanh hơn vì không cần chạy loader và không phải thực hiện relocation tại thời điểm exec. Ngược lại, allocator của musl được thiết kế để có kích thước nhỏ và hành vi dễ dự đoán, thay vì phục vụ nhiều thread cùng cấp phát tại một thời điểm. musl cũng có ít routine xử lý string và memory được tối ưu thủ công hơn glibc. Vì vậy, các chương trình nhiều thread, cấp phát nhiều hoặc xử lý string nhiều có thể chậm hơn đáng kể. Hãy benchmark chính chương trình trên chính server của bạn trước khi chấp nhận bất kỳ nhận định nào.
Vì sao bash báo "No such file or directory" cho một file đang tồn tại?
File bị thiếu là dynamic loader, không phải binary của bạn. Kernel đọc đường dẫn interpreter từ ELF header và trả về ENOENT khi không tìm thấy đường dẫn đó. Shell chỉ có thể hiển thị lỗi bằng thông báo này. Chạy file ./mytool và đọc field interpreter. Nếu field này hiển thị /lib/ld-musl-x86_64.so.1 trên server Debian hoặc Ubuntu, binary của bạn được link với musl nhưng hệ thống dùng glibc. Bạn cần tải bản tương thích với musl hoặc dùng bản build static.
Tôi có thể copy libc.so.6 từ server mới hơn để sửa lỗi này không?
Không. libc.so.6 và ld-linux-x86-64.so.2 là một cặp tương ứng từ cùng một bản build glibc, và mọi process trên máy đều sử dụng chúng. Vì vậy, ghi đè bản copy của system có thể khiến server không chạy được bất kỳ thứ gì, kể cả các tool cần dùng để khôi phục. Nếu bắt buộc phải chạy một binary mới hơn trên host cũ, hãy giải nén glibc mới hơn vào một thư mục riêng và khởi chạy chương trình đó qua loader riêng bằng --library-path. Cách này chỉ ảnh hưởng đến process đó. Tuy vậy, build lại dựa trên glibc cũ vẫn là phương án không gây bất ngờ cho bạn sau sáu tháng.