[Назад] [Далее]

7.1. Первая программа

В качестве нашего первого примера посмотрим, насколько проще написать под 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 перед каждым параметром вручную.


п»ї
"target=_blank><\/a>") //-->