О статическом связывании libc в UNIX
2012-12-04 02:48 pmМожно ли линковать libc статически в UNIX?
Это зависит от типа libc и от типа UNIX, а также от лицензии на ваш код
Сначала разберёмся с основами.
1. Разница между Windows и UNIX в части запуска процесса и загрузки динамических библиотек
В UNIX есть два типа исполняемых файлов: статически скомпонованные и динамически скомпонованные. Статически скомпонованные файлы включают в себя libc и все остальные необходимые библиотеки, при этом вызовы ядра они делают напрямую с помощью процессорных инструкций "int 0x80", "sysenter" или "syscall". Динамически скомпонованные файлы просто вызывают libc и другие библиотеки, если им нужно сделать системный вызов. Они начинают своё выполнение с загрузчика (/lib/ld-linux.so.N или /libexec/ld-elf.so.N), ссылка на который указана в их ELF-заголовке. Ядро передаёт загрузчику исполняемый файл уже в открытом виде, его файловый дескриптор передаётся в auxiliary vector по индексу AT_EXECFD. Работа загрузчика — замапить страницы, распарсить ELF-файл и загрузить данные с диска, задать флаги страниц, повторить всё то же самое для разделяемых библиотек и их зависимостей и передать управление точке входа основного ELF-файла. Загрузчик остаётся в памяти как ещё одна динамическая библиотека, так что его интерфейс доступен для основного исполняемого файла в виде функций dlopen(3), dlfunc(3) и т.д., а если загрузчика не было (статически скомпонованный исполняемый файл), то и функции эти не доступны.
В Windows NT нельзя создать статический исполняемый файл в UNIX-смысле. В памяти процесса всегда будет присутствовать NTDLL.DLL и далее, в зависимости от подсистемы, другие DLL (например для подсистемы Win32 — KERNEL32.DLL). NTDLL получает управление от ядра при создании процесса, она же создаёт первый SEH-фрейм и содержит загрузчик для PE-файлов (группа функций Ldr*).
Конечно, в винде бывают и более высокоуровневые "компиляторные" libc, такие как MSVCRT.DLL (Windows 95 OSR2+), CRTDLL.DLL (ещё древнее, Win32s); MSVCRnm[D].DLL (более новые версии Visual Studio n.m с опциональной отладкой). Однако никто не мешает использовать компилятор фирмы Borland со своими libc, или вообще избавиться от зависимости на них, скомпоновав свой exe-файл статически. Однако от зависимости от KERNEL32.DLL и NTDLL.DLL избавиться никак невозможно, и в общем эти два файла выполняют в винде ту же роль, что и libc в UNIX. В том числе, в винде невозможно избавиться от загрузчика (Ldr*-функции NTDLL.DLL), загрузчик всегда находится в памяти процесса. Клиентские функции загрузчика LoadLibrary и GetProcAddress доступны всегда, если ваше приложение относится к подсистеме Win32. В винде приложения никогда не делают прямых вызовов ядра с помощью процессорных инструкций "int 0x2E", "sysenter" или "syscall", а чтобы эта шальная мысль не пришла в чью-то дурную голову, номера системных вызовов периодически меняются (это последовательные целые числа в алфавитном списке системных вызов NT Native API, в очередной версии винды, сервиспаке или хотфиксе список раздвигается в середине — и номера всех последующих функций инкрементируются). В этом смысле все исполняемые файлы в Windows используют динамическую libc.
2. Разница между BSD-системами и Linux в части функциональности libc
В Linux'е glibc(eglibc) — это просто монстр. Помимо собственно libc, она включает libiconv (конверсию кодировок), libidn (поддержка доменных имён с неанглийскими буквами), librt (сигналы реального времени, точные часы, aio, POSIX mq, SysV shm), поддержку locale, полный набор файлов данных и утилит для работы с time zones, в т.ч. часто меняющиеся правила перевода часов на летнее/зимнее время для каждой страны, две реализации pthreads (1:1 nptl и N:M linuxthreads), libthread_db, библиотеку libresolv (DNS-клиент, преобразование доменных имён в IP-адреса), libnsl (SUN RPC, X/Open Transport Interface (XTI) и т.п.), а также криптографические функции MD5, SHA-256 и SHA-512, поддержу профилирования и т.д.
В операционной системе FreeBSD libc минималистична. Весь дополнительный функционал вынесен в отдельные библиотеки.
Почему так? Части "большой libc" очень тесно переплетены друг с другом. Разные части от разных версий "большой libc" нельзя смешивать. Поэтому shipping "большой libc" должен происходить одним пакетом, а вся она должна находиться в одном репозитарии. В случае с FreeBSD это возможно (вся base system, включая ядро, находится в одном репозитарии), в случае Linux приходится укрупнять саму libc.
3. Почему в Linux'е статическая компоновка glibc нормально не работает
Потому что линуксовая "большая libc" слишком долго бы инициализировалась для HelloWorld-приложения или например CGI-обработчика. Или создавала бы проблемы таким процессам, как init(8). По этой причине часть её функционала (например iconv/gconv) вынесена в .so-библиотеки, которые динамически догружаются с помощью dlopen(3) по мере надобности. Или не загружаются, если glibc статически связана и загрузчика нет.
Можно ли сделать так, чтобы dlopen(3) работала из статически скомпонованных бинарей? Можно! Но рассмотрим такой пример. Вы статически привязываетесь к glibc, в т.ч. к libstdc++, затем вызываете dlopen(3) для third-party so-библиотеки и она каким-то чудом срабатывает. Эта third-party so-библиотека тоже привязывается к libstdc++, только к динамическому её варианту (libstdc++.so). Получается две копии libstdc++ в адресном пространстве вашего процесса, с разным связыванием, и потенциально разных версий. С этого момента очень интересно будут себя вести глобальные переменные типа std::cout или, например, обработка исключений, пересекающих границы библиотеки и приложения. Также у вас будет две "чисто сишных" glibc в одном процессе: два объекта stdio, два списка atexit, два malloc-пула, война за TLS и так далее. Приедет ли автобус (SIGBUS) или же вы получите банальный SIGSEGV, я думаю, безразлично.
Кроме того, GNU libc находится под лицензией LGPL. Суть этой лицензии заключается в том, что пользователь имеет свободу компоновки (например, берёт ваш проприетарный бинарь, пробует на старой версии glibc с ошибками; обновляет свою версию glibc, чтобы исправить ошибки; и запускает ваш бинарь без перелинковки с помощью ld). Если же вы статически скомпоновались с GNU libc, тогда вы нарушаете права пользователя на свободу замены glibc.
Статическая компоновка с GNU libc вводит всё ваше приложение в сферу действия лицензии LGPL (или более сильной: GPL, AGPL на ваш выбор), хотите вы того, или нет. Таким образом, вы обязаны будете предоставить исходники, или же все ваши *.o-файлы для статической перелинковки в пользовательской среде с новой версией glibc.
4. Работает ли в BSD статическая компоновка libc
Да! Нет проблем ни с отложенной загрузкой зависимостей, ни с лицензией. Функция dlopen(3) при статической компоновке с libc в BSD просто честно не работает:
#pragma weak dlopen
void *
dlopen(const char *name, int mode)
{
_rtld_error(sorry);
return NULL;
}
5. Можно ли в Linux'е статически компоновать non-GNU libc (dietlibc, bionic libc, newlib и так далее)? Будет ли работать dlopen(3) в этом случае?
А хрен его знает.
Это зависит от типа libc и от типа UNIX, а также от лицензии на ваш код
Сначала разберёмся с основами.
1. Разница между Windows и UNIX в части запуска процесса и загрузки динамических библиотек
В UNIX есть два типа исполняемых файлов: статически скомпонованные и динамически скомпонованные. Статически скомпонованные файлы включают в себя libc и все остальные необходимые библиотеки, при этом вызовы ядра они делают напрямую с помощью процессорных инструкций "int 0x80", "sysenter" или "syscall". Динамически скомпонованные файлы просто вызывают libc и другие библиотеки, если им нужно сделать системный вызов. Они начинают своё выполнение с загрузчика (/lib/ld-linux.so.N или /libexec/ld-elf.so.N), ссылка на который указана в их ELF-заголовке. Ядро передаёт загрузчику исполняемый файл уже в открытом виде, его файловый дескриптор передаётся в auxiliary vector по индексу AT_EXECFD. Работа загрузчика — замапить страницы, распарсить ELF-файл и загрузить данные с диска, задать флаги страниц, повторить всё то же самое для разделяемых библиотек и их зависимостей и передать управление точке входа основного ELF-файла. Загрузчик остаётся в памяти как ещё одна динамическая библиотека, так что его интерфейс доступен для основного исполняемого файла в виде функций dlopen(3), dlfunc(3) и т.д., а если загрузчика не было (статически скомпонованный исполняемый файл), то и функции эти не доступны.
В Windows NT нельзя создать статический исполняемый файл в UNIX-смысле. В памяти процесса всегда будет присутствовать NTDLL.DLL и далее, в зависимости от подсистемы, другие DLL (например для подсистемы Win32 — KERNEL32.DLL). NTDLL получает управление от ядра при создании процесса, она же создаёт первый SEH-фрейм и содержит загрузчик для PE-файлов (группа функций Ldr*).
Конечно, в винде бывают и более высокоуровневые "компиляторные" libc, такие как MSVCRT.DLL (Windows 95 OSR2+), CRTDLL.DLL (ещё древнее, Win32s); MSVCRnm[D].DLL (более новые версии Visual Studio n.m с опциональной отладкой). Однако никто не мешает использовать компилятор фирмы Borland со своими libc, или вообще избавиться от зависимости на них, скомпоновав свой exe-файл статически. Однако от зависимости от KERNEL32.DLL и NTDLL.DLL избавиться никак невозможно, и в общем эти два файла выполняют в винде ту же роль, что и libc в UNIX. В том числе, в винде невозможно избавиться от загрузчика (Ldr*-функции NTDLL.DLL), загрузчик всегда находится в памяти процесса. Клиентские функции загрузчика LoadLibrary и GetProcAddress доступны всегда, если ваше приложение относится к подсистеме Win32. В винде приложения никогда не делают прямых вызовов ядра с помощью процессорных инструкций "int 0x2E", "sysenter" или "syscall", а чтобы эта шальная мысль не пришла в чью-то дурную голову, номера системных вызовов периодически меняются (это последовательные целые числа в алфавитном списке системных вызов NT Native API, в очередной версии винды, сервиспаке или хотфиксе список раздвигается в середине — и номера всех последующих функций инкрементируются). В этом смысле все исполняемые файлы в Windows используют динамическую libc.
2. Разница между BSD-системами и Linux в части функциональности libc
В Linux'е glibc(eglibc) — это просто монстр. Помимо собственно libc, она включает libiconv (конверсию кодировок), libidn (поддержка доменных имён с неанглийскими буквами), librt (сигналы реального времени, точные часы, aio, POSIX mq, SysV shm), поддержку locale, полный набор файлов данных и утилит для работы с time zones, в т.ч. часто меняющиеся правила перевода часов на летнее/зимнее время для каждой страны, две реализации pthreads (1:1 nptl и N:M linuxthreads), libthread_db, библиотеку libresolv (DNS-клиент, преобразование доменных имён в IP-адреса), libnsl (SUN RPC, X/Open Transport Interface (XTI) и т.п.), а также криптографические функции MD5, SHA-256 и SHA-512, поддержу профилирования и т.д.
В операционной системе FreeBSD libc минималистична. Весь дополнительный функционал вынесен в отдельные библиотеки.
Почему так? Части "большой libc" очень тесно переплетены друг с другом. Разные части от разных версий "большой libc" нельзя смешивать. Поэтому shipping "большой libc" должен происходить одним пакетом, а вся она должна находиться в одном репозитарии. В случае с FreeBSD это возможно (вся base system, включая ядро, находится в одном репозитарии), в случае Linux приходится укрупнять саму libc.
3. Почему в Linux'е статическая компоновка glibc нормально не работает
Потому что линуксовая "большая libc" слишком долго бы инициализировалась для HelloWorld-приложения или например CGI-обработчика. Или создавала бы проблемы таким процессам, как init(8). По этой причине часть её функционала (например iconv/gconv) вынесена в .so-библиотеки, которые динамически догружаются с помощью dlopen(3) по мере надобности. Или не загружаются, если glibc статически связана и загрузчика нет.
Можно ли сделать так, чтобы dlopen(3) работала из статически скомпонованных бинарей? Можно! Но рассмотрим такой пример. Вы статически привязываетесь к glibc, в т.ч. к libstdc++, затем вызываете dlopen(3) для third-party so-библиотеки и она каким-то чудом срабатывает. Эта third-party so-библиотека тоже привязывается к libstdc++, только к динамическому её варианту (libstdc++.so). Получается две копии libstdc++ в адресном пространстве вашего процесса, с разным связыванием, и потенциально разных версий. С этого момента очень интересно будут себя вести глобальные переменные типа std::cout или, например, обработка исключений, пересекающих границы библиотеки и приложения. Также у вас будет две "чисто сишных" glibc в одном процессе: два объекта stdio, два списка atexit, два malloc-пула, война за TLS и так далее. Приедет ли автобус (SIGBUS) или же вы получите банальный SIGSEGV, я думаю, безразлично.
Кроме того, GNU libc находится под лицензией LGPL. Суть этой лицензии заключается в том, что пользователь имеет свободу компоновки (например, берёт ваш проприетарный бинарь, пробует на старой версии glibc с ошибками; обновляет свою версию glibc, чтобы исправить ошибки; и запускает ваш бинарь без перелинковки с помощью ld). Если же вы статически скомпоновались с GNU libc, тогда вы нарушаете права пользователя на свободу замены glibc.
Статическая компоновка с GNU libc вводит всё ваше приложение в сферу действия лицензии LGPL (или более сильной: GPL, AGPL на ваш выбор), хотите вы того, или нет. Таким образом, вы обязаны будете предоставить исходники, или же все ваши *.o-файлы для статической перелинковки в пользовательской среде с новой версией glibc.
4. Работает ли в BSD статическая компоновка libc
Да! Нет проблем ни с отложенной загрузкой зависимостей, ни с лицензией. Функция dlopen(3) при статической компоновке с libc в BSD просто честно не работает:
#pragma weak dlopen
void *
dlopen(const char *name, int mode)
{
_rtld_error(sorry);
return NULL;
}
5. Можно ли в Linux'е статически компоновать non-GNU libc (dietlibc, bionic libc, newlib и так далее)? Будет ли работать dlopen(3) в этом случае?
А хрен его знает.
no subject
Date: 2012-12-04 01:06 pm (UTC)no subject
Date: 2012-12-04 01:24 pm (UTC)от libc зависит не только C и C++, но и питон, и жаба, и эрланг, и похапэ, и node.js
может быть её правильнее было бы назвать libkernelapi ? не знаю
в общем, как в 1970 году Керниган и Ритчи назвали, так и называется до сих пор
no subject
Date: 2012-12-04 01:36 pm (UTC)я к тому, что всякие ntdll это не libc. нельзя какие-то нюансы linux экстраполировать на все ОС.
no subject
Date: 2012-12-04 01:57 pm (UTC)так что другие языки (не си) используют только первую шнягу, но не используют вторую
к сожалению, термин для первой шняги в винде придумать забыли