Автор: (c)Крис Касперски ака мыщъх
Ассемблер - это удивительный язык, открывающий дверь в мир больших возможностей и неограниченного самовыражения. Состязания между программистами здесь - обычное дело. Выигрывает тот, у кого нестандартный взгляд, необычный подход. Зачастую самое "тупое" решение - самое быстрое и правильное.
Путь начинающего ассемблерщика не только долог, но еще и тернист. Повсюду торчат острые шипы, дорогу преграждают разломы, ловушки и капканы. В темной чаще горят злые глаза, доносятся какие-то ухающие звуки и прочие неблагоприятные факторы, нагнетающие мрачную атмосферу и серьезно затрудняющую продвижение вперед.
Большинство учебников затрагивают только MS-DOS, крайне поверхностно описывая практические проблемы программирования под Windows. Мыщъх делится с читателями рецептами, которые известны любому профессионалу, но совершенно неочевидны новичку.
Рисунок 1. Программирование на Ассемблере - это путь в никуда, магистраль, ведущая в вечность!
Апеллируя к житейской мудрости пса Фафика, пришедшего к выводу, что есть колбасу, иметь колбасу и пахнуть колбасой - это три большие разницы, мы можем сказать: изучать ассемблер, программировать на ассемблере и хвастаться знаниями ассемблера - совсем не одно и то же!
Каждый уважающий себя программист должен пройти стадию познания "голого" железа, системных вызовов, чистого API, чтобы знать, как устроена и работает операционная система, но писать большой GUI-проект с использованием Win32 API - это медленное и мучительное самоубийство.
Намного эффективнее воспользоваться готовыми интерфейсными библиотеками и компонентами. Зачем тратить время на создание и отладку кода, уже написанного и отлаженного другими программистами, которые, между прочим, совсем не дураки, и существенно превзойти их, не разорвав свою задницу напополам, все равно не получится!
Естественно, не нужно впадать и в другую крайность, используя для постройки собачьей конуры бетонные блоки и подъемный кран, типа визуальных средств разработчики, к кучей мастеров. Монументально, но слишком тяжеловесно даже для современных процессоров. Все равно ведь программировать приходится руками, думать - головой, а мышью и мастерами можно только соорудить только то, для чего они изначально предназначались, то есть быстро собрать еще одну типовую конуру, рыночная стоимость (в силу законов конкуренции) будет близка к нулю.
Рисунок 2. Некоторые программисты любят навороченные среды разработки типа WinAsm Studio (аналог Microsoft Visual Studio) с окнами, мастерами и прочими "перламутровыми пуговицами"...
Грань между плюсами "мышиного" и "рукописного" кода очень тонка. Отклонение в одну строну снижает продуктивность программы, в другую - увеличивает (причем зря) время разработки. Короче, не будем разводить демагогию, а рассмотрим фрагмент кода, запускающий процесс на выполнение стандартным способом через Win32 API-функцию CreateProcess:
xor eax,eax ; eax := 0 push offset pi ; lpProcessInformation push offset sis ; lpStartupInfo push eax ; lpCurrentDirectory push eax ; lpEnvironment push eax ; dwCreationFlags push eax ; bInheritHandles push eax ; lpThreadAttributes push eax ; lpProcessAttributes push offset file_name ; имя исполняемого файла с аргументами push eax ; lpApplicationName call ds:[CreateProcess] ; косвенный вызов API-функции через IAT
Листинг 1. Запуск процесса на выполнение через win32 API - 12 команд и 73h байта.
Ассемблированный код занимает 1Fh байт и еще 54h байта расходуются на структуры PROCESS_INFORMATION и STARTUPINFO плюс длина имени файла. А вот что получится, если воспользоваться морально "устаревшей" функцией WinExec, доставшийся в наследство от 16-разрядной старушки Windows (вопреки распространенному заблуждению, она реализована одновременно как 16- и 32-разрядная функция, а потому перехода в 16-разрядный режим при вызове WinExec из 32-разрядного кода не происходит, а, значит, не происходит и падения производительности):
push 00h ; uCmdShow (короче чем XOR EAX,EAX/PUSH EAX) push offset file_name ; имя исполняемого файла с аргументами call ds:[WinExec] ; косвенный вызов API-функции через IAT
Листинг 2. Запуск процесса на выполнение через "устаревшую" функцию WinExec - три команды и 1Eh байт машинного кода.
Всего три машинных команды, укладывающиеся в 1Eh байт (без учета имени файла) и никаких дополнительных структур! Расплатой за оптимизацию становится невозможность создания отладочных или "замороженных" процессов, не говоря уже про атрибуты безопасности и прочую хрень, реально необходимую в одном случаев из десяти-двадцати случаев, а то и реже.
Рисунок 3. ...А кто-то предпочитает простые, легковесные и аскетичные IDE, по функциональности сравнимые с блокнотом (например, fasmw).
Но это еще не предел оптимизации! Воспользовавшись функцией system из библиотеки MSVCRT.DLL (которая активно используется многими приложениями и практически всегда "болтается" в памяти), мы сократим код до 1Dh байт или даже до 1Ah, если отсрочим восстановление стека, выполнив команду add esp, x в конце функции, выталкивая все аргументы одним махом (подробнее см. "все аргументы в одном месте"):
push offset file_name ; имя исполняемого файла с аргументами call system ; прямой вызов функции (почему так - см. ниже) add esp,4 ; выталкиваем аргументы из стека (можно сделать позже)
Листинг 3. Запуск процесса на выполнение через функцию system библиотеки MSVCRT.DLL - три (две) команды и 1Dh (1Ah) байт кода.
То же самое относится и к функциям файлового ввода/вывода, преобразованиям данных и т.д., и т.п. Никто же не будет спорить, что вызов fopen намного короче, чем CreateFile, а скорость исполнения у них практически та же самая, тем более что библиотека MSVCRT.DLL всегда присутствует памяти, поскольку используются системными процессами. Windows просто спроецирует ее на наше адресное пространство - вот и все! Никакого увеличения потребляемой памяти не произойдет!
Наибольший выигрыш достигается на задачах, требующих перевода двоичных данных в ASCII-представление или наоборот. Собственно говоря, программирование на ассемблере и начинается с вывода на экран числа, заданного в двоичной форме. Конечно, "вручную" разработанная и оптимизированная функция намного быстрее стандартного sprintf, однако очень редко можно встретить программу, расходующую основное время на преобразование данных, поэтому использование библиотечных функций сокращает размер и время разработки программы.
Рисунок 4. Настоящие программисты (особенно старого поколения!) используют только консольные редакторы типа Multi-Edit или TSE-Pro с кучей специализированных функций и мощным макро-движком!
Приведенный ниже пример распечатывает число, содержащееся в регистре EAX, в шестнадцатеричной, десятичной и восьмеричной форме, автоматически дописывая ведущие нули, растягивающие число до 4 разрядов. А теперь попробуйте осуществить то же самое без использования библиотек и сравните размер полученного кода!
mov eax, 666h ; число, которое необходимо вывести на экран ; // переводим число в hex, dec и oct системы исчисления в ASCII-представлении sub esp, 60h ; резервируем память под буфер куда пойдет результат mov ebx, esp ; сохраняем указатель на буфер в регистре EBX push eax ; \ push eax ; + - передаем число для преобразования функции sprintf push eax ; / push offset s ; передаем в стек указатель на строку спецификаторов push ebx ; передаем указатель на буфер для получения результата call sprintf ; прямой вызов функции sprintf ; // вывод преобразованных данных на экран через диалоговое окно xor eax,eax ; eax := 0 push eax ; uType push eax ; lpCaption push ebx ; lpText (наши преобразованные данные) push eax ; hWnd call ds:[MessageBoxA] ; косвенный вызов API-функции MessageBox add esp, 60h + (5*4) ; выталкиваем аргументы из стека и уничтожаем буфер ... ... ... s db "%04X hex == %04d dec == %04o oct",0 ; строка спецификаторов
Листинг 4. Фрагмент программы, принимающей число в регистре EAX и выводящей его на экран в шестнадцатеричной, десятеричной и восьмеричной формах.
Рисунок 5. Вывод на экран числа в разных системах исчисления.
При вызове API и DLL-функций из ассемблерных вставок возникает множество проблем, довольно туманно описанных в документации, прилагаемой к компилятору. Возьмем, к примеру, Microsoft Visual C++ и попробуем вызывать функцию GetVersion так, как мы бы сделали бы это на чистом ассемблере:
Листинг 5. "Логичный", но неправильный способ вызова API-функций.
Компилируем файл с настройками по умолчанию и запускаем. Программа тут же рушится. Почему? Смотрим в дизассемблере:
.text:00401000 E8 FF 2F 00 00 call near ptr GetVersion ... .idata:00404004 ?? ?? ?? ?? extrn GetVersion:dword ; DWORD GetVersion(void)
Листинг 6. Дизассемблер показывает, что вместо запланированного вызова API-функции, управление получает двойное слово с указателем на нее.
Так вот, где собака порылась! Компилятор сгенерировал переход по адресу, где расположено двойное слово, принадлежащее таблице импорта (секция .idata) и содержащее указатель на API-функцию GetVersion.
Рисунок 6. Дизассемблер IDA Pro - мощное средство выявления ошибок в программах.
Неудивительно, что попытка интерпретации таблицы импорта как исполняемого кода приводит к краху и, чтобы программа заработала правильно, необходимо использовать косвенную адресацию, заключив имя функции в квадратные скобки и выставив перед ними знак префикса cs: или ds: (без разницы, но ds работает чуточку быстрее). Без префиксов компилятор просто не поймет, что мы от него хотим (любой ассемблер - понял бы). А, между прочим, префикс - это не только лишний байт, но и большая головная боль для процессорного конвейера, приводящая к тормозам, впрочем, практически незаметным на фоне тормозов самих API-функций, особенно тех из них, что обращаются к ядру операционной системы (переход в режим ядра - это тысячи процессорных тактов!).
Правильный код выглядит так:
Листинг 7. "Нелогичный", но правильный способ вызова API-функций
При вызове функций, представленных в двух вариантах - ASCII и UNICODE, мы можем указывать суффиксы A и W явно, а можем использовать "каноническое" имя функции без суффиксов и тогда компилятор самостоятельно выберет нужный вариант в зависимости от настроек по умолчанию или ключей компиляции.
Листинг 8. Косвенный вызов функции CreateProcess с явным заданием суффикса W.
.text:0040101E db 3Eh ; ds: .text:0040101E call CreateProcessW ; вызывается UNICODE-версия функции
Листинг 9. А вот его дизассемблерный листинг - вызывается именно та функция, которая была указана.
Листинг 10. Косвенный вызов функции CreateProcess без указания суффиксов, предоставляющий компилятору свободу выбора одного из двух вариантов.
.text:0040101E db 3Eh ; ds: .text:0040101E call CreateProcessA ; вызывается ASCII-версия функции
Листинг 11. Компилятор выбрал ASCII-вариант, что соответствует его настройкам по умолчанию.
А вот при вызове функций типа system квадратные скобки ставить уже не надо, точнее - нельзя! Функция system является частью библиотеки времени исполнения (RTL - Run Time Library), линкуемой статическим образом, поэтому call system сработает, как и ожидалось, а вот call ds:[system] передаст управление по адресу 83EC8B55h, попытавшись проинтерпретировать начало функции system как указатель:
.text:0040100B 3E FF 15 1A 10 40 00 call dword ptr system ; косвенный вызов статически линкуемой функции ; приводит к тому, что первые 4 байта функции ; интерпретируются как указатель и управление ; передается по адресу 83EC8B55h ... .text:00401018 system proc near ; начало функции system .text:00401018 55 push ebp .text:00401019 8B EC mov ebp, esp .text:0040101B 83 EC 10 sub esp, 10h .text:0040101E 56 push esi
Листинг 12. Косвенный вызов статически линкуемых функций приводит к краху.
Таким образом, при вызове функций из ассемблерных вставок всегда следует учитывать специфику конкретной вызываемой функции, не надеясь на то, что компилятор сделает это за нас.
При программировании на чистом ассемблере подобная проблема не возникает, поскольку имена и типы вызовов функций всегда объявляются вручную (или через включаемые файлы) и мы заранее знаем, как именно интерпретирует их транслятор. При работе с ассемблерными вставками подобной определенности у нас нет. В частности, если компилятор решил использовать инкрементную линковку, то имя функции интерпретируется уже не как указатель на двойное слово из таблицы импорта, а как указатель на "переходник", представляющего собой jmp [pFunc], то есть нам квадратные скобки снова отпадают!
Инкрементная линковка представляет собой попытку эмуляции секции .got, имеющейся в elf-файлах, но отсутствующей в Windows, и обычно включается в режиме оптимизации, а в отладочном варианте - отсутствующей. Сюрприз, да? При изменении ключей компиляции ассемблерные вставки изменяют свое поведение, причем безо всякого предупреждения!
Короче говоря, внешние функции из ассемблерных вставок лучше не вызывать, а если и вызывать, то очень осторожно.
Рисунок 7. А вообще же, выбор конкретного инструментария - дело вкуса, о которых, как известно, не спорят, в частности, потребности мыщъх'а вполне удовлетворяет редактор, встроенный в FAR плюс несколько плагинов.
На процессорах 8086/8088 существовала замечательная возможность - затолкать в стек аргумент-указатель с одновременным выделением памяти всего одной(!) однобайтовой(!) машинной командой PUSH ESP, которая сначала уменьшала значение ESP, а только потом заталкивала его в стек. То есть, в стек попадало уже уменьшенное значение ESP, что способствовало трюкачеству.
Рассмотрим конкретный пример - функцию, одним из аргументов которой является указатель на переменную, принимающую возвращаемый результат: f(int a, word *x). Предельно компактный вызов (на 8086!) выглядел так:
push sp ; передаем указатель на x с одновременным выделением памяти под сам x push si ; передаем переменную a call f ; зовем функцию
Листинг 13. Трюкаческий пример, передающий указатель на переменную с одновременным выделением под нее памяти (только для 8086/8088!)
Подвох в том, что переменная x возвращается в ячейке памяти, выделенной PUSH SP! То есть, указатель на x указывает сам на себя, что хорошо видно в отладчике:
---------------------------------------------------------------- 1832:FFB0| 02 11 54 12 B2 FF 00 00 00 00 00 00 00 00 00 00 | | ^^^^^ ^^^^^ ^^^^^ | | | | | | push si | адрес возврата push sp
Листинг 14. Содержимое стека на момент вызова функции f на древней XT, снабженной 8086-процессором.
Рисунок 8. В отладчике хорошо видно, что в стек попадает уже уменьшенное значение регистра SP, в результате чего указатель *x указывает сам на себя!
Начиная с 80286, логика работы инструкции PUSH ESP предательским образом изменилась и теперь процессор помещает в стек такое значение регистра ESP, каким оно было до модификации (кстати, псевдокод команды PUSH, приведенный в руководстве Intel содержит ошибку, из которой следует, что в стек помещается уменьшенное значение ESP, хотя на практике это не так!).
И пока программисты спорят, какое из двух решений "идеологически" более "правильное", прежний код отказывается работать, потому что команда PUSH ESP вместо указателя, указывающего на себя, теперь заталкивает в стек указатель на следующее двойное слово!
0012FF68 0E 10 40 00 34 FA 12 00 74 FF 12 00 00 00 00 00 ^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^ | | | | | push esi | куда указывает esp адрес возврата push esp
Листинг 15. Содержимое стека на момент вызова функции f на современных процессорах.
Рисунок 9. В отладчике хорошо видно, что в стек попадает такое значение регистра ESP, каким оно было до модификации, в результате чего указатель *x указывает на следующее двойное слово!
Поэтому при переходе с 8086 на 286+ приходится добавлять "лишнюю" команду PUSH EAX, резервирующую ячейку на стеке, на которую будет указывать значение ESP, засланное в стек инструкцией PUSH ESP
push eax ; выделяем память под переменную x (регистр может быть любым) push esp ; передаем указатель на x как аргумент функции f push esi ; передаем переменную a call f ; зовем f
Листинг 16. Трюкаческий пример, портированный на 286+ процессоры.
Несмотря на то, что 8086/8088 процессоры уже давно не встречаются в дикой природе (ну, разве что в виде эмуляторов, да и то...), многие программы, написанные под них, актуальны и сегодня. Это касается как уже откомпилированного машинного кода, так и различных ассемблерных библиотек, переносимых под современные процессоры. Одна из причин, по которой они могут не работать - это и есть различие в логике обработке команды PUSH ESP.
Вообще же, динамическое выделение памяти посредством PUSH + фиктивный регистр - вполне законный примем, которым пользуются не только люди, но и компиляторы. Это намного компактнее, чем обращение к локальным/глобальным переменным, выделяемым классическим способом.
Естественно, большие объемы памяти лучше всего выделять с помощью SUB ESP, XXh, но при этом следует помнить, как минимум, о двух вещах. Первое и главное - Windows-системы выделяют стековую память динамически, используя для этого специальную "сторожевую" страницу памяти (page guard). Как только к ней происходит обращение - система выделяет еще одну или несколько страниц памяти, перемещая сторожевую страницу наверх (в сторону меньших адресов памяти). При последовательном "росте" стека все работает нормально, но если попытаться прыгнуть за сторожевую страницу, сразу же возникнет непредвиденное исключение - ведь никакой памяти по данному адресу еще нет - и работа программы завершается в аварийном режиме. То есть, если у нас есть, к примеру, 1 Мбайт стекового пространства, это еще не значит, что код SUB ESP, 10000h/MOV [ESP],EAX будет работать. Тут уж, как повезет (или не повезет). Если ранее вызываемые функции выделяли стековую память планомерно, задвинув сторожевую страницу куда-то вглубь стекового пространства, то какие-то шансы у нас есть, но полагаться на них несерьезно. Поэтому при выделении под локальные переменные более 4 Кбайт необходимо выполнить цикл, последовательно обращающийся хотя бы к одной ячейке каждой из запрашиваемых страниц. Читать все ячейки необязательно, да и непроизводительно.
Компиляторы делают это автоматически, а вот многие ассеблерщики о таком коварстве Windows зачастую даже и не подозревают, а потом упорно ищут баг в своей программе, не понимая, почему она не работает!
Листинг 17. Пример программы на Си, выделяющей 1 Мбайт памяти под локальные переменные и обращающейся к самой "дальней" ячейке.
.text:00401000 _main proc near ; CODE XREF: start+AFvp .text:00401000 mov eax, 100000h .text:00401005 call __alloca_probe .text:0040100A movsx eax, byte ptr [esp] .text:00401012 add esp, 100000h .text:00401018 retn .text:00401018 _main endp ; sp = 100000h .text:00401020 __alloca_probe proc near ; CODE XREF: _main+5^p .text:00401020 .text:00401020 arg_0 = dword ptr 8 .text:00401020 .text:00401020 push ecx .text:00401021 cmp eax, 1000h .text:00401026 lea ecx, [esp+arg_0] .text:0040102A jb short loc_401040 .text:0040102C .text:0040102C loc_40102C: ; CODE XREF: __alloca_probe+1E|j .text:0040102C sub ecx, 1000h .text:00401032 sub eax, 1000h .text:00401037 test [ecx], eax .text:00401039 cmp eax, 1000h .text:0040103E jnb short loc_40102C .text:00401040 .text:00401040 loc_401040: ; CODE XREF: __alloca_probe+A|j .text:00401040 sub ecx, eax .text:00401042 mov eax, esp .text:00401044 test [ecx], eax .text:00401046 mov esp, ecx .text:00401048 mov ecx, [eax] .text:0040104A mov eax, [eax+4] .text:0040104D push eax .text:0040104E retn .text:0040104E __alloca_probe endp
Листинг 18. При выделении большого объема локальных переменных компилятор вызывает недокументированную функцию __alloca_probe, совершающую "пробежку" по стеку и, при необходимости, отодвигающую сторожевую страницу на требуемое расстояние - то же самое необходимо делать и в ассемблерных программах!
Но коварство Windows на этом не заканчиваются. Многие API-функции неявно закладываются на выравнивание стека и если нам, к примеру, требуется ровно 69h байт стековой памяти, ни в коем случае нельзя писать SUB ESP,69h, иначе все рухнет! Следует округлить 69h по границе двойного слова и запросить 6Ch байт или... между актами выделения/освобождения памяти не вызывать никаких API-функций.
Часто в погоне за оптимизацией программисты, борющиеся за каждый байт памяти, забывают о выравнивании и... часами ищут причину, по которой оптимизированный вариант программы отказывается работать.
Рисунок 10. Проблемы выравнивания в оптимизации.
При вызове функций по соглашениям cdecl или stdcall указатель стека постоянно "пляшет", что сбивает с толку процессорный кэш и снижает производительность. При соглашении stdcall от этого никуда не уйдешь, поскольку аргументы очищает непосредственно сама вызываемая функция, но для cdecl-функций кое-что все-таки можно сделать.
Во-первых, как уже говорилось, стек можно балансировать не сразу после выхода из функции, а спустя некоторое время, объединяя несколько команд ADD ESP,XXh в одну (конкретный пример показан в листинге 4). Однако это не работает с циклами и ветвлениями. Функция, вызываемая в цикле, буквально пожирает память, если только аргументы немедленно не выталкиваются из стека. Но памяти - много и на небольшом количестве итераций с этим еще можно хоть как-то смириться (правда, выигрыша в производительности мы уже не получим, будет просто чудо, если такой объем вообще уместится в кэше первого уровня).
Ветвления создают еще большую проблему, заставляя нас вводить дополнительные переменные (как правило, регистровые), запоминающие - вызывалась ли функция под ветвлением и, если да, то сколько байт стекового пространства "числится" за ней? Все это усложняет код и сводит на нет, в общем-то, неплохую идею.
А что если... передавать аргументы через однократно выделенный регион памяти? Это обеспечит максимальную скорость и минимальные потребности в стеке. Мы будем действовать так - на входе в функцию резервируем блок памяти, равный наибольшему объему аргументов, передаваемых функции, а затем просто кладем туда аргументы по ходу дела. Регистр ESP уже не "пляшет" и циклы выполняются с предельной скоростью. Единственный минус в том, что передавать аргументы приходится не инструкций PUSH, а более длинной командой MOV [EBP-XXh],YYYY.
Конкретная реализация может выглядеть, например, так:
PUSH EBP ; сохраняем EBP MOV EBP,ESP ; открываем кадр стека SUB ESP,10h ; выделяем память для аргументов ... ; (если надо, выделяем память для лок. пер.) MOV [EBP-10h], arg2 ; кладем на вершину стека крайний левый аргумент MOV [EBP-0Ch], arg1 ; кладем следующий аргумент CALL func_1 ; вызываем функцию func_1 ; не выталкиваем аргументы из стека rool: ; демонстрация вызова функции в цикле MOV [EBP-10h], arg5 ; кладем на вершину стека крайний левый аргумент MOV [EBP-0Ch], arg4 ; \ MOV [EBP-08h], arg3 ; + кладем все следующие аргументы MOV [EBP-04h], arg2 ; + MOV [EBP-00h], arg1 ; / CALL func_2 ; вызываем функцию func_2 ; не выталкиваем аргументы из стека DEC ECX ; мотаем цикл JNZ rool ; (только не спрашивайте, кто инициализирует ECX!) MOV [EBP-10h], EAX ; кладем на вершину стека крайний левый аргумент CALL func_3 ; вызываем функцию func_3 MOV ESP,EBP ; закрываем кадр, освобождая память аргументов POP EBP ; восстанавливаем EBP
Листинг 19. Демонстрация передачи аргументов cdecl-функциям через однократно выделяемый блок памяти.
Очень похоже на адресацию локальных переменных, но это все-таки не переменные, а аргументы. Точнее, локальные переменные, лежавшие на самой вершине стека и с точки зрения вызываемой функции выглядевшие как аргументы. Локальные переменные (если они есть) располагаются ниже их.
То есть, если мы резервируем 10h байт под все аргументы, то первый слева аргумент должен помещаться в ячейку [EBP-10h], второй - в [EBP-0Ch] и так далее. Главное - не перепутать порядок засылки аргументов. По соглашению cdecl переменные передаются справа налево, следовательно, в момент вызова функции на вершине стека лежит крайний левый аргумент, а под ним - все остальные.
Выигрыш в скорости на самом деле очень значительный, а небольшое "раздутие" кода за счет отказа от инструкции PUSH - это не такая уж значительная проблема.
Системное программирование хранит множество секретов, загадок и тайн, постепенно становясь уделом небольшой горстки профессионалов, в то время как мир дружно сходит с ума, подсаживаясь на языки высокого уровня, которые чем дальше - тем все выше и выше. Об ассемблере вспоминают только тогда, когда требуется что-то очень сильно нестандартное, с чем компилятор уже не справляется или сгенерированный им код не отвечает требованиям производительности.
Вот тут-то и выясняется, что специалистов, владеющих ассемблеров, практически нет, а те, что есть, уже утратили свои навыки и оптимизируют намного хуже компиляторов, разработчики которых за последние несколько лет сделали качественный рывок вперед и теперь просто так их не обгонишь! Сам по себе ассемблер не обеспечивает ни компактности кода, ни высокой скорости. Все решают хитрые трюки и приемы программирования, находчивость и инженерная смекалка, наконец!
Главное - выбрать верную стратегию поведения. Не пытаться сократить программу на пару байт, которые все равно будут потеряны при выравнивании, а реально оценивать свой творческий потенциал, сопоставляя его с целями и задачами операциями. Алгоритмическая оптимизация зачастую ускоряет программу в десятки раз, в то время как перенос С-шного кода на ассемблер дает в среднем 10%...15% выигрыш. Но это еще не значит, что ассемблер бесполезен. Просто, как и любой другой инструмент, он имеет границы своей применимости, с которыми следует считаться, чтобы не попасть впросак!