Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

articles:asm-extoptimize [2017/09/05 02:55] (текущий)
Строка 1: Строка 1:
 +====== asm-extoptimize ======
 +<​sub>​{{asm-extoptimize.odt|Original file}}</​sub>​
 +
 +====== ассемблер — экстремальная оптимизация ======
 +
 +крис касперски ака мыщъх, no-email
 +
 +**ассемблер — это удивительный язык, открывающий дверь в мир больших возможностей и неограниченного самовыражения. состязания между программистами здесь — обычное дело. выигрывает тот, у кого нестандартный взгляд,​ необычный подход. зачастую,​ самое "​тупое"​ решение — самое быстрое и правильное.**
 +
 +===== введение =====
 +
 +Путь начинающего ассемблерщика не только долог, но еще и тернист. Повсюду торчат острые шипы, дорогу преграждают разломы,​ ловушки и капканы. В темной чаще горят злые глаза, доносятся какие-то ухающие звуки и прочие неблагоприятные факторы,​ нагнетающие мрачную атмосферу и серьезно затрудняющую продвижение вперед.
 +
 +Большинство учебников затрагивают только MS-DOS, крайне поверхностно описывая //​**практические**//​ проблемы программирования под Windows. Мыщъх делиться с читателями рецептами,​ которые известны любому профессионалу,​ но совершенно неочевидны новичку.
 +
 +{{asm-extoptimize_Image_0.jpg?​553}}
 +
 +Рисунок 1 программирование на ассемблере это путь в никуда,​ магистраль,​ ведущая в вечность!
 +
 +===== готовые функции на блюдечке =====
 +
 +Апеллируя к житейской мудрости пса Фафика,​ пришедшего к выводу,​ что есть колбасу,​ иметь колбасу и пахнуть колбасой — это три большие разницы,​ мы можем сказать:​ изучать ассемблер,​ программировать на ассемблере и хвастаться знаниями ассемблера — совсем не одно и тоже!
 +
 +Каждый уважающий себя программист должен пройти стадию познания "​голого"​ железа,​ системных вызовов,​ чистого API, чтобы знать как устроена и работает операционная система,​ но писать большой GUI-проект с использованием win32 API — это медленное и мучительное самоубийство.
 +
 +Намного эффективнее воспользоваться готовыми интерфейсными библиотеками и компонентами. Зачем тратить время на создание и отладку кода, уже написанного и отлаженного другими программистами,​ которые,​ между прочим,​ совсем не дураки и существенно превзойти их, не разорвав свою задницу напополам,​ все равно не получится!
 +
 +Естественно,​ не нужно впадать и в другую крайность,​ используя для постройки собачьей конуры бетонные блоки и подъемный кран, типа визуальных средств разработчики,​ к кучей мастеров. Монументально,​ но слишком тяжеловесно даже для современных процессоров. Все равно ведь программировать приходится руками,​ думать — головой,​ а мышью и мастерами,​ можно только соорудить только то, для чего они изначально предназначались,​ то есть быстро собрать еще одну типовую конуру,​ рыночная стоимость (в силу законов конкуренции) будет близка к нулю.
 +
 +{{asm-extoptimize_Image_1.png?​552}}
 +
 +Рисунок 2 некоторые программисты любят навороченные среды разработки типа WinAsmStudio (аналог MicrosoftVisualStudio) с окнами,​ мастерами и прочими "​перламутровыми пуговицами"​…
 +
 +Грань между плюсами "​мышиным"​ и "​рукописным"​ кода очень тонка. Отклонение в одну строну — снижает продуктивность программы,​ в другую — увеличивает (причем зря) время разработки. Короче,​ не будем разводить демагогию,​ а рассмотрим фрагмент кода, запускающий процесс на выполнение стандартным способом через 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
 +
 +pushoffsetfile_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)
 +
 +pushoffsetfile_name;​ имя исполняемого файла с аргументами
 +
 +callds:​[WinExec];​ косвенный вызов API-функции через IAT
 +
 +Листинг 2 запуск процесса на выполнение через "​устаревшую"​ функцию WinExec - три команды и 1Eh байт машинного кода
 +
 +Всего три машинных команды,​ укладывающиеся в 1Eh байт (без учета имени файла) и никаких дополнительных структур! Расплатой за оптимизацию становится невозможность создания отладочных или "​замороженных"​ процессов,​ не говоря уже про атрибуты безопасности и прочую хрень, реально необходимую в одном случаев из десяти-двадцати случаев,​ а то и реже.
 +
 +{{asm-extoptimize_Image_2.png?​553}}
 +
 +Рисунок 3 …кто-то предпочитает простые,​ легковесные и аскетичные IDE по функциональности сравнимые с блокнотом (например,​ fasmw)
 +
 +Но это еще не предел оптимизации! Воспользовавшись функцией **system** из библиотеки MSVCRT.DLL (которая активно используется многими приложениями и практически всегда "​болтается"​ в памяти),​ мы сократим код до 1Dh байт или даже до 1Ah, если отсрочим восстановление стека, выполнив команду add esp,​ x в конце функции,​ выталкивая все аргументы одним махом (подробнее см. "​все аргументы в одном месте"​):​
 +
 +pushoffsetfile_name;​ имя исполняемого файла с аргументами
 +
 +callsystem; прямой вызов функции (почему так — см. врезку)
 +
 +addesp,4; выталкиваем аргументы из стека (можно сделать позже)
 +
 +Листинг 3 запуск процесса на выполнение через функцию system библиотеки MSVCRT.DLL – три (две) команды и 1Dh (1Ah) байт кода
 +
 +Тоже самое относится и к функциям файлового ввода/​вывода,​ преобразованиям данных и т. д., и т. п. Никто же не будет спорить,​ что вызов **fopen** намного короче,​ чем **CreateFile**,​ а скорость исполнения у них практически та же самая, тем более что, библиотека MSVCRT.DLL//​всегда//​ присутствует памяти,​ поскольку используются системными процессами. Windows просто спроецирует ее на наше адресное пространство — вот и все! Никакого увеличения потребляемой памяти не произойдет!
 +
 +Наибольший выигрыш достигается на задачах,​ требующих перевода двоичных данных в ASCII-представление или наоборот. Собственно говоря,​ программирование на ассемблере и начинается с вывода на экран числа, заданного в двоичной форме. Конечно,​ "​вручную"​ разработанная и оптимизированная функция намного быстрее стандартного **sprintf**,​ однако,​ очень редко можно встретить программу,​ расходующую основное время на преобразование данных,​ поэтому,​ использование библиотечных функций сокращает размер и время разработки программы.
 +
 +{{asm-extoptimize_Image_3.png?​553}}
 +
 +Рисунок 4 настоящие программисты (особенно старого поколения!) используют только консольные редакторы типа Multi-Edit или TSE-Pro с кучей специализированных функций и мощным макро-движком!
 +
 +Приведенный ниже пример распечатывает число, содержащееся в регистре EAX в шестнадцатеричной,​ десятичной и восьмеричной форме, автоматически дописывая ведущие нули, растягивающие число до 4х разрядов. А теперь попробуйте осуществить тоже самое без использования библиотек и сравните размер полученного кода!
 +
 +moveax, 666h; число, которое необходимо вывести на экран
 +
 +
 +
 +**; // переводим число в ****hex****,​ ****dec**** и ****oct**** системы исчисления в ****ASCII****-представлении**
 +
 +subesp, 60h; резервируем память под буфер куда пойдет результат
 +
 +movebx, esp; сохраняем указатель на буфер в регистре EBX
 +
 +push eax; \
 +
 +pusheax; ​ + - передаем число для преобразования ф-ции sprintf
 +
 +push eax; /
 +
 +pushoffsets;​ передаем в стек указатель на строку спецификаторов
 +
 +pushebx; передаем указатель на буфер для получения результата
 +
 +callsprintf;​ прямой вызов функции sprintf
 +
 +
 +
 +******; // вывод преобразованных данных на экран через диалоговое окно**
 +
 +xor eax,eax; eax := 0
 +
 +push eax; uType
 +
 +push eax; lpCaption
 +
 +pushebx; lpText (наши преобразованные данные)
 +
 +push eax; hWnd
 +
 +call ds:​[MessageBoxA];​ косвенныйвызов API-функции MessageBox
 +
 +
 +
 +addesp, 60h + (5*4); выталкиваем аргументы из стека и уничтожаем буфер
 +
 +...
 +
 +...
 +
 +...
 +
 +sdb "%04X hex == %04d dec == %04o oct",0
 +
 +; строка спецификаторов
 +
 +Листинг 4 фрагмент программы,​ принимающий число в регистре EAX и выводящий его на экран в шестнадцатеричной,​ десятеричной и восьмеричной формах
 +
 +{{asm-extoptimize_Image_4.png?​552}}
 +
 +Рисунок 5 вывод на экран числа в разных системах исчисления
 +
 +===== >>>​ врезка вызов API-функций из ассемблерный вставок =====
 +
 +При вызове API и DLL-функций из ассемблерных вставок возникает множество проблем,​ довольно туманно описанных в документации,​ прилагаемой к компилятору. Возьмем,​ к примеру,​ MicrosoftVisualC++ и попробуем вызывать функцию **GetVersion** так, как мы бы сделали бы это на чистом ассемблере:​
 +
 +__asm{
 +
 +call GetVersion; прямойвызов API-функции
 +
 +}
 +
 +Листинг 5 "​логичный",​ но неправильный способ вызова API-функций
 +
 +Компилируем файл с настройками по умолчанию и запускам. Программа тут же рушится. Почему?​ Смотрим в дизассемблере:​
 +
 +.text:​00401000 E8 FF 2F 00 00callnear ptr GetVersion
 +
 +...
 +
 +.idata:​00404004 ?? ?? ?? ??extrn GetVersion:​dword;​ DWORD GetVersion(void)
 +
 +Листинг 6 дизассемблер показываем,​ что вместо запланированного вызова API-функции,​ управление получает двойное слово с указателем на нее
 +
 +Так вот где собака порылась! Компилятор сгенерировал переход по адресу,​ где расположено двойное слово, принадлежащее таблице импорта (секция .idata) и содержащее указатель на API-функцию GetVersion.
 +
 +{{asm-extoptimize_Image_5.png?​553}}
 +
 +Рисунок 6 дизассемблер IDA Pro – мощное средство выявления ошибок в программах
 +
 +Неудивительно,​ что попытка интерпретации таблицы импорта как исполняемого кода приводит к краху и чтобы программа заработала правильно,​ необходимо использовать косвенную адресацию,​ заключив имя функции в квадратные скобки и выставив перед ними знак префикса **cs****:** или **ds****:** (без разницы,​ но ds работает чуточку быстрее). Без префиксов компилятор просто не поймет что мы от него хотим (любой ассемблер — понял бы). А между прочим,​ префикс — это не только лишний байт, но и большая головная боль для процессорного конвейера,​ приводящая к тормозам,​ впрочем,​ практически незаметным на фоне тормозов самих API-функций,​ особенно тем из них, что обращаются к ядру операционной системы (переход в режим ядра — это тысячи процессорных тактов!).
 +
 +Правильный код выглядит так:
 +
 +__asm{
 +
 +call ds:​[GetVersion];​ косвенныйвызов API-функции
 +
 +}
 +
 +Листинг 7 "​не логичный",​ но правильный способ вызова API-функций
 +
 +При вызове функций,​ представленных в двух вариантах — ASCII и UNICODE, мы можем указывать суффиксы **A** и **W** явно, а можем использовать "​каноническое"​ имя функции без суффиксов,​ и тогда компилятор самостоятельно выберет нужный вариант в зависимости от настроек по умолчанию или ключей компиляции.
 +
 +__asm{
 +
 +; тут мы передаем аргументы
 +
 +callds:​[CreateProcessW];​ косвенный вызов функции с суффиксом W
 +
 +}
 +
 +Листинг 8 косвенный вызов функции CreateProcess с явным заданием суффикса W
 +
 +.text:​0040101Edb3Eh;​ ds:
 +
 +.text:​0040101EcallCreateProcessW;​ вызывается UNICODE-версияфункции
 +
 +Листинг 9 а вот его дизассемблерный листинг — вызывается именно та функция,​ которая была указана
 +
 +__asm{
 +
 +; тут мы передаем аргументы
 +
 +callds:​[CreateProcess];​ косвенный вызов функции без суффиксов
 +
 +}
 +
 +Листинг 10 косвенный вызов функции CreateProcess без указания суффиксов,​ предоставляющий компилятору свободу выбора одного из двух вариантов
 +
 +.text:​0040101Edb3Eh;​ ds:
 +
 +.text:​0040101Ecall ​ CreateProcessA;​ вызывается ASCII-версияфункции
 +
 +Листинг 11 компилятор выбрал ASCII-вариант,​ что соответствует его настройкам по умолчанию
 +
 +А вот при вызове функций типа system квадратные скобки ставить уже не надо, точнее нельзя! Функция system является частью библиотеки времени исполнения (RTL — RunTimeLibrary),​ линкуемой статическим образом,​ поэтому call system сработает как и ожидалось,​ а вот call ds:​[system] передаст управление по адресу 83EC8B55h, попытавшись проинтерпретировать начало функции system как указатель:​
 +
 +.text:​0040100B 3E FF 15 1A 10 40 00calldword ptr system
 +
 +; косвенный вызов статически линкуемой функции
 +
 +; приводит к тому, что первые 4 байта функции
 +
 +; интерпретируются как указатель и управление
 +
 +; передается по адресу 83EC8B55h
 +
 +...
 +
 +.text:​00401018 systemprocnear;​ началофункции system
 +
 +.text:​00401018 **55**pushebp
 +
 +.text:​00401019 **8B EC**movebp, esp
 +
 +.text:​0040101B **83** EC 10subesp, 10h
 +
 +.text:​0040101E 56pushesi
 +
 +Листинг 12 косвенный вызов статически линкуемых функций приводит к краху
 +
 +Таким образом,​ //**при вызове функций из ассемблерных вставок всегда следует учитывать специфику конкретной вызываемой функции,​ не надеясь на то, что компилятор сделает это за нас.**//
 +
 +При программировании на чистом ассемблере подобная проблема не возникает,​ поскольку имена и типы вызовов функций всегда объявляются вручную (или через включаемые файлы) и мы заранее знаем как именно интерпретирует их транслятор. При работе с ассемблерными вставками подобной определенности у нас нет. В частности,​ если компилятор решил использовать инкрементную линковку,​ то имя функции интерпретируется уже не как указатель на двойное слово из таблицы импорта,​ а как указатель на "​переходник",​ представляющего собой jmp [pFunc],​ то есть нам квадратные скобки снова отпадают!
 +
 +Инкрементная линковка представляет собой попытку эмуляции секции .got, имеющийся в elf-файлах,​ но отсутствующей в Windows, и обычно включается в режиме оптимизации,​ а в отладочном варианте — отсутствующей. Сюрприз,​ да? При изменении ключей компиляции ассемблерные вставки изменяют свое поведение,​ причем безо всякого предупреждения!
 +
 +Короче говоря,​ внешние функции из ассемблерных вставок лучше не вызывать,​ а если и вызывать,​ то очень осторожно.
 +
 +{{asm-extoptimize_Image_6.png?​553}}
 +
 +Рисунок 7 а вообще же, выбор конкретного инструментария — дело вкуса, о которых,​ как известно,​ не спорят,​ в частности,​ потребности мыщъх'​а вполне удовлетворяет редактор,​ встроенный в FAR плюс несколько плагинов
 +
 +===== выделение памяти на стеке =====
 +
 +На процессорах 8086/8088 существовала замечательная возможность — затолкать в стек аргумент-указатель с одновременным выделением памяти всего одной (!) однобайтовой (!) машинной командой PUSH ESP, которая сначала уменьшала значение ESP, а только потом заталкивала его в стек. То есть, в стек попадало уже уменьшенное значение ESP, что способствовало трюкачеству.
 +
 +Рассмотрим конкретный пример — функцию,​ одним из аргументов которой является указатель на переменную,​ принимающую возвращаемый результат:​ f(inta, word *x). Предельно компактный вызов (на 8086!) выглядел так:
 +
 +pushsp; передаем указатель на x с одновременным выделением памяти под сам x
 +
 +pushsi; передаем переменную a
 +
 +callf; зовем функцию
 +
 +Листинг 13 трюкаческий пример,​ передающий указатель на переменную с одновременным выделением под нее памяти (только для 8086/8088!)
 +
 +Подвох в том, что переменная x возвращается в ячейке памяти,​ выделенной PUSHSP! То есть указатель на x указывает сам на себя, что хорошо видно в отладчике:​
 +
 +▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
 +
 +1832:​FFB0▐ 02 11 54 12  **B****2 ****FF** 00 00  00 00 00 00  00 00 00 00 ▌
 +
 + ▐ ^^^^^ ^^^^^  ^^^^^
 +
 + ​▐ ​ |  |  |
 +
 + ​▐ ​ |  push si  |
 +
 + ​адресвозврата push sp
 +
 +Листинг 14 содержимое стека на момент вызова функции f на древней XT снабженной 8086 процессором
 +
 +{{asm-extoptimize_Image_7.png?​553}}
 +
 +Рисунок 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
 +
 + ​^^^^^^^^^^^ ​ ^^^^^^^^^^^ ​ ^^^^^^^^^^^ ^^^^^^^^^^^
 +
 + ​| ​ |  |  |
 +
 + ​| ​ pushesi |  куда указывает esp
 +
 + ​адресвозврата push esp
 +
 +Листинг 15 содержимое стека на момент вызова функции f на современных процессорах
 +
 +{{asm-extoptimize_Image_8.png?​552}}
 +
 +Рисунок 9 в отладчике хорошо видно, что в стек попадает такое значение регистра ESP, каким оно было _до_ модификации,​ в результате чего указатель *x указывает на _следующее_ двойное слово!
 +
 +Поэтому,​ при переходе с 8086 на 286+ приходится добавлять "​лишнюю"​ команду PUSH EAX, резервирующую ячейку на стеке, на которую будет указывать значение ESP, засланное в стек инструкцией PUSH ESP
 +
 +pusheax; выделяем память под переменную x (регистр — может быть любым)
 +
 +pushesp; передаем указатель на x как аргумент функции f
 +
 +pushesi; передаем переменную a
 +
 +call f; зовем f
 +
 +Листинг 16 трюкаческий пример,​ портированный на 286+ процессоры
 +
 +Несмотря на то, что 8086/8088 процессоры уже давно не встречаются в дикой природе (ну разве что в виде эмуляторов,​ да и то…), многие программы,​ написанные под них, актуальны и сегодня. Это касается как уже откомпилированного машинного кода, так и различных ассемблерных библиотек,​ переносимых под современные процессоры. Одна из причин,​ по которой они могут не работать — это и есть различие в логике обработке команды PUSH ESP.
 +
 +Вообще же, динамическое выделение памяти посредством PUSH + фиктивный регистр — вполне законный примем,​ которым пользуются не только люди, но и компиляторы. Это намного компактнее,​ чем обращение к локальным/​глобальным переменным,​ выделяемым классическим способом.
 +
 +Естественно,​ большие объемы памяти лучше всего выделять с помощью **SUB ESP****,​**** XXh**,​ но при этом следует помнить как минимум о двух вещах. Первое и главное — Windows-системы выделяют стековую память динамически,​ используя для этого специальную "​сторожевую"​ страницу памяти (pageguard). Как только к ней происходит обращение — система выделяет еще одну или несколько страниц памяти,​ перемещая сторожевую страницу наверх (в сторону меньших адресов памяти). При последовательном "​росте"​ стека все работает нормально,​ но если попытаться прыгнуть за сторожевую страницу,​ сразу же возникнет непредвиденное исключение — ведь никакой памяти по данному адресу еще нет — и работа программы завершается в аварийном режиме. То есть, если у нас есть к примеру 1 Мбайт стекового пространства,​ это еще не значит,​ что код SUB ESP,​ 10000h/​MOV [ESP],​EAX будет работать. Тут уж как повезет (или не повезет). Если ранее вызываемые функции выделяли стековую память планомерно,​ задвинув сторожевую страницу куда-то вглубь стекового пространства,​ то какие-то шансы у нас есть, но полагаться на них — несерьезно. Поэтому,​ при выделении под локальные переменные более 4х Кбайт,​ необходимо выполнить цикл, последовательно обращающийся хотя бы к одной ячейке каждой из запрашиваемых страниц. Читать все ячейки — необязательно,​ да и непроизводительно.
 +
 +Компиляторы делают это автоматически,​ а вот многие ассеблерщики о таком коварстве Windows зачастую даже и не подозревают,​ а потом упорно ищут бага в своей программе,​ не понимая почему она не работает!
 +
 +main()
 +
 +{
 +
 +charx[1024*1024];//​ выделяем 1 Мбайт стековой памяти
 +
 +return *x;// обращаемся к наиболее "​дальней"​ стековой ячейке
 +
 +}
 +
 +Листинг 17 пример программы на Си, выделяющий 1 Мбайт памяти под локальные переменные и обращающийся к самой "​дальней"​ ячейке
 +
 +.text:​00401000 _mainproc near; CODE XREF: start+AF↓p
 +
 +.text:​00401000moveax,​ 100000h
 +
 +.text:​00401005call__alloca_probe
 +
 +.text:​0040100Amovsxeax,​ byte ptr [esp]
 +
 +.text:​00401012addesp,​ 100000h
 +
 +.text:​00401018retn
 +
 +.text:​00401018 _mainendp; sp =  100000h
 +
 +.text:​00401020 __alloca_probeproc near; CODE XREF: _main+5↑p
 +
 +.text:​00401020
 +
 +.text:​00401020 arg_0= dword ptr  8
 +
 +.text:​00401020
 +
 +.text:​00401020pushecx
 +
 +.text:​00401021cmpeax,​ 1000h
 +
 +.text:​00401026leaecx,​ [esp+arg_0]
 +
 +.text:​0040102Ajbshort loc_401040
 +
 +.text:​0040102C
 +
 +.text:​0040102C loc_40102C:;​ CODE XREF: __alloca_probe+1E↓j
 +
 +.text:​0040102Csubecx,​ 1000h
 +
 +.text:​00401032subeax,​ 1000h
 +
 +.text:​00401037test[ecx],​ eax
 +
 +.text:​00401039cmpeax,​ 1000h
 +
 +.text:​0040103Ejnbshort loc_40102C
 +
 +.text:​00401040
 +
 +.text:​00401040 loc_401040:;​ CODE XREF: __alloca_probe+A↑j
 +
 +.text:​00401040subecx,​ eax
 +
 +.text:​00401042moveax,​ esp
 +
 +.text:​00401044test[ecx],​ eax
 +
 +.text:​00401046movesp,​ ecx
 +
 +.text:​00401048movecx,​ [eax]
 +
 +.text:​0040104Amoveax,​ [eax+4]
 +
 +.text:​0040104Dpusheax
 +
 +.text:​0040104Eretn
 +
 +.text:​0040104E __alloca_probeendp
 +
 +Листинг 18 при выделении большого объема локальных переменных,​ компилятор вызывает недокументированную функцию __alloca_probe,​ совершающую "​пробежку"​ по стеку и при необходимости отодвигающую сторожевую страницу на требуемое расстояние,​ то же самое необходимо делать и в ассемблерных программах!
 +
 +Но коварство Windows на этом не заканчиваются. Многие API-функции неявно закладываются на выравнивание стека и если нам, к примеру,​ требуется ровно 69h байт стековой памяти,​ ни в коем случае нельзя писать SUB ESP,​69h,​ иначе все рухнет! Следует округлить 69h по границе двойного слова и запросить 6Ch байт или... между актами выделения/​освобождения памяти не вызывать никаких API-функций.
 +
 +Часто, в погоне за оптимизацией,​ программисты,​ борющиеся за каждый байт памяти,​ забывают о выравнивании и… часами ищут причину,​ по которой оптимизированный вариант программы отказывается работать.
 +
 +{{asm-extoptimize_Image_9.jpg?​516}}
 +
 +Рисунок 10 проблемы выравнивания в оптимизации
 +
 +===== все аргументы в одном месте =====
 +
 +При вызове функций по соглашениям cdecl или stdcall, указатель стека постоянно "​пляшет",​ что сбивает с толку процессорный кэш и снижает производительность. При соглашении stdcall от этого никуда не уйдешь,​ поскольку аргументы очищает непосредственно сама вызываемая функция,​ но для cdecl-функций кое-что все-таки можно сделать.
 +
 +Во-первых,​ как уже говорилось,​ стек можно балансировать не сразу после выхода из функции,​ а спустя некоторое время, объединяя несколько команд ADD ESP,​XXh в одну (конкретный пример показан в листинге 4). Однако,​ это не работает с циклами и ветвлениями. Функция,​ вызываемая в цикле, буквально пожирает память,​ если только аргументы немедленно не выталкиваются из стека. Но памяти — много и на небольшом количестве итераций с этим еще можно хоть как-то смириться (правда,​ выигрыша в производительности мы уже не получим,​ будет просто чудо, если такой объем вообще уместиться в кэше первого уровня).
 +
 +Ветвления создают еще большую проблему — заставляя нас вводить дополнительные переменные (как правило,​ регистровые),​ запоминающие:​ вызывалась ли функция под ветвлением и если да, то сколько байт стекового пространства "​числиться"​ за ней. Все это усложняет код и сводит на нет в общем-то неплохую идею.
 +
 +А что если… передавать аргументы через однократно выделенный регион памяти?​ Это обеспечит максимальную скорость и минимальные потребности в стеке. Мы будем действовать так: на входе в функцию резервируем блок памяти равный наибольшему объему аргументов,​ передаваемых функции,​ а затем просто кладем туда аргументы по ходу дела. Регистр ESP уже не "​пляшет"​ и циклы выполняются с предельной скоростью. Единственный минус в том, что передавать аргументы приходится не инструкций PUSH, а более длинной командой MOV [EBP‑XXh],​YYYY.
 +
 +Конкретная реализация может выглядеть,​ например,​ так:
 +
 +PUSHEBP; сохраняем EBP
 +
 +MOVEBP,ESP; открываем кадр стека
 +
 +SUBESP,10h; выделяем память для аргументов
 +
 +…; (если надо — выделяем память для лок. пер.)
 +
 +
 +
 +MOV [EBP-10h], arg2; кладем на вершину стека крайний левый аргумент
 +
 +MOV [EBP-0Ch], arg1; кладем следующий аргумент
 +
 +CALLfunc_1; вызываем функцию func_1
 +
 +**; не выталкиваем аргументы из стека**
 +
 +
 +
 +******rool****:;​ демонстрация вызова функции в цикле**
 +
 +MOV [EBP-10h], arg5; кладем на вершину стека крайний левый аргумент
 +
 +MOV [EBP-0Ch], arg4; \
 +
 +MOV [EBP-08h], arg3;  + кладем все следующие аргументы
 +
 +MOV [EBP-04h], arg2;  + 
 +
 +MOV [EBP-00h], arg1; /
 +
 +CALLfunc_2; вызываем функцию func_2
 +
 +**; не выталкиваем аргументы из стека**
 +
 +****
 +
 +DECECX; мотаем цикл
 +
 +JNZrool; (только не спрашивайте кто инициализирует ECX!)
 +
 +
 +
 +MOV [EBP-10h], EAX; кладем на вершину стека крайний левый аргумент
 +
 +CALLfunc_3; вызываем функцию func_3
 +
 +
 +
 +MOVESP,EBP; закрываем кадр, освобождая память аргументов
 +
 +POPEBP; восстанавливаем EBP
 +
 +Листинг 19 демонстрация передачи аргументов cdecl-функциям через однократно выделяемый блок памяти
 +
 +Очень похоже на адресацию локальных переменных,​ но это все-таки не переменные,​ а аргументы. Точнее,​ локальные переменные,​ лежавшие на самой вершине стека и с точки зрения вызываемой функции //​выглядевшие//​ как аргументы. Локальные переменные (если они есть) располагаются ниже их.
 +
 +То есть, если мы резервируем 10h байт под все аргументы,​ то первый слева аргумент должен помещаться в ячейку [EBP-10h], второй — в [EBP-0Ch] и так далее. Главное не перепутать порядок засылки аргументов. По соглашению cdecl переменные передаются справа налево,​ следовательно,​ в момент вызова функции на вершине стека лежит крайний левый аргумент,​ а под ним — все остальные.
 +
 +Выигрыш в скорости на самом деле очень значительный,​ а небольшое "​раздутие"​ кода за счет отказа от инструкции PUSH это не такая уж значительная проблема.
 +
 +===== заключение =====
 +
 +Системное программирование хранит множество секретов,​ загадок и тайн, постепенно становясь уделом небольшой горстки профессионалов,​ в то время как мир дружно сходит с ума, подсаживаясь на языки высокого уровня,​ которые чем дальше — тем все выше и выше. Об ассемблере вспоминают только тогда, когда требуется что-то очень сильно нестандартное,​ с чем компилятор уже не справляется или сгенерированный им код не отвечает требованиям производительности.
 +
 +Вот тут-то и выясняется,​ что специалистов,​ владеющих ассемблеров,​ практически нет, а те что есть, уже утратили свои навыки и оптимизируют намного хуже компиляторов,​ разработчики которых за последние несколько лет сделали качественный рывок вперед и теперь просто так их не обгонишь! Сам по себе ассемблер не обеспечивает ни компактности кода, ни высокой скорости. Все решают хитрые трюки и приемы программирования,​ находчивость и инженерная смекалка наконец!
 +
 +Главное — выбрать верную стратегию поведения. Не пытаться сократить программу на пару байт, которые все равно будут потеряны при выравнивании,​ а реально оценивать свой творческий потенциал,​ сопоставляя его с целями и задачами операциями. Алгоритмическая оптимизация зачастую ускоряет программу в десятки раз, в то время как перенос Сишного кода на ассемблер дает в среднем случае 10%-15% выигрыш. Но это еще не значит,​ что ассемблер бесполезен. Просто,​ как и любой другой инструмент,​ он имеет границы своей применимости,​ с которыми следует считаться,​ чтобы не попасть впросак!
 +
 +