А теперь давайте устроим этому
. А теперь давайте устроим этому исходнику разборку.
Бряк 1. Таким образом мы определяем локальную переменную с именем hStdout и размером двойное слово (DWORD). Почему локальная? А потому, что она существует только внутри процедуры Main, и если бы мы попытались обращаться к переменной hStdout за пределами этой процедуры, ассемблер бы ругал нас всякими нехорошими словами - в отличие от, скажем, константы sWriteText, имя которой "известно" в любом месте нашей программы.
Обратите внимания на префикс h в названии переменной. Это я просто оставил для себя памятку, что переменная заведена под хэндл.
Бряк 2. Апишная функция SetConsoleTitleA - устанавливаем титл (заголовок) для нашего консольного окошка. Вот выдержка из MSDN'а: BOOL SetConsoleTitle( LPCTSTR lpConsoleTitle // new console title );
Как видим, функция требует один-единственный параметр - указатель на строку символов, которую мы хотим вывести в заголовке окна. Строка должна заканчиваться нулем.
Команда push offset sConsoleTitle помещает в стек (push) адрес (offset) строки символов (помеченной как sConsoleTitle). Ну а далее следует, собственно, сам вызов (call) функции SetConsoleTitle.
Заметьте, для указания адреса используется префикс под названием offset. Это потому, что берется смещение (offset) относительно начала сегмента, которое и является "ближним адресом". Есть еще "дальние" адреса, в которых задействуется также сам сегмент, но это тема будущих разговоров - сейчас это нас не должно волновать.
Здесь у вас должен возникнуть вполне закономерный вопрос - почему мы дописали букву А в конец функции? В MSDN'е ведь нет никакой буквы A... Я отвечу на этот вопрос немного позже.
Бряк 3. Консоль мы можем использовать как устройство ввода (input device), устройство вывода (output device), устройство для отчета об ошибках (error device). Для того чтобы работать с этим "девайсом", мы должны получить его хэндл при помощи следующей функции: HANDLE GetStdHandle( DWORD nStdHandle // input, output, or error device );
Единственный параметр, который она от нас требует - указание, на какое устройство мы желаем получить "квиток"-хендл. Вот табличка:
Хэндл стандартного ввода |
-10 |
Хэндл стандартного вывода |
-11 |
Хэндл "ошибок" |
-12 |
Что нам нужно? Вывести строчку! Значит - запрашиваем хэндл для стандартного вывода, то есть перед вызовом функции "суем" в стек -11. После выполнения функции регистр EAX содержит столь желанный "хэндл стандартного вывода". Кладем этот хэндл в переменную hStdout (которую мы столь предусмотрительно определили на бряке 1) для последующего использования.
- Это ж что за безобразие? - воскликните вы. - Что это за таблица такая нездоровая? Какие-то отрицательные числа, которые ни в жисть не запомнить! Хотим таблицу как в MSDN'е! Чтобы не -10, -11, -12, а длинные мнемонические STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE!
Спокойно! Исходник, который мы сейчас рассматриваем, весьма точно отображает реальные процессы, происходящие в программе. Чуть позже мы приведем его к варианту в стиле Cи и посмотрим, как можно использовать некоторые высокоуровневые конструкции, значительно облегчающие жизнь низкоуровневому программисту.
Бряк 4. Ну наконец-то, самое главное - функция, которая, собственно, и выводит на консоль строку символов. Вот ее описание: BOOL WriteConsole( HANDLE hConsoleOutput, // handle to screen buffer CONST VOID *lpBuffer, // write buffer DWORD nNumberOfCharsToWrite, // number of characters to write LPDWORD lpNumberOfCharsWritten, // number of characters written LPVOID lpReserved // reserved );
Расшифровываем. Перед вызовом функции WriteConsole мы должны поместить в стек целых пять параметров:
- Хэндл. Какие проблемы? Мы его уже получили и предусмотрительно сохранили в переменной hStdout. Командой push hStdout заносим его в стек, и все дела.
- Указатель на строку символов, которую мы хотим напечатать. Сама строка у нас определена в секции констант под именем sWriteText. Получить ее адрес мы можем при помощи offset. Укладываем все в одну строчку - push offset sWriteText. Два в одном - и адрес получаем и в стек его заталкиваем :).
- Число символов, которые мы хотим напечатать. В смысле - число "буковок" из строки sWriteText. Сколько символов в строке "hEILo, Wo(R)LD!!"? Включая пробелы - 16d. Пишем - push 16d. Заметьте, функция WriteConsole не требует нуля в конце буфера!
- Указатель на переменную, в которой будет возвращено число напечатанных символов. Функция нам любезно сообщает, сколько символов из шестнадцати ей удалось напечатать. И требует переменную, в которую эту информацию ей занести. Давайте сделаем вид, что она нам не нужна, то есть напишем 0. Ничего страшного не случится, а в ошибочности подобного рода игнорирований убедимся в следующей главе. Пишем - push 0, но для себя оставляем пометку, что что-то функция от нас все же хотела.
- Резерв. Так сказать, зарезервировано для следующих версий. Смело пишем - push 0.
Теперь, когда мы разобрали все параметры, обратите внимание на то, что MSDN'овская очередность параметров не соответствует той очередности, в которой мы записываем их в стек в нашем исходнике. Вернитесь еще раз к Минимальному приложению, п.12 и внимательно прочитайте пункты соглашения stdcall. Теперь понятно?
Бряк 5. Дабы мы успели полюбоваться результатом трудов своих праведных, при помощи функции Sleep вызываем программную задержку в 2 секунды. Думаю, с параметрами вы без труда разберетесь.
И, наконец, бряк 6 - выход из программы.
Вообще-то, правильный стиль предполагает явное освобождение всех занятых ресурсов по минованию надобности в них, в том числе и хэндлов, несмотря на то что они автоматически закрываются ExitProcess'ом. Но будем надеяться, что если мы не сделаем это в такой маленькой программулине как наша, ничего страшного не случится. Естественно, "формат цэ" не в счет.
Содержание раздела