В качестве упражнения я хочу написать программу на С. Достаточно простую для того, чтобы дизассемблировать ее и объяснить весь код самой себе.
Звучит несложно, правильно?
У читателя предполагается наличие опыта компиляции программ и работы в Линуксе. Небольшое умение читать ассемблерный код тоже пригодится.
Итак, вот наш простейший хелловорлд:
Скомпилируем его и посчитаем количество символов:
Фигасе! Откуда берутся эти 11 килобайт?
Так что мы не будем ее использовать. И
Перекомпилируем и пересчитаем количество символов:
Почти ничего не изменилось? Ха!
Проблема в том, что gcc все ещё использует startup files (?) во время линкования. Доказательства? Скомпилируем с ключом
Всего лишь предупреждение, все равно попробуем:
Выглядит неплохо! Мы уменьшили размер до значительно более вменяемого (аж на целый порядок!)…
…и заплатили за это сегфолтом. Блин.
Для развлечения сделаем нашу программу запускаемой до того, как начнем разбираться в ассемблере.
Что же делает символ
По умолчанию с точки зрения линкера именно
Проверка сообщила, что на этом компьютере
… и поехало.
Так вот зачем нужен
Скомпилируем и выполним хелловорлд с ассемблерной заглушкой
Ура, с компиляцией проблем больше нет. Но сегфолт никуда не делся. Почему? Скомпилируем с отладочной информацией и заглянем в gdb. Установим брейкпоинт на
Что?
Хех! Подробный разбор ассемблера оставим на потом, отметив вкратце следующее: после возврата из
Буквально. После возврата из
Ура! Программа компилируется, запускается, при прогоне через gdb даже нормально завершается.
Привет из свободного от libc мира!
Оставайтесь со мной, во второй части разберем ассемблерный код подробно, посмотрим что случится, если сделать программу более сложной, и еще немного разберемся в линковании, соглашениях о вызовах и структуре двоичного ELF файл в х86 архитектуре.
Звучит несложно, правильно?
У читателя предполагается наличие опыта компиляции программ и работы в Линуксе. Небольшое умение читать ассемблерный код тоже пригодится.
Итак, вот наш простейший хелловорлд:
jesstess@kid-charlemagne:~/c$ cat hello.c #include <stdio.h> int main() { printf("Hello World\n"); return 0; }
Скомпилируем его и посчитаем количество символов:
jesstess@kid-charlemagne:~/c$ gcc -o hello hello.c jesstess@kid-charlemagne:~/c$ wc -c hello 10931 hello
Фигасе! Откуда берутся эти 11 килобайт?
objdump -t hello
показывает 79 записей в таблице идентификаторов, за большинство из которых ответственна стандартная библиотека.Так что мы не будем ее использовать. И
printf
мы тоже не будем использовать, чтобы избавиться от инклюда:jesstess@kid-charlemagne:~/c$ cat hello.c int main() { char *str = "Hello World"; return 0; }
Перекомпилируем и пересчитаем количество символов:
jesstess@kid-charlemagne:~/c$ gcc -o hello hello.c jesstess@kid-charlemagne:~/c$ wc -c hello 10892 hello
Почти ничего не изменилось? Ха!
Проблема в том, что gcc все ещё использует startup files (?) во время линкования. Доказательства? Скомпилируем с ключом
-nostdlib
, после чего (в соответствии с документацией) gcc «не будет использовать при линковании системные библиотеки и startup files. Использоваться будут только явно переданные линкеру файлы».jesstess@kid-charlemagne:~/c$ gcc -nostdlib -o hello hello.c /usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 00000000004000e8
Всего лишь предупреждение, все равно попробуем:
jesstess@kid-charlemagne:~/c$ wc -c hello 1329 hello
Выглядит неплохо! Мы уменьшили размер до значительно более вменяемого (аж на целый порядок!)…
jesstess@kid-charlemagne:~/c$ ./hello Segmentation fault
…и заплатили за это сегфолтом. Блин.
Для развлечения сделаем нашу программу запускаемой до того, как начнем разбираться в ассемблере.
Что же делает символ
_start
, который похоже нужен для запуска программы? Где он обычно определяется при использовании libc?По умолчанию с точки зрения линкера именно
_start
, а не main
, является настоящей точкой входа в программу. Обычно _start
определяется в перемещаемом ELF crt1.o
. Убедимся в этом, слинковав хелловорлд c crt1.o
и заметив, что _start
теперь обнаруживается (но взамен появились другие проблемы из-за того, что не определены другие startup symbols libc):# компилируем исходники не линкуя jesstess@kid-charlemagne:~/c$ gcc -Os -c hello.c # теперь попытаемся слинковать jesstess@kid-charlemagne:~/c$ ld /usr/lib/crt1.o -o hello hello.o /usr/lib/crt1.o: In function `_start': /build/buildd/glibc-2.9/csu/../sysdeps/x86_64/elf/start.S:106: undefined reference to `__libc_csu_fini' /build/buildd/glibc-2.9/csu/../sysdeps/x86_64/elf/start.S:107: undefined reference to `__libc_csu_init' /build/buildd/glibc-2.9/csu/../sysdeps/x86_64/elf/start.S:113: undefined reference to `__libc_start_main'
Проверка сообщила, что на этом компьютере
_start
живет в исходнике libc: sysdeps/x86_64/elf/start.S
. Этот восхитительно комментированный файл экспортирует символ _start
, инициализирует стек, некоторые регистры и вызывает __libc_start_main
. Если посмотреть в самый низ csu/libc-start.c
, можно увидеть вызов _main
нашей программы:/* Ничего особенного, просто вызвать функцию */ result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
… и поехало.
Так вот зачем нужен
_start
. Для удобства подытожим происходящее между _start
и вызовом main
: инициализировать кучу вещей для libc и вызвать main
. А раз libc нам не нужен, экспортируем собственный символ _start
, который только и умеет, что вызывать main
, и слинкуем с ним:jesstess@kid-charlemagne:~/c$ cat stubstart.S .globl _start _start: call main
Скомпилируем и выполним хелловорлд с ассемблерной заглушкой
_start
:jesstess@kid-charlemagne:~/c$ gcc -nostdlib stubstart.S -o hello hello.c jesstess@kid-charlemagne:~/c$ ./hello Segmentation fault
Ура, с компиляцией проблем больше нет. Но сегфолт никуда не делся. Почему? Скомпилируем с отладочной информацией и заглянем в gdb. Установим брейкпоинт на
main
и пошагово исполним программу до сегфолта:jesstess@kid-charlemagne:~/c$ gcc -g -nostdlib stubstart.S -o hello hello.c jesstess@kid-charlemagne:~/c$ gdb hello GNU gdb 6.8-debian Copyright (C) 2008 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu"... (gdb) break main Breakpoint 1 at 0x4000f4: file hello.c, line 3. (gdb) run Starting program: /home/jesstess/c/hello Breakpoint 1, main () at hello.c:5 5 char *str = "Hello World"; (gdb) step 6 return 0; (gdb) step 7 } (gdb) step 0x00000000004000ed in _start () (gdb) step Single stepping until exit from function _start, which has no line number information. main () at helloint.c:4 4 { (gdb) step Breakpoint 1, main () at helloint.c:5 5 char *str = "Hello World"; (gdb) step 6 return 0; (gdb) step 7 } (gdb) step Program received signal SIGSEGV, Segmentation fault. 0x0000000000000001 in ?? () (gdb)
Что?
main
исполняется два раза? …Пришло время взяться за ассемблер:jesstess@kid-charlemagne:~/c$ objdump -d hello hello: file format elf64-x86-64 Disassembly of section .text: 00000000004000e8 <_start>: 4000e8: e8 03 00 00 00 callq 4000f0 4000ed: 90 nop 4000ee: 90 nop 4000ef: 90 nop 00000000004000f0 : 4000f0: 55 push %rbp 4000f1: 48 89 e5 mov %rsp,%rbp 4000f4: 48 c7 45 f8 03 01 40 movq $0x400103,-0x8(%rbp) 4000fb: 00 4000fc: b8 00 00 00 00 mov $0x0,%eax 400101: c9 leaveq 400102: c3 retq
Хех! Подробный разбор ассемблера оставим на потом, отметив вкратце следующее: после возврата из
callq
в main
мы исполняем несколько nop
и возвращаемся прямо в main
. Поскольку повторный вход в main
был осуществлен без установки указателя инструкции возврата на стеке (как части стандартной подготовки к вызову функции), второй вызов retq
пытается достать из стека фиктивный указатель инструкции возврата и программа вылетает. Нужен способ завершения.Буквально. После возврата из
callq
в %eax
делается push 1
, код системного вызова sys_exit
, и т.к. нужно сообщить о правильном завершении кладем в %ebx 0
, единственный аргумент SYS_exit
. Теперь входим в ядро с прерыванием int $0x80
.jesstess@kid-charlemagne:~/c$ cat stubstart.S .globl _start _start: call main movl $1, %eax xorl %ebx, %ebx int $0x80 jesstess@kid-charlemagne:~/c$ gcc -nostdlib stubstart.S -o hello hello.c jesstess@kid-charlemagne:~/c$ ./hello jesstess@kid-charlemagne:~/c$
Ура! Программа компилируется, запускается, при прогоне через gdb даже нормально завершается.
Привет из свободного от libc мира!
Оставайтесь со мной, во второй части разберем ассемблерный код подробно, посмотрим что случится, если сделать программу более сложной, и еще немного разберемся в линковании, соглашениях о вызовах и структуре двоичного ELF файл в х86 архитектуре.