Программирование

индекс
178,71

Привет из свободного от libc мира! (Часть 1)

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

Звучит несложно, правильно?

У читателя предполагается наличие опыта компиляции программ и работы в Линуксе. Небольшое умение читать ассемблерный код тоже пригодится.

Итак, вот наш простейший хелловорлд:

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 © 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 архитектуре.
+114
19 марта 2010, 01:12
80

комментарии (54)

НЛО прилетело и опубликовало эту надпись здесь
+9
Mezomish #
Имхо, написано неплохо вне зависимости от пола.
НЛО прилетело и опубликовало эту надпись здесь
+21
lyola #
«для девушки очень неплохо написано» — для тролля так себе провокация.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
0
sigizmund #
Я хочу подчеркнуть что вы идиот и поставить вам за это минус :-)
Хотя нет. Вы жирный и толстый. Обойдетесь.
НЛО прилетело и опубликовало эту надпись здесь
–2
ccrypt #
А я с тобой согласен полностью и не минусую ни разу, а кто минусует, тот не шарит. Только ты заметил, что это западная «девушка», блог расположен, там где занимаются ядром linux (видимо, поэтому и отвязка от userspace идет). А перевел, вроде, всеравно парень.
НЛО прилетело и опубликовало эту надпись здесь
–3
ccrypt #
А неважно. Я всегда могу просто ответить.
+3
Q2W #
А чем так плох libc кроме лишних 11Кб?
ИМХО, сейчас очень редко 11Кб могут стать проблемой.
+1
ayambit #
Ничем не плох, смысл статьи вот же в чем:
>В качестве упражнения я хочу написать программу на С. Достаточно простую для того, чтобы дизассемблировать ее и объяснить весь код самой себе.
–3
egorinsk #
А зачем эти негодяи засовывают в мою программу 11 Кб ненужного кода? Какой-то индус-стайл подход!

Кстати, сам libc мне тоже не нравится, весит по моему больше мегабайта, при том что ничего значительного не содержит, так, всякие вспомогательные функции.
+7
Bambr #
Да дело не в 11 кб, а в том, что автор заметила в поведении компилятора нечто, что не смогла объяснить, но не прошла мимо, а докопалась до истины. По-моему, это заслуживает глубокого респекта.
0
ayambit #
Расскажите какой быстрый и простой сделать расцветку кода, а то я не нашел как.
0
alexbooter #
0
iley #
В хабраредакторе есть подсветка кода.
+1
ayambit #
Криво там реализовано, нужно же как:
а) идеальный вариант — вбил текст вперемежку с кодом, тапнул «тут есть код» и готово (а совсем идеальный — и тапать не надо);
б) годный вариант — выделил блок когда мышкой, тапнул кнопку «подсветить».

А не копипейстить туда-сюда. Пока что опробую pastebin.com или буду использовать тег pre.
+4
gribozavr #
> char *str = «Hello World»;

const char *str
0
fwm #
а почему? просто хороший тон?
0
gribozavr #
Потому что строки-литералы — это константы и могут размещаться (и размещаются некоторыми компиляторами) в памяти только для чтения.
+3
rule #
«фигасе» (с), забавный хак :-), действительно было интересно, не часто встретишь достойное почитать, написаное достаточно легко, чтоб не напрягаться и достаточно сложно, чтоб не было скушно,
А вообще свободный од glibc мир — это uClibc, dietlibc, Newlib. С первой работал, вот такой простой примерчик занял у меня около 3 кб на SH4 архитектуре и около 14 на PPC405. Практически уверен, что для 386 архитектуры, собраный интеловским компилятором будет меньше 3кб.
+25
Iskin #
Кстати, фото автора ;)

–3
ekzo #
а по посту и не скажешь, habrahabr.ru/blogs/nix_coding/88101/#comment_2640714
+4
andoriyu #
это как бы перевод ее поста/статьи…
+6
ekzo #
чёрт, я не понял намёка Z>Я!

где тут кнопка искреннего покаяния на хабре?
+11
hardex #
Reset в профиле
+1
gt_x #
Вот сайт автора переведенной статьи
web.mit.edu/jesstess/www/
+1
Bugblind #
Люблю вот такие вещи, поковыряться да посмотреть как оно там работает :-)
–2
mikhanoid #
Странный призыв: программировать на Си и Ассемблере, чтобы избавиться от libc :). А главное ещё 'добро пожаловать'. Чего-то мне кажется, что она в итоге начнёт фигачить ображение к системе через syscall'ы, и добра совсем от этого не прибавится. Надо было статью назвать как-то вроде: как происходит запуск программ в Linux.

P.S. Йэхъ. Побольше бы подобных русскоязычных деффушек :)
0
norguhtar #
Если вам надо меньше кода, то лучше все же взять ulibc :)
+1
protagonist #
На второй странице программа перестает выводить Hello world, но главное, что автор — девушка.
0
ayambit #
Есть намек:
>во второй части разберем ассемблерный код подробно, посмотрим что случится, если сделать программу более сложной
+2
GHS #
Когда-то тоже игрался, убирал райнтайм. Бессмысленное занятие.
+2
ayambit #
Но интересно же!
+3
trak #
Бессмысленных занятий не так уж и много, и это тоже не быссмысленное. «Лучше ковыряться отладчиком в чужом коде, чем пальцем в собственной заднице» ©. А такие упражнения дают полезный опыт.
+1
OmIkRoNiXz #
захотелось даже снова за Си взяться :)
+4
mike_mvk #
Так, хорошо, от libc мы освободились. Теперь бы аналог банального printf…
+3
xenon #
write() — оно почти ровно в syscall перейдет.

Только форматирование — ручками. Но для hello world — вполне пойдет.
0
samlabs821 #
Крутооо!!!
0
RinOS #
Без подсветки кода, трудно читать :(
А вообще классно) Такие в дебри компилятора никогда не лазил)
0
ayambit #
Было бы там что-то длинное я бы нашел как подсветить, но там мало, и я счел, что усилия того не стоят. Да, в её блоге тоже не расцвечено. Но там удобнее немного читать, на хабре шрифт меньше.
+5
sch #
Выдержка из резюме «девочки»:

Projects and Awards
• (2009 ) Mobile Application Competition: wrote a location-aware, collaborative task manager
with persistent storage for the iPhone. Winner of the Qualcomm award.
• (2009 )Web Programming Competition (6.470): wrote a music-exploration website using AJAX,
JSON, PHP, and SQL. Finalist; honorable mention for “Best Minimalist Site”.
• (2008 ) Operating Systems Engineering (6.828): wrote a microkernel/exokernel hybrid OS in C
from scratch, including memory management, process creation, an Ethernet driver, and several
networking extensions to a lightweight TCP/IP stack including NAT, firewalls, and DNS resolution.
• (2008 ) MASLAB (Mobile Autonomous System Laboratory): built a fully autonomous,
vision-based robot with control software written in Java to navigate an unknown playing field.
• (2007 ) Autonomous Robotics Design Competition (6.270): built a fully autonomous, sensorbased
robot with control software written in C to compete against an opponent robot on a known
playing field.
+2
gwinn #
"wrote a microkernel/exokernel hybrid OS in C" вот это круто!!! Эх… надо догонять и перегонять Америку.
+1
syndicut #
А еще она участвует в разработке Twisted
+2
dzmitryc #
Написано красиво. С литературной точки зрения :)
А вот сама идея статьи и выеденного яйца не стоит. Потому как туториалов о программировании на асме под линукс дофига.

dchekmarev@dchekmarev:~$ cat test.s && as test.s -o test.o && ld test.o -o test && ls -l test* && ./test
.section .data
hello:
.ascii «hello, world!\n»
hello_len:
.long. — hello

.section .text
.globl _start

_start:

xorl %ebx, %ebx
movl $4, %eax
xorl %ebx, %ebx
incl %ebx
leal hello, %ecx
movl hello_len, %edx
int $0x80

xorl %eax, %eax
incl %eax
xorl %ebx, %ebx
int $0x80

-rwxr-xr-x 1 dchekmarev dchekmarev 640 2010-03-19 14:38 test
-rw-r--r-- 1 dchekmarev dchekmarev 624 2010-03-19 14:38 test.o
-rw-r--r-- 1 dchekmarev dchekmarev 317 2010-03-19 14:34 test.s
hello, world!
dchekmarev@dchekmarev:~$

Обидно, хоть и очевидно почему глупые статьи пытающихся разобраться нубов набирают тучу плюсов в то время как сложные для основной массы, пусть и полезные, статьи плюсов получают на порядки меньше.
+1
ayambit #
Это риторический вопрос, или ответить?
0
dzmitryc #
У меня это даже не вопрос был, а продолжение «повествовательного предложения»
+1
ayambit #
Я все же немножко разовью свою мысль. Хабр как точка запуска, перед ним не стоит задачи научить чему-то глубокому. Скорее дать некоторое представление или развлечь.

Вот есть вебдев, он кодит на flex и php, си знает чуть менее, чем никак. Прочитал статью — немножко расширил кругозор. Хорошо.

И так со всеми статьями. За глубокими же знаниями лезут в msdn или на форумы программистов. Это очевидно.

Или вот знакомство завязать. Тоже хорошо. Я через Хабр нашел на прошлую работу человека.

У Хабра своя задача, которую он, как я считаю, замечательно выполняет.

Это мысли вслух, конечно вы все это знаете ^__^
0
Gasoid #
и все же круто!
0
oleg_bunin #
Коллега, а Вы не хотите сделать доклад на РИТ++ (http://www.ritconf.ru/)?
Если интересно, то я могу выслать подробную информацию.
0
Klaus #
Когда-то проводил эксперименты с gcc и as. Но там ассемблер был.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.