علت خطای GLIBC_2.34 در اجرای باینری لینوکس
خطای GLIBC_2.34 not found نشان میدهد نسخه glibc سیستم مقصد قدیمی است. در این مقاله 4 روش برای رفع این مشکل و ساخت باینری لینوکس قابلحمل با استفاده از ldd و patchelf را میخوانید.
چرا یک باینری لینوکس روی سرور قدیمی اجرا نمیشود
تولید یک باینری لینوکس قابلحمل دشوار است، زیرا glibc (کتابخانه C گنو) تنها در یک جهت وعده سازگاری میدهد. یک باینری قدیمی روی glibc جدید اجرا میشود، اما یک باینری جدید روی glibc قدیمی اجرا نخواهد شد. ماشینی که روی آن کامپایل میکنید، حداقل نسخه مورد نیاز برای تمام ماشینهایی که برنامه را روی آنها مستقر میکنید، تعیین میکند.
خطای دریافتی، نسخه دقیق مورد نیاز را مشخص میکند:
./mytool: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./mytool)هیچچیز خراب نیست و هیچچیز اشتباه پیکربندی نشده است. لودر پویا (dynamic loader) یک برچسب نسخه را که درون باینری ثبت شده خوانده، به دنبال آن برچسب در libc سیستم گشته، آن را نیافته و از اجرای برنامه خودداری کرده است. دانلود مجدد فایل و اجرای chmod +x هیچ تغییری ایجاد نمیکند، زیرا این پیشنیاز درون خود فایل نوشته شده است. شما یا باید نحوه ساخت باینری را تغییر دهید، یا آنچه را که منتشر میکنید اصلاح کنید.
نسخهبندی نمادها در glibc دقیقاً چه کاری انجام میدهد
هر تابعی که glibc صادر میکند، دارای یک برچسب نسخه است. printf در داخل libc.so.6 در واقع همان printf@@GLIBC_2.2.5 است. هنگامی که glibc رفتار یا ABI (رابط باینری برنامه) یک تابع را تغییر میدهد، نسخه قدیمی را جایگزین نمیکند. این کتابخانه کد قدیمی را تحت همان برچسب قدیمی نگه میدارد و کد جدید را با برچسبی تازه اضافه میکند؛ بنابراین یک libc.so.6 واحد، چندین نسخه از یک نماد را همزمان در خود جای میدهد. یک فایل باینری متعلق به سال 2009، برچسب سال 2009 را همچنان در جای خود مییابد و به همین دلیل است که سازگاری رو به جلو (forward compatibility) به خوبی کار میکند.
مرحله لینک کردن، آنچه را که استفاده شده است ثبت میکند. فایل باینری شما یک بخش .gnu.version_r دریافت میکند که در عمل میگوید: «من به GLIBC_2.38 از libc.so.6 نیاز دارم». یک نسخه قدیمیتر از libc هرگز آن برچسب را نداشته است، بنابراین لودر پیش از اجرای main متوقف میشود. هیچ مکانیزم بازگشتی (fallback) وجود ندارد، زیرا بازگشت به معنای ارائه بیسروصدای تابعی متفاوت از آنچه برنامه با آن کامپایل شده است، خواهد بود.
رایجترین عامل محرک از سال 2021، نسخه glibc 2.34 است. این نسخه، libpthread و libdl را در libc ادغام کرد و __libc_start_main، یعنی تابعی که هر برنامه C را آغاز میکند، به برچسب GLIBC_2.34 منتقل نمود. بنابراین، یک برنامه hello-world که روی هر توزیعی با glibc 2.34 یا جدیدتر کامپایل شده باشد، نیازمند GLIBC_2.34 است، حتی اگر کد منبع هیچ تابع مدرنی را فراخوانی نکند. به همین دلیل است که این مشکل بهطور ناگهانی برای کسانی که کدشان سالها تغییری نکرده بود، ظاهر شد.
توزیع من از کدام نسخه glibc استفاده میکند؟
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
}
]اینها نسخههایی هستند که هر توزیع در مخازن خود ارائه میدهد؛ این اطلاعات بر اساس دادههای منتشرشده توسط توزیعها و وضعیت آنها در اوت 2026 است. عمر CentOS 7 مدتهاست که به پایان رسیده، اما همچنان در این لیست باقی مانده است زیرا سرورهای قدیمی هنوز از آن استفاده میکنند. هر سیستمی که در اختیار دارید را با دستور ldd --version بررسی کنید که نسخه glibc را در خط اول چاپ میکند، یا از دستور getconf GNU_LIBC_VERSION استفاده نمایید.
این 9 ردیف را مانند یک نردبان در نظر بگیرید. اگر برنامه را روی Debian 13 با نسخه glibc 2.41 کامپایل کنید، خروجی روی هیچ سیستمعاملی قدیمیتر از Debian 13 اجرا نخواهد شد. اگر برنامه را روی Ubuntu 20.04 با نسخه glibc 2.31 کامپایل کنید، همان فایل اجرایی روی تمام ردیفهای بالاتر از آن خط، از جمله Debian 13، اجرا میشود. قدیمیترین سروری که باید پشتیبانی کنید، تنها میزبان کامپایل (build host) مهم است؛ بنابراین توزیعی که به عنوان استاندارد انتخاب میکنید، کفِ ABI برای تمام نرمافزارهایی که برای آن کامپایل میکنید را تعیین میکند. بهتر است پیش از شکلگیری زیرساخت، در کنار سایر ملاحظات مربوط به انتخاب سیستمعامل برای VPS، در این مورد تصمیمگیری کنید.
چگونه حداقل نسخه glibc مورد نیاز یک فایل باینری را پیدا کنم؟
این اطلاعات را از داخل فایل بخوانید. این روش روی هر فایلی کار میکند، از جمله فایلهای باینری که فروشنده بدون هیچ یادداشت ساخت (build notes) به شما تحویل داده است.
objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5آخرین خط نشاندهنده حداقل نسخه (floor) است. اگر objdump موجود نیست، بسته binutils را نصب کنید. اگر امکان نصب هیچ بستهای روی آن سیستم وجود ندارد، strings -a ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5 نتیجه نزدیکی به شما میدهد، زیرا این برچسبها به صورت رشتههای متنی ساده درون فایل ذخیره شدهاند.
دستور readelf -V ./mytool همان نیازمندیها را با ساختار مشخص نمایش میدهد. به دنبال بلوکی بگردید که با Version needs section '.gnu.version_r' شروع میشود. این دستور به ازای هر برچسب، یک خط Name: GLIBC_x.y را فهرست میکند که ذیل کتابخانهای که باید آن را فراهم کند، گروهبندی شده است.
برای اینکه بفهمید کدام تابع باعث تعیین این حداقل نسخه شده است، برچسب مورد نظر را با grep جستجو کنید: objdump -T ./mytool | grep GLIBC_2.38. این معمولاً یک نماد (symbol) تکی است و گاهی تابعی است که میتوانید از آن صرفنظر کنید. اگر objdump -T هیچ خروجیای چاپ نکرد، یعنی فایل باینری به صورت استاتیک (statically linked) لینک شده است؛ بنابراین جدول نمادهای پویا (dynamic symbol table) ندارد و هیچ نیازمندی نسخهای برای نمایش وجود ندارد.
دستورات file و readelf -d درباره فایل باینری که دریافت کردهاید چه میگویند
دستور file معماری و نوع لینکشدن فایل را در یک خط نمایش میدهد.
file ./mytoolیک بیلد معمولی glibc به این صورت خوانده میشود:
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 strippedیک بیلد استاتیک عبارت statically linked را نشان میدهد و هیچ مفسری (interpreter) را نام نمیبرد. یک بیلد musl مفسر متفاوتی را ذکر میکند:
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, ...مفسر، فیلدی است که اهمیت دارد. این همان لودر پویا (dynamic loader) است که هسته سیستمعامل پیش از اجرای برنامه شما آن را اجرا میکند و آن مسیر دقیق باید روی ماشین مقصد وجود داشته باشد. مسیر /lib/ld-musl-x86_64.so.1 روی سرورهای Debian یا Ubuntu وجود ندارد، مگر اینکه شخصی musl را در آنجا نصب کرده باشد.
دستور readelf -d ./mytool کتابخانههای اشتراکی مورد نیاز باینری را بر اساس نام لیست میکند:
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]هر ورودی NEEDED یک الزام قطعی است که با soname مطابقت داده میشود. عبارت libssl.so.3 مربوط به OpenSSL 3 است، بنابراین آن باینری روی سروری که فقط libssl.so.1.1 دارد اجرا نخواهد شد و دلیل آن همان soname است: تفاوت در soname به معنای ناسازگاری عمدی در ABI است. متغیر RUNPATH به لودر میگوید که جستجو را از کجا آغاز کند و $ORIGIN به دایرکتوری حاوی فایل باینری گسترش مییابد؛ این همان روشی است که یک بسته خودکفا (self-contained bundle) کتابخانههای خود را پیدا میکند.
هرگز ldd را روی باینریهایی که به آنها اعتماد ندارید اجرا نکنید. در glibc، دستور ldd میتواند وابستگیها را با اجرای برنامه تحت لودر حل کند، بنابراین یک فایل مخرب فرصت اجرای کد پیدا میکند. دستورات readelf و objdump فقط بایتها را میخوانند. پیش از انجام هر یک از این مراحل، تأیید کنید که فایل دانلود شده همان فایلی است که پروژه منتشر کرده است، زیرا تأیید دانلود با استفاده از چکسام منتشر شده تنها مرحلهای است که به شما میگوید بایتهای چه کسی را در اختیار دارید.
خطاهایی که واقعاً مشاهده میکنید و معنای هر یک
لودر به نسخهای از GLIBC اشاره میکند که نمیتواند آن را پیدا کند. فایل باینری با نسخهای از glibc کامپایل شده که جدیدتر از نسخه موجود در سیستم مقصد است. هیچچیز نصبشدهای روی سیستم مقصد این مشکل را بهصورت ایمن حل نمیکند. یکی از چهار گزینه زیر را انتخاب کنید.
شل برای فایلی که بهوضوح آن را میبینید، خطای No such file or directory گزارش میدهد. مورد مفقود، مفسر (interpreter) است، نه فایل باینری. دستور execve زمانی که مسیر لودر در هدر ELF وجود ندارد، ENOENT برمیگرداند و شل تنها پیامی که در اختیار دارد را چاپ میکند. دستور file را اجرا کنید و مسیر مفسر را بخوانید. یک فایل باینری لینکشده با musl روی سروری که فقط glibc دارد، دقیقاً همین نشانه را بروز میدهد.
cannot execute binary file: Exec format error به این معناست که معماری اشتباه است: یک فایل باینری x86-64 روی سرور arm64 یا برعکس. خط اول خروجی file به شما میگوید که با چه چیزی سروکار دارید.
error while loading shared libraries: libssl.so.3: cannot open shared object file به این معناست که یک کتابخانه NEEDED وجود ندارد یا با یک soname متفاوت موجود است. بسته توزیع منطبق را نصب کنید. نام بستهها در خانوادههای مختلف توزیعها متفاوت است، بنابراین پیش از کپی کردن یک خط apt از فایل README، آن را ترجمه کنید؛ معادلهای دستورات dnf و apt این نگاشت را پوشش میدهند.
یک خطای Segmentation fault ساده از یک بیلد musl که تحت glibc بهخوبی اجرا میشود. معمولاً مربوط به اندازه stack ترد است که در گزینه 2 پوشش داده شده است.
چهار روش برای ارائه یک باینری قابلحمل لینوکس
هر پاسخ هزینههای واقعی خاص خود را دارد. انتخاب خود را بر اساس عملکرد برنامه در زمان اجرا انجام دهید، نه بر اساس اینکه کدام روش تمیزتر به نظر میرسد.
گزینه 1: ساخت روی قدیمیترین توزیعی که پشتیبانی میکنید
این کسلکنندهترین پاسخ و معمولاً درستترین آنهاست. برنامه را درون یک image کانتینر از قدیمیترین توزیعی که وعده پشتیبانی از آن را دادهاید، کامپایل کنید. linker فقط میتواند تگهایی را ثبت کند که glibc قدیمی واقعاً دارد؛ بنابراین کف نسخه به آن release کاهش مییابد، در حالی که binary همچنان یک binary پویا (dynamic) عادی باقی میماند و تمام رفتارهای glibc را حفظ میکند.
docker run --rm -v "$PWD:/src" -w /src ubuntu:20.04 sh -c 'apt-get update && apt-get install -y build-essential && make'خروجی روی glibc 2.31 و تمام نسخههای بالاتر از آن اجرا میشود. بهجای فرض کردن، بررسی کنید: دستور یکخطی objdump -T که پیشتر ذکر شد را روی نتیجه اجرا کنید و مطمئن شوید که بالاترین تگ، همان تگی است که انتظار داشتید.
هزینه این کار، toolchain است. یک base image قدیمی، یک کامپایلر قدیمی نیز به همراه دارد که وقتی کد شما به استانداردهای جدید C++ نیاز دارد، مشکلساز میشود. زبانهای Go و Rust عمدتاً از این مشکل دور هستند، زیرا toolchain آنها مستقل از بستههای توزیع، درون کانتینر نصب میشود. برای C و C++ میتوانید یک کامپایلر جدیدتر از کانال toolchain خودِ توزیع اضافه کنید، یا از imageهای manylinux استفاده کنید که دقیقاً برای ترکیب یک glibc باستانی با یک GCC بهروز ایجاد شدهاند. کامپایلر C همراه Zig نیز میتواند مستقیماً یک glibc خاص را هدف قرار دهد، همانطور که در zig cc -target x86_64-linux-gnu.2.28 آمده است؛ این کار بدون نیاز به نگهداری یک image قدیمی، همان کف نسخه را برای شما فراهم میکند.
گزینه 2: لینک کردن ایستا با musl
کتابخانه musl یک کتابخانه کوچک C است که با هدف لینک کردن ایستا (static linking) طراحی شده است. یک فایل باینری ایستا که با musl ساخته شده، شامل libc اختصاصی خود است، هیچ مفسری (interpreter) را فراخوانی نمیکند و روی هر هسته لینوکسی با معماری سازگار اجرا میشود. اکثر ابزارهایی که به صورت یک فایل واحد برای دانلود ارائه میشوند، به این روش ساخته شدهاند.
sudo apt install -y musl-tools
musl-gcc -static -O2 hello.c -o hello
file ./helloخروجی file اکنون باید statically linked را نشان دهد، بدون اینکه فیلد interpreter وجود داشته باشد. برای Rust، هدف (target) مورد نظر را اضافه کرده و با آن بیلد کنید:
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-muslزبان Go به هیچکدام از این موارد نیاز ندارد. با تنظیم CGO_ENABLED=0 go build، خروجی بهصورت پیشفرض ایستا است و اصلاً به هیچ libc لینک نمیشود.
جستجوهای NSS تغییر میکنند. کتابخانه glibc کاربران و نامهای میزبان را از طریق NSS (سوییچ سرویس نام) حل میکند که در حین اجرای برنامه، ماژولهای libnss_* را با استفاده از dlopen بارگذاری میکند. یک برنامه glibc که بهصورت ایستا لینک شده باشد نمیتواند این کار را انجام دهد و لینکر با این پیام به شما هشدار میدهد:
warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linkingکتابخانه musl با عدم پیادهسازی NSS، از این هشدار عبور میکند. این کتابخانه حلکننده (resolver) اختصاصی خود را دارد و فایلهای /etc/resolv.conf و /etc/hosts را مستقیماً میخواند. در یک VPS معمولی، این موضوع مشکلی ایجاد نمیکند و حتی سادهتر است. اما در میزبانی که حسابهای کاربری یا نامها از طریق LDAP یا SSSD مدیریت میشوند، باینری شما آنها را نخواهد دید، در حالی که سایر برنامههای موجود در سیستم به آنها دسترسی دارند. حلکننده musl همچنین نسبت به glibc جدیدتر است: قابلیت بازگشت به TCP برای پاسخهای DNS بزرگتر از 512 بایت در نسخه 1.2.4 از musl در سال 2023 اضافه شد؛ بنابراین بیلدها با نسخههای قدیمیتر musl، پاسخهای بزرگ را کوتاه (truncate) میکنند.
دستور dlopen کار نمیکند. در یک باینری ایستا با musl، دستور dlopen یک stub است که همیشه با شکست مواجه میشود. هر چیزی که کد را در زمان اجرا بارگذاری کند، غیرقابل استفاده است: سیستمهای پلاگین، ماژولهای PAM، درایورهای GPU و ماژولهای مجموعه کاراکتر iconv خودِ glibc. اگر برنامه شما به dlopen نیاز دارد، لینک کردن ایستا منتفی است و باید از یکی از سه روش دیگر استفاده کنید.
بهروزرسانیهای امنیتی بر عهده شماست. یک باینری که بهصورت پویا لینک شده، به محض اینکه سرور بستههای خود را ارتقا دهد، اصلاحات libc را دریافت میکند. اما یک باینری ایستا هرگز بهروز نمیشود. هنگامی که یک CVE (آسیبپذیریها و مواجهههای رایج) برای libc شما یا یک OpenSSL ایستا که همراه برنامه بستهبندی کردهاید منتشر شود، شما باید برنامه را دوباره بیلد و توزیع کنید. تمام نسخههایی که قبلاً مستقر شدهاند تا زمانی که کسی فایل را جایگزین نکند، آسیبپذیر باقی میمانند. لیستی از آنچه لینک کردهاید نگه دارید، زیرا در مقصد هیچ ابزاری وجود ندارد که به مدیر سیستم بگوید باینری یکفایلی شما حاوی کتابخانهای از دو سال پیش است.
مجوز (Licence) تغییر میکند. کتابخانه glibc تحت مجوز LGPL است و لینک کردن ایستا، تعهد بازلینک (relinking) را در LGPL فعال میکند: شما باید به دریافتکنندگان، آنچه برای بازلینک کردن برنامه شما با یک glibc متفاوت نیاز دارند را ارائه دهید. کتابخانه musl تحت مجوز MIT است و چنین شرطی ندارد. این دلیل اصلی انتخاب musl به جای glibc ایستا در پروژههایی است که باینریهای تکفایلی ارائه میدهند.
دو تفاوت در زمان اجرا باعث کرشهای گیجکننده میشود. پشته (stack) پیشفرض تردها در musl برابر با 128 KiB است، در حالی که در glibc برابر با 8 MiB است؛ بنابراین کدی که بافر بزرگی را روی پشته ترد قرار میدهد، بدون هیچ پیام یا لاگی دچار segfault میشود. اندازه پشته را بهطور صریح با pthread_attr_setstacksize تنظیم کنید یا بافر را به heap منتقل کنید. تخصیصدهنده حافظه (allocator) در musl نیز برای اندازه کوچک و رفتار قابل پیشبینی نوشته شده است، نه برای تعداد زیادی ترد که همزمان حافظه تخصیص میدهند؛ بنابراین برنامههای تردشدهای که تخصیص حافظه زیادی دارند، ممکن است بهطور محسوسی کندتر اجرا شوند. بهجای اعتماد به شهرت هر یک از این کتابخانهها، بار کاری خود را بنچمارک بگیرید.
گزینه 3: بستهبندی لودر و کتابخانهها
کتابخانهها را در کنار فایل باینری و به همراه لودر منطبق با آنها قرار دهید، سپس برنامه را از طریق همان لودر اجرا کنید.
./ld-linux-x86-64.so.2 --library-path ./lib ./mytoolبرای دائمی کردن این تنظیمات، مسیرها را با استفاده از patchelf در فایل بنویسید:
patchelf --set-interpreter "$PWD/lib/ld-linux-x86-64.so.2" --set-rpath '$ORIGIN/lib' ./mytoolیک قانون تعیین میکند که آیا این روش کار میکند یا خیر: لودر و libc.so.6 باید از یک نسخه glibc یکسان باشند. آنها یک جفت منطبق هستند و ترکیب لودر میزبان با یک libc بستهبندیشده، بهجای نمایش یک خطای خوانا، باعث کرش کردن برنامه در هنگام شروع میشود. هر دو را با هم بستهبندی کنید یا هیچکدام را بستهبندی نکنید.
AppImage همین الگو را بستهبندی میکند؛ با این تفاوت که محموله (payload) در یک ایمیج squashfs قرار دارد و یک runtime کوچک آن را mount میکند. این ابزار یک ویژگی دارد که معمولاً نادیده گرفته میشود: AppImage شامل glibc نیست، بنابراین یک AppImage که روی توزیعهای جدید ساخته شده، همچنان روی سرورهای قدیمی با همان خطای نسخه مواجه میشود. توصیه خودِ AppImage این است که برنامه را روی قدیمیترین توزیعی که پشتیبانی میکنید بسازید؛ این یعنی AppImage یک فرمت تحویل است که روی گزینه 1 لایهبندی شده، نه جایگزینی برای آن.
در سرورهای بدون رابط گرافیکی (headless)، ابزار AppImage برای mount کردن محموله خود به FUSE (سیستمفایل در فضای کاربری) نیاز دارد و ایمیجهای حداقلی VPS اغلب آن را ندارند. خطا با نام libfuse.so.2 ظاهر میشود. شما میتوانید با استفاده از ./App.AppImage --appimage-extract-and-run بهطور کامل از مرحله mount صرفنظر کنید؛ این دستور محتویات را در یک دایرکتوری موقت استخراج کرده و از همانجا اجرا میکند.
گزینه 4: ارائه به صورت Container image
کل محیط کاربری (userland) را به همراه برنامه منتقل کنید. این image کتابخانه libc اختصاصی خود را به همراه دارد، بنابراین نسخه glibc میزبان دیگر اهمیتی ندارد و تنها کافی است هسته سیستمعامل (kernel) و معماری سختافزار با هم سازگار باشند. این سادهترین راهکار است و کل ردهٔ مشکلات مربوط به وابستگیها را حذف میکند؛ به همین دلیل است که بخش بزرگی از نرمافزارهای سرور به این روش توزیع میشوند. اجرای Docker روی VPS روش معمول برای استفاده از این مدل است.
هسته سیستمعامل میزبان همچنان محدودیتهایی ایجاد میکند. نسخههای جدیدتر glibc از system callهای جدیدتری استفاده میکنند و یک میزبان قدیمی ممکن است آنها را مسدود کند: glibc 2.34 و نسخههای پس از آن از clone3 استفاده میکنند که پروفایلهای پیشفرض قدیمی seccomp آنها را رد میکنند. نشانه این مشکل، شکست فوری برنامه با خطای Operation not permitted است، بدون آنکه هیچ اشارهای به glibc در پیام خطا وجود داشته باشد. ارتقای container runtime روی میزبان این مشکل را حل میکند، زیرا این مسدودسازی در فیلتر syscall مربوط به runtime اعمال شده است، نه در خود هسته.
هزینههای این روش معمول است. مقصد به یک container runtime و مجوز استفاده از آن نیاز دارد. حجم فایل دانلودی شما از یک فایل به دهها یا صدها مگابایت افزایش مییابد. اکنون شما مسئول یک base image و برنامه زمانبندی وصلههای آن هستید، بنابراین وظایف امنیتی که در گزینه 2 از آنها اجتناب کرده بودید، در قالب بازسازی (rebuild) تصاویر بازمیگردند. برای یک سرویس که طولانیمدت اجرا میشود، این معاملهای منصفانه است. اما برای یک ابزار خط فرمان که کاربر فقط یکبار آن را اجرا میکند، چنین نیست.
کدام گزینه را باید انتخاب کنید؟
- یک ابزار داخلی برای سرورهایی که تحت کنترل شما هستند و همگی از یک توزیع استفاده میکنند: برنامه را بهصورت پویا (dynamically) روی همان توزیع بسازید و به همان بسنده کنید.
- یک فایل واحد که افراد ناشناس دانلود و اجرا میکنند: از musl ایستا (static) استفاده کنید، به شرطی که برنامه به
dlopenنیاز نداشته باشد و نیازی به جستجوهای مبتنی بر NSS نداشته باشد. - برنامهای با افزونهها یا دسترسی به GPU: از حالت پویا استفاده کنید، روی یک نسخه پایه قدیمی بسازید و برای توزیع آن از گزینه 3 استفاده کنید.
- یک سرویس طولانیمدت روی ماشینی که از قبل runtime دارد: image را ارسال کنید.
هر گزینهای که انتخاب میکنید، آن را مستند کرده و بررسی کنید. نسخه glibc میزبانِ ساخت (build host)، اکنون بخشی از فرایند انتشار شماست. ارتقای ماشینِ ساخت از یک نسخه LTS به نسخه بعدی، بهطور نامحسوس حداقلِ نیازمندیها را بالا میبرد و باعث خرابی برنامههای کاربرانی میشود که تا ماه گذشته مشکلی نداشتند. image ساخت را با استفاده از tag ثابت (pin) کنید و بالاترین tag نسخه GLIBC_ را در خروجی به عنوان یک مرحله از ساخت تأیید کنید تا در صورت بروز خطا، این موضوع در pipeline شما مشخص شود، نه در ترمینال کاربران دیگر.
FAQ
چرا در سرور خود با خطای "version GLIBC_2.38 not found" مواجه میشوم؟
فایل باینری روی سیستمی کامپایل شده است که نسخه glibc آن جدیدتر از سرور شماست. سیستم نسخهگذاری نمادهای glibc فقط سازگاری رو به جلو را تضمین میکند: فایلهای باینری قدیمی روی glibc جدید اجرا میشوند، اما فایلهای باینری جدید روی glibc قدیمی اجرا نمیشوند؛ زیرا لودر به برچسب نسخه دقیق ثبتشده در فایل نیاز دارد و libc قدیمی فاقد آن برچسب است. هیچچیز که روی سرور نصب کنید این مشکل را بهصورت ایمن حل نمیکند. برنامه را روی یک سیستم پایه قدیمیتر بازسازی کنید، یک build استاتیک با musl ارائه دهید، کتابخانهها را به همراه لودر منطبق بر آنها بستهبندی کنید یا یک image کانتینر ارائه دهید.
چگونه بفهمم یک فایل باینری به کدام نسخه از glibc نیاز دارد؟
برچسبهای نسخه را با استفاده از objdump -T ./mytool | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5 از فایل استخراج کنید. آخرین خط، پایینترین نسخه glibc مورد نیاز برای لود کردن آن است. دستور readelf -V ./mytool همان نیازمندیها را در بخش .gnu.version_r و دستهبندیشده بر اساس کتابخانهای که باید آنها را فراهم کند، نشان میدهد. اگر objdump -T خروجی خالی برگرداند، فایل باینری استاتیک است و هیچ نیازی به glibc ندارد. نتیجه را با نسخه موجود روی سرور خود که از طریق ldd --version قابل مشاهده است، مقایسه کنید.
آیا فایل باینری musl از فایل باینری glibc کندتر است؟
این موضوع به نوع بار کاری بستگی دارد و پاسخ صادقانه این است که باید اندازهگیری کنید. یک فایل باینری استاتیک musl سریعتر اجرا میشود، زیرا لودری برای اجرا وجود ندارد و در زمان exec نیازی به relocation نیست. در مقابل، تخصیصدهنده حافظه (allocator) در musl برای حجم کم و رفتار قابل پیشبینی طراحی شده است، نه برای تعداد زیادی ترد که همزمان در حال تخصیص حافظه هستند. همچنین musl روتینهای رشتهای و حافظه کمتری نسبت به glibc دارد که با دست بهینهسازی شدهاند؛ بنابراین برنامههای ترد-محوری که تخصیص حافظه یا پردازش رشتهای سنگینی دارند، ممکن است بهطور محسوسی کندتر باشند. پیش از پذیرش هر ادعایی، برنامه خود را روی سرور خود بنچمارک کنید.
چرا bash برای فایلی که وجود دارد، خطای "No such file or directory" میدهد؟
فایل گمشده، لودر پویا (dynamic loader) است، نه فایل باینری شما. هسته سیستمعامل مسیر مفسر را از هدر ELF میخواند و در صورت نبود آن مسیر، خطای ENOENT را برمیگرداند و شل نیز آن را با تنها پیامی که در اختیار دارد گزارش میکند. دستور file ./mytool را اجرا کنید و فیلد interpreter را بخوانید. اگر در یک سرور Debian یا Ubuntu عبارت /lib/ld-musl-x86_64.so.1 را مشاهده کردید، یعنی یک فایل باینری لینکشده با musl را روی سیستمی با glibc اجرا کردهاید؛ بنابراین به نسخه سازگار با musl یا یک build استاتیک نیاز دارید.
آیا میتوانم برای حل این مشکل، فایل libc.so.6 را از یک سرور جدیدتر کپی کنم؟
خیر. فایلهای libc.so.6 و ld-linux-x86-64.so.2 یک جفت منطبق از یک build خاص glibc هستند و تمام پردازشهای سیستم از آنها استفاده میکنند. بازنویسی نسخه سیستمی میتواند باعث شود سرور دیگر قادر به اجرای هیچ برنامهای نباشد، از جمله ابزارهایی که برای بازگرداندن تغییرات به آنها نیاز دارید. اگر مجبورید یک فایل باینری جدیدتر را روی یک میزبان قدیمی اجرا کنید، glibc جدیدتر را در یک دایرکتوری مجزا استخراج کرده و آن برنامه را از طریق لودر مخصوص خودش با --library-path اجرا کنید؛ این کار فقط روی همان پردازش تأثیر میگذارد. بازسازی برنامه با glibc قدیمیتر همچنان بهترین راهکاری است که شش ماه بعد شما را غافلگیر نخواهد کرد.