Учебное пособие по созданию консольных приложений на ассемблере под Win32. Win32 - название интерфейса прикладных программ (application programming interface, API), содержит совокупность функций, к которым может обращаться приложение. В данном пособии рассматривается Win32 для операционных систем Windows NT и Windows 9x. В Win32 существует только одна модель памяти - FLAT. Память представляется одним большим непрерывным пространством размером 4Гб. В этом пространстве располагаются и данные и коды программы. Структуры программы. .386 .MODEL FLAT, STDCALL .DATA <Инициализация компиляции> необходимых данных, значения которых определены к моменту . DATA? <Резервирование места для данных, которые определяются в процессе работы программы> .CONST <Константы, т.е. данные, не изменяемые в процессе работы программы> .CODE <Метка> <Коды программы> END <Метка> Анализ структуры программы. .386 Это директива, сообщающая ассемблеру использовать набор инструкций 80386. Вы также можете использовать .486, .586. .MODEL FLAT, STDCALL .MODEL это директива ассемблера, которая описывает модель памяти, используемую вашей программой. Обращение к сервису операционной системы в Windows осуществляется посредством вызова функций, а не прерываний, что было характерно для DOS. Здесь нет передачи параметров в регистрах при обращении к сервисным функциям, параметры передаются в функции через стек. STDCALL сообщает транслятору о способе передачи аргументов в функцию (parameter passing convention). Способ передачи аргументов определяет, в какой последовательности аргументы будут передаваться в функцию: слева направо или справа налево, и каким образом будет осуществляться коррекция стека после вызова функции. Существует два способа передачи аргументов, как в Си и как в Паскале. Сиподобным способом аргументы передаются справа налево, то есть самый правый аргумент передается первым. Программа сама отвечает за корректировку стека после вызова. Например, если необходимо вызвать функцию void funcf (int first_param, int second_param, int third_param); Си-подобным способом, код на Ассемблере будут выглядеть следующим образом: push push push call add [third_param] [second_param] [first_param] func esp,12 ; ; ; ; ; передача третьего параметра передача второго параметра передача первого параметра вызов функции программа самостоятельно корректирует стек В Паскаль-подобном способе все наоборот по сравнению с Си-подобным. Передача аргументов осуществляется слева направо и за корректировку стека после вызова функции отвечает сама функция. Си-подобный способ полезен в том случае, когда вы заранее не знаете количество передаваемых параметров в функцию, как в случае с wsprintf(). В этом случае функция не может заранее определить сколько параметров будет передано в стек, поэтому не сможет правильно провести коррекцию стека. STDCALL - является объединением Си- и Паскаль - подобного способа - аргументы передаются справа налево, но коррекцию стека после вызова функции осуществляет сама эта функция. Платформа Win32 использует исключительно STDCALL. Кроме одного случая: wsprintf(). Для этой функции используется Си-подобный способ передачи аргументов в функцию. Используя способ передачи параметров STDCALL, приведенный выше вызов функции func будет выглядеть так: push push push call [third_param] [second_param] [first_param] func ; ; ; ; передача третьего параметра передача второго параметра передача первого параметра вызов функции За коррекцию стека отвечает сама вызываемая функция. .DATA .DATA? .CONST .CODE Все четыре директивы являются именами секций памяти. В Win32 сегмейтов нет. Но вы можете поделить все адресное пространство на секции. Начало одной секции является концом предыдущей. Существует две группы секций: секция данных и кода. Секции данных, в свою очередь делятся на 3 категории: .DATA - В этой секции содержатся инициализируемые данные программы. .DATA? В этой секции содержатся не инициализируемые данные программы. Иногда вам просто необходимо зарезервировать некоторый объем памяти, но вы не хотите его инициализировать, тогда для этой цели используйте данную секцию. .CONST - В этой секции содержатся константы, используемые в программе. Значения в этой секции программой изменяться не могут. Вам нет необходимости использовать все секции. Пользуйтесь только теми из них, которые вам необходимы. Секция кода всего одна: .CODE. В ней содержатся все инструкции программы. <Метка> <Коды программы> end <Метка> где <Метка> программы. это стартовая метка, с которой начинается выполнение кода вашей При написании консольных приложений понадобятся несколько функций. Прототипы этих функций на языке С и вызов на ассемблере приводятся ниже. При использовании пакета MASM в вызове функций с параметрами будет использоваться invoke вместо call. 1. Получение дескриптора ввода/вывода. Прототип на языке С: HANDLE GetstdHandle(DWORD nStdHandle); Вызов на Ассемблере: call GetstdHandle, nStdHandle nStdHandle сообщает функции какой дескриптор нужно возвратить. Принимает следующие значения: STD_INPUT_HANDLE - дескриптор ввода. STD_OUTPUT_HANDLE - дескриптор вывода. STD_ERROR_HANDLE - дескриптор вывода ошибок. Если функция выполнена успешно, то возвращаемое значение есть дескриптор, который можно применять в дальнейшем при работе с функциями ввода/вывода. В случае неудачи функция возвращает INVALID_HANDLE_VALUE. 2. Чтение символов с консоли (Enter - Ввод) Прототип на языке С: BOOL ReadConsole (HANDLE hConsoleInput, LPVOID IpBuffer, DWORD nNumberOfCharsToRead, LPDWORD lpNumberOfCharsRead, LPVOID lpReserved); Вызов на Ассемблере: call ReadConsole, hConsoleInput, lpBuffer, nNumberOfCharsToRead, lpNumberOfCharsRead, lpReserved hConsoleInput - дескриптор ввода, полученный ранее, lpBuffer - адрес буфера, в который будут помещаться прочитанные символы, nNumberOfCharsToRead - количество символов, которые необходимо прочитать, lpNumberOfCharsRead - адрес переменной, в которую будет записано количество прочитанных символов, lpReserved - зарезервировано (не используется). Если функция выполнена успешно, то она возвращает ненулевое значение. В случае неудачи функция возвращает ноль . 3. Запись символов на консоль ($ - конец строки) Прототип Вызов на на языке С: BOOL WriteConsole (HANDLE hConsoleOutput, CONST VOID * lpBuffer, DWORD nNumberOfCharsToWrite, LPDWORD lpNumberOfCharsWritten, LPVOID lpReserved); Ассемблере: call WriteConsole, hConsoleOutput, lpBuffer, NumberOfCharsToWrite, lpNumberOfCharsWritten, lpReserved hConsoleOutput - дескриптор вывода, lpBuffer - адрес буфера, содержимое которого необходимо вывести, nNumberOfCharsToWrite - количество символов для вывода, lpNumberOfCharsWritten - адрес переменной, в которую будет записано количество выведенных символов, lpReserved - зарезервировано (не используется) . Если функция выполнена успешно, то она возвращает ненулевое значение. В случае неудачи функция возвращает ноль. 4. Выход из программы с кодом возврата. Прототип на языке С: VOID ExitProcess (UINT uExitCode); Вызов на Ассемблере: call ExitProcess, uExitCode uExitCode - код возврата. Данная функция прекращает выполнение программы, поэтому она должна быть последней вызываемой в программе функцией. При вызове функций, параметры передаются через стек, как описывалось ранее, а возвращаемое функцией значение передается через регистр еах - для целых и st(0) - для вещественных. В качестве передаваемых в функцию данных могут выступать как данные, так и адреса. При использовании различных трансляторов, например TASM и MASM, код программы может несколько отличатся. Рассмотрим пример простой программы, которая демонстрирует применение приведенных выше функций при использовании трансляторов TASM и MASM. Пример программы для TASM .386 ;использование инструкций 386 процессора .model flat,stdcall ;использование модели памяти flat и способа ;передачи аргументов в функции stdcall ;описание используемых функций extern ExitProcess:proc extern GetStdHandle:proc extern WriteConsoleA:proc extern ReadConsoleA:proc includelib import32.lib .data? hInput hOutput bw br buffer dd dd dd dd db ? ? ? ? 100 dup (?) .data string string_len db equ ‘Введите строку: ’ $-string ;$ - текущее значение счетчика адреса. Его ;удобно использовать для определения длины строки. ;Выражение $-string определяет длину строки string. .const STD_INPUT_HANDLE equ STD_OUTPUT_HANDLE equ ;дескриптор ввода ;дескриптор вывода ;количество записанных символов ;количество прочитанных символов ;буфер на 100 символов -10 -11 ; начало сегмента кода .code main: ;получение дескриптора вывода call GetStdHandle,STD_OUTPUT_HANDLE mov hOutput, eax ;получение дескриптора ввода call GetStdHandle, STD_INPUT_HANDLE mov hInput,eax ;вывод строки call WriteConsoleA,hOutput,offset string,string_len,offset bw,0 ;чтение строки call ReadConsoleA,hInput,offset buffer,100,offset br,0 ;вывод прочтенной строки call WriteConsoleA,hOutput,offset buffer,br,offset bw, 0 ;выход из приложения с кодом возврата О call ExitProcess,О end main Пример программы для MASM .386 .model flat,stdcall include windows.inc include kernel32.inc includelib kernel32.1ib .data? hinput houtput bw br buffer dd dd dd dd db ;использование инструкций 386 процессора ;использование модели памяти flat и способа ;передачи аргументов в функции stdcall ;описание констант ;описание прототипов функций ; библиотека с используемыми фущщиями ? ;дескриптор ? ;дескриптор ? ;количество ? ;количество 100 dup (?) ;буфер на ввода вывода записанных символов прочитанных символов 100 символов .data string string_len db equ ‘Введите строку: ’ $-string ;$ - текущее значение счетчика адреса. Его ;удобно использовать для определения длины строки. ;Выражение $-string определяет длину строки string. ; начало сегмента кода .code main: ; получение дескриптора вывода invoke GetStdHandle,STD_OUTPUT_HANDLE mov houtput, eax ; получение дескриптора ввода invoke GetStdHandle,STD_INPUT_HANDLE mov hlnput,eax ; вывод строки invoke WriteConsole,hOutput, offset string,string_len,offset bw, 0 ; чтение введенной строки invoke ReadConsole,hInput,offset buffer,100,offset br,0 ; вывод только, что введенной строки invoke WriteConsole, hOutput,offset buffer,br,offset bw, 0 ; выход из приложения с кодом возврата О invoke ExitProcess,О end main