Коротко о FASM, ассемблере, WinAPI
-
Что такое FASM? — Это компилятор ассемблера (flat assembler).
-
Что такое ассемблер? — это машинные инструкции, то есть команды что делать процессору.
-
Что такое Windows API/WinAPI? — Это функции Windows, без них нельзя работать с Windows.
Что дают WinAPI функции? — Очень много чего:
-
Работа с файлами.
-
Работа с окнами, отрисовка картинок, OpenGL, DirectX, GDI, и все в таком духе.
-
Взаимодействие с другими процессами.
-
Работа с портами.
-
Работа с консолью Windows
-
И еще очень много интересных функций.
Зачем нужен ассемблер?
На нем можно сделать все что угодно, от ОС до 3D игр.
Вот плюсы ассемблера:
-
Он очень быстрый.
-
На нем можно сделать любую программу.
А вот минусы ассемблера:
-
Долго делать программу. (относительно)
-
Сложен в освоении.
Что нужно для программирования на ассемблере (FASM)?
-
FASM компилятор — https://flatassembler.net/
-
FASM Editor 2.0 — Удобная IDE для FASM, от fasmworld.ru (asmworld), качаем от сюда: https://fasmworld.ru/content/files/tools/FEditor-v2.0.rar
-
OlyDbg — удобный отладчик ассемблера от ollydbg.de: https://www.ollydbg.de/odbg201.zip
Это все мероприятие весит всего лишь 8.5MB.
Установка компонентов (если можно так назвать)
Архив FASM-а распаковуем в C:\\FASM\ или любой другой, но потом не забудьте настроить FASMEditor.
Архив FASMEdit-a распаковуем куда-то, в моем случае C:\\FASM Editor 2.0\
Архив OlyDbg распаковуем тоже куда-то, в моем случае C:\\Users\****\Documents\FasmEditorProjects\
Настройка FASM Editor-a
Для этого его нужно запустить.
Сразу вас приветствует FASM Editor соей заставкой.
Теперь вам нужно зайти в вкладку «Сервис» (на картинке выделил синим) -> «Настройки…»
Жмем на кнопку с названием «…» и выбираем путь к файлам или папкам.
Теперь мы полностью готовы. К началу.
Пишем «Hello world!» на FASM
В Fasm Editor нужно нажать на кнопку слева сверху или «файл» -> «новый». Выбираем любое, но можно выбрать «Console»
По началу вас это может напугать, но не боимся и разбираемся.
format PE Console ; говорим компилятору FASM какой файл делать
entry start ; говорим windows-у где из этой каши стартовать программу.
include 'win32a.inc' ; подключаем библиотеку FASM-а
;можно и без нее но будет очень сложно.
section '.data' data readable writeable ; секция данных
hello db 'hello world!',0 ; наша строка которую нужно вывести
section '.code' code readable writeable executable ; секция кода
start: ; метка старта
invoke printf, hello ; вызываем функцию printf
invoke getch ; вызываем её для того чтоб программа не схлопнулась
;то есть не закрылась сразу.
invoke ExitProcess, 0 ; говорим windows-у что у нас программа закончилась
; то есть нужно программу закрыть (завершить)
section '.idata' data import readable ; секция импорта
library kernel, 'kernel32.dll',\ ; тут немного сложней, объясню чуть позже
msvcrt, 'msvcrt.dll'
import kernel,\
ExitProcess, 'ExitProcess'
import msvcrt,\
printf, 'printf',\
getch, '_getch'
На самом деле из всей этой каши текста, команд всего 3: на 16, 18, 21 строках. (и то это не команды, а макросы. Мы к командам даже не подобрались)
Все остальное это просто подготовка программы к запуску.
Программа при запуске должна выглядеть так:
Самое интересное то что программа весит 2КБ. (Можно сократить и до 1КБ, но для упрощения и так пойдет)
Разбор: что значат этот весь текст?
На 1 строчке: «format PE Console» — это строчка говорит FASM-у какой файл скомпилировать, точнее 1 слово, все остальные слова это аргументы (можно так сказать).
PE — EXE файл, программа.
Console — говорим что это у нас консольная программа, но вам некто не мешает сделать из консольной программы оконную и наоборот.
Но есть кроме это остальные:
-
format MZ — EXE-файл НО под MS-DOS
-
format PE — EXE-файл под Windows, аналогично format PE GUI 4.0
-
format PE64 — EXE-файл под Windows, 64 битное приложение.
-
format PE GUI 4.0 — EXE-файл под Windows, графическое приложение.
-
format PE Console — EXE-файл под Windows, консольная программа. (просто подключается заранее консоль)
-
format PE Native — драйвер
-
format PE DLL — DLL-файл Windows, поясню позднее.
-
format COFF — OBJ-файл Linux
-
format MS COFF — аналогично предыдущему
-
format ELF — OBJ-файл для gcc (Linux)
-
format ELF64 — OBJ-файл для gcc (Linux), 64-bit
Сразу за командой (для компилятора) format PE Console
идет ;
это значит комментарий. К сожалению он есть только однострочный.
3 строка: entry start
-
Говорим windows-у где\в каком месте стартовать. «start» это метка, но о метках чуть позже.
5 строка: include 'win32a.inc'
-
Подключает к проекту файл, в данном случае «win32a.inc» он находиться в папке INCLUDE (в папке с FASM). этот файл создает константы и создает макросы для облегчения программирования.
8 строка: section '.data' data readable writeable
-
Секция данных, то есть программа делиться на секции (части), к этим секциям мы можем дать разрешение, имя.
Флаг «data» (Флаг это бит\байт\аргумент хранившей в себе какую-то информацию) говорит то что эта секция данных.
Флаги «readable writeable» говорят то что эта секция может читаться кем-то и записываться кем-то.
Текст ‘.data’ — имя секции
10 строка: hello db 'hello world!',0
hello — это метка, она может быть любого имени (почти, есть некоторые зарезервированные имена), эта метка хранит в себе адрес строки, это не переменная, а просто адрес, но чтобы не запоминать адреса в ручную, помогает FASM он запоминает адрес и потом когда видит эту метку снова, то он заменяет слово на адрес.
db — говорит то что под каждый символ резервируем 1 байт. То есть 1 символ храниться в одном байте.
‘hello world!’ — наша строка в кодировке ASCII
Что значит «,0» в конце строки? — это символ с номером 0 (или просто ноль), у вас на клавиатуре нет клавиши которая имела символ с номером 0, по этому этот символ используют как показатель конца строки. То есть это значит конец строки. Просто ноль записываем в байт после строки.
12 строка: section '.code' code readable writeable executable
Флаг «code» — говорит то что это секция кода.
Флаг «executable» — говорит то что эта секция исполняема, то есть в этой секции может выполняться код.
Все остальное уже разобрали.
14 строка: start:
Это второй вид меток. Просто эта метка указывает на следующую команду. Обратите внимание на то что в 3 строке мы указали start как метку входа в программу, это она и есть. Может иметь эта метка любое имя, главное не забудьте ваше новое имя метки вписать в entry
15 строка: invoke printf, hello
-
Функция printf — выводит текст\число в консоль. В данном случае текст по адресу «hello»
Это штото на подобие команды, но это и близко не команда ассемблера, а просто макрос.
Макрос — Это макро команда для компилятора, то есть вместо имени макроса подставляется что-то другое.
Например, макро команда invoke делиться на такие команды: (взят в пример команда с 15 строки)
push hello
call [printf]
Не переживайте если нечего не поняли.
17 строка: invoke getch
-
getch — функция получения нажатой кнопки, то есть просто ждет нажатия кнопки и потом возвращает нажатую кнопку.
20 строка: invoke ExitProcess, 0
-
ExitProcess — WinAPI функция, она завершает программу. Она принимает значение, с которым завершиться, то есть код ошибки, ноль это нет ошибок.
23 строка: section '.idata' data import readable
Флаг «import» — говорит то что это секция импорта библиотек.
24-25 строки:
library kernel, 'kernel32.dll',\
msvcrt, 'msvcrt.dll'
-
Макро команда «library» загружает DLL библиотеки в виртуальную память (не в ОЗУ, вам ОЗУ не хватит чтоб хранить всю виртуальную память).
Что такое DLL объясню позже.
kernel — имя которое привязывается к библиотеке, оно может быть любым.
Следующий текст после запятой: 'kernel32.dll'
— это имя DLL библиотеки который вы хотите подключить.
Дальше есть знак \
это значит что текст на следующей строке нужно подставить в эту строку.
То есть код:
library kernel, 'kernel32.dll',\
msvcrt, 'msvcrt.dll'
Заменяется на:
library kernel, 'kernel32.dll', msvcrt, 'msvcrt.dll'
Это нужно потому что у ассемблера 1 строка это 1 команда.
27-28 строка:
import kernel,\
ExitProcess, 'ExitProcess'
import
— Макро команда, которая загружает функции из DLL.
kernel
— Имя к которой привязана DLL, может быть любым.
ExitProcess
— Как будет называться функция в программе, это имя будет только в вашей программе, и по этому имени вы будете вызывать функцию. (WinAPI функция)
'ExitProcess'
— Это имя функции которое будет загружено из DLL, то есть это имя функции которое прописано в DLL.
Дальше думаю не стоит объяснять, вроде все понятно.
Что такое DLL библиотека?
Это файл с расширением DLL. В этом файле прописаны функции (какие ни будь). Это обычная программа, но которая не запускается по двойному щелчку, а загружается к программе в виртуальную память, и потом вызываются функции находящиеся в этой DLL.
Подводим итог
На ассемблере писать можно не зная самого языка, а используя всего лишь макро команды компилятора. За всю статью я упомянул всего 2 команды ассемблера это push hello
и call [printf]
. Что это значит расскажу в следующей статье.
flat assembler
Documentation and tutorials.
Windows programming headers
1. Basic headers
1.1 Structures
1.2 Imports
1.3 Procedures (32-bit)
1.4 Procedures (64-bit)
1.5 Customizing procedures
1.6 Exports
1.7 Component Object Model
1.8 Resources
1.9 Text encoding
2. Extended headers
2.1 Procedure parameters
2.2 Structuring the source
With the Windows version of flat assembler comes the package of
standard includes designed to help in writing the programs for
Windows environment.
The includes package contains the headers for 32-bit and 64-bit Windows
programming in the root folder and the specialized includes in the
subfolders. In general, the headers include the required specialized files for you, though sometimes you might prefer to include some of the macroinstruction packages yourself (since few of them are not included by some or even all of the headers).
There are six headers for 32-bit Windows that you can choose from, with names starting with win32 followed by either a letter a for using the ASCII encoding, or a letter w for the WideChar encoding. The win32a.inc and win32w.inc are the basic headers,
the win32ax.inc and win32wx.inc are the extended headers, they provide more advanced macroinstructions, those extensions will be discussed separately. Finally the win32axp.inc and win32wxp.inc are the same extended headers with enabled feature of checking the count of parameters in procedure calls.
There are analogous six packages for the 64-bit Windows, with
names starting with win64. They provide in general the
same functionality as the ones for 32-bit Windows, with just a few
differences explained later.
You can include the headers any way you prefer, by providing the full path or using the custom environment variable, but the simplest method is to define the INCLUDE environment variable properly pointing to the directory containing headers and then include them just like:
include 'win32a.inc'
It’s important to note that all macroinstructions, as opposed to internal directives of flat assembler, are case sensitive and the lower case is used for the most of them. If you’d prefer to use the other case than default, you should do the appropriate adjustments with fix directive.
1. Basic headers
The basic headers win32a.inc, win32w.inc,
win64a.inc and win64w.inc
include declarations of Windows equates and structures and provide the standard set of macroinstructions.
1.1 Structures
All headers enable the struct macroinstruction, which allows to define structures in a way more similar to other assemblers than the struc directive. The definition of structure should be started with struct macroinstruction followed by the name, and ended with ends macroinstruction. In lines between only data definition directives are allowed, with labels being the pure names for the fields of structure:
struct POINT x dd ? y dd ? ends
With such definition this line:
point1 POINT
will declare the point1 structure with the point1.x and point1.y fields, giving them the default values — the same ones as provided in the definition of structure (in this case the defaults are both uninitialized values). But declaration of structure also accepts the parameters, in the same count as the number of fields in the structure, and those parameters, when specified, override the default values for fields. For example:
point2 POINT 10,20
initializes the point2.x field with value 10, and the point2.y with value 20.
The struct macro not only enables to declare the structures of given type, but also defines labels for offsets of fields inside the structure and constants for sized of every field and the whole structure. For example the above definition of POINT structure defines the POINT.x and POINT.y labels to be the offsets of fields inside the structure, and sizeof.POINT.x, sizeof.POINT.y and sizeof.POINT as sizes of the corresponding fields and of the whole structure. The offset labels may be used for accessing the structures addressed indirectly, like:
mov eax,[ebx+POINT.x]
when the ebx register contains the pointer to POINT structure. Note that field size checking will be performed with such
accessing as well.
The structures itself are also allowed inside the structure definitions, so the structures may have some other structures as a fields:
struct LINE start POINT end POINT ends
When no default values for substructure fields are specified, as in this example, the defaults from the definition of the type of substructure apply.
Since value for each field is a single parameter in the declaration of the structure, to initialize the substructures with custom values the parameters for each substructure must be grouped into a single parameter for the structure:
line1 LINE <0,0>,<100,100>
The above declaration initializes each of the line1.start.x and line1.start.y fields with 0, and each of the line1.end.x and line1.end.y with 100.
When the size of data defined by some value passed to the declaration structure is smaller than the size of corresponding field, it is padded to that size with undefined bytes (and when it is larger, the error happens). For example:
struct FOO data db 256 dup (?) ends some FOO <"ABC",0>
fills the first four bytes of some.data with defined values and reserves the rest.
Inside the structures also unions and unnamed substructures can be defined. The definition of union should start with union and end with ends, like in this example:
struct BAR field_1 dd ? union field_2 dd ? field_2b db ? ends ends
Each of the fields defined inside union has the same offset and they share the same memory. Only the first field of union is initialized with given value, the values for the rest of fields are ignored (however if one of the other fields requires more memory than the first one, the union is padded to the required size with undefined bytes). The whole union is initialized by the single parameter given in structure declaration, and this parameter gives value to the first field of union.
The unnamed substructure is defined in a similar way to the union, only starts with the struct line instead of union, like:
struct WBB word dw ? struct byte1 db ? byte2 db ? ends ends
Such substructure only takes one parameter in the declaration of whole structure to define its values, and this parameter can itself be the group of parameters defining each field of the substructure. So the above type of structure may get declared like:
my WBB 1,<2,3>
The fields inside unions and unnamed substructures are accessed just as if the were directly the fields of the parent structure. For example with above declaration my.byte1 and my.byte2 are correct labels for the substructure fields.
The substructures and unions can be nested with no limits for the nesting depth:
struct LINE union start POINT struct x1 dd ? y1 dd ? ends ends union end POINT struct x2 dd ? y2 dd ? ends ends ends
The definition of structure may also be based on some of the already defined structure types and it inherits all the fields from that structure, for example:
struct CPOINT POINT color dd ? ends
defines the same structure as:
struct CPOINT x dd ? y dd ? color dd ? ends
All headers define the CHAR data type, which can be used to
define character strings in the data structures.
1.2 Imports
The import macroinstructions help to build the import data for PE file (usually put in the separate section).
There are two macroinstructions for this purpose. The first one is called library, must be placed directly in the beginning of the import data and it defines from what libraries the functions will be imported. It should be followed by any amount of the pairs of parameters, each pair being the label for the table of imports from the given library, and the quoted string defining the name of the library. For example:
library kernel32,'KERNEL32.DLL',\ user32,'USER32.DLL'
declares to import from the two libraries. For each of libraries, the table of imports must be then declared somewhere inside the import data. This is done with import macroinstruction, which needs first parameter to define the label for the table (the same as declared earlier to the library macro), and then the pairs of parameters each containing the label for imported pointer and the quoted string defining the name of function exactly as exported by library. For example the above library declaration may be completed with following import declarations:
import kernel32,\ ExitProcess,'ExitProcess' import user32,\ MessageBeep,'MessageBeep',\ MessageBox,'MessageBoxA'
The labels defined by first parameters in each pair passed to the import macro address the double word pointers, which after loading the PE are filled with the addresses to exported procedures.
Instead of quoted string for the name of procedure to import, the number may be given to define import by ordinal, like:
import custom,\ ByName,'FunctionName',\ ByOrdinal,17
The import macros optimize the import data, so only imports for functions that are used somewhere in program are placed in the import tables, and if some import table would be empty this way, the whole library is not referenced at all. For this reason it’s handy to have the complete import table for each library — the package contains such tables for some of the standard libraries, they are stored in the APIA and APIW subdirectories and import the ASCII and WideChar variants of the API functions. Each file contains one import table, with lowercase label the same as the name of the file. So the complete tables for importing from the KERNEL32.DLL and USER32.DLL libraries can be defined this way (assuming your INCLUDE environment variable points to the directory containing the includes package):
library kernel32,'KERNEL32.DLL',\ user32,'USER32.DLL' include 'apia\kernel32.inc' include 'apiw\user32.inc'
1.3 Procedures (32-bit)
Headers for 32-bit Windows provide four macroinstructions for calling procedures with parameters passed on stack.
The stdcall calls directly the procedure specified by the first argument
using the STDCALL calling convention. The rest of arguments passed to macro define the parameters to
procedure and are stored on the stack in reverse order. The invoke macro does the same, however it
calls the procedure indirectly, through the pointer labelled by the first argument. Thus invoke
can be used to call the procedures through pointers defined in the import tables. This line:
invoke MessageBox,0,szText,szCaption,MB_OK
is equivalent to:
stdcall [MessageBox],0,szText,szCaption,MB_OK
and they both generate this code:
push MB_OK push szCaption push szText push 0 call [MessageBox]
The ccall and cinvoke are analogous to
the stdcall and invoke, but they should be
used to call the procedures that use the C calling convention, where the stack frame has to be restored by the caller.
To define the procedure that uses the stack for parameters and local variables, you should use the
proc macroinstruction. In its simplest form it has to be followed by
the name for the procedure and then names for the all the parameters it takes, like:
proc WindowProc,hwnd,wmsg,wparam,lparam
The comma between the name of procedure and the first parameter is optional.
The procedure instructions should follow in the next lines, ended with the endp macroinstruction.
The stack frame is set up automatically on the entry to procedure, the EBP register is used as a base
to access the parameters, so you should avoid using this register for other purposes.
The names specified for the parameters are used to define EBP-based labels, which you can use to access
the parameters as regular variables. For example the mov eax,[hwnd] instruction inside the procedure defined as in above sample, is equivalent to mov eax,[ebp+8]. The scope of those labels is limited to the procedure, so you may use the same names for other purposes outside the given procedure.
Since any parameters are pushed on the stack as double words when calling such procedures, the labels for
parameters are defined to mark the double word data by default, however you can you specify the sizes for the parameters if you want, by following the name of parameter with colon and the size operator.
The previous sample can be rewritten this way, which is again equivalent:
proc WindowProc,hwnd:DWORD,wmsg:DWORD,wparam:DWORD,lparam:DWORD
If you specify a size smaller than double word, the given label applies to the smaller portion of the
whole double word stored on stack. If you you specify a larger size, like far pointer of quad word, the
two double word parameters are defined to hold this value, but are labelled as one variable.
The name of procedure can be also followed by either the stdcall or
c keyword to define the calling convention it uses. When no such type is
specified, the default is used, which is equivalent to STDCALL. Then also the uses keyword may follow, and after it the list of registers (separated only with spaces) that will be automatically stored on entry to procedure and restored on exit.
In this case the comma after the list of registers and before the first parameter is required.
So the fully featured procedure statement might look like this:
proc WindowProc stdcall uses ebx esi edi,\ hwnd:DWORD,wmsg:DWORD,wparam:DWORD,lparam:DWORD
To declare the local variable you can use the local macroinstruction,
followed by one or more declarations separated with commas, each one consisting of the name for
variable followed by colon and the type of variable — either one of the standard types (must be upper case) or the name of data structure. For example:
local hDC:DWORD,rc:RECT
To declare a local array, you can follow the name of variable by the size of array enclosed in square
brackets, like:
local str[256]:BYTE
The other way to define the local variables is to declare them inside the block started with «locals» macroinstruction and ended with «endl», in this case they can be defined just like regular data. This declaration is the equivalent of the earlier sample:
locals hDC dd ? rc RECT endl
The local variables can be declared anywhere inside the procedure, with the only limitation that they have to be declared before they are used. The scope of labels for the variables defined as local is limited to inside the procedure, you can use the same names for other purposes outside the procedure.
If you give some initialized values to the variables declared as local, the macroinstruction generates the instructions that will initialize these variables with the given values and puts these instruction at the same position in procedure, where the declaration is placed.
The ret placed anywhere inside the procedure, generates the complete code
needed to correctly exit the procedure, restoring the stack frame and the registers used by procedure.
If you need to generate the raw return instruction, use the retn mnemonic, or follow the ret with the number parameter, what also causes it to be interpreted as single instruction.
To recapitulate, the complete definition of procedure may look like this:
proc WindowProc uses ebx esi edi,hwnd,wmsg,wparam,lparam local hDC:DWORD,rc:RECT ; the instructions ret endp
1.4 Procedures (64-bit)
In 64-bit Windows there is only one calling convention, and thus only two macroinstructions for calling procedures are provided.
The fastcall calls directly the procedure specified by the first argument using the standard convention of 64-bit Windows system.
The invoke macro does the same, but indirectly, through the pointer labelled by the first argument.
Parameters are provided by the arguments that follow, and they can be of any size up to 64 bits.
The macroinstructions use RAX register as a temporary storage when some parameter value cannot be copied directly into the stack using the
mov instruction. If the parameter is preceded with addr word, it is treated as an address and is
calculated with the lea instruction — so if the address is absolute, it will get calculated as RIP-relative,
thus preventing generating a relocation in case of file with fixups.
Because in 64-bit Windows the floating-point parameters are passed in a different way, they have to be marked by preceding each one of them
with float word. They can be either double word or quad word in size.
Here is an example of calling some OpenGL procedures with either double-precision or single-precision parameters:
invoke glVertex3d,float 0.6,float -0.6,float 0.0 invoke glVertex2f,float dword 0.1,float dword 0.2
The stack space for parameters are allocated before each call and freed immediately after it.
However it is possible to allocate this space just once for all the calls inside some given block of code,
for this purpose there are frame and endf macros provided. They should be used to enclose a block,
inside which the RSP register is not altered between the procedure calls and they prevent each call from allocating
stack space for parameters, as it is reserved just once by the frame macro and then freed at the end by the endf macro.
frame ; allocate stack space just once invoke TranslateMessage,msg invoke DispatchMessage,msg endf
The proc macro for 64-bit Windows has the same syntax and features as 32-bit one (though stdcall and c options are of no use in its case).
It should be noted however that in the calling convention used in 64-bit Windows first four parameters are passed in registers (RCX, RDX, R8 and R9),
and therefore, even though there is a space reserved for them at the stack and it is labelled with name provided in the procedure definition,
those four parameters will not initially reside there. They should be accessed by directly reading the registers. But if those registers are
needed to be used for some other purpose, it is recommended to store the value of such parameter into the memory cell reserved for it.
The beginning of such procedure may look like:
proc WindowProc hwnd,wmsg,wparam,lparam mov [hwnd],rcx mov [wmsg],edx mov [wparam],r8 mov [lparam],r9 ; now registers can be used for other purpose ; and parameters can still be accessed later
1.5 Customizing procedures
It is possible to create a custom code for procedure framework when using proc macroinstruction.
There are three symbolic variables, prologue@proc, epilogue@proc and close@proc,
which define the names of macroinstructions that proc calls upon entry to the procedure,
return from procedure (created with ret macro) and at the end of procedure (made with endp macro).
Those variables can be re-defined to point to some other macroinstructions, so that all the code
generated with proc macro can be customized.
Each of those three macroinstructions takes five parameters.
The first one provides a label of procedure entry point, which is the name of procedure aswell.
The second one is a bitfield containing some flags, notably the bit 4 is set when the caller is supposed to restore the stack, and cleared otherwise.
The third one is a value that specifies the number of bytes that parameters to the procedure take on the stack.
The fourth one is a value that specified the number of bytes that should be reserved for the local variables.
Finally, the fifth an last parameter is the list of comma-separated registers, which procedure declared to be used
and which should therefore be saved by prologue and restored by epilogue.
The prologue macro apart from generating code that would set up the stack frame and the pointer to local variables
has to define two symbolic variables, parmbase@proc and localbase@proc.
The first one should provide the base address for where the parameters reside, and the second
one should provide the address for where the local variables reside — usually relative to
EBP/RBP register, but it is possible to use other bases if it can be ensured that those pointers
will be valid at any point inside the procedure where parameters or local variables are accessed.
It is also up to the prologue macro to make any alignments necessary for valid procedure implementation;
the size of local variables provided as fourth parameter may itself be not aligned at all.
The default behavior of proc is defined by prologuedef and epiloguedef macros
(in default case there is no need for closing macro, so the close@proc has an empty value).
If it is needed to return to the defaults after some customizations were used, it should be done
with the following three lines:
prologue@proc equ prologuedef epilogue@proc equ epiloguedef close@proc equ
As an example of modified prologue, below is the macroinstruction that implements stack-probing prologue for
32-bit Windows. Such method of allocation should be used every time the area of local variables may
get larger than 4096 bytes.
macro sp_prologue procname,flag,parmbytes,localbytes,reglist { local loc loc = (localbytes+3) and (not 3) parmbase@proc equ ebp+8 localbase@proc equ ebp-loc if parmbytes | localbytes push ebp mov ebp,esp if localbytes repeat localbytes shr 12 mov byte [esp-%*4096],0 end repeat sub esp,loc end if end if irps reg, reglist \{ push reg \} } prologue@proc equ sp_prologue
It can be easily modified to use any other stack probing method of the programmer’s preference.
The 64-bit headers provide an additional set of prologue/epilogue macros, which allow to define
procedure that uses RSP to access parameters and local variables (so RBP register is free to use for any other by procedure)
and also allocates the common space for all the procedure calls made inside, so that fastcall
or invoke macros called do not need to allocate any stack space themselves. It is an effect
similar to the one obtained by putting the code inside the procedure into frame block, but in
this case the allocation of stack space for procedure calls is merged with the allocation of space
for local variables. The code inside such procedure must not alter RSP register in any way.
To switch to this behavior of 64-bit proc, use the following instructions:
prologue@proc equ static_rsp_prologue epilogue@proc equ static_rsp_epilogue close@proc equ static_rsp_close
1.6 Exports
The export macroinstruction constructs the export data for the PE file
(it should be either placed in the section marked as export, or within the data export block. The first argument should be quoted string defining the
name of library file, and the rest should be any number of pairs of arguments, first in each pair being the name of procedure defined somewhere inside the source, and the second being the quoted string containing the name under which this procedure should be exported by the library. This sample:
export 'MYLIB.DLL',\ MyStart,'Start',\ MyStop,'Stop'
defines the table exporting two functions, which are defined under the names MyStart and MyStop in the sources, but will be exported by library under the shorter names. The macroinstruction take care of the alphabetical sorting of the table, which is required by PE format.
1.7 Component Object Model
The interface macro allows to declare the interface of the COM object type, the first parameter is the name of interface, and then the consecutive names of the methods should follow, like in this example:
interface ITaskBarList,\ QueryInterface,\ AddRef,\ Release,\ HrInit,\ AddTab,\ DeleteTab,\ ActivateTab,\ SetActiveAlt
The comcall macro may be then used to call the method of the given object.
The first parameter to this macro should be the handle to object, the second one should be name of COM interface implemented by this object,
and then the name of method and parameters to this method. For example:
comcall ebx,ITaskBarList,ActivateTab,[hwnd]
uses the contents of EBX register as a handle to COM object with ITaskBarList interface,
and calls the ActivateTab method of this object with the [hwnd] parameter.
You can also use the name of COM interface in the same way as the name of data structure, to define the variable that will hold the handle to object of given type:
ShellTaskBar ITaskBarList
The above line defines the variable, in which the handle to COM object can be stored. After storing there the handle to an object, its methods can be called with the cominvk. This macro needs only the name of the variable with assigned interface and the name of method as first two parameters, and then parameters for the method. So the ActivateTab method of object whose handle is stored in the ShellTaskBar variable as defined above can be called this way:
cominvk ShellTaskBar,ActivateTab,[hwnd]
which does the same as:
comcall [ShellTaskBar],ITaskBarList,ActivateTab,[hwnd]
1.8 Resources
There are two ways to create resources, one is to include the external resource file created with some other program,
and the other one is to create resource section manually. The latter method, though doesn’t need any additional program to be involved,
is more laborious, but the standard headers provide the assistance — the set of elementary macroinstructions that serve as bricks to compose the resource section.
The directory macroinstruction must be placed directly in the beginning of manually built resource data and it defines what types of resources it contains. It should be followed by the pairs of values, the first one in each pair being the identifier of the type of resource, and the second one the label of subdirectory of the resources of given type. It may look like this:
directory RT_MENU,menus,\ RT_ICON,icons,\ RT_GROUP_ICON,group_icons
The subdirectories can be placed anywhere in the resource area after the main directory, and they have to be defined with the resource macroinstruction, which requires first parameter to be the label of the subdirectory (corresponding to the entry in main directory) followed by the trios of parameters — in
each such entry the first parameter defines the identifier of resource (this value is freely chosen by the programmer and is then used to access the given resource from the program), the second specifies the language and the third one is the label of resource. Standard equates should be used to create language identifiers. For example the subdirectory of menus may be defined this way:
resource menus,\ 1,LANG_ENGLISH+SUBLANG_DEFAULT,main_menu,\ 2,LANG_ENGLISH+SUBLANG_DEFAULT,other_menu
If the resource is of kind for which the language doesn’t matter, the language identifier LANG_NEUTRAL should be used. To define the resources of various types there are specialized macroinstructions, which should be placed inside the resource area.
The bitmaps are the resources with RT_BITMAP type identifier. To define the bitmap resource use the bitmap macroinstruction with the first parameter being the label of resource (corresponding to the entry in the subdirectory of bitmaps) and the second being the quoted string containing the path to the bitmap file, like:
bitmap program_logo,'logo.bmp'
The are two resource types related to icons, the RT_GROUP_ICON is the type for the resource, which has to be linked to one or more resources of RT_ICON type, each one containing single image. This allows to declare images of different sizes and color depths under the common resource identifier. This identifier, given to the resource of RT_GROUP_ICON type may be then passed to the LoadIcon function, and it will choose the image of suitable dimensions from the group. To define the icon, use the icon macroinstruction, with first parameter being the label of RT_GROUP_ICON resource, followed by the pairs of parameters declaring the images. First parameter in each pair should be the label of RT_ICON resource, and the second one the quoted string containing the path to the icon file. In the simplest variant, when group of icon contains just one image, it will look like:
icon main_icon,icon_data,'main.ico'
where the main_icon is the label for entry in resource subdirectory for RT_GROUP_ICON type, and the icon_data is the label for entry of RT_ICON type.
The cursors are defined in a way similar to icons, with the RT_GROUP_CURSOR and RT_CURSOR types and the cursor macro, which takes parameters analogous to those taken by icon macro. So the definition of cursor may look like this:
cursor my_cursor,cursor_data,'my.cur'
The menus have the RT_MENU type of resource and are defined with the menu macroinstruction followed by few others defining the items inside the menu. The menu itself takes only one parameter — the label of resource. The menuitem defines the item in the menu, it takes up to five parameters, but only two are required — the first one is the quoted string containing the text for the item, and the second one is the identifier value (which is the value that will be returned when user selects the given item from the menu).
The menuseparator defines a separator in the menu and doesn’t require any parameters.
The optional third parameter of menuitem specifies the menu resource flags. There are two such flags available — MFR_END is the flag for the last item in the given menu, and the MFR_POPUP marks that the given item is the submenu, and the following items will be items composing that submenu until the item with MFR_END flag is found. The MFR_END flag can be also given as the parameter to the menuseparator and is the only parameter this macroinstruction can take. For the menu definition to be complete, every submenu must be closed by the item with MFR_END flag, and the whole menu must also be closed this way. Here is an example of complete definition of the menu:
menu main_menu menuitem '&File',100,MFR_POPUP menuitem '&New',101 menuseparator menuitem 'E&xit',109,MFR_END menuitem '&Help',900,MFR_POPUP + MFR_END menuitem '&About...',901,MFR_END
The optional fourth parameter of menuitem specifies the state flags for the given item, these flags are the same as the ones used by API functions, like MFS_CHECKED or MFS_DISABLED. Similarly, the fifth parameter can specify the type flags. For example this will define item checked with a radio-button mark:
menuitem 'Selection',102, ,MFS_CHECKED,MFT_RADIOCHECK
The dialog boxes have the RT_DIALOG type of resource and are defined with the dialog macroinstruction followed by any number of items defined with dialogitem ended with the enddialog.
The dialog can take up to eleven parameters, first seven being required. First parameter, as usual, specifies the label of resource, second is the quoted string containing the title of the dialog box, the next four parameters specify the horizontal and vertical coordinates, the width and the height of the dialog box window respectively. The seventh parameter specifies the style flags for the dialog box window, the optional eighth one specifies the extended style flags. The ninth parameter can specify the menu for window — it should be the identifier of menu resource, the same as one specified in the subdirectory of resources with RT_MENU type. Finally the tenth and eleventh parameter can be used to define the font for the dialog box — first of them should be the quoted string containing the name of font, and the latter one the number defining the size of font. When these optional parameters are not specified, the default MS Sans Serif of size 8 is used.
This example shows the dialog macroinstruction with all the parameters except for the menu (which is left with blank value), the optional ones are in the second line:
dialog about,'About',50,50,200,100,WS_CAPTION+WS_SYSMENU,\ WS_EX_TOPMOST, ,'Times New Roman',10
The dialogitem has eight required parameters and one optional. First parameter should be the quoted string containing the class name for the item. Second parameter can be either the quoted string containing text for the item, or resource identifier in case when the contents of item has to be defined by some additional resource (like the item of STATIC class with the SS_BITMAP style). The third parameter is the identifier for the item, used to identify the item by the API functions. Next four parameters specify the horizontal, vertical coordinates, the width and height of the item respectively. The eighth parameter specifies the style for the item, and the optional ninth specifies the extended style flags. An example dialog item definition:
dialogitem 'BUTTON','OK',IDOK,10,10,45,15,WS_VISIBLE+WS_TABSTOP
And an example of static item containing bitmap, assuming that there exists a bitmap resource of identifier 7:
dialogitem 'STATIC',7,0,10,50,50,20,WS_VISIBLE+SS_BITMAP
The definition of dialog resource can contain any amount of items or none at all, and it should be always ended with enddialog macroinstruction.
The resources of type RT_ACCELERATOR are created with accelerator macroinstruction. After first parameter traditionally being the label of resource, there should follow the trios of parameters — the accelerator flags followed by the virtual key code or ASCII character and the identifier value (which is like the identifier of the menu item). A simple accelerator definition may look like this:
accelerator main_keys,\ FVIRTKEY+FNOINVERT,VK_F1,901,\ FVIRTKEY+FNOINVERT,VK_F10,109
The version information is the resource of type RT_VERSION and is created with the versioninfo macroinstruction. After the label of the resource, the second parameter specifies the operating system of PE file (usually VOS__WINDOWS32), third parameter the type of file (the most common are VFT_APP for program and VFT_DLL for library), fourth the subtype (usually VFT2_UNKNOWN), fifth the language identifier, sixth the code page and then the quoted string parameters, being the pairs of property name and corresponding value. The simplest version information can be defined like:
versioninfo vinfo,VOS__WINDOWS32,VFT_APP,VFT2_UNKNOWN,\ LANG_ENGLISH+SUBLANG_DEFAULT,0,\ 'FileDescription','Description of program',\ 'LegalCopyright','Copyright et cetera',\ 'FileVersion','1.0',\ 'ProductVersion','1.0'
Other kinds of resources may be defined with resdata macroinstruction, which takes only one parameter — the label of resource, and can be followed by any instructions defining the data, ended with endres macroinstruction, like:
resdata manifest file 'manifest.xml' endres
1.9 Text encoding
The resource macroinstructions use the du directive to define any Unicode strings inside resources — since this directive simply zero extends the characters to the 16-bit values, for the strings containing some non-ASCII characters, the du may need to be redefined. For some of the encodings the macroinstructions redefining the du to generate the Unicode texts properly are provided in the ENCODING subdirectory. For example if the source text is encoded with Windows 1250 code page, such line should be put somewhere in the beginning of the source:
include 'encoding\win1250.inc'
2. Extended headers
The files win32ax.inc, win32wx.inc, win64ax.inc and win64wx.inc provide all the functionality of base headers and include a few more features involving more complex macroinstructions. Also if no PE format is declared before including the extended headers, the headers declare it automatically. The files win32axp.inc, win32wxp.inc, win64axp.inc and win64wxp.inc are the variants of extended headers which additionally perform checking the count of parameters to procedure calls.
2.1 Procedure parameters
With the extended headers the macroinstructions for calling procedures allow more types of parameters than just the double word values as with basic headers. First of all, when the quoted string is passes as a parameter to procedure, it is used to define string data placed among the code, and passes to procedure the double word pointer to this string. This allows to easily define the strings that don’t have to be re-used, just in the line calling the procedure that requires pointers to those strings, like:
invoke MessageBox,HWND_DESKTOP,"Message","Caption",MB_OK
If the parameter is the group containing some values separated with commas, it is treated in the same way as simple quoted string parameter.
If the parameter is preceded by the addr word, it means that this value is an address and this address should be passed to procedure, even if it cannot be done directly — like in the case of local variables, which have addresses relative to EBP/RBP register.
In 32-bit case the EDX register is used temporarily to calculate the value of address and pass it to the procedure. For example:
invoke RegisterClass,addr wc
in case when the wc is the local variable with address EBP-100h, will generate this sequence of instructions:
lea edx,[ebp-100h] push edx call [RegisterClass]
However when the given address is not relative to any register, it is stored directly.
In 64-bit case the addr prefix is allowed even when only standard headers are used, as it can be useful even in case of the regular addresses, because it enforces RIP-relative address calculation.
With 32-bit headers, if the parameter is preceded by the word double, it is treated as 64-bit value and passed to the procedure as two 32-bit parameters. For example:
invoke glColor3d,double 1.0,double 0.1,double 0.1
will pass the three 64-bit parameters as six double words to procedure. If the parameter following double is the memory operand, it should not have size operator, the double already works as the size override.
Finally, the calls to procedures can be nested, that is call to one procedure may be used as the parameter to another. In such case the value returned in EAX/RAX by the nested procedure is passed as the parameter to the procedure which it is nested in. A sample of such nesting:
invoke MessageBox,<invoke GetTopWindow,[hwnd]>,\ "Message","Caption",MB_OK
There are no limits for the depth of nesting the procedure calls.
2.2 Structuring the source
The extended headers enable some macroinstructions that help with easy structuring the program. The .data and .code are just the shortcuts to the declarations of sections for data and for the code. The .end macroinstruction should be put at the end of program, with one parameter specifying the entry point of program, and it also automatically generates the import section using all the standard import tables.
In 64-bit Windows the .end automatically aligns the stack on 16 bytes boundary.
The .if macroinstruction generates a piece of code that checks for some simple condition at the execution time, and depending on the result continues execution of following block or skips it. The block should be ended with .endif, but earlier also .elseif macroinstruction might be used once or more and begin the code that will be executed under some additional condition, when the previous conditions were not met, and the .else as the last before .endif to begin the block that will be executed when all the conditions were false.
The condition can be specified by using comparison operator — one of the =, <, >, <=, >=, and <>
— between the two values, first of which must be either register or memory operand.
The values are compared as unsigned ones, unless the comparison expression is preceded by the word signed.
If you provide only single value as a condition, it will be tested to be zero,
and the condition will be true only if it’s not. For example:
.if eax ret .endif
generates the instructions, which skip over the ret when the EAX is zero.
There are also some special symbols recognized as conditions: the ZERO? is true when the ZF flag is set,
in the same way the CARRY?, SIGN?, OVERFLOW? and PARITY? correspond to the state of CF, SF, OF and PF flags.
The simple conditions like above can be composed into complex conditional expressions
using the &, | operators for conjunction and alternative, the~ operator for negation, and parenthesis. For example:
.if eax<=100 & ( ecx | edx ) inc ebx .endif
will generate the compare and jump instructions that will cause the given block to get executed only when EAX is below or
equal 100 and at the same time at least one of the ECX and EDX is not zero.
The .while macroinstruction generates the instructions that will repeat executing the given block (ended with .endw macroinstruction) as long as the condition is true. The condition should follow the .while and can be specified in the same way as for the .if.
The pair of .repeat and .until macroinstructions define the block that will be repeatedly executed until the given condition will be met — this time the condition should follow the .until macroinstruction, placed at the end of block, like:
.repeat add ecx,2 .until ecx>100
Надумал я тут написать небольшую утилиту.. и понял, что писать-то я и не умею. Смеялись всем селом!! Если рассматривать вопрос по существу, то сегодня мы раскроем некоторые особенности синтаксиса языка ассемблер в свете использования компилятора FASM, и приведем типовой шаблон оконного приложения на ассемблере, а так же выполним разбор структуры для дальнейшего использования в качестве базиса в различного рода проектах. Быть может, когда-то статья и станет звеном в цикле по изучению программирования на языке Ассемблер под Windows, но на данный момент она представляет собой обособленный материал.
Не смотря на то, что данная публикация представляет из себя пособие для начинающих, изучение приведенной здесь теории требует хотя бы базового уровня знаний языка Ассемблер.
Я попытался до определенной степени детализировать небольшой накопленный опыт, дабы читатель любого уровня подготовки смог увидеть весь диапазон направлений, требуемых для более глубокого изучения особенностей языка Ассемблера при разработке приложений под операционную систему Windows, если появится желание дальнейшего продвижения. Будут рассмотрены основные (базовые) директивы ассемблера FASM, которые позволяют существенно влиять на структуру исполняемого файла программы. Некоторые из приведенных разделов вполне могли бы дорасти до размера самостоятельной статьи, однако пока подобная структура не создана, информация будет приводиться здесь. Темой данной статьи станет создание простейшего графического оконного приложения на ассемблере для Windows, потому как данная категория приложений является наиболее распространенной, соответственно и востребованной.
Оконное приложение Windows — это класс приложений, использующих для взаимодействия с пользователем элементы графического пользовательского интерфейса, то есть объекты типа: окна, кнопки, поля ввода, элементы контроля и многие другие. При помощи устройств ввода (клавиатуру/мышь/тачпад и прочие), пользователь имеет возможность взаимодействовать с объектами оконного приложения: перемещать, активировать, прокручивать. Примером данного класса являются классические графические приложения, работающие с окнами.
За основу для изучения я взял стандартный шаблон 32-битного оконного приложения на ассемблере с именем template.asm, поставляемый в составе пакета FASM и размещающийся в поддиректории \EXAMPLES\TEMPLATE\, и слегка модифицировал его для некоторой наглядности. Для начала представим исходный код программы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
format PE GUI 4.0 ; Формат PE. Версия GUI 4.0. entry start ; Точка входа include ‘%fasminc%\win32a.inc’ ; Делаем стандартное включение описателей. _style equ WS_VISIBLE+WS_DLGFRAME+WS_SYSMENU ; Стили окна. ЭКВИВАЛЕНТЫ должны задаваться ДО основного кода ;=== сегмент кода ============================================================ section ‘.text’ code readable executable start: invoke GetModuleHandle,0 ; Получим дескриптор приложения. mov [wc.hInstance],eax ; Сохраним дескриптор приложения в поле структуры окна (wc) invoke LoadIcon,0,IDI_ASTERISK ; Загружаем стандартную иконку IDI_ASTERISK mov [wc.hIcon],eax ; Сохраним дескриптор иконки в поле структуры окна (wc) invoke LoadCursor,0,IDC_ARROW ; Загружаем стандартный курсор IDC_ARROW mov [wc.hCursor],eax ; Сохраним дескриптор курсора в поле структуры окна (wc) mov [wc.lpfnWndProc],WindowProc ; Зададим указатель на нашу процедуру обработки окна mov [wc.lpszClassName],_class ; Зададим имя класса окна mov [wc.hbrBackground],COLOR_WINDOW+1 ; Зададим цвет кисти invoke RegisterClass,wc ; Зарегистрируем наш класс окна test eax,eax ; Проверим на ошибку (eax=0). jz error ; Если 0, то ошибка — прыгаем на error. invoke CreateWindowEx,0,_class,_title,_style,128,128,256,192,NULL,NULL,[wc.hInstance],NULL ; Создадим экземпляр окна на основе зарегистрированного класса. в eax возвращает дескриптор окна. test eax,eax ; Проверим на ошибку (eax=0). jz error ; Если 0, то ошибка — прыгаем на error. mov [wHMain],eax ; сохраним дескриптор созданного окна ;— цикл обработки сообщений ———————————————— msg_loop: invoke GetMessage,msg,NULL,0,0 ; Получаем сообщение из очереди сообщений приложения or eax,eax ; Сравнивает eax с 0 jz end_loop ; Если 0 то пришло сообщение WM_QUIT — выходим из цикла ожидания сообщений, если не 0 — продолжаем обрабатывать очередь msg_loop_2: invoke TranslateMessage,msg ; Дополнительная функция обработки сообщения. Конвертирует сообщения клавиатуры отправляет их обратно в очередь. invoke DispatchMessage,msg ; Пересылает сообщения соответствующим процедурам обработки сообщений (WindowProc …). jmp short msg_loop ; Зацикливаемся error: invoke MessageBox,NULL,_error,NULL,MB_ICONERROR+MB_OK ; Выводим окно с ошибкой end_loop: invoke ExitProcess,[msg.wParam] ; Выход из программы. ;— процедура обработки сообщений окна (функция окна, оконная процедура, оконная функция) proc WindowProc hWnd,wMsg,wParam,lParam push ebx esi edi ; сохраним все регистры cmp [wMsg],WM_DESTROY ; Проверим на WM_DESTROY je .wmdestroy ; на обработчик wmdestroy cmp [wMsg],WM_CREATE ; Проверим на WM_CREATE je .wmcreate ; на обработчик wmcreate .defwndproc: invoke DefWindowProc,[hWnd],[wMsg],[wParam],[lParam] ; Функция по умолчанию. Обрабатывает все сообщения, которые обрабатывает наш цикл. jmp .finish .wmcreate: xor eax,eax jmp .finish .wmdestroy: ; Обработчик сообщения WM_DESTROY. Обязателен. invoke PostQuitMessage,0 ; Посылает сообщение WM_QUIT в очередь сообщений, что вынуждает GetMessage вернуть 0. Посылается для выхода из программы. Посылается только основным окном. xor eax,eax ; Если наша процедура окна обрабатывает какое-либо сообщение, то она должна вернуть в eax 0. Иначе программа поведет себя непредсказуемо. .finish: pop edi esi ebx ; восстановим все регистры ret endp ;=== сегмент данных ========================================================== section ‘.data’ data readable writeable _class db ‘FASMWIN32’,0 ; Название собственного класса. _title db ‘Win32 program template’,0 ; Текст в заголовке окна. _error db ‘Startup failed.’,0 ; Текст ошибки wHMain dd ? ; дескриптор окна wc WNDCLASS ; Структура окна. Для функции RegisterClass msg MSG ; Структура системного сообщения, которое система посылает нашей программе. ;=== таблица импорта ========================================================= section ‘.idata’ import data readable writeable library kernel32,‘KERNEL32.DLL’,user32,‘USER32.DLL’ import kernel32,\ GetModuleHandle,‘GetModuleHandleA’,\ ExitProcess,‘ExitProcess’ import user32,\ LoadIcon,‘LoadIconA’,\ LoadCursor,‘LoadCursorA’,\ RegisterClass,‘RegisterClassA’,\ CreateWindowEx,‘CreateWindowExA’,\ GetMessage,‘GetMessageA’,\ TranslateMessage,‘TranslateMessage’,\ DispatchMessage,‘DispatchMessageA’,\ MessageBox,‘MessageBoxA’,\ DefWindowProc,‘DefWindowProcA’,\ PostQuitMessage,‘PostQuitMessage’ |
Как Вы видите, я включил в него собственные небольшие комментарии, которые, впрочем, будут разворачиваться по ходу изложения. Перед тем, как приступить к описанию структуры нашей программы, давайте несколько слов скажем об архитектуре системы Windows.
Идеология программирования под Windows
В операционной системе MS-DOS исполняемая программа (читать: код) имела монопольный (бесконтрольный) доступ к большинству аппаратных ресурсов системы, поэтому говорить о полноценной многозадачности не приходилось. Другой особенностью программ, работающих под MS-DOS, была необходимость самостоятельно инициировать взаимодействие с операционной системой: делать вызовы функций [программных прерываний] с целью организации обмена данными с пользователем/операционной системой/устройствами (например: ожидание ввода с клавиатуры). Но вот в Windows все изменилось, мало того, что в системе могут уже одновременно функционировать несколько процессов (читать: программ), но и для взаимодействия с пользователем (обмена данными), коду пользовательского приложения уже вовсе не обязательно делать вызовов каких бы то ни было специализированных функций, ожидающих ввода (нажатия клавиш клавиатуры/мыши, ввод символов в поле ввода). Объясняется всё это сменой парадигмы программирования на событийно-ориентированное программирование.
Событиийно-ориентиированное программиирование (англ.: event-driven programming) — подход к программированию, при котором ход выполнения программы определяется [внешними/внутренними] событиями: действиями пользователя (клавиатура, мышь, сенсорный экран и прч.), сообщениями других программ и потоков, событиями операционной системы (например, поступлением сетевого пакета).
Соответственно:
Приложение в Windows «пассивно», поскольку в ходе функционирования оно ждет когда операционная система уделит ей внимание.
Подход к программированию в среде Windows существенно изменился и теперь основная концепция программирования ориентирована на так называемые события. Это означает, что ядро системы постоянно следит за аппаратной/программной активностью, и в ответ [на эту активность] генерирует специальные сообщения, которые затем, в зависимости от необходимости, передает (через специальные системные механизмы) приложениям, ожидающим наступления этих событий. Таким образом, можно утверждать что приложения в Windows управляются событиями. С событиями ассоциируется любая пользовательская/системная активность: перемещение окон, нажатие клавиш клавиатуры/мыши, изменения состояния буфера обмена, изменения в аппаратурной конфигурации, изменение статуса энергопотребления, изменение значений таймеров и другая. Поэтому, когда происходит какое-либо событие (к коим относятся и любые действия пользователя), система сама «предоставляет» для пользовательского приложения входные данные, и делает она это посредством передачи сообщений. А прикладная программа, в свою очередь, должна содержать обработчики этих самых поступающих в неё сообщений, которые требуемым образом реагируют на данные сообщения.
Таким образом приложение в Windows должно обеспечивать ту или иную обработку при поступлении разного рода сообщений. Какие именно события обрабатывать и как именно — решает разработчик приложения.
При завершении программой обработки события, управление возвращается [обратно] ядру системы. Вы уже поняли, что все это коренным образом меняет подход к написанию программ, поскольку в MSDOS программа контролировала собственные действия/события, теперь же сама операционная система Windows проводит большинство работы, а пользовательской программе передает лишь управляющие сообщения, которые могут активизировать те или иные процедуры обработки (которые посчитал нужным добавить в код автор) в программе.
Программирование под Windows — это, в основе своей, программирование обработчиков сообщений, то есть реакции приложения на необходимый набор внешних событий.
Но помимо обработки ввода или реакции на иные входящие системные сообщения, прикладной программе требуется выполнять и некоторые другие действия над объектами операционной системы. С целью обеспечения доступа пользовательских программ ко всему спектру исполняемых компонентов Windows, предоставляется так называемый программный интерфейс приложений (API). А это означает, что весь функционал операционной системы доступен через функции, и чтобы программисту что-либо сделать — надо вызвать функцию соответствующего назначения.
API (Интерфейс прикладного программирования, Application Programming Interface) — многообразие системных функций, посредством которых приложение (процесс) может взаимодействовать с операционной системой Windows.
Заголовок
Рассмотрение исходного кода начнем мы с заголовка, или, если выразиться более точно — «так называемого заголовка». Я возьму на себя смелость подобным образом именовать область исходного кода, начинающуюся непосредственно с первого символа и идущую до директивы объявления первой секции. Начало области не обозначается специальными директивами, это просто начальная часть листинга программы. В этом месте (у нас строка 1
) может использоваться директива format
, которая предназначается для указания формата результирующего исполняемого файла, получаемого на выходе после компиляции. Форматы можно указать следующие:
Имя | Расшифровка | Описание |
---|---|---|
MZ |
Mark Zbikowski | формат 16-битных исполняемых файлов с расширением .exe для ОС MSDOS |
PE PE64 |
Portable Executable | формат 32/64-битных исполняемых файлов с расширением .exe для ОС Windows |
COFF MS COFF MS64 COFF |
Common Object File Format | формат объектного файла, содержащий промежуточное представление кода программы, предназначенный для объединения с другими объектными файлами (проектами/ресурсами) с целью получения готового исполнимого модуля. |
ELF ELF64 |
Executable and Linkable Format | формат исполняемых файлов систем семейства UNIX. Объектный файл (.obj) для компилятора gcc. |
ARM |
Advanced RISC Machine | формат исполняемых файлов под архитектуру ARM (?) |
Binary |
файлы бинарной структуры. Что зададите, то и соберется. Например, выставив смещение 100h (org 100h) от начала, можно получить старый-добрый .com-файл под MSDOS. Формат имеет ряд аналогичных применений для создания произвольных бинарных приложений или файлов данных. |
Вторым параметром (после указания формата исполняемого файла) директивы format
может указываться тип подсистемы для создаваемого приложения:
Имя | Описание |
---|---|
GUI |
Графическое (оконное) приложение. Выходной исполняемый файл, который подразумевает создание типовых оконных приложений и инициализацию на начальной стадии всех соответствующих библиотек Win32 API. Выходной исполняемый файл, у которого в структуре PE-заголовка IMAGE_NT_HEADERS , подструктуре OptionalHeader, значение поля Subsystem = 2 (оно же IMAGE_SUBSYSTEM_WINDOWS_GUI). |
console |
Консольное приложение. Выходной исполняемый файл, подразумевающий выполнения кода в консоли, без участия оконного интерфейса. Выходной исполняемый файл, у которого в структуре PE-заголовка IMAGE_NT_HEADERS , подструктуре OptionalHeader, значение поля Subsystem = 3 (оно же IMAGE_SUBSYSTEM_WINDOWS_CUI). |
native |
Родное/нативное приложение. Выходной исполняемый файл, у которого в структуре PE-заголовка IMAGE_NT_HEADERS , подструктуре OptionalHeader, значение поля Subsystem = 1 (оно же IMAGE_SUBSYSTEM_NATIVE). Подобное значение поля обычно характерно для драйверов, библиотек и приложений режима ядра, которым не требуется инициализация подсистемы Win32 на стадии подготовки образа к выполнению. |
DLL |
Динамическая библиотека. Особый формат выходного исполняемого файла, предназначающийся для экспорта (предоставления) функций сторонним приложениям, у которого в структуре PE-заголовка IMAGE_NT_HEADERS , подструктуре IMAGE_FILE_HEADER, в поле Characteristics включен флаг IMAGE_FILE_DLL (2000h ). |
WDM |
Системный драйвер, построенный на основе модели WDM (Windows Driver Model). |
EFI EFIboot EFIruntime |
UEFI-приложение. Выходной исполняемый файл, у которого в структуре PE-заголовка IMAGE_NT_HEADERS , подструктуре OptionalHeader, значение поля Subsystem = 10 | 11 | 12 | 13 (оно же IMAGE_SUBSYSTEM_EFI_APPLICATION, IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER, IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER, IMAGE_SUBSYSTEM_EFI_ROM). Подобное значение поля требуется для создания UEFI-приложений различных стадии/типа: загрузки, выполнения и драйвера. |
Роль данного параметра достаточно велика, поскольку именно он определяет, какая именно подсистема будет вызываться для запуска исполняемого файла, то есть фактически определяет программное окружение при запуске процесса. Если используется тип приложения GUI, необходимо уточнять минимальную версию системы (у нас: 4.0), под которую создается наш исполняемый модуль.
Затем, в строке под номером 2
в нашем исходном коде располагается директива с именем entry
, которая определяет точку входа в программу.
Точка входа — адрес первой инструкции (в адресном пространстве процесса), с которой начинается выполнение кода приложения.
В качестве аргумента директивы entry
указывается метка в коде, с которой у нас начнется выполнение кода скомпилированной программы. Становится очевидным, что именно на основе данной директивы компилятор формирует значения соответствующих полей результирующего исполняемого PE-файла. При запуске .exe-файла, загрузчик образов (динамический компоновщик) создаст адресное пространство процесса нашего приложения, подгрузит и разберет исполняемый образ, сопоставив все необходимые сегменты с регионами памяти, сформировав иные необходимые структуры, передаст управление именно по адресу, где будет располагаться инструкция, описанная в исходном коде меткой, указанной в директиве entry
. В нашем случае точку входа определяет метка start, располагающаяся в сегменте кода в строке 11
.
В строке 3
мы обнаруживаем директиву компилятора include
, при помощи которой в исходный код нашей программы (в позицию нахождения директивы) включается текст внешнего модуля (файла), указанного в ней в качестве параметра.
Включение позволяет подключать необходимые программе структуры данных из внешних файлов. Основной файл у нас содержит код программы, а специфичные внешние данные, такие как системные константы, переменные и определения макросов, размещаются в отдельных заголовочных файлах. Заголовочные файлы FASM несколько отличаются от привычных нам по языку C/C++ тем, что не описывают прототипов процедур/функций.
В нашем случае подключается файл %fasminc%\win32a.inc, который, в свою очередь, содержит ссылки на другие подключаемые файлы, содержащие определения ключевых структур, требуемых для компиляции нашей программы: макросов, типов данных, констант, системных структур. Без включения этого файла у нас попросту не пройдет процесс компиляции нашего исходного кода, то есть исполняемый файл не будет создан (не соберется).
Как Вы уже наверное заметили, в параметре директивы include
используется переменная пути %fasminc%. В общем случае она создается для удобства указания пути к поддиректории \INCLUDE основной директории FASM в исходниках. Вам тоже необходимо задать полный путь к каталогу дистрибутива в настройках операционной системы: окно Свойства системы -> вкладка Дополнительно -> раздел Переменные среды -> добавить новый параметр с именем FASMINC, имеющий значение пути (например: C:\YandexDisk\_Project\ASM\FASM\INCLUDE) в области Переменные среды пользователя или Системные переменные.
Непосредственно за подключением внешнего файла, в строке 5
у нас располагается объявление внутренней константы _style
, которая используется в нашем коде и принимает значение WS_VISIBLE+WS_DLGFRAME+WS_SYSMENU, определяющее внешний вид окна. Ключевые слова WS_VISIBLE, WS_DLGFRAME, WS_SYSMENU являются не чем иным, как символическими именами глобальных констант, или битовых флагов (содержащихся во внешних файлах включений, подключаемых на этапе компиляции), определенных в системе Windows и иначе именуемых стилями окна.
Заметьте, что константы объединяются операцией + (логическое/побитовое ИЛИ, or), с целью получить сумму значений нескольких свойств, то есть применить их совокупность.
С возможными вариантами стилей можно ознакомиться на соответствующей странице, описывающей стили окна.
Секции
Непосредственно за определяющими заголовок директивами, следует исходный код, разделенный на области, называемые секциями. Присмотритесь к приведенному исходному коду и вы увидите что весь листинг фактически разделен на своеобразные логические блоки, начинающиеся с директивы section
и именуемые секциями. Наряду с заголовком, секции являются неотъемлемыми составными частями как файла исходного кода, так и получающегося на выходе у компилятора исполняемого PE-файла.
Секция — область (блок) в структуре исполняемого PE-файла, предназначающаяся для разделения всего массива данных приложения на логические части, подразумевающие хранение кода/данных, объединенных единым назначением/методом доступа. В исполняемом файле каждая секция характеризуется собственным именем, смещением в исполняемом файле, виртуальным адресом для копирования содержимого, размером и атрибутами. Все эти параметры определяют способ загрузки секции, способ формирования страниц виртуальной памяти и управления другими параметрами данных на стадии загрузки образа.
Использование секций регламентировано структурой формата исполняемых PE-файлов, используемых в системе Windows. Именно спецификация формата PE определяет требования к наличию определенных структур в исполняемых файлах и предписывает использование тех или иных секции для разделения информационных блоков. Сразу после директивы section
в одинарных кавычках (апостроф) задается имя (название) секции и ряд параметров: тип секции, флаги (атрибуты) секции.
Наименование секции может быть произвольным (но не более 8 символов) либо отсутствовать вовсе, это никак не сказывается на процессе компиляции и исполнения программы, поскольку ключевым для компилятора является тип секции. Исключением является, разве что, секция ресурсов с именем .rsrc.
Флаги могут принимать следующие значения: code
, data
, readable, writeable, executable, shareable, discardable, notpageable, в дополнение к ним могут использоваться спецификаторы секции данных, такие как export, import, resource, fixups, которые определяют структуру (строение) секции. Типы секций, флаги и их комбинации я свел в таблицу:
Наименование | Обозначение FASM | Описание |
---|---|---|
Секция кода | code |
Секция, в которой предписывается размещать исполняемый код приложения. Обычно в данную секцию включается весь ассемблерный код, фактически реализующий логику работы приложения. |
Секция данных | data |
В данной секции предписывается размещать все динамические (изменяемые) данные (локальные/глобальные переменные, строки, структуры и т.п.), которые активно используются в коде приложения. |
Секция импорта | import |
Расхожее название: Таблица импорта. В данной секции размещаются строковые литералы (наименования) библиотек и таблицы подключаемых (импортируемых) из этих библиотек виртуальных функций, которые требуются нашей программе для работы. Функции могут импортировать по наименованию (символическое имя) или по ординалу (числовой идентификатор). |
Секция ресурсов | resource |
Данная секция содержит данные, которые преобразуются в исполняемом файле в многоуровневое двоичное дерево (индексированный массив), построенное определенным образом для ускорения доступа к данным. Эти данные называются ресурсами, доступны из кода через специальные идентификаторы, статичны, описывают различные используемые в программе объекты: меню, диалоги, иконки, курсоры, картинки, звуки и прочее. |
Таблица перемещений (таблица настроек адресов, релокации) | fixups |
Релокации — набор таблиц (fixup blocks) со смещениями (Relevant Virtual Addresses, RVA) от базового адреса загрузки образа (фактически указателями на абсолютные адреса в коде), которые загрузчик образа должен скорректировать (исправить) в памяти процесса, если образ загружается по адресу, отличному от предпочитаемого. Иначе (проще) можно представить как список ячеек памяти, которые нуждаются в корректировке при загрузке образа в памяти процесса по произвольному адресу. Таблица перемещений применяется только для фиксированных адресов в коде приложения, то есть адресов тех инструкций, которые компилятор задал в явном виде (например: mov al, [01698745]). |
Таблица экспорта | export |
Секция описывает экспортируемые нашей программой функции. Обычно используется при создании библиотек DLL. |
Обычно тип секции предписывает размещать внутри неё код/данные требуемого назначения.
Однако на практике же это вовсе не строгое правило, поскольку как минимум можно привести одно исключение — размещение данных в секции кода. Однако, подобное смешивание кода и данных может привести к проблемам с безопасностью (нарушение прав доступа если секция кода не маркирована для записи), равно как и проблемам кеширования на уровне процессора, что может сказаться на снижении быстродействия приложения. Других исключений из правила и примеров я назвать не могу, поскольку просто не тестировал. Относительно количества однотипных секций в исходном коде можно сказать что все они собираются в единую секцию на этапе компиляции исходного кода.
Секция кода (code)
Пожалуй, без преувеличения, данную секцию можно смело назвать наиболее значимой, поскольку именно она определяет всю логику работы создаваемого нами приложения. Именно в секции code
содержится описание того, как именно работает и что делает наше оконное приложение на ассемблере, другими словами именно секцией кода определяется алгоритм работы приложения. Как Вы уже поняли, изучаемый нами тестовый пример достаточно прост и всю его логику можно описать следующим небольшим списком:
- Получаем дескриптор экземпляра текущего процесса (в контексте которого и выполняется наш код);
- Регистрируем класс окна. Регистрация собственного класса требуется во всех случаях за исключением тех, когда Вы используете стандартные (предопределенные, предоставляемые системой) типы окон;
- Создаем главное (и единственное) окно на основе только что зарегистрированного класса и сообщаем Windows адрес функции обработки событий для этого окна;
- В нашем примере не используется: отображение основного окна (функция ShowWindow) и обновление клиентской области окна (функция UpdateWindow) в промежутке между созданием окна и началом очереди обработки сообщений программы. В примерах кода от MS часто можно встретить связку данных функций, вероятно они используются в случаях, когда: 1) требуется дополнительная перерисовка (в случае манипуляций с видимой частью окна) клиентской области в промежутке между созданием окна и началом обработки очереди сообщений программы, 2) когда окно создается с классическими стилями, не отображающими окно. В нашем же случае мы используем дополнительные стили, которые сразу делают окно видимым.
- Входим в бесконечный цикл обработки сообщений для всех окон, принадлежащих нашему процессу. В данном примере обрабатываются сообщения к единственному [основному] окну;
- (специальной функцией) обрабатываем сообщения, поступающие для любого из контролируемых нами окон;
- Выходим из программы по нажатию пользователем кнопки Закрыть [X] или комбинации клавиш Alt+F4;
Визуальным результатом работы нашей программы является вывод на рабочий стол обычного окна с единственной системной кнопкой (закрыть) в правом верхнем углу.
Соответственно, вся логика нашей программы укладывается в создание окна и обработку нажатия в нем одной-единственной кнопки: выход. Так же, в окне можно увидеть выбранную нами типовую иконку (левый верхний угол) и окно имеет заданные нами размеры.
Ну а теперь самое время разобраться с алгоритмом работы. Перво-наперво мы получаем дескриптор (handle) нашего модуля при помощи вызова функции GetModuleHandle. Немного оторвемся от изучения логики и обратим внимание на строку 12
вызова данной функции, тут мы впервые встречаемся с ключевым словом invoke. Во с этого самого момента для новичков начинается знакомство с реалиями современного программирования под Windows на языке ассемблер. Для людей, которые разбираются с языком даже на начальном уровне, очевидно, что такой команды в ассемблере нет, но это и не команда, это макрос. Макрос invoke содержится в файлах определения макросов \INCLUDE\MACRO\PROC32.INC и \INCLUDE\MACRO\PROC64.INC пакета FASM и вот его объявление:
macro invoke proc,[arg] ; indirectly call STDCALL procedure { common if ~ arg eq reverse pushd arg common end if call [proc] } |
Из алгоритма макроса видно, что при наличии аргументов функции, выполняется помещение их в стек в обратном порядке, следом уже вызывается сама функция. И зачем нам это всё нужно? Для облегчения процесса разработки приложения! В принципе, Вы можете и не использовать макрос invoke, но тогда от Вас потребуется масса дополнительной работы: Вам придется самостоятельно заносить входные аргументы в стек в порядке, определенном для той или иной функции. Дело в том, что порядок занесения определяется так называемым соглашением о вызовах, которое повсеместно применяется в программах Windows и которое предписывает заносить параметры в стек строго в требуемом порядке.
Вернемся обратно к основному алгоритму. В официальной документации сказано, что функция GetModuleHandle возвращает дескриптор приложения. Если в качестве входного параметра функции GetModuleHandle используется значение 0, то функция возвращает дескриптор для исполняемого образа, участвующего в создании вызывающего её процесса, то есть (проще) для того процесса, из которого она вызвана. Возвращаемый функцией дескриптор не глобальный и не наследуемый, он актуален в контексте только текущего процесса.
Дескриптор (описатель, handle) — косвенный (абстрактный, виртуальный, локальный) указатель на системный ресурс (окно, элемент управления, курсор, иконка, адрес памяти, открытый файл, канал и тому подобное). В большинстве случаев это всего-лишь абстракция, позволяющая предотвратить непосредственный доступ приложений к системным структурам данных. Она скрывает от разработчика некий физический ресурс, позволяя ядру реорганизовать аппаратные ресурсы удобным ему образом, абсолютно прозрачно для приложений. Фактически это своеобразный индекс (используемый на входе/выходе функций), преобразуемый внутри ядра на основе специальных системных таблиц соответствия во внутреннее представление — указатель непосредственно на физический объект. Поэтому дескриптор должен рассматриваться исключительно в качестве локального значения, имеющего смысл в контексте API в пределах текущего приложения. Не использовать подобный механизм мы не можем, поскольку тогда лишимся связывания различных структур системы друг с другом. Все объекты (окна, файлы, процессы, потоки, события и т.д.) и системные ресурсы в Windows описываются с помощью дескрипторов.
Далее у нас следует блок кода, который отвечает за регистрацию класса окна. Тут у нас впервые в коде появляется структура wc
(будет подробно описана в секции данных), которая описывает все необходимые параметры будущего окна, поэтому все члены (поля) этой структуры должны быть предварительно инициализированы. Например, функция GetModuleHandle возвращает дескриптор приложения (процесса) в регистре eax, и мы сохраняем его в wc.hInstance (строка 13
), тем самым инициализируя член hInstance структуры wc
. Затем мы загружаем иконку при помощи функции LoadIcon и инициализируем дескриптор иконки для будущего окна wc.hIcon. Можно использовать пользовательскую иконку, определённую в секции ресурсов, но обычно, для сокращения кода и упрощения логики, используют один из типовых значков, именно так и сделано в нашей программе. Затем загружаем курсор при помощи функции LoadCursor и инициализируем дескриптор курсора wc.hCursor будущего окна. Опять же, тут каждый волен задавать собственный пользовательский курсор, либо использовать стандартный предопределенный. Обратите внимание на то, что в структуре wc
имеется член lpfnWndProc, в который мы записываем адрес начала процедуры обработки событий окна WindowProc
, о которой будет рассказано далее. Еще инициализируется дескриптор кисти фона окна hbrBackground. Вызовы всех этих процедур нам необходимы для заполнения структуры wc
, используемой в дальнейшем для регистрации класса нашего окна при помощи функции RegisterClass.
Класс окна — набор свойств и методов (спецификация), определяющий требуемые параметры (куpсоp, иконка, адрес функции обработки сообщений окна и прочие), в соответствии с которыми будут создаваться окна в нашем приложении.
В нашем примере используется собственный (пользовательский) класс окна. Однако, в операционной системе Windows для разработчика предлагается и несколько предопределенных классов окон, которые могут быть использованы в приложении через определенные ключевые слова. Для чего вообще нужна регистрация некоего класса окна, почему нельзя создать сразу непосредственно само окно? Ответ дает концепция объектно-ориентированного программирования, и класс в рамках концепции существует для того, чтобы задать все параметры будущего окна в одной точке кода, ведь если Вы на основе одного-единственного класса будете создавать множество окон, то подобная стратегия полностью себя оправдает. Отсюда следует вывод:
Операционная система выводит на экран и обслуживает окна только зарегистрированных классов. Соответственно, что бы система узнала о вашем собственном пользовательском классе — его необходимо зарегистрировать.
После регистрации класса окна, при помощи системной функции CreateWindowEx, мы создаём экземпляр окна зарегистрированного ранее класса. Как Вы уже заметили, в нашей программе вместо типовой функции CreateWindow используется расширенная версия функции по имени CreateWindowEx, отличающаяся от типовой поддержкой дополнительных стилей окна (параметр dwExStyle). В качестве входных параметров для функции Вы должны указывать некоторое множество параметров:
Наименование | Тип в ASM | Тип в C | Описание |
---|---|---|---|
dwExStyle | DD | DWORD | Расширенный стиль описания создаваемого окна. Содержит разнообразные украшательства, которые не входят в основное описание стиля dwStyle.Список возможных значений можно посмотреть в Extended Window Styles. |
lpClassName | DD | LPCTSTR | Указатель (адрес) на строку с именем класса окна. В нашем случае используется строка _class. |
lpWindowName | DD | LPCTSTR | Указатель на строку с именем окна, отображаемым в заголовке окна. В нашем случае используется строка _title. |
dwStyle | DD | DWORD | Константа, определяющая стиль окна. В нашем случае используется константа _style, содержащая битовые флаги. |
x | DD | int | Горизонтальная координата (X) левого верхнего угла окна. В координатах экрана. |
y | DD | int | Вертикальная координата (Y) левого верхнего угла окна. В координатах экрана. |
nWidth | DD | int | Ширина окна в пикселях. |
nHeight | DD | HCURSOR | Высота окна в пикселях. |
hWndParent | DD | HWND | Дескриптор родительского (порождающего) окна. |
hMenu | DD | HMENU | Дескриптор меню, используемого окном. В случае, если окно основано на предопределенном системном классе окна, оно не может содержать меню, тогда параметр используется как идентификатор дочернего элемента управления. |
hInstance | DD | HINSTANCE | Дескриптор приложения (модуля), создающего данное окно. |
lpParam | DD | LPVOID | Опциональный указатель на дополнительную структуру данных (CREATESTRUCT), посылаемых окну. Если параметр задан, то в первом сообщении WM_CREATE параметр lParam указывает на требуемую структуру. Или же данный указатель может принимать значение NULL, сообщая, что никаких данных посредством функции CreateWindow не передается. |
Непосредственно после вызова функции CreateWindowEx наше окно появляется на экране. Всё, окно выведено. Но сам по себе факт отрисовки окна нам мало что дает, окно должно функционировать. Для этого нам необходимо обеспечить получение информации от пользователя/системы (пользовательский/системный ввод) и делается это при помощи очереди сообщений.
Очередь сообщений
Давайте сделаем небольшое отступление и поговорим об одной из фундаментальных основ большинства приложений операционной системы Windows: очереди сообщений. Дело в том, что событийно-ориентированное программирование, о котором мы упоминали выше, предполагает, что главный цикл программы (приложения) должен состоять как минимум из двух частей: процедуры выборки событий и процедуры обработки событий (события/данные поступают в виде сообщений).
Сообщения — один из основных механизмов операционной системы Windows, обеспечивающий взаимодействие между различными процессами и объектами в пределах операционной системы.
Как уже отмечалось, процессы в операционной системе обмениваются между собой сообщениями, которые представляют из себя предопределенные константы, однозначно характеризующие произошедшее событие. Каждый раз, когда для нашего процесса имеются входные данные (информация, действия пользователя: сообщения от клавиатуры/мыши и прч.), ядро передает ей эти данные в виде сообщений, помещая их в очередь сообщений (message queue) того или иного потока (зачастую единственного) в рамках процесса.
Операционная система Windows поддерживает очередь сообщений для каждой программы (функционирующей в данный момент в виде процесса).
То есть ядро передает события приложению в форме сообщения, помещая их в очередь того программного потока, которому принадлежит окно, над которым в данный момент проводятся те или иные действия, являющиеся источниками событий. Ядро может генерировать сообщения не только в ответ на какие-то глобальные изменения в системе, инициированные данным приложением (изменение пула ресурсов системных шрифтов, изменение размера одного из своих окон и прч), но и по действиям над объектами оконного интерфейса (окна, дочерние элементы), принадлежащими процессу приложения. Чтобы не было путаницы с пониманием типов и целей сообщений, надо уяснить, что сообщения бывают:
- Синхронные (поставленные в очередь, queued): сообщения, поступающие в основную очередь сообщений потока (которому принадлежит окно), а затем уже, в зависимости от назначения, обслуживаемые в основной очереди, либо диспетчеризируемые в соответствующую процедуру обработки сообщений окна (оконную процедуру);
- Асинхронные (не поставленные в очередь, nonqueued): сообщения, поступающие напрямую в процедуру
WindowProc
соответствующего окна, минуя очередь сообщений потока;
Ядро является не единственным источником сообщений, поскольку сообщения могут создаваться и самим пользовательским приложением. Оконное приложение само может генерировать сообщения напрямую к своим собственным или чужим окнам с целью выполнения внутренних задач либо для обеспечения взаимодействия с окнами других приложений.
Каждое предопределенное системное сообщение имеет размерность в 16 байт, собственный уникальный идентификатор и соответствующее символическое значение, которое определяет категорию и назначение сообщения.
Например, существует сообщение WM_PAINT, которое предписывает целевому окну отрисовать собственное содержимое. Как видно из наименования WM_PAINT, символическое значение содержит в своем имени префикс (WM), указывающий на его категорию. Символьные значения сообщений (WM_CREATE, WM_DESTROY, WM_PAINT и прч.) определены в файлах стандартных включений \include\equates\user32.inc / \include\equates\user64.inc пакета FASM. Это сделано по аналогии со стандартными файлах заголовков Windows (windows.h и прочие), которые включаются в программы C/C++.
В каждой программе определяется цикл обработки сообщений (message loop), который призван обрабатывать (выбирать/транслировать/диспетчеризировать) поступающие приложению сообщения. В нашем примере, в начале этого цикла функция GetMessage проверяет, есть ли какие-либо сообщения от операционной системы. Особенностью данной функции является то, что она не возвращает управление в вызывающую программу, пока не появится какое-нибудь сообщение, затем извлекает сообщение из очереди сообщений потока и помещает его в структуру с именем msg
типа MSG
. Как мы видим, параметр hWnd (второй параметр) для функции установлен в ноль (NULL), поэтому извлекаются все сообщения, адресованные любому окну, ассоциированного с текущим потоком, и любые сообщения для текущего потока, чьи hwnd равны нулю (NULL). Таким образом, если hWnd равно нулю, и оконные сообщения и сообщения потока обрабатываются.
Традиционно для Windows-функций, результат выполнения кода функции возвращается в регистре eax.
Поэтому в нашем коде (строка 35
) мы анализируем содержимое данного регистра и если оно равно 0
, то это значит, что пришло сообщение WM_QUIT, в случае чего переходим на метку end_loop с последующим выходом. Во всех остальных случаях подразумевается, что пришло сообщение, отличное от WM_QUIT, и его требуется обработать. Обработка начинается с вызова вспомогательной функции TranslateMessage (с аргументом в виде структуры msg
), которая предназначена для дополнения (расширения) сообщений клавиатуры WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN и WM_SYSKEYUP сообщениями WM_CHAR, WM_DEADCHAR, WM_SYSCHAR, WM_SYSDEADCHAR, содержащими ASCII-значения нажатых клавиш. Согласитесь, что иметь дело с ASCII-значениями проще, нежели со scan-кодами. Если её исключить из цикла, то вероятно мы не получим символических значений, а будем довольствоваться лишь скан-кодами нажатых в окне клавиш. Затем у нас вызывается функция DispatchMessage (аргументом которой все так же является ссылка на нашу структуру msg
), которая отправляет сообщение в процедуру окна, поскольку главное её предназначение разбирать сообщения, извлеченные функцией GetMessage.
Функция DispatchMessage в коде обработки очереди сообщений потока проверяет, для какого именно класса окна предназначено сообщение и вызывает соответствующую оконную процедуру.
Обратите внимание, что тут возникает один тонкий момент: зачем нам фактически две логики разбора очереди через GetMessage и через WindowProc
, ведь можно было обойтись одной, зачем нам нужно вызывать еще отдельную процедура обработки сообщений окна, когда можно обработать сообщение в основном цикле? Так то оно так, но как мы уже упоминали, сообщения могут быть синхронными и асинхронными. Синхронные сообщения помещаются в очередь сообщений потока, соответственно извлекаются и диспетчеризируются они в основном цикле обработки сообщений: при помощи GetMessage, а затем могут быть отправлены в оконную процедуру через связку DispatchMessage+WindowProc
. Асинхронные сообщения передаются непосредственно окну путем прямого вызова оконной процедуры. Это как, неужели ядро что-то напрямую вызывает в пользовательском коде? Я думаю, что тут всё несколько иначе и «прямой» передачей (асинхронных сообщений) занимается исключительно функция DispatchMessage, потому как данные сообщения не извлекаются из очереди функцией GetMessage? В любом случае, оконная процедура принимает все типы сообщений, адресованные заданному окну: синхронные и асинхронные. Именно поэтому цикл обработки сообщений у нас выглядит так а не иначе.
Ну и, наконец, вся эта логика обработки сообщений завершается в строке 40
командой jmp, которая зацикливает прием и обработку сообщений. Таким образом, если сообщений на данный момент нет, то функция все равно ожидает появления сообщения в бесконечном цикле, выходом из которого является лишь действие по закрытию окна.
Основная логика работы большинства Windows-программы с оконным интерфейсом сводится к работе с сообщениями окна.
Код под меткой error, на которую осуществляет переход из нескольких мест обработки ошибочных ситуаций в нашей программе, служит для обработки критической ситуации, когда функции у нас по каким-либо причинам возвращают ошибку и дальнейшее выполнение кода программы становится бессмысленным. В этом случае мы выдаем окно с ошибкой при помощи функции MessageBox, а затем вызывается функция ExitProcess с аргументом msg.wParam, который содержит код выхода.
В случае [внезапного] завершения цикла обработки сообщений, код выхода хранится в члене wParam структуры msg
. Этот код выхода необходимо вернуть ядру операционной системы посредством вызова функции ExitProcess с входным параметром, равным значению msg.wParam.
Тут возникает резонный вопрос, почему метка error располагается в основном цикле обработки сообщений, ведь логичнее было бы её вообще вынести в другое место кода. Да, это действительно так, но тогда бы нам пришлось дублировать выход из программы при помощи функции ExitProcess, а так мы можем использовать уже существующую точку выхода (используемую в цикле), не прибегая к дублированию кода. Исключительно с этой целью логика обработки ошибки встроена в цикл обработки сообщений.
Процедура обработки сообщений окна
В исходном коде нашего оконного приложения на ассемблере, реакцию на те или иные сообщения окна обеспечивает процедура WindowProc
, которая еще называется оконной процедурой или оконной функцией.
Оконная (процедура) функция обеспечивает непосредственную обработку нужных нам сообщений: сравнивает идентификаторы и если они относятся к обрабатываемым её классам, то выполняет необходимую работу.
В отличии от основного цикла выборки, тут у нас происходит обработка сообщений, посылаемых окну нашей программы системой Windows. Дело в том, что окно это не просто некая область на экране, посредством которой приложение может предоставить на всеобщее обозрение свои данные, это еще и адресат (цель) событий и сообщений в операционной системе Windows.
Оконная (процедура) функция – функция обратного вызова. Ядро Windows само посылает сообщения окну, что в свою очередь, инициирует вызов сопоставленной оконной процедуры.
Фактически, вызов (опосредовано или напрямую) кодом ядра системы оконной процедуры соответствующего окна, называют «обратным вызовом» или (по-английски) callback’ом. И что такое функция обратного вызова? Это функция, которая вызывается ядром при наступлении определенных условий. В действительности система не может так вот запросто взять и вызвать любую произвольную функцию вашего приложения, вместо этого она предоставляет специальный механизм, посредством которого может вызывать только заранее определенную функцию в вашем пользовательском коде. Вот именно эта функция обратного вызова называется оконной (функцией) процедурой (обычно носящей имя WndProc
, в нашем случае WindowProc
) и ассоциируется со всеми графическими окнами процесса. Задается адрес функции обратного вызова через специальный член структуры класса окна lpfnWndProc, на этапе регистрации класса. Каждый раз, когда для какого-либо окна нашего процесса или его дочерних элементов (элементы меню, поля, кнопки, радиокнопки, элементы управления и прочее) имеются входные данные (информация, действия пользователя: сообщения от клавиатуры/мыши и прч.), ядро сначала передает сообщение в цикл обработки сообщений, а затем через функцию DispatchMessage опосредованно вызывает соответствующую оконную процедуру и передает ей данные в виде сообщений, которые поступают через входные параметры процедуры. На каждое событие (например, набор пользователем символов с клавиатуры, движение курсора мыши в пределах границ окна, щелканье по элементам управления (кнопка, скролл-бар и прч.)), относящееся к окну, ядро генерирует определенное сообщение. В конкретном примере алгоритм процедуры обрабатывает всего два сообщения: WM_DESTROY и WM_CREATE. Процедура WindowProc
предваряется у нас в коде неким ключевым словом proc и получает четыре входных параметра (hWnd,wMsg,wParam,lParam), но что это за proc? А это ни что иное как, опять же, макрос, настраивающий пролог/эпилог вызываемой процедуры. Давайте немного отступим от основной линии повествования и познакомимся с макросом proc поближе:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
macro proc [args] ; define procedure { common match name params, args> \{ define@proc name,<params \} } prologue@proc equ prologuedef macro prologuedef procname,flag,parmbytes,localbytes,reglist { local loc loc = (localbytes+3) and (not 3) parmbase@proc equ ebp+8 localbase@proc equ ebp—loc if parmbytes | localbytes push ebp mov ebp,esp if localbytes sub esp,loc end if end if irps reg, reglist \{ push reg \} } epilogue@proc equ epiloguedef macro epiloguedef procname,flag,parmbytes,localbytes,reglist { irps reg, reglist \{ reverse pop reg \} if parmbytes | localbytes leave end if if flag and 10000b retn else retn parmbytes end if } close@proc equ |
Структура данного макроса лишний раз говорит за то, что в FASM реализован очень продвинутый макроязык. Ведь данным макросом, компилятор фактически «настраивает» любую процедуру, описываемую при помощи ключевого слова proc. И настройка эта состоит в автоматическом создании пролога и эпилога процедуры, настройке стекового фрейма (кадра), резервации места в стеке под локальные переменные, восстановлении стека в эпилоге, сохранении/восстановление регистров общего назначения. Все эти подготовительные действия при входе в процедуру и выходе из неё, считаются типовыми и используются уже давно в компиляторах языков различных уровней. В отсутствии данного макроса программисту пришлось бы писать весь «обвес» процедуры самостоятельно, затрачивая на это драгоценное время, либо тратя его на то, чтобы самостоятельно создавать подобные облегчающие программирование макросы. Поверьте, совокупность подобных (с виду незначительных) автоматизаций серьёзно облегчает работу разработчика. Поэтому, хочу отдельно отметить неоспоримые достоинства макроязыка FASM, поскольку именно с его помощью Вы можете создавать поистине грандиозные конструкции, которые могут кардинально изменить синтаксис языка.
Оконная процедура занимает в исходном тексте строки с 48
по 66
. В начале процедуры проверяем идентификатор входящего сообщения wMsg на стандартную константу WM_DESTROY. Данное сообщение посылается окну в случае его закрытия.
WM_DESTROY — единственное сообщение, которое непременно (всегда, в любом случае) должно быть обработано в вашей оконной процедуре!
Если сообщение WM_DESTROY поступило, то прыгаем на локальную метку .wmdestroy, в которой у нас располагается функция PostQuitMessage, фактически посылающая в очередь сообщений сообщение WM_QUIT, которое затем обрабатывается уже в основном цикле функцией GetMessage и предписывает ей вернуть 0 (в регистре eax), что в итоге ведет к выходу из приложения (строка 36
).
В строках 58
и 62
у нас присутствует команда xor eax,eax, которая обнуляет регистр eax. Интересно, для чего нам вдруг понадобилось его обнулять? Дело в том, что это регламентируется общим правилом API: функция должна возвращать в регистре eax либо код завершения, либо один из результатов своей работы. Соответственно, если оконная процедура WindowProc
обрабатывает какое-либо сообщение, то она должна возвратить 0 в случае успешного завершения, либо любое другое значение в случае ошибочного. Вот именно по этой причине у нас тут и располагается команда обнуления регистра, поскольку подразумевается, что все обрабатываемые нашей процедурой сообщения обрабатываются успешно.
Затем в строке 52
сравниваем значение поля wMsg со значением WM_CREATE, фактически этим мы проверяем, не поступило ли сообщение о создании окна? У нас данная ветка кода пустует, мы как бы реагируем на это сообщение в очереди, но в действительности ничего не делаем. Далее, для всех сообщений, которые не обрабатываются нашей оконной процедурой, мы должны вызвать функцию DefWindowProc, как того предписывает Microsoft. Фактически функция DefWindowProc является функцией обработки по умолчанию и гарантирует, что каждое поступающее в очередь сообщение (даже то, которое нас не интересует), будет обработано. Далее следует локальная метка .finish, которая восстанавливает сохраненные на входе оконной процедуры регистры и выходит из неё.
Секция данных (data)
Как уже следовало из описания секции, в данном разделе помещаются данные, необходимые исполняемому коду, отсюда происходит и название секции. Ключевыми данными тут являются две структуры: wc
и msg
, на которых стоит остановиться подробнее. Структура wc
имеет прототип структуры WNDCLASS
. А уже сама структура WNDCLASS
является стандартной для библиотек Win32 API и описана в файлах \INCLUDE\EQUATES\USER32.INC и \INCLUDE\EQUATES\USER64.INC пакета FASM. Давайте подробнее её изучим, я просто скопирую содержимое:
struct WNDCLASS style dd ? lpfnWndProc dd ? cbClsExtra dd ? cbWndExtra dd ? hInstance dd ? hIcon dd ? hCursor dd ? hbrBackground dd ? lpszMenuName dd ? lpszClassName dd ? ends |
Поля требуют пояснения, поэтому я свел их в небольшую таблицу:
Наименование | Тип в ASM | Тип в C | Описание |
---|---|---|---|
style | DD | UINT | Определяет стиль окна. |
lpfnWndProc | DD | WNDPROC | Адрес процедуры обработки событий окна. |
cbClsExtra | DD | int | Информация о дополнительных байтах для структуры класса окна. |
cbWndExtra | DD | int | Информация о дополнительных байтах для структуры экземпляра окна. |
hInstance | DD | HINSTANCE | Дескриптор экземпляра приложения, которое содержит оконную процедуру для класса окна. |
hIcon | DD | HICON | Дескриптор загруженной иконки окна. Может принимать значение дескриптора иконки приложения, отображаемой в верхнем левом углу окна. |
hCursor | DD | HCURSOR | Дескриптор загруженного курсора окна. Может принимать значение дескриптора курсора, используемого в пределах окна. |
hbrBackground | DD | HBRUSH | Дескриптор загруженной кисти фона окна. Этот член может принимать значение дескриптора кисти, используемой для отрисовки фона, или принимать значение цвета. Цвет должен быть одним из определенных стандартных цветов (правило: к значению цвета должна добавляться 1). |
lpszMenuName | DD | LPCTSTR | Имя (идентификатор) ресурса для класса меню. Это имя должно быть определено в секции/файле ресурсов. 0 означает, что меню отсутствует. |
lpszClassName | DD | LPCTSTR | Определяет имя класса окна. Это обычная текстовая строка с завершающим нулем. Имеются предопределенные классы, однако можно задавать и собственный класс. |
Непосредственно перед регистрацией класса, структура WNDCLASS
должна быть заполнена. Наиболее важные значениями являются: дескриптор приложения hInstance, дескриптор иконки для окна hIcon, дескриптор курсора окна hCursor, дескриптор кисти фона окна hbrBackground. В случае с кистью можно оставить значение по умолчанию, либо использовать значение стандартного системного цвета? Во многих исходниках это значение не используется, но я думаю, что шаблон будет более универсальным и готовым к широкому применению, если в нем описать наибольшее количество параметров.
Затем у нас описывается еще одна структура msg
, которая имеет прототип системной структуры MSG
. Это критически важная структура, поскольку сообщения, передаваемые в приложение, имеют структуру MSG
, включающую 6 полей. Описание этой структуры содержится в файлах \INCLUDE\EQUATES\USER32.INC и \INCLUDE\EQUATES\USER64.INC пакета FASM. Проведем детальное рассмотрение данной структуры:
struct MSG hwnd dd ? message dd ? wParam dd ? lParam dd ? time dd ? pt POINT ends |
Так же, при помощи сообщений подобной структуры, система посылает сообщения в процедуру окна. Как мы видим, в структуре сообщения используется несколько параметров, которые хорошо бы разобрать подробнее:
Наименование | Тип в ASM | Тип в C | Описание |
---|---|---|---|
hwnd | DD | HWND | Дескриптор окна, оконная процедура которого получила данное сообщение. Поле равно нулю (NULL) когда сообщение принадлежит потоку (thread message). |
message | DD | UINT | Идентификатор сообщения. Представляет из себя именованную (символическую) константу, которая определяет назначение сообщения. Когда оконная процедура получает сообщение, код использует данный идентификатор для определения перехода на конкретный обработчик. В нашем примере, идентификатор WM_DESTROY определяет переход на метку .wmdestroy, код которой инициирует закрытие окна. Приложения могут использовать только младшее слово двойного слова message, поскольку старшее слово зарезервировано за системой. |
wParam | DD | WPARAM | Дополнительные данные сообщения. Параметр, уточняющий смысл сообщения: сами данные или расположение данных. Смысл и значение этого параметра зависит исключительно от типа сообщения, в совокупности с которым этот параметр рассматривается. Параметр может содержать целочисленное значение, флаги, указатель на структуру, содержащую дополнительные данные и тому подобное. Когда сообщение не использует дополнительных параметров, значение обычно устанавливается в 0 (NULL). Код оконной процедуры обычно проверяет идентификатор сообщения message для уточнения, как именно интерпретировать этот параметр. |
lParam | DD | LPARAM | Дополнительные данные сообщения. Параметр, уточняющий смысл сообщения: сами данные или расположение данных. Смысл и значение этого параметра зависит исключительно от типа сообщения, в совокупности с которым этот параметр рассматривается. Параметр может содержать целочисленное значение, флаги, указатель на структуру, содержащую дополнительные данные и тому подобное. Когда сообщение не использует дополнительных параметров, значение обычно устанавливается в 0 (NULL). Код оконной процедуры обычно проверяет идентификатор сообщения message для уточнения, как именно интерпретировать этот параметр. |
time | DD | DWORD | Время, в которое данное сообщение было помещено в очередь. Формат? |
pt | DD,DD | POINT | Позиция курсора, где сообщение было опубликовано. В координатах экрана. |
Секция импорта (import data)
Для чего вообще нужна секция импорта? Если вы заметили, наша программа в своей работе использует разнообразные системные функции Win32 API. Все эти функции распределены в системе по различным библиотекам, которые располагаются в соответствующих .DLL-файлах. Например, наша программа использует несколько системных функций (GetModuleHandle, LoadIcon, LoadCursor, RegisterClass, CreateWindowEx и прочие), размещенных во внешних системных библиотеках (KERNEL32.DLL, USER32.DLL). Все эти функции вызываются в коде нашего приложения через таблицу импорта: к примеру, когда в процессе выполнения кода встречается вызов функции GetModuleHandle, на самом деле в исходном коде исполняемого файла вызов выглядит как:
call [адрес_в_таблице_импорта]
То есть производится вызов функции через адрес, указанный в конкретном поле таблицы импорта. Так вот, чтобы функции, используемые в нашем коде, могли корректно вызываться в процессе исполнения приложения, динамический компоновщик на этапе подготовки образа к выполнению, производит связывание действительных адресов функций (по которым расположены функции библиотек в виртуальном адресном пространстве процесса нашего приложения) с полями таблицы импорта. Только после выполнения вышеописанной процедуры внешние функции доступны для вызова.
Секцию импорта в нашем примере компилятор создает при помощи макроса library
, который имеется в стандартных библиотечных файлах пакета FASM. На деле, макрос ответственен за создание в выходном исполняемом .exe-файле специального блока данных (секции), в самом начале которого записываются смещения имен библиотек (DLL), а затем смещения, по которым располагаются имена (или ординалы) импортируемых из этих библиотек функций. Директива import
(строки 86
,90
) предписывает компилятору подключить перечисленные функции (используемые в нашей программе) из библиотек. Зачем на этом? Дело в том, что в диалекте FASM использование одного лишь макроса library
в секции импорта недостаточно, в дополнение к нему есть два основных подхода:
- Описание имен библиотек через макрос
library
+ указание через директивуimport
списка используемых функций. Этот метод более универсальный, поскольку позволяет работать со всеми имеющимися в системе библиотеками. - Описание имен библиотек через макрос
library
+ указание через директивуinclude
файлов-включений для используемых библиотек. Это метод более простой, однако имеет существенный недостаток: .inc-файлы в комплекте FASM имеются только к основным системным библиотекам. Поэтому, если у вас нет .inc-файла к какой-либо используемой вами библиотеке — используйте первый метод!!
Как вы видите, у меня используется первый подход. Однако никто не запрещает пользоваться и вторым подходом, в этом случае окончание секции будет выглядеть (к примеру) вот так:
. . . include ‘%fasminc%\api\kernel32.inc’ include ‘%fasminc%\api\user32.inc’ . . . |
О плюсах и минусах того или иного подхода в процессе построения секции импорта можно дискутировать бесконечно, тем не менее их можно и комбинировать: для библиотек без инклюдов использовать первый метод, для библиотек с инклюдами — второй.
12 Sep 2015
In this post, I’ll walk through the steps required to bootstrap your development experience against the Win32 API using the Flat Assembler.
Prerequisites
Everything is provided for you when you download the fasmw package, including libraries, include files. fasmw even includes a linker pre-geared for windows as your target.
Test program
Here’s the code to present a message box.
include 'win32ax.inc'
.code
start:
invoke MessageBox, HWND_DESKTOP, 'This is a test', 'Hey, Hey!', MB_OK
invoke ExitProcess, 0
.end start
Everything is provided to you through win32ax.inc
. This can be swapped out easily enough with a 64 bit counterpart if you’re targeting different architectures.
Assembling and linking
Now that you’ve got your source file, hello.asm
you can produce an object file with the following:
C:\src> fasm hello.asm
That’s all there is to it. You’ll now have an exe available to you to run. The only gotcha is giving fasm
a hint as to where your include files are, and you do this through the INCLUDE
environment variable:
SET INCLUDE=C:\fasmw\include
Опубликовал: devprog на 17 октября, 2008
Привет. Итак приступим к делу, для этого нам нужно скачать какой-нибудь компилятор ассемблера, качайте или FASM или MASM, так как отличия в синтаксисе минимальны. Выбрать компилятор по душе можете вот сдесь. Лучше конечно скачать FASM, так как этот компилятор нравиться мне больше остальных и обьяснять я буду именно на нём.
Итак, давайте напишем приложение которое будет показывать простое окошко с какой-либо фразой, как и подобает обучаться програмированию. Вообще я сомневаюсь что имеено так нужно учиться но не будем нарушать традиций.
Создадим простой текстовый файлик и назовём его «first.asm» и начинаем программировать:
include ‘win32ax.inc’
.code
start:
invoke ExitProcess,0 ; вызываем функцию ExitProcess c параметром 0 (нуль)
.end start
Что делает этот код? Да ничего… Если мы его скомпилируем то получим полноценную программу которая просто завершает сама себя. Компилируем:
fasm.exe first.asm
Итак, вы попытались скомпилировать… Но не получилось, а всё потому что компилятор не знает где находиться файл-инклудник — «win32ax.inc». Необходимо ему явно указать этот файл для этого меняем код программы, например вот так:
include ‘D:\FASM\include\win32ax.inc’
.code
start:
invoke ExitProcess,0 ; вызываем функцию ExitProcess c параметром 0 (нуль)
.end start
Теперь всё скомпилируеться отлично, но чтобы это не писать каждый раз, можно задействовать переменные среды, присвоив например переменной %inc% путь «D:\FASM\include». Но этим мы займемся в следующем туториале, так как в этом я собираюсь показать как вызываеться функция Windows API (какой и являеться ExitProcess).
Кстати invoke — это макрос, который обьявлен в инклуднике win32ax.inc. Он позволяет нам вызывать функции привычным образом как например в С++ или Delphi. Без него мы будем писать в следующих туториалах. Короче усвойте что invoke это вызов функций (но только на первый туториал, ну или на все если в будущем вы собираетесь программировать именно с его помощью).
Теперь давайте добавим в нашу программу ещё одну функцию — MessageBox:
include ‘D:\FASM\include\win32ax.inc’
.code
start:
invoke MessageBox,HWND_DESKTOP,»Hallo Xaker.Name»,»Caption»,MB_ICONASTERISK
invoke ExitProcess,0 ; вызываем функцию ExitProcess c параметром 0 (нуль)
.end start
Первый параметр функции MessageBox являеться дескриптор окна-родителя нашего окошка, в качестве него мы передаём константу HWND_DESKTOP — дескриптор Рабочего Стола.
Второй параметр — адрес на текст самого окошка. Возникает вопрос — почему же мы не пишем: invoke MessageBox,HWND_DESKTOP,offset Message….? Да потому что, макрос invoke достаточно универсален, чтобы принимать в параметре просто текст… Подробнее обьясню в следующих уроках.
Третий параметр — заголовок окна, то есть его адрес.
Ну а четвёртый параметр — стиль окна, у нас это MB_ICONASTERISK то есть окошко с восклицанием.
Кстати дополнительную информацию об API — можно получить либо на MSDN либо из любой справки Windows SDK, которая поставляеться с любым продуктом Borland. Вот и написали и разобрали вызов функций и написали полноценное приложение. Всё, до следующего туториала, всего хорошего…
———-
Интересное на блогах:
Сокращаем JavaScript код
Сервисы для программистов
———-
Полезная заметка? Есть способ получать новые посты не заходя на этот сайт! Как? Да просто подпишись на RSS-Фид !
This entry was posted on 17 октября, 2008 в 2:51 дп and is filed under Ассемблер, Новичку.
Отмечено: Ассемблер, Новичку, fasm, MASM32. You can follow any responses to this entry through the RSS 2.0 feed.
You can leave a response, или trackback from your own site.