Coding — Привет из свободного от libc мира. Часть 2
Буквально вчера наткнулся на статью под названием "Привет из свободного от libc мира. Часть 1". Автором этой статьи была девушка и, наверное, именно этот факт заставил меня посетить ее блог, где я нашел продолжение первой части (и другие интересные посты, о которых скажу в конце), которое и хотел бы представить здесь. Перед тем, как прочитать вторую часть, прочитайте первую, ее перевод есть на хабре. Итак...
В предыдущем посте мы справились с компиляцией, написав маленькую программу, которая может быть собрана без использования libc. Понимание объектного кода и деталей структуры ELF исполняемых файлов — следующий шаг в нашем приключении.
Мы остановились на следующем коде:
И что мы получили, проделав всю эту работу?
Теперь наш исполняемый файл занимает немногим более 1300 байт и менее 100 линий, это довольно разумное количество для анализа. Маленькая часть ассемблерного кода не может испугать нас к этому моменту, давайте теперь взглянем на него с использованием objdump -D, который покажет содержимое всех секций (вывод здесь). Если вывод кажется пугающим, то быстро пробегите по нему, я обещаю вам, что к концу этого поста от страха ничего не останется.
Итак, мы имеем 5 секций: .text, которая содержит знакомые символы _start и main, .rodata, .eh_frame_hdr, .eh_frame и .comment.
У исполняемого файла, как результата нашей компиляции, есть два представления: он содержит program header, описывающий сегменты, которые содержат информацию, используемую во время выполнения, и section header, описывающий секции, которые содержат информацию для линковки и перемещения. Мы можем получить информацию о сегментах или секциях с помощью readelf -l или readelf -S соответственно (вывод здесь). Результат работы этих двух команд для нашей программы сведен в рисунок 1.
Рис.1. Сегменты и секции нашего ELF
Говоря о избавлении от секций: для нас, приверженцев минимализма, strip(1) хороший друг. Мы можем воспользоваться --remove-section по несущественным секциям, таким как .comment, чтобы полностью от них избавиться; file(1) сообщит нам, если исполняемая программа была укорочена (удалены некоторые из секций).
Мы не видим другие секции в нашем примере (они не были включены в исполняемый файл), потому что они были пустыми:
На этом все о секциях. Теперь мы знаем, что символы, такие как _start и main, находятся в этих секциях, но есть ли в программе еще символы?
Таблица символов для нашего исполняемого файла имеет 11 записей. Странным образом только редкие версии документации (man pages) по objdump, как эта, объясняет таблицу символов столбец за столбцом. Таблица поделена следующим образом:
Столбец 1: значение/адрес символа.
Столбец 2: набор символов и пробелов представляющих флаговые биты, установленные для символа. Есть 7 групп, три из которых представлены в этой таблице символов. Значение из первой группы может быть — l, g, <пробел> или !, если символ локальный, глобальный, ни то ни другое или оба сразу, соответственно. Значение из шестой группы может быть — d, D или <пробел> для debugging, dynamic или normal соответственно. Значение из седьмой может быть — F, f, O или <пробел> для функции, файла, объекта или нормального символа, соответственно. Описание оставшихся 4 групп может быть найдено в unusally comprehensive objdump manpage.
Столбец 3: в какой секции расположен символ. *ABS* (абсолютный) означает символ, не связанный с определенной секцией.
Столбец 4: размер/выравнивание символа.
Столбец 5: имя символа.
Все наши 5 секций имеют связи с local (l), debugging (d) и symbols (s). main действительно функция (F), hello.c действительно файл (f), и он не связан с какой-либо секцией (*ABS*). _start и main — часть исполняемых инструкций для нашей программы и, таким образом, расположены в секции .text, как мы и предполагали. Единственной причудой здесь является __bss_start, _edata и _end, все *ABS*, глобальные символы, которые мы не писали в нашей программе. Откуда они взялись?
Виновником на этот раз является скрипт компоновщика. gcc неявно вызывает ld, как часть процесса компиляции. ld --verbose предоставит сценарий компоновщика, который был использован и, глядя на него (вывод здесь), мы видим, что _edata определен как конец секции .data, __bss_start и _end отмечают начало и конец секции .bss. Эти символы могли быть использованы механизмом управления памятью (например, если sbrk хочет знать, где начинается «куча») и сборщиком «мусора»».
Следует отметить, что str, наша инициализированная локальная переменная, не представлена в таблице символов. Почему? Потому что она размещается в стеке (возможно в регистре) во время выполнения. Однако, что-то связанное с str находиться в секции .rodata, несмотря на то, что мы не видим это в таблице символов...
Теперь о char *str = «Hello World»; на самом деле мы создаем два различных объекта. Первый это строковый литерал «Hello World», который представляет собой просто массив символов и имеет некоторый адрес, но явного имени не имеет. Это массив «только для чтения» и расположен в секции .rodata. Второй — локальная переменная str, которая имеет тип «pointer to char». Она и располагается в стеке, а ее начальное значение — адрес строкового литерала, который был создан.
Мы можем доказать это и получить некоторую другую полезную информацию, смотря на содержимое наших секций, используя декодирование строк:
Вуаля! Наша строка «Hello World» расположена в .rodata и наша секция .comment объяснена: она просто содержит строку с версией gcc, используемой для компиляции программы.
.rodata в действительности содержит только строку «Hello World», а .comment содержит только версию gcc. «Инструкции» для этих секций, показанные в выводе objdump -D получаются в следствии ошибочного представления objdump ASCII символов как инструкций и попытке дизассемблировать их. Мы можем конвертировать первую пару чисел из секции .comment в ASCII символы, чтобы доказать это. На Python:
В секции .text, _start вызывает main, и в main в стек вталкивается указатель на адрес в памяти, где хранится «Hello World», 0x40010b (где начинается секция .rodata, как видно из вывода objdump -D). Затем мы возвращаемся из main в _start, который заботится о выходе из программы, как описано в Части 1.
И это все! Все секции и символы учтены. Никакого волшебства (я имею ввиду волшебство в хорошем смысле Я-бы-прошел-это-испытание, а не в смысле прости-Jimmy-Santa-не-настоящий). Вот так. Whew.
Ссылки:
P.S. В блоге у этой милой девушки есть еще две статьи, которые меня заинтересовали, эта и эта. Возможно кто-то захочит оформить перевод (я могу помочь), если кто-то согласиться напишите в комментарии.
Если заметите ошибки по переводу, то пишите в ЛС.
В предыдущем посте мы справились с компиляцией, написав маленькую программу, которая может быть собрана без использования libc. Понимание объектного кода и деталей структуры ELF исполняемых файлов — следующий шаг в нашем приключении.
Мы остановились на следующем коде:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
jesstess@kid-charlemagne:~$ cat stubstart.S |
И что мы получили, проделав всю эту работу?
1 2 3 4 |
jesstess@kid-charlemagne:~/c$ wc -c hello |
Теперь наш исполняемый файл занимает немногим более 1300 байт и менее 100 линий, это довольно разумное количество для анализа. Маленькая часть ассемблерного кода не может испугать нас к этому моменту, давайте теперь взглянем на него с использованием objdump -D, который покажет содержимое всех секций (вывод здесь). Если вывод кажется пугающим, то быстро пробегите по нему, я обещаю вам, что к концу этого поста от страха ничего не останется.
Итак, мы имеем 5 секций: .text, которая содержит знакомые символы _start и main, .rodata, .eh_frame_hdr, .eh_frame и .comment.
Шаг 1: Собрались – что же такое «секция»?
Если мы стряхнем пыль с нашей любимой копии документа Tool Interface Standard ELF Specification и заглянем в нее, то она скажет нам:У исполняемого файла, как результата нашей компиляции, есть два представления: он содержит program header, описывающий сегменты, которые содержат информацию, используемую во время выполнения, и section header, описывающий секции, которые содержат информацию для линковки и перемещения. Мы можем получить информацию о сегментах или секциях с помощью readelf -l или readelf -S соответственно (вывод здесь). Результат работы этих двух команд для нашей программы сведен в рисунок 1.
Рис.1. Сегменты и секции нашего ELF
Шаг 2: Что происходит в нашей секции?
Спецификация также сообщает нам, что и в каком порядке располагается в нашем исполняемом файле:- .text — содержит исполняемые инструкции программы (попросту код программы);
- .rodata — содержит константные значения, это сегмент «только для чтения»;
- .eh_frame — информация, необходимая для frame-unwinding во время обработки исключений;
- .eh_frame_hdr — цитата из Linux Standard Base Specification: "Эта секция содержит указатель на секцию .eh_frame, которая доступна для «runtime support code» С++ приложения. Эта секция также может содержать двоичную таблицу поиска, которая может быть использована «runtime support code» для более эффективного доступа к записям секции .eh_frame".
Мы не должны волноваться об исключениях в нашем примере, поэтому .eh_frame и .eh_frame_hdr не должны нас беспокоить и компилирование с флагом -fno-asynchronous-unwind-tables подавит создание этих двух секций; - .comment — информация о версии компилятора.
Говоря о избавлении от секций: для нас, приверженцев минимализма, strip(1) хороший друг. Мы можем воспользоваться --remove-section по несущественным секциям, таким как .comment, чтобы полностью от них избавиться; file(1) сообщит нам, если исполняемая программа была укорочена (удалены некоторые из секций).
Мы не видим другие секции в нашем примере (они не были включены в исполняемый файл), потому что они были пустыми:
- .data — в этой секции инициализируются глобальные и локальные переменные;
- .bss — секция неинициализированных глобальных и локальных переменных, то есть заполненных нулями.
На этом все о секциях. Теперь мы знаем, что символы, такие как _start и main, находятся в этих секциях, но есть ли в программе еще символы?
Шаг 3: Понимание символов и почему они расположены там, где расположены.
Мы можем получить информацию о символах нашего исполняемого файла с помощью objdump -t:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
jesstess@kid-charlemagne:~/c$ objdump -t hello |
Таблица символов для нашего исполняемого файла имеет 11 записей. Странным образом только редкие версии документации (man pages) по objdump, как эта, объясняет таблицу символов столбец за столбцом. Таблица поделена следующим образом:
Столбец 1: значение/адрес символа.
Столбец 2: набор символов и пробелов представляющих флаговые биты, установленные для символа. Есть 7 групп, три из которых представлены в этой таблице символов. Значение из первой группы может быть — l, g, <пробел> или !, если символ локальный, глобальный, ни то ни другое или оба сразу, соответственно. Значение из шестой группы может быть — d, D или <пробел> для debugging, dynamic или normal соответственно. Значение из седьмой может быть — F, f, O или <пробел> для функции, файла, объекта или нормального символа, соответственно. Описание оставшихся 4 групп может быть найдено в unusally comprehensive objdump manpage.
Столбец 3: в какой секции расположен символ. *ABS* (абсолютный) означает символ, не связанный с определенной секцией.
Столбец 4: размер/выравнивание символа.
Столбец 5: имя символа.
Все наши 5 секций имеют связи с local (l), debugging (d) и symbols (s). main действительно функция (F), hello.c действительно файл (f), и он не связан с какой-либо секцией (*ABS*). _start и main — часть исполняемых инструкций для нашей программы и, таким образом, расположены в секции .text, как мы и предполагали. Единственной причудой здесь является __bss_start, _edata и _end, все *ABS*, глобальные символы, которые мы не писали в нашей программе. Откуда они взялись?
Виновником на этот раз является скрипт компоновщика. gcc неявно вызывает ld, как часть процесса компиляции. ld --verbose предоставит сценарий компоновщика, который был использован и, глядя на него (вывод здесь), мы видим, что _edata определен как конец секции .data, __bss_start и _end отмечают начало и конец секции .bss. Эти символы могли быть использованы механизмом управления памятью (например, если sbrk хочет знать, где начинается «куча») и сборщиком «мусора»».
Следует отметить, что str, наша инициализированная локальная переменная, не представлена в таблице символов. Почему? Потому что она размещается в стеке (возможно в регистре) во время выполнения. Однако, что-то связанное с str находиться в секции .rodata, несмотря на то, что мы не видим это в таблице символов...
Теперь о char *str = «Hello World»; на самом деле мы создаем два различных объекта. Первый это строковый литерал «Hello World», который представляет собой просто массив символов и имеет некоторый адрес, но явного имени не имеет. Это массив «только для чтения» и расположен в секции .rodata. Второй — локальная переменная str, которая имеет тип «pointer to char». Она и располагается в стеке, а ее начальное значение — адрес строкового литерала, который был создан.
Мы можем доказать это и получить некоторую другую полезную информацию, смотря на содержимое наших секций, используя декодирование строк:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
jesstess@kid-charlemagne:~$ objdump -s hello |
Вуаля! Наша строка «Hello World» расположена в .rodata и наша секция .comment объяснена: она просто содержит строку с версией gcc, используемой для компиляции программы.
Шаг 4: Исключим лишнее и соединим все вместе
Исполняемый файл содержит 5 секций: .text, .rodata, .eh_frame_hdr, .eh_frame и .comment. В действительности только одна из них (.text) содержит код, определяющий, что делает эта маленькая программа. В этом можно убедиться, используя objdump -d (дизассемблирует только те секции, которые должны содержать инструкции) вместо objdump -D (дизассемблирует содержимое всех секций, не только содержащих инструкции), использованного в начале поста, отмечая, что выведено только содержимое .text (используя objudump -d)..rodata в действительности содержит только строку «Hello World», а .comment содержит только версию gcc. «Инструкции» для этих секций, показанные в выводе objdump -D получаются в следствии ошибочного представления objdump ASCII символов как инструкций и попытке дизассемблировать их. Мы можем конвертировать первую пару чисел из секции .comment в ASCII символы, чтобы доказать это. На Python:
1 2 |
>>> "".join(chr(int(x, 16)) for x in "47 43 43 3a 20 28 55 62 75 6e 74 75".split()) |
В секции .text, _start вызывает main, и в main в стек вталкивается указатель на адрес в памяти, где хранится «Hello World», 0x40010b (где начинается секция .rodata, как видно из вывода objdump -D). Затем мы возвращаемся из main в _start, который заботится о выходе из программы, как описано в Части 1.
И это все! Все секции и символы учтены. Никакого волшебства (я имею ввиду волшебство в хорошем смысле Я-бы-прошел-это-испытание, а не в смысле прости-Jimmy-Santa-не-настоящий). Вот так. Whew.
Ссылки:
- Первая часть;
- Перевод первой части;
- Оригинал второй части;
- pdf этого перевода.
P.S. В блоге у этой милой девушки есть еще две статьи, которые меня заинтересовали, эта и эта. Возможно кто-то захочит оформить перевод (я могу помочь), если кто-то согласиться напишите в комментарии.
Если заметите ошибки по переводу, то пишите в ЛС.