Программирование на Си без CRT
Если я правильно понял, CRT - это стандартная библиотека.
Так вот, где-то увидел несколько сообщений в стиле: "Писать без CRT, делать вызовы к кернелу напрямую", "работать с ОС напрямую".
Есть ли где-нибудь источник, где можно про это прочитать, посмотреть?
Ответы (1 шт):
Приложенная к комментарию Grundy ссылка связана с окружением Windows. Я предлагаю дополнительно ответ для системы Linux.
Для запуска выбранной программы, программа-оболочка (например, bash) использует системный вызов ядра exec
, передавая вместе с самим вызовом путь нужной программы. Ядро, в свою очередь, выбирает в файле функцию (символ) с именем _start
, и загружает её на исполнение. Функция main
в свою очередь вызывается как обычная функция из _start
или из другой посредственной функции.
Функция _start
называется точкой входа в программу. У всех исполняемых программ пользовательского пространства (обычно, в системе таких большинство) она одна. Функция _start
-- своеобразный универсальный ключ взаимодействия ядра с пользовательской программой. В теории, мы можем написать эту функцию на Си:
void _start ()
{
}
Функция с таким именем будет распознана системой как точка входа, а значит, сама программа будет теперь запускаема. В таком виде программу можно скомпилировать и скомпоновать, не получая на своём пути ошибок:
Осторожно: может быть опасна для работы системы без защиты памяти
$ gcc -c some.c # компиляция: флаг '-c' значит 'compile' -- так мы экстрагируем стадию компиляцию от последующих стадий (в том числе от связывания с crt0)
$ ld some.o # компоновка:
Запуск такой программы завершится ошибкой. Скорее всего, с сообщением "Ошибка сегментирования". На самом деле, эта ошибка возникает вследствие НЕвыхода из программы:
(завершение процедуры != завершение процесса).
Для благополучного закрытия программы используется отдельный системный вызов ядра. Сам системный вызов обычно прописывается на языках ассемблеров. Но мы можем воспользоваться библиотекой <unistd.h>
, в которой все нужные нам вызовы определены как функции:
#include <unistd.h>
/* Кстати, вместо использования библиотеки, мы могли бы сами определить функцию:
void _exit (int);
*/
void _start ()
{
_exit(79);
}
Не путайте функцию-обёртку для системного вызова ядра
_exit(int)
из заголовочного файла <unistd.h> с функциейexit(int)
(без знака подчёркивания спереди) из заголовочного файла <stdlib.h>. Вторую функцию мы тоже можем здесь использовать, но для чистоты эксперимента будет лучше всё же функция-обёртка.
Команда для компиляции не изменяется, но для компоновки придётся вручную указать библиотеку-солянку, где и находятся объявленные в заголовочнике (или нами вручную) функции:
$ gcc -c some.c # компиляция:
$ ld -lc --dynamic-linker=/lib/ld-linux-x86-64.so.2 some.o # компоновка: для 64-битной системы
$ ld -lc --dynamic-linker=/lib/ld-linux.so.2 some.o # компоновка: для 32-битной системы
Если после запуска программы (и её завершения) мы проверим её код возврата командой $ echo $?
, то мы увидим число 79 -- то самое, что мы записали в аргументе функции _exit()
. Это значит, что программа выполняется корректно.
Теперь мы можем написать свой Hello_world.c без стороннего crt0:
#include <stdlib.h> /* 'exit(int)' -- другая функция, но её мы здесь тоже можем задействовать */
#include <stdio.h>
#include <string.h>
#define BUFSIZE 64
void _start ()
{
unsigned char buffer[BUFSIZE];
memset(buffer, 0, BUFSIZE);
for (int i = 0, c = getchar(); i < BUFSIZE && c != '\n'; i++, c = getchar()) {
buffer[i] = c;
}
printf("Привет, %s!\n", buffer);
fflush(stdout); /* обычно, это ложится на crt0 */
exit(0);
}
Компиляция, компоновка и работа программы:
$ gcc -c Hello_world.c
$ ld -lc -dynamic-linker=/lib/ld-linux-x86-64.so.2 Hello_world.o
$ ./a.out
бобёр # пользовательский ввод
Привет, бобёр # вывод программы
Тем не менее, не стоит отходить от использования стороннего crt0: в его функции входит масштабная подготовка окружения программы. Так, например, в нашей программе недоступны аргументы командной строки, использующиеся классическим main (int argc, char* argv[], char* env[])
, а отсутствие поддержки функционала подобного atexit(void*)
делает использование стандартных заголовочных файлов, таких как <stdio.h> или <stdlib.h>, непривычным, а местами невозможным (например, нам нужно будет помнить каждый раз перед закрытием опустошать буфер stdout -- иначе вывод производиться не будет).