Первая программа
В качестве нашего первого примера посмотрим, насколько проще написать под Windows программу, которая загружает другую программу. В DOS (см. главу 4.10) нам приходилось изменять распределение памяти, заполнять специальный блок данных EPBВ и только затем вызывать DOS. Здесь же не только вся процедура сокращается до одного вызова функции, а еще оказывается, что можно точно так же загружать не только программы, но и документы, графические и текстовые файлы и даже почтовые и WWW-адреса — все, для чего в реестре Windows записано действие, выполняющееся при попытке открытия.
; winurl.asm ; Пример програмы для win32. ; Запускает установленный по умолчанию броузер на адрес, указанный в строке URL ; аналогично можно запускать любую программу, документ, и любой другой файл, ; для которого определена операция open ; include shell32.inc include kernel32.inc
.386 .model flat .const URL db 'http://www.lionking.org/~cubbi/',0 .code _start: ; метка точки входа должна начинаться с подчеркивания xor ebx,ebx push ebx ; для исполнимых файлов - способ показа push ebx ; рабочий каталог push ebx ; командная строка push offset URL ; имя файла с путем push ebx ; операция open или print (если NULL - open) push ebx ; идентификатор окна, которое получит сообщения call ShellExecute ; ShellExecute(NULL,NULL,url,NULL,NULL,NULL) push ebx ; код выхода call ExitProcess ; ExitProcess(0) end _start
Итак, в этой программе выполняется вызов двух системных функций Win32 — ShellExecute() (открыть файл) и ExitProcess() (завершить процесс). Чтобы вызвать системную функцию Windows, программа должна поместить в стек все параметры от последнего к первому и передать управление дальней командой CALL. Все эти функции сами освобождают стек (завершаясь командой RET N) и возвращают результат работы в регистре ЕАХ. Такая договоренность о передаче параметров называется STDCALL. С одной стороны, это позволяет вызывать функции с нефиксированным числом параметров, а с другой — вызывающая сторона не должна заботиться об освобождении стека. Кроме того, функции Windows сохраняют значение регистров ЕВР, ESI, EDI и EBX, этим мы пользовались в нащем примере — хранили 0 в регистре EBX и применили 1-байтную команду PUSH EBX вместо 2-байтной PUSH 0.
Прежде чем мы сможем скомпилировать winurl.asm, нужно создать файлы kernel32.inc и shell32.inc, в которые поместим директивы, описывающие вызываемые системные функции:
; kernel32.inc ; включаемый файл с определениями функций из kernel32.dll ; ifdef _TASM_ includelib import32.lib ; имена используемых функций extrn ExitProcess:near else includelib kernel32.lib ; истинные имена используемых функций extrn __imp__ExitProcess@4:dword ; присваивания для облегчения читаемости кода ExitProcess equ __imp__ExitProcess@4 endif
; shell32.inc ; включаемый файл с определениями функций из shell32.dll ifdef _TASM_ includelib import32.lib ; имена используемых функций extrn ShellExecuteA:near ; присваивания для облегчения читаемости кода ShellExecute equ ShellExecuteA else includelib shell32.lib ; истинные имена используемых функции extrn __imp__ShellExecuteA@24:dword ; присваивания для облегчения читаемости кода ShellExecute equ __imp__ShellExecuteA@24 endif
Имена всех системных функций Win32 модифицируются так, что перед именем функции ставится подчеркивание, а после — знак «@» и число байт, которое занимают параметры, передаваемые ей в стеке, так ExitProcess() превращается в _ExitProcess@4(). Компиляторы с языков высокого уровня часто останавливаются на этом и вызывают функции по имени _ExitProcess@4(), но реально вызывается небольшая процедура-заглушка, которая ничего не делает, а только передает управление на такую же метку, но с добавленным «__imp_» — __imp__ExitProcess@4(). Во всех наших примерах мы будем обращаться напрямую к __imp__ExitProcess@4(). К сожалению, TASM (а точнее TLINK32) использует собственный способ вызова системных функций, который нельзя так обойти, и программы, скомпилированные с его помощью, оказываются намного больше и в некоторых случаях работают медленнее. Мы отделили описания функций для TASM во включаемых файлах при помощи директив условного ассемблирования, которые будут использовать их, если в командной строке ассемблера указать /D_TASM_.
Кроме этого, все функции, работающие со строками (как, например, ShellExecute()), существуют в двух вариантах. Если строка рассматривается в обычном смысле, как набор символов ASCII, к имени функции добавляется «A» (ShellExecuteA()). Другой вариант функции, использующий строки в формате UNICODE (два байта на символ), заканчивается буквой «U». Во всех наших примерах будем использовать обычные ASCII-функции, но, если вам потребуется перекомпилировать программы на UNICODE, достаточно только поменять «А» на «U» во включаемых файлах.
Итак, теперь, когда у нас есть все необходимые файлы, можно скомпилировать первую программу для Windows.
Компиляция MASM:
ml /с /coff /Cp winurl.asm link winurl.obj /subsystem:windows
(здесь и далее используется 32-битная версия link.exe)
Компиляция TASM:
tasm /m /ml /D_TASM_ winurl.asm tlink32 /Tpe /aa /c /x winurl.obj
Компиляция WASM:
wasm winurl.asm wlink file winurl.obj form windows nt op с
Также для компиляции потребуются файлы kernel32.lib и shell32.lib в первом и третьем случае и import32.lib — во втором. Ёти файлы входят в дистрибутивы любых средств разработки для Win32 от соответствующих компаний — Microsoft, Watcom (Sybase) и Borland (Inprise), хотя их всегда можно воссоздать из файлов kernel32.dll и shell32.dll, находящихся в каталоге WINDOWS/SYSTEM.
Иногда вместе с дистрибутивами различных средств разработки для Windows идет файл windows.inc, в котором дано макроопределение Invoke или заменена макросом команда call так, что они принимают список аргументов, первым из которых идет имя вызываемой функции, а затем через запятую — все параметры. С использованием этих макроопределений наша программа выглядела бы так:
_start: xor ebx,ebx Invoke SnellExecute, ebx, ebx, offset URL, ebx, \ ebx, ebx Invoke ExitProcess, ebx end _start
И этот текст компилируется в точно такой же код, что и у нас, но выполняется вызов не функции __imp__ExitProcess@4(), а промежуточной функции _ExitProcess@4(). Использование этой формы записи не позволяет применять отдельные эффективные приемы оптимизации, которые мы будем приводить в наших примерах, — помещение параметров в стек заранее и вызов функции командой JMP. И наконец, файла windows.inc у вас может просто не оказаться, так что будем писать push перед каждым параметром вручную.
Содержание раздела