Соберите статический ELF без libc, используя unistd.h из заголовков Linux

Я заинтересован в создании статической программы ELF без (g) libc, используя unistd.h, предоставленный заголовками Linux.

Я прочитал эти статьи/вопросы, которые дают приблизительное представление о том, что я пытаюсь сделать, но не совсем: http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html

Компиляция без libc

https://blogs.oracle.com/ksplice/entry/hello_from_a_libc_free

У меня есть базовый код, который зависит только от unistd.h, из которых, как я понимаю, каждая из этих функций предоставляется ядром, и что libc не нужен. Вот путь, который я выбрал, который кажется наиболее многообещающим:

    $ gcc -I /usr/include/asm/ -nostdlib grabbytes.c -o grabbytesstatic
    /usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000400144
    /tmp/ccn1mSkn.o: In function `main':
    grabbytes.c:(.text+0x38): undefined reference to `open'
    grabbytes.c:(.text+0x64): undefined reference to `lseek'
    grabbytes.c:(.text+0x8f): undefined reference to `lseek'
    grabbytes.c:(.text+0xaa): undefined reference to `read'
    grabbytes.c:(.text+0xc5): undefined reference to `write'
    grabbytes.c:(.text+0xe0): undefined reference to `read'
    collect2: error: ld returned 1 exit status

До этого мне приходилось вручную определять SEEK_END и SEEK_SET в соответствии со значениями, найденными в заголовках ядра. В противном случае было бы ошибкой сказать, что они не были определены, что имеет смысл.

Я предполагаю, что мне нужно подключиться к неразрезанному vmlinux, чтобы предоставить символы для использования. Тем не менее, я прочитал символы, и хотя было много llseek, они не были llseek дословно.

Итак, мой вопрос может идти в нескольких направлениях:

Как я могу указать файл ELF для использования символов? И я предполагаю, что если/как это возможно, символы не будут совпадать. Если это правильно, существует ли существующий заголовочный файл, который переопределит llseek и default_llseek или что-то еще в ядре?

Есть ли лучший способ написать код Posix на C без libc?

Моя цель состоит в том, чтобы написать или портировать довольно стандартный код C, используя (возможно, исключительно) unistd.h и вызывать его без libc. Я, вероятно, в порядке без нескольких функций unistd, и я не уверен, какие из них существуют «чисто» как вызовы ядра или нет. Я люблю сборку, но это не моя цель. Надеясь оставаться как можно более строгим C (я согласен с несколькими внешними файлами сборки, если мне нужно), чтобы в какой-то момент использовать статическую систему без libc.

Спасибо за чтение!


person sega01    schedule 18.01.2013    source источник
comment
Сначала я подумал, что вы хотите использовать этот статический двоичный файл из пользовательского пространства (в этом случае ответ заключается в том, что вам нужны оболочки системных вызовов, если вы хотите использовать системные вызовы либо из libc, либо написать свои собственные). Но затем вы упомянули о связывании с (неразрезанным) ядром, поэтому я предполагаю, что вы ожидаете запустить этот код непосредственно на «голом железе» (т.е. вместо ядра). Уточните, пожалуйста, ваш вопрос по этому поводу.   -  person Celada    schedule 19.01.2013
comment
Спасибо за ответ! Я хотел связать, используя ядро ​​​​в качестве ссылки на таблицу символов, и запустить его в пользовательской области хоста Linux. Я поищу существующие оболочки системных вызовов и посмотрю, не похожи ли они на то, что я пытаюсь сделать.   -  person sega01    schedule 19.01.2013
comment
Хорошо, хорошо, если вы собираетесь запускать его в пользовательском пространстве, вы не можете связываться с ядром (в случае успеха это приведет к тому, что реализации этих системных вызовов в ядре будут добавлены в ваш код, а это не то, что вам нужно: вы хотите вызов в ядро). Вы должны реализовать open() и read() самостоятельно, вызвав соответствующие действия, как указано в ABI ядра, что обычно включает в себя настройку регистров и последующее выполнение какой-либо инструкции прерывания процессора. Проблема в том, что детали этого ЧРЕЗВЫЧАЙНО зависят от архитектуры (ARM против x86 и т. д.) и усложняются такими вещами, как vsyscalls.   -  person Celada    schedule 19.01.2013
comment
Я не вижу смысла этим заниматься. Используйте libc — нет больших накладных расходов, если вы не используете сложные функции — используйте -static, и вы получите двоичный файл, содержащий только те функции, которые вам нужны. Какова цель отказа от использования libc? Обратите внимание, что вы не можете вызывать ядро ​​​​из пользовательского режима без какой-либо оболочки системных вызовов, так как вам нужен соответствующий метод вызова для перехода из пользовательского режима в режим ядра — это невозможно сделать на чистом C, нужно написать на ассемблер для соответствующего процессора [и может быть изменен при изменении ядра].   -  person Mats Petersson    schedule 19.01.2013
comment
Я предполагаю, что идеальным сценарием были бы встроенные файлы заголовков сборки. Я нашел этот вопрос и он дает результат, который почти соответствует моим ожиданиям, но я не могу заставить argc/argv работать с void _start(). @MatsPetersson: с glibc много накладных расходов, это приводит к файлам размером 800 КБ или больше, даже если используется только unistd.h. Насколько я знаю, все в моем коде — это просто системные вызовы, поэтому я не понимаю, почему я не могу просто заставить gcc генерировать код, вызывающий их напрямую через заголовки Linux.   -  person sega01    schedule 19.01.2013
comment
Если вы получаете 800 КБ от компоновки glibc, значит что-то перетаскивается, что не нужно, или вы делаете что-то не так.   -  person Mats Petersson    schedule 19.01.2013
comment
Итак, я только что провел несколько экспериментов, и я считаю, что в основном это код запуска C, который тащит огромное количество другого кода. Так что, если вы можете избавиться от (некоторых из них), тогда вы сможете уменьшить код. Я немного поработаю над этим и вернусь к вам. Альтернативой является перетаскивание тех частей glibc, которые вам нужны, и связывание с ними... [Но вам все равно нужно избавиться от любого кода, который вызывает любые другие функции, например код запуска C, который вызывает большое количество других функций. , что тащить в кухонную раковину]   -  person Mats Petersson    schedule 19.01.2013


Ответы (2)


Это далеко не идеально, но немного ассемблера (x86_64) сократило мой размер чуть менее 5 КБ (но большая часть этого — «другие вещи, кроме кода» — фактический код меньше 1 КБ [771 байт, если быть точным], но размер файла намного больше, я думаю, потому что размер кода округляется до 4 КБ, а затем к нему добавляется некоторый заголовок/нижний колонтитул/дополнительный материал]

Вот что я сделал: gcc -g -static -nostdlib -o glibc start.s glibc.c -Os -lc

glibc.c содержит:

#include <unistd.h>

int main()
{
    const char str[] = "Hello, World!\n";
    write(1, str, sizeof(str));

    _exit(0);
}

start.s содержит:

    .globl _start
_start: 
    xor %ebp, %ebp
    mov %rdx, %r9
    mov %rsp, %rdx
    and $~16, %rsp
    push    $0
    push    %rsp

    call    main

    hlt


    .globl _exit
_exit:
    //  We known %RDI already has the exit code... 
    mov $0x3c, %eax
    syscall
    hlt

Основная цель этого заключается не в том, чтобы показать, что это не часть системного вызова glibc, которая занимает много места, а «подготовка вещей» - и будьте осторожны, если вы вызовете, например, printf, возможно, даже (v) sprintf, или exit(), или любой другой функции "стандартной библиотеки", вы находитесь в стране "никто не знает, что произойдет".

Изменить: обновлен «start.s», чтобы поместить argc/argv в нужные места:

_start: 
    xor %ebp, %ebp
    mov %rdx, %r9
    pop     %rdi
    mov %rsp, %rsi
    and $~16, %rsp
    push    %rax
    push    %rsp

    // %rdi = argc, %rsi=argv
    call    main

Обратите внимание, что я изменил, какой регистр что содержит, чтобы он соответствовал основному — в предыдущем коде я имел немного неправильный порядок.

person Mats Petersson    schedule 19.01.2013
comment
Спасибо! Ваше решение очень близко к этому один. Я могу подтвердить, что это работает с моей стороны, однако передача argc/argv не работает. Вы знаете хороший ресурс, который я могу просмотреть для поддержки argc/argv в разделе сборки start.s? Я не знаком с тем, как работает argc/argv. - person sega01; 19.01.2013
comment
Я не знаю, но я уверен, что argc/argv передаются из ядра в регистрах. Я покопаюсь. - person Mats Petersson; 19.01.2013
comment
Я отредактировал новую функцию _start. Не спрашивайте меня, как вы относитесь к окружающей среде, я не уверен, что это так просто. - person Mats Petersson; 19.01.2013
comment
Большое спасибо за помощь, Матс! Это работает отлично. Интересно, что если open() вызывается для несуществующего файла (динамическая версия этого не делает), это вызывает сбой, но это задача для другого дня. - person sega01; 19.01.2013
comment
Весьма вероятно, что в этом случае glibc использует какие-то неинициализированные переменные или что-то в этом роде. Как я пытался сказать, это не то, что я бы рекомендовал делать... - person Mats Petersson; 19.01.2013
comment
-1: Это абсолютно точно вводит glibc с -lc. Решение проблемы OP состоит не в том, чтобы сломать glibc, не позволяя ему инициализировать себя в поисках меньшего размера двоичного файла. Вместо этого следует использовать встроенную libc, такую ​​как musl или dietlibc. - person Dave; 29.04.2013
comment
@Dave: Я вообще сказал, что это не приносит glibc? И я думаю, по крайней мере, комментарий выше вашего объясняет, что это плохой поступок. Моя точка зрения заключалась в том, что это другие вещи в glibc, которые вызывают жир, а не системные вызовы. - person Mats Petersson; 29.04.2013
comment
Я хочу сказать, что даже write() является стандартной библиотечной функцией, и отсутствие связи с _start() в значительной степени неоправданно. И, если ваш двоичный файл все еще 5K, то write() glibc абсолютно раздут. - person Dave; 29.04.2013
comment
Разве вы не читали следующее предложение или два: фактический код меньше 1 КБ [771 байт, если быть точным]. Остальное - различные другие отступы/символы и прочее... Например, я никогда не удалял двоичный файл. Я согласен, что не связывать _start — плохая идея. И я так и сказал. Использование облегченной библиотеки C — хорошее решение, я с этим не спорю. - person Mats Petersson; 29.04.2013
comment
Да, но я также читал, как вы размахивали руками, чтобы оправдать размер файла 5K. Файл диетической библиотеки, правильно инициализированный и неразрезанный, имеет размер 2,7 КБ. Я нахожу интересным посмотреть, сколько именно glibc раздулось из-за запуска. Я думаю, что то, что вы делаете, может быть более явным с самого начала, и что следует использовать -nostartfiles , а не -nostdlib -lc - person Dave; 29.04.2013
comment
Итак, используя -nostartfiles с start.s сверху (поскольку вам нужно где-то _start), после strip a.out с -nostartfiles файл составляет 11584 байта. Это 608 байт текста и 12 байт данных, всего 620 байт. - person Mats Petersson; 30.04.2013
comment
Массив envp начинается после массива argv. Как только вы загрузите argc и argv в rdi и rsi, 'lea 8(%rsi,%rdi, 8), %rdx' должен загрузить envp в rdx. пример - person eloj; 20.09.2015

Если вы хотите писать код POSIX на C, отказ от libc не поможет. Хотя вы можете реализовать функцию syscall на ассемблере и скопировать структуры и определения из заголовка ядра, вы, по сути, будете писать свою собственную libc, которая почти наверняка не будет совместима с POSIX. Со всеми замечательными реализациями libc почти нет причин начинать реализовывать свои собственные.

dietlibc и musl libc являются экономными реализациями libc, которые производят впечатляюще маленькие двоичные файлы. Компоновщик, как правило, умен; пока библиотека написана так, чтобы избежать случайного извлечения многочисленных зависимостей, только те функции, которые вы используете, будут фактически связаны с вашей программой.

Вот простая программа hello world:

#include<unistd.h>

int main(){
    char str[] = "Hello, World!\n";
    write(1, str, sizeof str - 1);
    return 0;
}

Компиляция его с помощью musl ниже дает двоичный файл размером менее 3K.

$ musl-gcc -Os -static hello.c
$ strip a.out 
$ wc -c a.out
2800 a.out

Dietlibc создает еще меньший двоичный файл, менее 1,5 КБ:

$ diet -Os gcc hello.c
$ strip a.out 
$ wc -c a.out
1360 a.out
person Dave    schedule 29.04.2013