Win32ASM: "Hello, World" и три халявы MASM32

Дата публикации 21 авг 2002

Win32ASM: "Hello, World" и три халявы MASM32 — Архив WASM.RU

  #1. С легкой левой руки Дениса Ричи повелось начинать освоение нового языка программирования с создания простейшей программы "Hello, World". Ничто человеческое нам не чуждо - давайте и мы совершим сей грех.
  В позапрошлом выпуске я уже рассказал о том, как работать в ассемблере с апишными функциями, однако вы наверняка не поняли ;). Это нормально, и не нужно из-за этого беспокоиться. Все станет более чем ясным после того как мы с вами напишем одну-две простенькие программки и разберем их по строчкам.
  Заново перечитайте "Минимальное приложение" и набейте следующий исходник:

Код (Text):
  1.  
  2. ;-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  3. ;        ПРОЦ, МОДЕЛЬ, ОПЦИИ, ИНКЛУДЫ, БИБЛИОТЕКИ ИМПОРТА
  4. ;-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  5.  
  6.  .386
  7.  .model flat,stdcall
  8. option casemap:none
  9.  
  10. includelib kernel32.lib
  11.  
  12. SetConsoleTitleA PROTO :DWORD
  13. GetStdHandle PROTO     :DWORD
  14. WriteConsoleA PROTO    :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
  15. ExitProcess PROTO      :DWORD
  16. Sleep PROTO            :DWORD
  17.  
  18.  
  19. ;-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  20. ;                         СЕКЦИЯ КОНСТАНТ
  21. ;-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  22.  
  23.  .const
  24.  
  25. sConsoleTitle  db 'My First Console Application',0
  26. sWriteText  db 'hEILo, Wo(R)LD!!'
  27.  
  28. ;-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  29. ;                          СЕКЦИЯ КОДА
  30. ;-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  31.  
  32.  .code
  33.  
  34. ;-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  35. ;                    Самая Главная Процедура
  36. ;-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  37.  
  38. Main PROC
  39.   LOCAL hStdout :DWORD        ;(1)
  40.  
  41.   ;титл консоли
  42.   push offset sConsoleTitle   ;(2)
  43.   call SetConsoleTitleA
  44.  
  45.   ;получаем хэндл вывода      ;(3)
  46.   push -11
  47.   call GetStdHandle
  48.   mov hStdout,EAX
  49.  
  50.   ;выводим HELLO, WORLD!      ;(4)
  51.   push 0
  52.   push 0
  53.   push 16d
  54.   push offset sWriteText
  55.   push hStdout
  56.   call WriteConsoleA
  57.  
  58.   ;задержка, чтобы полюбоваться ;(5)
  59.   push 2000d
  60.   call Sleep
  61.  
  62.   ;выход                       ;(6)
  63.   push 0
  64.   call ExitProcess
  65.  
  66. Main ENDP
  67.  
  68. ;-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  69.  
  70. end Main
  71.  
  72.  

  Вот две строчки из моего батника (*.bat), который позволяет не "парится" с командной строкой:

Код (Text):
  1.  
  2. c:\tools\masm32\bin\ml /c /coff hello.asm
  3. c:\tools\masm32\bin\link /SUBSYSTEM:CONSOLE /LIBPATH:c:\masm32\lib hello.obj
  4.  

  Обращаю внимание, что для сборки консольного приложения необходимо использовать ключ /SUBSYSTEM:CONSOLE. Несмотря на то что окошко, в котором оно запустится, до боли напоминает "сеанс MS-DOS", получившаяся программа - полноценное виндозное 32-битное приложение в формате PE. Ассемблируем, линкуем, запускаем, наслаждаемся...

  #2. А теперь давайте устроим этому исходнику разборку.
  Бряк 1. Таким образом мы определяем локальную переменную с именем hStdout и размером двойное слово (DWORD). Почему локальная? А потому, что она существует только внутри процедуры Main, и если бы мы попытались обращаться к переменной hStdout за пределами этой процедуры, ассемблер бы ругал нас всякими нехорошими словами - в отличие от, скажем, константы sWriteText, имя которой "известно" в любом месте нашей программы.
  Обратите внимания на префикс h в названии переменной. Это я просто оставил для себя памятку, что переменная заведена под хэндл.
  Бряк 2. Апишная функция SetConsoleTitleA - устанавливаем титл (заголовок) для нашего консольного окошка. Вот выдержка из MSDN'а:

Код (Text):
  1.  
  2. BOOL SetConsoleTitle(
  3.   LPCTSTR lpConsoleTitle // new console title
  4. );
  5.  

  Как видим, функция требует один-единственный параметр - указатель на строку символов, которую мы хотим вывести в заголовке окна. Строка должна заканчиваться нулем.
  Команда push offset sConsoleTitle помещает в стек (push) адрес (offset) строки символов (помеченной как sConsoleTitle). Ну а далее следует, собственно, сам вызов (call) функции SetConsoleTitle.
  Заметьте, для указания адреса используется префикс под названием offset. Это потому, что берется смещение (offset) относительно начала сегмента, которое и является "ближним адресом". Есть еще "дальние" адреса, в которых задействуется также сам сегмент, но это тема будущих разговоров - сейчас это нас не должно волновать.
  Здесь у вас должен возникнуть вполне закономерный вопрос - почему мы дописали букву А в конец функции? В MSDN'е ведь нет никакой буквы A... Я отвечу на этот вопрос немного позже.

  Бряк 3. Консоль мы можем использовать как устройство ввода (input device), устройство вывода (output device), устройство для отчета об ошибках (error device). Для того чтобы работать с этим "девайсом", мы должны получить его хэндл при помощи следующей функции:

Код (Text):
  1.  
  2. HANDLE GetStdHandle(
  3.   DWORD nStdHandle   // input, output, or error device
  4. );
  5.  

  Единственный параметр, который она от нас требует - указание, на какое устройство мы желаем получить "квиток"-хендл. Вот табличка:

Хэндл стандартного ввода-10
Хэндл стандартного вывода-11
Хэндл "ошибок"-12

  Что нам нужно? Вывести строчку! Значит - запрашиваем хэндл для стандартного вывода, то есть перед вызовом функции "суем" в стек -11. После выполнения функции регистр EAX содержит столь желанный "хэндл стандартного вывода". Кладем этот хэндл в переменную hStdout (которую мы столь предусмотрительно определили на бряке 1) для последующего использования.

  - Это ж что за безобразие? - воскликните вы. - Что это за таблица такая нездоровая? Какие-то отрицательные числа, которые ни в жисть не запомнить! Хотим таблицу как в MSDN'е! Чтобы не -10, -11, -12, а длинные мнемонические STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE!
  Спокойно! Исходник, который мы сейчас рассматриваем, весьма точно отображает реальные процессы, происходящие в программе. Чуть позже мы приведем его к варианту в стиле Cи и посмотрим, как можно использовать некоторые высокоуровневые конструкции, значительно облегчающие жизнь низкоуровневому программисту.

  Бряк 4. Ну наконец-то, самое главное - функция, которая, собственно, и выводит на консоль строку символов. Вот ее описание:

Код (Text):
  1.  
  2. BOOL WriteConsole(
  3.   HANDLE hConsoleOutput,          // handle to screen buffer
  4.   CONST VOID *lpBuffer,           // write buffer
  5.   DWORD nNumberOfCharsToWrite,    // number of characters to write
  6.   LPDWORD lpNumberOfCharsWritten, // number of characters written
  7.   LPVOID lpReserved               // reserved
  8. );
  9.  

  Расшифровываем. Перед вызовом функции WriteConsole мы должны поместить в стек целых пять параметров:

  1. Хэндл. Какие проблемы? Мы его уже получили и предусмотрительно сохранили в переменной hStdout. Командой push hStdout заносим его в стек, и все дела.
  2. Указатель на строку символов, которую мы хотим напечатать. Сама строка у нас определена в секции констант под именем sWriteText. Получить ее адрес мы можем при помощи offset. Укладываем все в одну строчку - push offset sWriteText. Два в одном - и адрес получаем и в стек его заталкиваем :smile3:.
  3. Число символов, которые мы хотим напечатать. В смысле - число "буковок" из строки sWriteText. Сколько символов в строке "hEILo, Wo(R)LD!!"? Включая пробелы - 16d. Пишем - push 16d. Заметьте, функция WriteConsole не требует нуля в конце буфера!
  4. Указатель на переменную, в которой будет возвращено число напечатанных символов. Функция нам любезно сообщает, сколько символов из шестнадцати ей удалось напечатать. И требует переменную, в которую эту информацию ей занести. Давайте сделаем вид, что она нам не нужна, то есть напишем 0. Ничего страшного не случится, а в ошибочности подобного рода игнорирований убедимся в следующей главе. Пишем - push 0, но для себя оставляем пометку, что что-то функция от нас все же хотела.
  5. Резерв. Так сказать, зарезервировано для следующих версий. Смело пишем - push 0.

  Теперь, когда мы разобрали все параметры, обратите внимание на то, что MSDN'овская очередность параметров не соответствует той очередности, в которой мы записываем их в стек в нашем исходнике. Вернитесь еще раз к Минимальному приложению, п.12 и внимательно прочитайте пункты соглашения stdcall. Теперь понятно?

  Бряк 5. Дабы мы успели полюбоваться результатом трудов своих праведных, при помощи функции Sleep вызываем программную задержку в 2 секунды. Думаю, с параметрами вы без труда разберетесь.

  И, наконец, бряк 6 - выход из программы.

  Вообще-то, правильный стиль предполагает явное освобождение всех занятых ресурсов по минованию надобности в них, в том числе и хэндлов, несмотря на то что они автоматически закрываются ExitProcess'ом. Но будем надеяться, что если мы не сделаем это в такой маленькой программулине как наша, ничего страшного не случится. Естественно, "формат цэ" не в счет.

  #3. Теперь делаем первый шаг по приведению нашего сырца в более читабельный вид.
  Итак, первое, с чем мы ознакомимся - это эквиваленты, прописанные в файле /MASM32/windows.inc.
  Мы уже сталкивались с MSDN'овской табличкой:

ValueMeaning
STD_INPUT_HANDLEStandard input handle
STD_OUTPUT_HANDLEStandard output handle
STD_ERROR_HANDLEStandard error handle

  Однако вместо мнемонического интуитивно-понятного аргумента STD_OUTPUT_HANDLE вносили в стек значение -11, неизвестно откуда взятое.   Давайте напишем сразу же после директивы includelib следующую строчку:

Код (Text):
  1.  
  2. STD_OUTPUT_HANDLE equ -11d
  3.  

  А строчку push -11 заменим на push STD_OUTPUT_HANDLE.
  Что получилось? Программа откомпилировалась без проблем, ибо в самом начале листинга мы прописали equ[валент]. Проще говоря, мы сказали ассемблеру: "если ты встретишь в тексте программы STD_OUTPUT_HANDLE, то имей в виду, что это то же самое, что и -11". Другими словами, завели нечто типа константы (не переменную!) с именем STD_OUTPUT_HANDLE и значением -11.
  Теперь откройте файл windows.inc и полюбуйтесь его содержимым. Там целая куча "эквивалентов", наподобие вышерассмотренного! И чтобы воспользоваться этой халявой - вовсе не обязательно копировать ту или иную константу через буфер обмена. Можно поступить намного проще - добавить в исходник директиву

Код (Text):
  1.  
  2. include [путь к файлу] windows.inc
  3.  

  В ответ на это ассемблер сам извлечет из windows.inc всю имеющуюся в этом файле информацию и преподнесет ее транслятору на блюдечке с голубой каемочкой.

  #4. Вторая халява, которой мы с вами воспользуемся - это "инклуды" (давайте именно так будем называть файлы *.inc) с прототипами функций. Мы уже рассматривали, что такое прототипы, и какую роль они играют при линковке нашей программы с библиотеками импорта. Конечно же, мы можем сами, на основе MSDN'овкого описания функции, вывести ее прототип, но зачем нам приумножать сущности сверх необходимого? Ведь в MASM32 для каждой из библиотек импорта есть и одноименный файл с прототипами. В нашем примере мы использовали функции kernel32 и для этого линковали его с библиотекой kernel32.lib? Ну а соответствующий файл с прототипами называется kernel32.inc!
  Что может быть проще? Из нашего исходника вырезаем к черту блок с прототипами, а на его место лепим директиву include [путь] kernel32.inc. Компилим, и, как говорят по телику, "теперь вы можете забыть об этих неудобных промокающих :" (ууупс... опять пошли брутальные фантазии; время начинать новый абзац...).
  Теперь, пожалуй, пришло время сдержать свое обещание и объяснить - какого черта мы к концу функции WriteConsole прилепили букву "А". Объясняю - а потому что нет в винде функции WriteConsole!

  #5. ...зато есть функции WriteConsoleA и WriteConsoleW. "A" - это если вы хотите напечатать строку в формате ASCII (т.е. каждый знак занимает один байт), а "W" - если в Unicode (W - от wide, широкий. В Unicode знаки не 8-битные, а 16-битные, и занимают два байта). Подобные окончания имеют только те функции, которые тем или иным образом работают со строковыми значениями. Функция ExitProcess, например, подобного буквенного окончания не имеет - посудите сами, не все ли равно, на каком национальном языке завершать работу приложения?
  Откроем файл kernel32.inc и пристально посмотрим на его содержимое, в частности, на следующее:

Код (Text):
  1.  
  2. WriteConsoleA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
  3. WriteConsole equ <WriteConsoleA>
  4.  

  Как видим, команда разработчиков MASM32 позаботилась не только о простыне прототипов, но и о "независимости" нашего исходника от выбранной кодировки. То есть для того, чтобы "перезаточить" программу под UNICODE, нам вовсе не нужно заменять окончание A на W в имени функции. Достаточно просто приинклюдить другой файл с прототипами и эквивалентами наподобие

Код (Text):
  1.  
  2. WriteConsoleW PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
  3. WriteConsole equ <WriteConsoleW>
  4.  

и не "париться" с переписыванием исходника.
  Надо отметить, в MASM32 подобного "юникодного" инклуда нет, однако вы легко можете сделать его сами.

  #6. И, наконец, третья, самая большая "халява" - это маленькая фенечка, использование которой сразу же превращает макроассемблер из языка кодирования в язык программирования!
  С помощью этой "фенечки" целый блок инструкций:

Код (Text):
  1.  
  2. push 0
  3. push 0
  4. push 16d
  5. push offset sWriteText
  6. push hStdout
  7. call WriteConsoleA
  8.  

  мы с легкостью можем заменить одной-единственной строчкой:

Код (Text):
  1.  
  2. invoke WriteConsoleA, hStdout, offset sWriteText, 16d, 0, 0
  3.  

  Обратите внимание, что при использовании этой команды параметры мы передаем слева направо, в той же очередности, что и вещает нам MSDN. В отличие от простыни "пушей" c "каллом" в конце.

  #7. Теперь самый главный момент... Затаите дыхание!
  В свете вышесказанного, вышерасписанного и вышерасжеванного наш исходник принимает весьма красивый "высокоуровневый" вид:

Код (Text):
  1.  
  2.  .386
  3.  .model flat,stdcall
  4. option casemap:none
  5.  
  6. includelib kernel32.lib
  7. include windows.inc
  8. include kernel32.inc
  9.  
  10.  .const
  11.  
  12. sConsoleTitle db 'My First Console Application',0
  13. sWriteText db 'hEILo, Wo(R)LD!!'
  14.  
  15.  .code
  16.  
  17. Main PROC
  18.   LOCAL hStdout :DWORD
  19.  
  20.   invoke SetConsoleTitle, offset sConsoleTitle
  21.   invoke GetStdHandle, STD_OUTPUT_HANDLE
  22.   mov hStdout,EAX
  23.   invoke WriteConsole, hStdout, offset sWriteText, 16d, NULL, NULL
  24.   invoke Sleep, 2000d
  25.   invoke ExitProcess, NULL
  26.  
  27. Main ENDP
  28.  
  29. end Main
  30.  

  А что? Самое время выпить бутылочку пива ;). © Serrgio / HI-TECH


0 2.106
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532