Программирование на Си без CRT

Если я правильно понял, CRT - это стандартная библиотека.

Так вот, где-то увидел несколько сообщений в стиле: "Писать без CRT, делать вызовы к кернелу напрямую", "работать с ОС напрямую".

Есть ли где-нибудь источник, где можно про это прочитать, посмотреть?


Ответы (1 шт):

Автор решения: leb

Приложенная к комментарию 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 -- иначе вывод производиться не будет).

→ Ссылка