Экстремальная оптимизация или гонка на виражах
Держитесь! Мы вошли в раж и не оторвемся от клавиатуры, пока не сократим файл хотя бы на десяток байт. Больше всего нас раздражают e_ident
байты, оставленные для выравнивания в количестве целых девяти штук. Плюс один байт версии elf-файла, которую все равно никто не проверяет! А что если... разместить строку "hello,elf" именно здесь?! Сказано— сделано! Ведь elf-заголовок отображается на память и вполне пригоден для хранения переменных.
Но это еще не все! Даже поверхностный взгляд показывает, что 8 последних байт elf-заголовка совпадать с 8 первыми байтами program header table, следующего непосредственно за ним. вот они, красавчики: 01h 00h 00h 00h 00h 00h 00h 00h 01h 00h 00h 00h 00h 00h 00h 00h. А почему бы не сдвинуть начало program header table так, чтобы оба заголовка перекрывались? Для этого будет достаточно всего лишь скорректировать поле e_phoff, переместив метку phdr
вглубь elf заголовка.
Оптимизировав служебные структуры насколько это возможно, займемся "несущим" кодом. Команда MOV EAX, 4
отъедает целых 5 байт, но если немного подумать, можно "отвоевать" 1 байт, заменив ее эквивалентной конструкцией: XOR EAX,EAX/MOV AL, 4. Тоже самое относится и MOV EDX,MSG_END-MSG.
Проделав все эти операции, мы получим следующий файл:
BITS 32
org 8048000h
ehdr: ; // Elf32_Ehdr
; db 7Fh, "ELF", 1, 1, 1 ; // e_ident
db 7Fh, "ELF", 1, 1 ; // e_ident
; // размещаем выводимую строку в поле e_ident
; // в EI_PAD байтах, оставленных для выравнивания
; // "захватывая" и байт EI_VERSION
msg db "hello,elf",0Ah
msg_end:
dw 2 ; // e_type
dw 3 ; // e_machine
dd 1 ; // e_version
Эльфы большие и маленькие
крис касперски ака мыщъх, no-email
даже при программировании на чистом ассемблере, elf файлы обычно получаются очень большими, но существует масса способов уменьшить их размер (с потерей совместимости с другими системами или без). давайте напишем обычную ассемблерную программу и, вырезая все лишнее, постепенно будем оптимизировать ее вплоть до полного экстрима.
Конструирование elf'а своими руками
Программирование без libc значительно сокращает размер программ, однако, полученные файлы все равно остаются большими и толстыми. Самый крошечный эльф, который нам только удалось получить, весит целых 388байт и это при том, что он не насчитывает и десятка ассемблерных команд. Что же такое содержится в нем? Возьмем любой hex-редактор и посмотрим (см. рис. 4).
![](image/konstruirovanie-elf-a-svoimi-rukami_1.jpg)
оптимизированный файл elf_tinix.asm с перекрывающимися заголовками
Транслируем его тем же путем, что и раньше и получаем... 98 байт! Самое интересное, что под Linux'ом этот файл еще и работает, а вот BSD, увы, — шуток с перекрытием заголовков не понимает.
![](image/listing-14-optimizirovannyj-fajl-elf-tinix-asm-s_1.jpg)
простейшая ассемблерная программа elf_libc.S
Чтобы вдохнуть в ассемблерный файл жизнь, его необходимо прогнать через транслятор, чем мы сейчас и займемся:
$gcc -o elf_libc elf_libc.S
$ls -l elf_libc
-rwxr-xr-x 1 root staff 12.096 2006-04-20 18:32 elf_libc
$./elf_libc
hello,elf
сборка нашей первой программы
На диске образуется файл elf_libc, победоносно выводящий "hello,elf" на экран, но занимающий при этом… целых 12.096 байт (при трансляции под BSD – 4.270). Ну и монстр! Куда это годится?! А все потому, что компилятор самовольно прицепил символьную информацию, которая нам совершенно ни к чему. К счастью, ее очень легко отрезать штатной утилитой strip.
$strip elf_libс
$ls -l elf_libc
-rwxr-xr-x 1 root staff 2.892 2006-04-20 18:36 elf_libc
$./elf_libc
hello,elf
обрезание символьной информации
Файл сразу же похудел до 2.892 байт (под BSD — до 2.744), полностью сохранив свою работоспособность. С таким размером уже можно жить (особенно под BSD, где у мыщъха установлена старая версия компилятора, с годами становящегося все прожорливее и прожорливее). Естественно, сама операционная система тут не причем.
А теперь, отказавшись от услуг gcc, попробуем собрать файл вручную. Под BSD это осуществляется так (см. листинг 4):
$as -o elf_libc.o elf_libc.S
$ld -s -o elf_libc /usr/lib/crt1.o elf_libc.o -lc
$ls -l elf_libc
-rwxr-xr-x 1 root wheel 2.108 2108 Apr 18:39 elf_libc
$./elf_libc
hello,elf
ручная" сборка
На диске образуется файл elf_libc с размером всего 2.108 байт, что на 636 байт короче сборки gcc с последующем стрипаньем символьной информации. То есть, "ручная" сборка намного эффективнее!
C Linux'ом и всякими прочими SUN'ми и Solaris'ми в этом плане намного сложнее и стартовый код у них расположен черт знает где, но это еще полбеды. Значительно хуже, что стартовый код содержит дикие зависимости, влекущее за собой дополнительные библиотеки, находящиеся в самых непредсказуемых местах (см. рис. 1). Зубков дает несколько рецептов сборок (http://www.msiu.ru/~law10/index.htm?page=source%2Fhtml%2Fch11_04.htm), но на проверку они оказываются нерабочими. В частности, он пишет, что под Linux, программа должна компоноваться так: "ld -s -m elf_i386 -o helloelf.lnx /usr/lib/crt1.o /usr/lib/crti.o -L/usr/lib/gcc-lib/i586-cubbi-linuxlibc1/2.7.2 helloelf.o -lc -lgcc /usr/lib/crtn.o". Это же умом поехать можно, пока наберешь такую строку, но на моем knoppix'е она не работает, потому что директория /usr/lib/gcc-lib/ не содержит никакого i586-cubbi-linuxlibc1, а опция -lgcc
дает ошибку, поскольку предписывает включать библиотеку libgcc.a, которая у меня (то есть у knoppix'а) опять-таки находится совсем не там, где ожидается. Если мыщъх не ошибается, то вариант Зубкова больше для cygwin'а подходит.
![](image/listing4-ruchnaja-sborka-assemblernoj-programmy_1.jpg)
полуручная", "полуавтоматическая" сборка
Выходит, что "полуавтоматическая" сборка под Linux'ом дает тот же самый результат, что и автоматическая, поэтому, никакого смысла работать руками здесь нет.
автоматическая сборка ассемблерной программы с отладочной информацией под Linux
Ого! Размер файла после подключения отладочной информации возрос до 12.268 байт, что на 172 байта больше, чем у файла, собранного нормальным способом (без отрезания символьной информации, конечно).
Грузим программу в отладчик, вновь брякаемся на main, говорим "r" и... чудо! Команды "s" и "n" теперь нормально работают, отображая программу так, как она выглядела в исходном тексте!
![](image/listing6-avtomaticheskaja-sborka-assemblernoj_1.jpg)
ручная сборка ассемблерной программы с отладочной информацией под Linux и BSD
Размер файла с отладочной информацией составляет… всего 3.145 байта, что намного меньше чем при автоматической сборке с gcc, при этом программа нормально отлаживается! Так что делайте выводы и решайте на чем сидеть и с кем дружить!
макет программы, определяющей номера системных вызовов в Linux и BSD
Теперь лезем в man ("man 2 write") и смотрим какие параметры этот вызов принимает. Ага: write(int d, const void *buf, size_t n_bytes). То есть, мы должны занести #4 в eax, файловый дескриптор – в ebx, указатель на выводимую строку – в ecx и количество выводимых байт – в edx, после чего вызвать прерывание INT 80h.
BSD-системы используют гибридный механизм: прерывание INT 80h и FAR CALL 0007h:00000000h. Номера системных вызовов так же как и в Linux помещаются в регистр eax, а вот параметры передаются через стек по Си-подобному соглашению (то есть, первым заносится крайний правый параметр, последним в стек ложится фиктивный dword, стек чистит за собой вызывающий код). Поскольку, номера базовых системных вызовов в обоих системах совпадают, можно исхитриться и написать программу, работающую под обоими операционными системами: Linux не обращает внимание на стек, а BSD — на регистры, что позволяет нам продублировать параметры и там, и там. Естественно, это увеличивает размер программы, но, к нашему счастью, BSD позволяет эмулировать Linux-интерфейс, достаточно дать команду "brandelf ?t Linux имя_файла", после чего нам останется только запустить его! А Linux в свою очередь умеет эмулировать BSD, SunOS и еще много чего!
Но довольно слов, переходим к делу! Перепишем нашу программу, чтобы она выводила приветствие через системный вызов write без использования libc. Стартовый код в этом случае исчезает и точкой входа в программу становится метка _start, объявленная как global. Ну а сама программа выглядит так:
.text
.globl _start
_start:
movl $4,%eax ; // системный вызов #4 "write"
movl $1,%ebx ; // 1 -- stdout (xorl %ebx,%ebx/incl %ebx)
movl $msg,%ecx ; // смещение выводимой строки
movl $len,%edx ; // длина строки
int $0x80 ; // write(1, msg, len);
movl $1, %eax ; // системный вызов #1 "exit"
xorl %ebx,%ebx ; // код возврата
int $0x80 ; // exit(0);
.data
msg: .ascii "hello,elf\n"
len
= . - msg
ассемблерная программа
Пара замечаний к программе. Инструкция movl $1,%ebx
занимает пять байт, но при желании ее можно ужать до трех: xorl %ebx,%ebx/incl %ebx, однако, учитывая размер служебных полей elf файла, выигрыш не составит и доли процента, так что над оптимизацией кода можно не напрягаться.
Сборка для всех систем осуществляется ручным путем и осуществляется она так:
$as -о elf_80h.o elf_80h.S
$ld -s -o elf_80h elf_80h.o
$ls -l elf_80h
-rwxr-xr-x 1 root staff 388 2006-04-20 19:27 elf_80h
ручная сборка файла elf_80h.S под Linux и BSD
Под Linux'ом размер файла составляет всего 388 байт, BSD слегка отстает — 452 байта (сказываются разные версии трансляторов и линкеров). Под Linux файл запускается сразу же и без вопросов, а вот под BSD требует предварительной эмуляции:
$brandelf -t Linux elf_80h
$./elf_80h
hello,elf
запуск файла elf_80h под BSD в режиме эмуляции
Кстати, говоря, под Linux'ом существует альтернативный вариант автоматической сборки при помощи все того же gcc, запущенного с ключом -nostartfiles, но в этом случае размер полученного файла (даже после стрипа) будет составлять 928 байт, что не есть хорошо, тем не менее все равно меньше, чем с использованием libc.
ассемблерный файл elf_tiny.asm, сконструированный голыми руками
Теперь, когда борьба идет за каждый байт, воспользуется ассемблерными трюками, оптимизирующими размер ассемблерного кода. Во-первых, заменим MOV EBX, 1 на XOR EBX,EBX/INC EBX
(напоминаю, NASM использует INTEL'й синтаксис), во вторых, сохраним это значение в стеке однобайтовой командой PUSH EBX — позднее оно нам понадобиться для системного вызова exit. В-третьих, не будет явно инициализировать код возврата — он ведь нам все равно не нужен.
$nasm -f bin -o elf_tiny elf_tiny.asm
$chmod +x elf_tiny
$ls -l elf_tiny
-rwxr-xr-x 1 root staff 118 2006-04-20 19:29 elf_tiny
./elf_tiny
hello, elf
ручная сборка и запуск файла elf_tiny.asm под Linux
После сборки образуется двоичный elf-файл размеров всего в... 118 байт, что в три с лишним раза короче аналогично файла, собранного стандартным линкером. Но это еще не предел!
фрагмент файла elf_tinyh.asm, оптимизированного Юрием Хароном
Как видно, Харон использовал прямую засылку константы в стек командой PUSH 1, занимающий всего два байта — 6Ah 01h, которую коварный NASM растянул до целых 5 байт 68h 01h 00h 00h 00h, поэтому пришлось прибегнуть к прямой машиннокодовой вставке директивой dw.
Еще Харон использовал могучую инструкцию LEA, о существовании которой никогда нельзя забывать (а вот мыщъх забыл и проиграл).
Отладка ассемблерных программ— ночной кошмар
Редкая программа начинает работать сразу же после запуска. Практически всегда она содержит ошибки, требующие отладки. Высокоуровневые программисты находятся в более выгодном положении, поскольку значительная часть ошибок отсеивается компилятором еще на стадии трансляции, к тому же сам синтаксис языка делает программу намного более выразительной. Одиночные ассемблерные команды в отрыве от своего окружения — абсолютно бессмысленны и обнаружить ошибку путем визуального просмотра листинга очень тяжело.
Отладка ассемблерных программ — это тот вопрос, который большинство составителей tutorial'ов предпочитают обходить стороной. Существует даже мнение, что нормальных отладчиков под UNIX вообще нет, а "великий и могучий" gdb ассемблер не переваривает в принципе (http://www.wasm.ru/comment.php?artcode=asmunixlot).
Что ж! Давайте посмотрим, насколько это утверждение близко к истине. Пропустим ассемблерную программу через gcc, но на этот раз не будем удалять символьную информацию, которая, собственно говоря, для отладчика и предназначена.
Загружаем elf_libc в gdb ("gdb elf_libc"), тут же брякаемся на main ("b main"), запускаем программу командой "r" и, дождавшись срабатывания точки останова, пробуем трассировать (команда "s" — трассировка без захода в функции, "n" – с заходом). Отладчик тут же слетает с катушек, ругаясь на отсутствие информации о номерах строк. Оба на!
И хотя отладка на ассемблерном уровне (не путать с уровнем исходных текстов!) все-таки доступна (даем команду "display/i $pc" для отображения ассемблерных мнемоник и ведем трассировку командами "si" и "ni" соответственно), но в этом случае мы теряем всю информацию об именах функций, метках, переменных, короче говоря, львиная доля смысла листинга уходит в никуда. Вот тут кто-то говорит, а какая нам, собственно, разница? Ведь ассемблерные команды одни и те же, ну а без имен и меток мы как ни будь переживем. Ага!!! Попробуйте отладить реальную программу, а не учебный пример, сразу же взвоете! Нет, надо действовать не так!
![](image/otladka-assemblernyh-programm-nochnoj-koshmar_1.jpg)
Программирование без libc— штурм ядра
Интерфейс системных вызовов (они же syscall'ы) это "задний двор" операционной системы, это ее собственная и к тому же недокументированная кухня. Системные вызовы и native-API Windows NT стоят на одной степени, причем у Гейтса native-API намного более предсказуемо и документировано. Вот тут кто-то опять порывается крикнуть, что Linux'у никакая документация совсем не нужна, он распространяется в исходных текстах и документирует себя сам. Чушь! Если не сказать провокация.
Исходный текст — это не документация! Это программа, в которой нужно очень долго и нужно ковыряться, прежде чем удаться хоть что-то понять. Термин "документация" происходит от слова "документ", а документ предполагает внятное описание материала, изложенного в установленной форме. Более того, "документированные API функции" это стандартизированные API-функции! Различные клоны UNIX'а используют свои собственные способы взаимодействия с ядром, число которых намного больше десятка! Даже разные ядра одной и той же системы могут вносить в syscall'ы непредсказуемые изменения. Возьмите Linux и сравните ядро 2.4 с ядром 2.6. А для большинства коммерческих UNIX'ов исходные тексты вообще недоступны. Что толку с того, что мы знаем как вызвать такой-то syscall на отдельно взятом ядре? Где надежда (я уже не говорю о "гарантиях") что наш файл запуститься на соседней машине?
Реально в syscall'ах нуждаются одни лишь черви, распространяющиеся через переполняющиеся буфера и потому очень ограниченные в размерах, чтобы реализовать процедуру поиска libc в памяти. И еще — драйвера. Но драйвера пишутся под конкретные системы и никто не собирается требовать от них переносимости, а мы говорим про прикладные программы! Какой ассемблерный tutor не возьми, там обязательно будут syscall'ы. Что ли мода пошла такая или это просто эпидемия? Ладно, неважно! Рассмотрим и syscall'ы, если народу так будет угодно.
Лучшее руководство по интерфейсам системных вызовов можно найти на сайте Last Stage of Delirium Research Group или сокращенно LSD. Оно так и называется "UNIX Assembly Codes Development for Vulnerabilities Illustration Purposes" (http://www.blackhat.com/presentations/bh-usa-01/LSD/bh-usa-01-lsd.pdf), так же хочется порекомендовать неплохой сайт http://www.lxhp.in-berlin.de/lhpsyscal.html — настоящую энциклопедию системных вызовов.
Если отбросить всякие редкоземельные UNIX'ы, то интерфейсов системных вызов всего два — Linux и BSD. Рассмотрим их поближе.
Linux использует fastcall-соглашение о передаче параметров, это значит, что номер системного вызова помещается в регистр eax, параметры передаются слева направо через регистры ebx, ecx, edx, esi, edi, ebp. Если системный вызов принимает больше шести параметров, они передаются со структурой, указатель на которую заносится в ebx. Передача управления происходит путем вызова прерывания INT 80h.
Разумеется, это только общая схема и на практике постоянно приходится сталкиваться с отступлением от правил. Общение с системными вызовами напоминает хождение по минному полю — один шаг в сторону и ты покойник. Вот как это приблизительно выглядит. Допустим, мы хотим вызвать системный вызов write. Для начала необходимо узнать его номер. Системные вызовы перечислены в файле /usr/include/sys/syscall.h, в BSD-системах номера присутствуют сразу, а вот Linux нас отсылает к файлу /usr/include/bits/syscall.h, в котором номеров нет, зато есть нисходящие определения. Короче, чтобы не парится, номер нужного syscall'а проще выяснить с помощью следующей программы (см. листинг 8). Определения syscall'ов обычно имеют префикс SYS_, в частности, системный вызов write определяется как SYS_write, а номер его — #4.
#include <stdio.h>
#include <sys/syscall.h>
main()
{
printf("%x\n",SYS_write);
}
Программирование с libc— семейная идиллия
Почему-то считается, что программировать на ассемблере под UNIX начинается с "прямого" общения с ядром в обход стандартной библиотеки libc. Мотивы этого заблуждения обычно крутятся вокруг чрезмерного увлечения оптимизацией. Дескать, файлы, использующие libc, медленные, неповоротливые и большие как слонопотамы. Согласен, в отношении программ типа "hello, world!" это действительно так, однако, в реальной жизни отказ от libc означает потерю совместимости с другими системами и ведет к необходимости переписывания уже давно написанного и отлаженного кода, в результате чего оптимизация превращается в "пессимизацию".
Никаких убедительных доводов для отказа от высокоуровневых языков еще никто не привел предложил ### приводил и прибегать к ассемблеру следует лишь тогда, когда компиляторы уже не справляются. На ассемблере обычно пишутся критические к быстродействию вычислительные модули, "перемалывающие" данные и вообще не обращающиеся ни к libc, ни к ядру. Если же все-таки по каким-то мотивам программа должна быть написана на ассемблере целиком, интерфейс libc будет хорошим выбором, уж поверьте мыщъх'у! Короче, первую брачную ночь с ассемблером мы проведем именно с этой библиотекой, а дальше вы сами решайте — оставаться ли с ней и дальше или идти штурмовать ядро.
Ассемблерные файлы имеют традиционное расширение .S, скрывающие сакраментальный мистический смысл, позволяющий нам ассемблировать программы при помощи… компилятора gcc! Кто сказал, что это извращение? Напротив! Распознав по расширению ассемблерную природу транслируемого файла, gcc пропускает его через gas, передавая полученный результат линкеру, благодаря чему процесс сборки существенно упрощается и мы получаем в распоряжение достаточно мощный сишный препроцессор, хоть и не такой мощный как в TASM, но это все-таки лучше, чем совсем ничего.
Естественно, ассемблируя программы "вручную", мы можем назначать им любые расширения, какие только захотим, и .asm в том числе (cuestión de gustos — как говорят в этих случаях испанцы). Но прежде, чем ассемблировать программу, ее нужно создать! Мы будем использовать стандартный для UNIX'а ассемблер 'as', на самом деле представляющий собой целое семейство ассемблеров для платформ различного типа (подробности в "man as").
Структурно, программа состоит из секции кода, объявленной директивой ".text" и секции данных (".data"), которые могут располагаться в любом порядке, на размер сгенерированного файла это никак не влияет, все равно линкер переставит их по-своему.
Объявлять вызываемые libc-функции "внешними" (директива ".extern"), как это советует целый ряд авторов, совершенно необязательно. Имена функций пишутся как они есть, то есть без всяких там символов прочерка, на которые в частности ссылается Зубков в своей книге "Assembler — язык неограниченных возможностей", дескать иначе под BSD программа ассемблироваться не будет. Ничего подобного! Все работает только так!
Точка входа в программу означается меткой main, которая обязательно должна быть объявлена как global. В действительности, при запуске программы, первым управление получает стартовый код библиотеки libc, который уже и вызывает main. Если же такой метки там не окажется, линкер сообщит о неразрешимой ссылке и все.
Выходить из main можно как по exit(err_code), так и по машинной команде RET, возвращающей нас в стартовый код, корректно завершающий выполнение. Это короче, однако, в последнем случае мы теряем возможность передавать код возврата, который можно "подсмотреть" командой "echo $?"
после завершения работы программы.
Согласно Си-соглашению, аргументы функций заносятся в стек справа налево, стек "чистит" вызывающий код. Вот, собственно, и все. С полученным "багажом" знаний уже можно писать программу. В нашем случае она будет выглядеть так (см. листинг 1).
.text
// используемые функции объявлять внешними необязательно
//.extern write
//.extern exit
.global main
main:
pushl $len
pushl $msg
pushl $1
call write
addl $12, %esp
ret
.data
msg: .ascii "hello,elf\n"
len = . - msg
внутри elf-файла находится просто море пустоты
Нашему взору представиться одна вода, то есть нули, "заботливо" вставленные тупым линкером. А что если… отказаться от услуг линкера и попробовать соорудить elf-файл голыми руками? Для этого, нам во-первых, потребуется подробное описание всех служебных структур elf'а (последний draft лежит здесь http://www.caldera.com/developers/gabi/), а, во-вторых, транслятор, умеющий генерировать двоичные файлы, например, NASM, входящий в большинство Linux-дистрибутивов, но к, сожалению, не в BSD. Во всяком случае, его всегда можно скачать с "родной" страницы проекта: http://nasm.sourceforge.net/.
Исполняемый elf-файл нуждается в двух структурах: elf-header'e, описывающим основные параметры файла (платформа, адрес точки входа и т. д.) и program header table, перечисляющего все сегменты. Как минимум должен быть один сегмент с правами на чтение, запись и исполнение. Наконец, чтобы elf заработал, требуется добавить "боевую начинку", то есть непосредственно сам ассемблерный код. Минимальный адрес, с которого в UNIX-системах может загружаться elf, равен 8048000h, поэтому нам понадобится директива ORG, задающая начальное смещение в файле.
Остается только изучить документацию и заполнить все служебные структуры соответствующим образом:
BITS 32
org 8048000h
ehdr: ; // Elf32_Ehdr
db 7Fh, "ELF", 1, 1, 1 ; // e_ident
times 9 db 0
dw 2 ; // e_type
dw 3 ; // e_machine
dd 1 ; // e_version
dd _start ; // e_entry
dd phdr - $$ ; // e_phoff
dd 0 ; // e_shoff
dd 0 ; // e_flags
dw ehdrsize ; // e_ehsize
dw phdrsize ; // e_phentsize
dw 1 ; // e_phnum
dw 0 ; // e_shentsize
реакция Linux'а на попытку ручкой сборки по типу BSD
Что же делать? Приходится обращаться за помощью к gcc — уж он-то наверняка знает, где расположены его библиотеки. Ассемблируем файл транслятором as и передаем полученный elf_libc.o на компоновку компилятору gcc. Стрипаем символьную информацию и… получаем те же самые 2.892 байт, что и при автоматической сборке.
$as -o elf_libc.o elf_libc.S
$gcc elf_libc.o -o elf_libc
$strip elf_libc
$ls -l elf_libc
-rwxr-xr-x 1 root staff 2.892 2006-04-20 19:06 elf_libc
$./elf_libc
hello,elf
отладка ассемблерной программы без символьной информации
Если отладочной информации нет, это еще не означает, что ее нельзя подключить! В частности, у gcc за это отвечает ключ "-g", а сам процесс сборки выглядит так (см. листинг 6).
$gcc -g -o elf_libc elf_libc.S
$ls -l elf_libc
-rwxr-xr-x 1 root staff 12.268 2006-04-20 19:09 elf_libc
$dbg elf_libc
отладка ассемблерной программы на уровне исходных текстов
Правда, под BSD этот прием не срабатывает и для подключения отладочной информации приходится собирать программу вручную. Транслятору ассемблера необходимо указать ключ "--gstabs", а у линкера — отобрать ключ "-s", отвечающий за удаление всей отладочной информации.
Короче, это выглядит так:
$as --gstabs -o elf_libc.o elf_libc.S
$ld -o elf_libc /usr/lib/crt1.o elf_libc.o -lc
$ls -l elf_libc
-rwxr-xr-x 1 root wheel 3.145 2108 Apr 19:09 elf_libc
$dbg elf_libc
BSD 4.5 не поддерживает elf-файлы с перекрывающимися заголовками
Но 98 байт это еще не предел! Переписав "несущий" код легендарный хакер Юрий Харон с ходу сократил его еще на 2 байта, сказав при этом "…а вот дальше уже думать надо, но лень ;-)".
dw 016Ah ; // push 01
pop ebx ; // ebx := 1
lea eax,[ebx+4-1] ; // eax := #4 (сист. вызов "write")
lea edx,[ebx+(msg_end-msg)-1] ; // хитрый трюк, но так короче
push ebx ; // сохраняем ebx для syscall'а "exit"
mov ecx, msg ; // смещение выводимой строки
int 80h ; // write(stdout, msg, len);
pop eax ; // системный вызов #1 "exit"
int 80h ; // exit(0);
версии программных продуктов, используемые в статье
При использовании других версий, размер получаемых файлов может быть другим, что следует учитывать читателям, при сравнении своих результатов с результатами автора.
>>> Врезка график похудания elf-файла
стадия оптимизации | размер, байт | ||||
Linux | BSD | ||||
elf_libc.S, автоматически собранный gcc | 12.096 | 4.270 | |||
elf_libc.S, автоматически собранный gcc после стрипа | 2.892 | 2.744 | |||
eflf_libc.S, собранный вручную as-ld | (2.892) | 2.108 | |||
eflf_libc.S, собранный с отладочной информацией | 12.268 | 3145 | |||
elf_80h.S, собранный вручную/автоматически | 388/928 | 452/— | |||
elf_tiny.asm, сконструированный голыми руками | 118 | 118 | |||
elf_tinyx.asm оптимизированный мыщъх'ем | 98 | — | |||
elf_tinyh.asm оптимизированный Юрием Хароном | 96 | — |
>>> Врезка кто есть где
В своих экспериментах автор использовал дистрибутив KNOPPIX и BSD4.5, включающие в себя следующие версии трансляторов:
программный продукт | KNOPPIX | BSD | |||
as | 2.15 | 2.11.2 | |||
ld | 2.15 | 2.11.2 | |||
gcc | 3.3.4 | 2.95.3 | |||
NASM | 0.98.38 | – |
ом многими рассматривается как извращение
Программирование на ассемблере под UNIX' ом многими рассматривается как извращение (http://www.wasm.ru/article.php?article=asmunixlot), граничащее со злостным преступлением, препятствующим переносу программы на другие платформы, даже если никакой перенос не планируется. Являясь колыбелью десятков языков, таких как C, Perl, Haskell, Lisp, Simula и AWK, UNIX поддерживает ассемблер лишь формально. Богатство средств разработки и нищета документации создают проблему даже для опытных программистов, впервые увидевших ассемблер еще на ZX-Spectrum (Электроника BK) и не расстававшихся с ним ни в MS-DOS, ни в Windows.
Классические трансляторы ассемблера (такие, например, как GAS – GNU Assembler) придерживается AT&T синтаксиса, совершенно несовместимого с "официальным" x86 синтаксисом, декларируемым самой фирмой Intel (достаточно сказать, что порядок операндов поменялся местами и всюду торчат эти жуткие суффиксы и префиксы #, $,%, l и проч. дребедень). Ну, тут можно и возразить, что AT&T-синтаксис появился в те "геральдические" времена, когда парни из Intel еще не слезли с деревьев. С другой стороны, в UNIX существуют множество трансляторов типа NASM'а и FASM'а, "переваривающие" привычный нам синтаксис, правда, не совпадающий ни с MASM'ом, ни с TASM'ом, а это значит, что прежде, чем начать программировать нам снова придется учиться!
Процесс обучения погружен в эротический полумрак, в котором, как и в первую ночь с женщиной, приходится действовать наугад. От изобилия сопутствующей литературы буквально рябит в глазах, но в большинстве случаев действие заканчивается там, где у нормальных людей процесс получения удовольствия только начинается. Отчаявшись найти нормальное руководство, мыщъх, махнул хвостом, и обобщив свой опыт ассемблерных похождений, попробовал заточить его сам. И вот что из этого получилось…
это успех, которым можно гордиться.
Мы прошли длинный путь и добились впечатляющих результатов. 96 байт для программы "hello,elf" – это успех, которым можно гордиться. Если убрать перекрытие заголовков, мы получим 100 байт, но тогда файл будет работать как под Linux, так и под BSD. Но цепная реакция оптимизации на этом еще не заканчивается. Кто из читателей примет вызов и сократит файл хотя бы еще на один байт?