Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #90, ИЮНЬ 2006 г.

Самый маленький эльф

Крис Касперски ака мыщъх

Хакер, номер #090, стр. 090-110-1


Посади ELF-файлы на диету!

Даже при программировании на чистом ассемблере elf-файлы обычно получаются очень большими, но существует масса способов уменьшить их размер. Давай напишем обычную ассемблерную программу и, убирая все лишнее, постепенно будем оптимизировать ее, вплоть до полного экстрима.

Программирование с libc — семейная идиллия

Почему-то считается, что программирование на ассемблере под UNIX начинается с "прямого" общения с ядром в обход стандартной библиотеки libc. Мотивы этого заблуждения обычно крутятся вокруг чрезмерного увлечения оптимизацией. Дескать, файлы, использующие libc, медленные, неповоротливые и большие, как слонопотамы. Согласен, в отношении программ типа "hello, world!" это действительно так, однако в реальной жизни отказ от libc означает потерю совместимости с другими системами и ведет к необходимости переписывания уже давно написанного и отлаженного кода, в результате чего оптимизация превращается в "пессимизацию".

Никаких убедительных доводов для отказа от высокоуровневых языков еще никто не привел, и прибегать к ассемблеру следует лишь в том случае, когда компиляторы уже не справляются. На ассемблере обычно пишутся критические к быстродействию вычислительные модули, "перемалывающие" данные и вообще не обращающиеся ни к libc, ни к ядру. Если же все-таки по каким-то причинам программа должна быть написана на ассемблере целиком, интерфейс libc будет хорошим выбором. Первую брачную ночь с ассемблером мы проведем именно с этой библиотекой, а дальше — на твое усмотрение: оставаться с ней и дальше или идти штурмовать ядро.

Ассемблерные файлы имеют традиционное расширение ".S", что позволяет нам ассемблировать программы при помощи... компилятора gcc! Кто сказал, что это извращение? Напротив! Распознав по расширению ассемблерную природу транслируемого файла, gcc пропускает его через gas, передавая полученный результат линкеру, благодаря чему процесс сборки существенно упрощается, и мы получаем в распоряжение достаточно мощный сишный препроцессор, хоть и не такой мощный, как в TASM.

Естественно, ассемблируя программы "вручную", мы можем назначать им любые расширения, какие только захотим, и ".asm" в том числе. Прежде чем ассемблировать программу, ее нужно создать! Мы будем использовать стандартный для UNIX'а ассемблер as, на самом деле представляющий собой целое семейство ассемблеров для платформ различного типа (подробности в "man as").

Структурно программа состоит из секции кода, объявленной директивой ".text" и секции данных (".data"), которые могут располагаться в любом порядке. На размер сгенерированного файла это никак не влияет — все равно линкер переставит их по-своему. Объявлять вызываемые libc-функции "внешними" (директива ".extern") совершенно необязательно. Имена функций пишутся, как они есть, без всяких символов прочерка. Точка входа в программу означается меткой main, которая обязательно должна быть объявлена как global. В действительности при запуске программы первым управление получает стартовый код библиотеки libc, который уже и вызывает main. Если такой метки там не окажется, линкер сообщит о неразрешимой ссылке — и все. Выходить из main можно как по exit(err_code), так и по машинной команде RET, возвращающей нас в стартовый код, корректно завершающий выполнение. Это короче, но в последнем случае мы теряем возможность передавать код возврата, который можно "подсмотреть" командой "echo $?" после завершения работы программы. Согласно Си-соглашению, аргументы функций заносятся в стек справа налево. Стек "чистит" вызывающий код. Вот, собственно, и все. С полученным "багажом" знаний уже можно писать программу. В нашем случае она будет выглядеть так:

Содержание  Вперед на стр. 090-110-2
Hosted by uCoz