Опубликовано: Sept. 8, 2020, 9:32 p.m.

Hello world под Linux без libc (на x86-64)

Disclaimer: код на GitHub.

В этой заметке хочу зафиксировать свои знания, полученные в процессе сборки простейшей программы без libc.

Сборка пустой программы без libc хорошо описана в этом посте на хабре. Будем считать, что у нас уже есть два файла со следующим содержанием:

start.S main.c
int main()
{
    return 0;
}

Таблицу с номерами системных вызовов под x86-64 можно посмотреть на этой странице; благо, ChromiumOS основана на Linux. На той же странице есть таблицы для других архитектур, в том числе x86. Стоит отметить, что номера одних и тех же системных вызовов в них отличаются (как и регистры, через которые происходит передача параметров).

Создадим функцию hello, не принимающую и не возвращающую ничего и выводящую константную строчку. Ассемблерный файл с ней будет выглядеть так:

hello.S

В main.c добавим описание данной функции и ее вызов:

main.c
extern void hello();

int main()
{
    hello();
    return 0;
}

Собрать код можно командой:

gcc -nostdlib -no-pie start.S -o wolibc hello.S main.c

Вывод программы:

Assembly

Отлично, мы научились выводить константную строчку. Тем не менее, это не очень интересно, и хочется иметь что-то похожее на обычный системный вызов write, которому можно передать параметры. Оказывается, что код получится даже проще из-за того, что нужные значения автоматически оказываются в регистрах rdi, rsi и rdx:

mywrite.S main.c
#include "types.h"

#define STDOUT 1

extern ssize_t mywrite(int fd, const void* buf, size_t count);

int main()
{
    mywrite(STDOUT, "Hello ", 6);
    mywrite(STDOUT, "without libc!\n", 14);
    return 0;
}

Кроме того, добавился один новый заголовок, в котором определяются size_t и ssize_t:

types.h
#ifndef TYPES_H
#define TYPES_H

#if SIZE_MAX == UINT_MAX     // common 32 bit case
typedef int ssize_t;
typedef unsigned int size_t;
#elif SIZE_MAX == ULONG_MAX  // linux 64 bits
typedef long ssize_t;
typedef unsigned long size_t;
#elif SIZE_MAX == ULLONG_MAX // windows 64 bits
typedef long long ssize_t;
typedef unsigned long long size_t;
#elif SIZE_MAX == USHRT_MAX  // is this even possible?
typedef short ssize_t;
typedef unsigned short size_t;
#else
#error Platform has exotic SIZE_MAX
#endif

#endif // TYPES_H

В качестве бонуса я написал функцию write_num для вывода целочисленных типов в десятичном формате и при помощи нее проверил, что mywrite возвращает корректные значения. Для корректной работы write_num с разными целочисленными типами удобно воспользоваться шаблонами из C++, поэтому код был переписан на этот язык.

main.cpp

Вывод программы:

Hello without libc!
Results:
Got 6, expected 6
Got 14, expected 14
Got -9, expected -9