Написание своей StdLibC
LVEE 2016
В последнее время на страницах информационных ресурсов можно увидеть статьи “Привет из свободного от libc мира” 1, “Минимальный Elf” 2 и пр. Все они показывают настройку получаемого исполняемого файла, стараются его уменьшить. Например, получен работающий Elf-файл, занимающий всего 45 байт дискового пространства. Однако за данной минимизацией забывается такое важное свойство, как юзабилити. Elf-файл в 45 байт умеет только запускаться и корректно завершаться. При этом если добавить хотя бы вывод классического сообщения “Hello World”, то размер станет далеко не таким маленьким и от многих техник придётся отказаться.
Существует множество свободных реализаций стандартной библиотеки Си 3. Минимальный размер имеет dietlibc, которая позволяет скомпилировать приложение размером всего 200 байт. Или 6 килобайт если используется функция printf. Однако значительная часть функционала там не реализована, или реализована не в соответствии со стандартом. Кроме того, dietlibc лицензирована под GPL2, что ставит ряд значительных ограничений (свои приложения придётся так же лицензировать под GPL2). Ещё одной хорошей альтернативой в плане размера является musl: 1800 байт минимум, 13 килобайт с printf. Библиотека musl лицензирована под MIT и качественно реализует стандарт. Однако именно из-за полного охвата стандарта Си и получаются 13 килобайт, которые хотелось бы уменьшить.
Целью данной работы является создание библиотеки, с помощью которой можно было бы более-менее привычно программировать, но в то же время минимизировать размер получаемого исполняемого файла, вообще не использовать стандартную библиотеку. Данная работа представляет больше исследовательский интерес, чем практическую ценность. И в конечном счёте приближает к пониманию, как именно работают те или иные функции из стандартной библиотеки языка Си. Предложенные реализации далеко не оптимальны или безопасны, однако просты для понимания.
В итоге необходимо получить корректное функционирование данного кода:
printf("Hello %s LVEE %d!\n", "Summer", 2016);
В этой простейшей, казалось бы, строке кода, поднимается сразу несколько вопросов: 1) непосредственно сама функция printf и её форматный вывод; 2) функции работы со строками; 3) конвертирование чисел в строку; 4) непосредственно вывод символов на экран. Всё это обычно реализуется средствами стандартной библиотеки, и всё придётся реализовать самостоятельно.
Syscall – вызов функций ядра Linux, для x86 реализуется через 0×80 ассемблерное прерывание. Полный код для функции с 3 аргументами (взят из 4):
static long _syscall3(int number, unsigned long arg1, unsigned long arg2, unsigned long arg3){
long result;
__asm__ volatile ("int $0x80"
: "=a" (result)
: "0" (number), "b" (arg1), "c" (arg2), "d" (arg3)
: "memory", "cc");
return result;
}
Если вызываемой функции необходимо менее 3 аргументов, то завершающие аргументы можно передать нулевыми. Например, функция close принимает всего 1 аргумент – файловый дескриптор. Реализация может выглядеть следующим образом:
return (int)syscall3(__NR_close, fd, 0, 0);
Константы названий функций (NR_close и пр.) задаются для каждой архитектуры отдельно в файле unistd.h. Это заголовочный файл для доступа к API POSIX-совместимой операционной системы. В unistd.h можно посмотреть полный список функций ядра Linux. И не увидеть там ни строковых функций, ни потоков (thread), ни malloc/calloc/free, ни printf. Весь этот функционал реализуется на функциях более низкого уровня в стандартной библиотеке. Вывод символов на экран консоли осуществляется с помощью функции write:
return (ssize_t)syscall3(__NR_write, fd, buf, count);
Как видно, функция write использует 3 аргумента: файловый дескриптор, буфер и размер буфера в байтах. Однако бывают ситуации, когда размер буфера заранее неизвестен. Обычно в таких случаях используется функция получения длины строки – strlen. Реализуется она средствами чистого Си, без вызова функций ядра. Это является преимуществом, так как такой код может быть скомпилирован любым компилятором, реализующим стандарт Си. Начиная от свободных GCC, Clang, Tiny C Compiler, и заканчивая проприетарной Visual Studio. Пример реализации поиска длины строки:
len=0; while( str[len] != 0 ) len++;
Аналогично реализуются и многие другие функции работы со строками – происходит посимвольная обработка всей строки в цикле. В качестве примера обработки одного символа можно рассмотреть функцию перевода символа в верхний регистр. Если символ лежит в интервале от a до z, то от его кода отнимается ‘a’ и прибавляется код ‘A’:
return symb - 'a' + 'A';
Конвертирование целых чисел в строку – деление на 10 до тех пор, пока не останется 0. Остаток от деления складывать в буфер. Существует множество способов сделать перевод из числа в строку, но они всё-равно опираются на какой-либо один базовый, который и необходимо реализовать.
Функция printf может принимать разное количество аргументов. Эта “перегрузка” параметров функции осуществляется средствами Си через списки варьирующихся аргументов: va_list, va_start, va_arg, va_end.
Пример реализации:
void Myprintf(char* format, ...){
char* s; // Буфер для вывода строк
int i; // Счётчик текущей позиции в форматной строке
va_list arg; // Список варьирующихся аргументов
va_start(arg, format); // Инициализируем список
for(i=0; i<strlen(format); i++){
if( format[i] == '%' ) switch( format[i+1] ){
case 'd': // вызов своей функции для чисел
MyPutInt( va_arg(arg, int) ); i++; break;
case 's': // получение строки и её вывод
s = va_arg(arg, char*);
MyWrite( s, strlen(s) ); i++; break;
case 'c': // аргумент - в int
MyPutchar( va_arg(arg, int) ); i++; break;
} else MyPutchar( format[i] );
} // вывод одного символа реализуется через write
va_end(arg); // Необходимая очистка списка
}
Суть так же проста – перебираем символы форматной строки, если встречаем символ ‘%’, то смотрим следующий за ним. По следующему символу узнаём тип аргумента (если символ ‘d’, то нужно конвертировать в int). Далее просто берём следующий аргумент из списка варьируемых, сразу просим привести его к нужному типу. Нужно помнить, что нельзя сразу конвертировать во float, char или short – изначально должно быть преобразование в double или int.
Теперь остаётся только скомпилировать приложение с флагами nodefaultlibs и nostartfiles (подробнее про это можно почитать в статье “Привет из свободного от libc мира” 1). Если всё собрано правильно, то при вызове ldd будет писать “not a dynamic executable”. Это означает, что полученное приложение вообще не зависит ни от каких системных библиотек. Но в то же время оно написано на Си и использует функцию printf, что и требовалось получить.
Литература:
1 Hello from a libc-free world! https://blogs.oracle.com/ksplice/entry/hello_from_a_libc_free
2 A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux. http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html
3 Comparison of C/POSIX standard library implementations for Linux. http://www.etalabs.net/compare_libcs.html
4 Program in C without any C library. https://github.com/fishilico/shared/tree/master/linux/nolibc
Abstract licensed under Creative Commons Attribution-ShareAlike 3.0 license
Back