International conference of developers
and users of free / open source software

Написание своей StdLibC

Дмитрий Храбров, Homel, Belarus

LVEE 2016

This article shows how you can implement basic stdlib's functions. You can see here: string functions, formatted output, usage of *Nix kernel functions etc.

В последнее время на страницах информационных ресурсов можно увидеть статьи “Привет из свободного от 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