Reviews / articles about OS/2 |
Operating systems: ArcaOS, eComStation, IBM OS/2 Warp |
|
|
DATE: 2001-10-02 16:44:08 AUTHOR: Андрей А. Породько
Перевод с английского: Андрей А. Породько. Оригинал: Секреты мастеров программирования раскрыты! (Michal Necasek, Сентябрь 2001) В августе 2001 меня посетила дикая идея: я собрался и анонсировал в группе новостей comp.os.os2.programmer.misc "Конкурс миниатюризации для OS/2", конкурс для программистов OS/2, целью которого должно было быть создание минимально возможной программы удовлетворяющей следующим условиям. Наиболее важными из них были:
Для того, чтобы сделать конкурс более интересным и привлекательным для программистов, имеющих различный опыт, сначала я объявил конкурс в двух категориях, которые потом были дополнены еще одной:
Для минимизации организационных усилий и чтобы соревнование было честным, я использовал одну удачную идею: все конкурсанты сообщали мне только минимальные достигнутые ими размеры по каждой предложенной категории, и только финалист должен был опубликовать свой исходный код и передать его мне. Я регулярно публиковал достигнутые участниками результаты и каждый знал, к чему стремится. Я думаю что именно это привело к таким результатам. Перед тем как объявить о конкурсе у меня были несколько своих идей как получить очень маленький исполняемый модуль. Но даже я был удивлен программой-победителем. Победителями в трех категориях стали:
Возможно, наиболее интересным результатом конкурса явился тот факт что программы на C и ассемблере очень незначительно отличаются размером, в отличие от первоначальных ожиданий. Это в основном благодаря тому факту, что Watcom C предоставляет очень хорошие средства управления генерируемыми объектными файлами, почти такое же хорошее как в ассемблере. Но, конечно же, наиболее замечательный результат получил победитель в категории Custom. Особенно если Вы понимаете что размер заголовка LX-модуля составляет 196 байт и загрузчик программ OS/2 откажется запустить что-либо меньшее по размеру 196 байт. Но тем не менее этот исполняемый модуль размером с заголовок печатает 17-ти байтное сообщение и корректно возвращает управление операционной системе. Я не буду здесь обсуждать эту категорю в деталях и отправлю читателя к автору Martin Lafaix'-у за разбором его программы-победителя (справедливо названному Fandango on Core). Теперь, после того как я обрисовал условия и ход конкурса, давайте перейдем к рассмотрению исходных текстов. Исходные тексты, которые я представлю, это мои собственные тексты, но с использованием многих идей победителей. Я строил все программы с помощью Watcom 11.0c и их вполне возможно приспособить к другим ассемблерам/компиляторам, возможно за исключением наиболее продвинутого C-кода. Я буду объяснять все ключи компилятора и линкера которые не совсем очевидны. Я предполагаю что читатель имеет представление об использовании ассемблера, C, архитектуры x86, формата исполняемого модуля OS/2 и среды исполнения. Это определенно не для новичков даже если новичок сможет перенять прием или два не перегрузившись. Если все же перегрузился, не беда, возвращайтесь через год или два! Я тоже когда-то начинал и был новичком. Категория High Octane StockСначала я хочу представить категорию, в которой разрешалось использование ассемблера, так как текст здесь более "прямой" и не используется столько таинственных приемов (трюков) как в C версии. Надо сказать, что в ассемблерной версии все ясно видно, тогда как в C версии многие приемы скрыты за используемыми прагмами и ключами компилятора. Хорошо, как вы выводите сообщение на консоль с использованием OS/2 АПИ? Первый очевидный выбор - через DosWrite. Но DosWrite имеет несколько серьезных недостатков. Он требует четырех параметров (т.е. четырех DWORD слов в стеке) и располагается в системной библиотеке DOSCALLS.DLL (на самом деле в ядре OS/2), которая имеет достаточно длинное имя, которое должно быть включено в исполняемый файл. Давайте разберем внимательнее. В этом случае можно воспользоваться другим, редко используемым, вызовом OS/2 АПИ, это DosPutMessage. Он имеет только три параметра и, что еще лучше, располагается в MSG.DLL (более короткое имя библиотеки). Полностью исходный текст программы (названной asm1.asm) с использованием DosPutMessage выглядит примерно так: .386p EXTRN DosPutMessage:BYTE _DATA SEGMENT BYTE PUBLIC USE32 'STACK' _msg: DB "I'm really small!",0aH _DATA ENDS _TEXT SEGMENT BYTE PUBLIC USE32 'CODE' ASSUME CS:_TEXT, DS:_TEXT, SS:_TEXT startup: push offset flat:_msg push 12H push 1 call near ptr flat:DosPutMessage add esp,0CH ret _TEXT ENDS END startup Как Вы видите, эта программа очень короткая и не использует никаких трюков. Единственным "трюком", пожалуй, является использование вместо DosExit простого RETs. Это приводит к тому же эффекту, что и вызов DosExit с параметрами по-умолчанию. После компилирования WASM-ом и линковки WLINK-ом в конце концов Вы получите программу размером в 545 байт. Вот точный набор команд, который я использовал: wasm asm1.asm wlink file asm1 lib os2386 option st=32k Единственным, не совсем очевидным ключом является st=32k который устанавливает размер стека программы в 32 килобайта. Без этого ключа, программа имеет только 18 байт стека (сообщение которое она должна вывести) и трапнется сразу после старта. С точки зрения "жирности" программы 545 байт не так уж и плохо, однако легко уменьшить размер с помощью LxLite: lxlite /T /ZS:512 asm1.exe Здесь трюком является удаление стаба MS-DOS (ключи /T и /ZS LxLite), размером в 128 байт, что значительно влияет на размер программы (в нашем случае. прим. пер.). OS/2 вполне нормально запустит и программу без этого стаба. MS-DOS не будет способен запустить это программу, но этого и не требовалось по условиям конкурса. LxLite проделывает некоторую дополнительную оптимизацию и результат - программа в 325 байт. Это значительно лучше чем 545, но все еще далеко от победителей. Мастера очевидно имеют какие-то козыри в рукавах. Хорошо, давайте посмотрим на них. Для достижения минимального размера необходимо атаковать с нескольких направлений. Скорее всего одно из них позволит уменьшить размер программы. Одним, таким не очевидным ходом является уменьшение избыточности структуры LX модуля. Но мы начнем с чего-то совсем отличного. Существует более чем один путь уменьшения размера файла программы. Пока я думал что нет лучше способа печатать сообщение чем использование OS/2 АПИ, система Warp 4 (базовая ОС с конкурсе) предоставляет несколько менее известный путь, который подходит гораздо больше. Warp 4 поставляется с динамической библиотекой среды C, представляющей собой несколько видоизмененный вариант библиотеки от VisualAge C++ 3-й версии. На самом деле это несколько DLL-библиотек - LIBCS.DLL (для задач не использующих треды), LIBCM.DLL (для задач использующих треды) и LIBCN.DLL (подсистема). В них имеются vprintf() и puts() которые требуют только одного аргумента. Я выбрал puts() так как это позволяет еще оптимизировать код, опустив символ перевода строки в сообщении (puts() добавляет перевод строки автоматически). Тем не менее ординал (ссылка в DLL на функцию. прим. пер.) для vprintf() требует на один байт меньше в таблице импорта исполняемого модуля и, таким образом результат будет таким же. Как бы то ни было, вот asm2.asm: .386p EXTRN puts:BYTE _DATA SEGMENT BYTE PUBLIC USE32 'STACK' _msg: DB "I'm really small!" _DATA ENDS _TEXT SEGMENT BYTE PUBLIC USE32 'CODE' ASSUME CS:_TEXT, DS:_TEXT, SS:_TEXT startup: mov eax,offset flat:_msg jmp near puts _TEXT ENDS END startup Заметьте что puts() (и vprintf() тоже) требует в качестве параметра строку заканчивающуюся NULL, в отличие от DosPutMessage. Но так как мы знаем (ведь правда?) что сегмент данных гарантированно инициализируется нулями, мы этот факт опустим. Другой важный факт который следует запомнить - это то, что puts() использует соглашение _Optlink для вызова функций, при котором параметры передаются через регистры, а не через стек. Эта программа использует другой интересный прием оптимизации. Вместо прямого вызова puts(), она делает на него переход. Когда puts() сделает возврат, управление вернется непосредственно в тот модуль который вызывал нашу программу, что избавляет нас от использования команды RET у себя. И что еще лучше, такой прием оставляет переход на puts() инициализированным нулями в файле исполняемого модуля, что позволяет линкеру и/или LxLite еще сэкономить 4 байта. Наша программа теперь имеет размер в 318 байт. Получите удовольствие от красоты программы, состоящей всего из двух инструкций и которая, тем не менее, что-то делает, безо всяких критических ошибок. Теперь давайте пройдемся по действительно интересному. Чтобы достичь размеров программ-победителей, нам еобходимо избавится от 45 байт лишнего веса. Это можеть быть невозможно, потому что программа и так достаточно мала. Давайте попроказичаем и будем делать вещи, которые детям делать не разрешают. Это на самом деле просто, но совершенно не очевидно, это показано в листинге asm3.asm: .386p EXTRN puts:BYTE _TEXT SEGMENT BYTE PUBLIC USE32 'STACK' ASSUME CS:_TEXT, DS:_TEXT, SS:_TEXT _msg: DB "I'm really small!",0 startup: mov eax,offset flat:_msg jmp near puts _TEXT ENDS END startup Да, у программы только один сегмент ! Этот сегмент не имеет флага исполняемого, но она запускается и работает и что важно... что более важно, программа имеет размер только 283 байт. Мы достигли размеров программ-победителей! А можно ли еще сэкономть ? Тут два пути. Один чрезвычайно прост: переименовать исполняемый модуль! Имя исполняемого файла сохраняется внутри его самого и для экономии драгоценных байтов, вы можете использовать пустое имя (.exe). Другой путь сэкономить место - это первая инструкция программы. Команда MOV занимает пять байт в исполняемом модуле тогда как JMP занимает только один байт (потому что адрес заполнен нулямя и будет подставлен на этапе загрузки программы). Так получилось, что мы знаем что команда MOV помещает в EAX. Это адрес 10000H (64K) потому что в линейной модели памяти исполняемый модуль всегда загружается по этому адресу. Knut St. Osmundsen нашел более элегантный метод загрузки значения 10000H в EAX длиной в 3 байта кода. Вот финальная версия исходного текста программы (asm4.asm): .386p EXTRN puts:BYTE _TEXT SEGMENT BYTE PUBLIC USE32 'STACK' ASSUME CS:_TEXT, DS:_TEXT, SS:_TEXT _msg: DB "I'm really small!",0 startup: dec ax inc eax jmp near puts _TEXT ENDS END startup А вот команды, которые я использовал для получения исполняемого модуля: wasm asm4.asm wlink f asm4 n .exe imp puts LIBCS.362 op st=32k ren .exe asm4.exe lxlite /T /ZS:512 asm4.exe Необычная последовательность команд WLINK, imp puts LIBCS.362 показывает линкеру что символ (имя) должен быть экспортирован из библиотеки LIBCS как ординал 362. Мы могли бы использовать и LIBCM, но так как наша мини-программа имеет один тред, то и с LIBCS она работает нормально. Что касается номера ординала, то его легко найти с помощью EXEHDR. Конечно возможно испоьзование библиотеки импорта, но для получения доступа к одной функции совсем не обязательно ее иметь. Это способ для тех бедных (заблудших ? прим. пер. ;-) душ которые до сих пор не обзавелись Warp 4 Toolk для написания своих программ. Хитрым приемем является также директива n .exe (Имя). Она указывает WLINK использовать пустое имя для файла исполняемого модуля. И конечный размер теперь 274 байта! Так как я использовал инструменты Watcom (по крайней мере я так думаю), программа-победитель от Knut-а на один байт меньше вследствие разницы между испольняемыми модулями получаемыми с помощью WLINK и ILINK, и не существует способа проверить и проконтролировать это. Хей, однако и 274 байт совсем не плохо! Интересно также отметить что весь исходный код программы очень мал и даже команды для получения исполняемого модуля не очень сложные. Но это обманчивое представление, потому что программа явно использует некоторые особенности архитектуры x86 и среды исполнения OS/2. Категория StockВ предыдущем разделе я раскрыл все важные приемы достижения микроскопических размеров программы. Теперь мы попытаемся получить сходные результаты без использования ассемблера. Это требует достаточно хорошего знания специфики компилятора, такой как различные малоизвестные ключи и прагмы. Я сразу перейду к финальному коду и проанализирую его. Встречайте ! mini.c: void puts(char *s); #pragma data_seg("MYDATA", "STACK") #pragma code_seg("MYDATA", "STACK") char msg[] = "I'm really small!"; void _System startup(void) { puts(msg); } Исполняемый модуль получается следующими командами: wcc386 -s -g=DGROUP mini.c wlink sys os2v2 name .exe f mini imp puts_ LIBCM.362 op start=startup,st=32k,nod ren .exe minic.exe lxLite.exe /T /ZS:512 minic.exe Конечный исполняемый модуль, minic.exe, имеет размер всего 276 байт! Как это возможно? Близкое изучение раскрывает только одно отличие между C и ассемблерным вариантами машинного кода. C генерирует код эквивалентный: mov eax,offset flat:_msg jmp near puts Да, компилятор чертовски умен и оптимизировал код настолько, насколько это возможно! Из-за того что компилятор не имеет информации как результирующий код будет загружаться, он не может заменить смещение текста сообщения константой. Во всем остальном, версия C использует все приемы описанные для ассемблерого варианта. Трюком здесь является то, как компилятор и линкер убедили постороить эту программу. Для достижения этого были использованы две редко применяемые прагмы компилятора. Прагмы #pragma data_seg и code_seg вполне стандартные прагмы (они поддерживаются в большинстве компиляторов и делают одно и тоже) и определяют сегмент - где должны быть размещены данные и код. В нашем случае данные и код конечно же размещаются в одном сегменте. Но почему они имеют класс STACK? Это потому что WLINK требует именованного сегмента который уже существует при сборке исполняемого модуля. Теперь ключи компилятора. Ключ -s (отключить проверку стека) вполне очевиден. Если мы оставим проверку стека это увеличит размер кода и что гораздо хуже, потянет за собой исполняющую библиотеку C (runtime). Это без вопросов. Ключ -g управляет группой где будет размещен сегмент кода. Это должна быть DGROUP, в противном случае линкер не объединит сегменты данных и кода. К счастью Watcom крайне гибок в этих вопросах. Ключи линкера сходны с теми, что мы использовали для ассемблерной версии, за исключанием одного дополнительного. Опция NOD - это сокращение от NODefaultlibs (нет библиотеки по умолчанию) и отключает поиск библиотеки по умолчанию (для этого компилятор генерирует специальную запись в оъектном модуле). Такого же эффекта можно добится ключом -zl компилятора. Обнако ключ к успеху заключается в опции start. В ассемблерном варианте мы использовали директиву END для указания точки старта программы. В C нет этому эквивалента, однако IBM C поддеживает прагму #pragma entry. Watcom C не имеет такой прагмы, но позволяет указать точку старта опцией start=symbol линкера WLINK. Конечная версия исходного текста на C возможно выглядит более сложной чем ассемблерная, но все еще не слишком сложной для понимания. Результат гарантирован многочасовой интенсивной борьбой с компилятором, попытками найти правильное соотношение между ингредиентами и попытками заставить компилятор и линкер делать то, что они определенно делать не хотели. И как это не смешно, но результат борьбы не слишком велик (;-)))), прим. пер.). ЗаключениеЯ надеюсь, что это небольшое приключение по минимизации программы развлекло Вас или как минимум не шокировало. В лучшем случае Вы изучили некоторые приемы которые Вы сможете применить в своих программах, как это сделал я. Мораль все истории такова, если Вы потрудитесь достаточно усердно, то Вы сможете достичь результатов которые Вы и представить не могли (да-да-да, я знаю что это звучит глупо)! Да, между прочим, если Вы знаете еще приемы которые можно применить для уменьшения размеров исполняемых программ, дайте мне знать по адресу MichalN@prodigy.net. Автор статьи: Michal Necasek Переводчик: Андрей Породько
Комментарии:
|
|
|||||||||||||||||||||||||||||
(C) OS2.GURU 2001-2021