Я оставил эту тему на сладкое. Теперь, когда вы знаете так много или хотя бы слышали о многом, можно представить себе, что вы пишете свою программу. Если среди вас есть анархисты, то пусть они лучше выключат компьютер. Потому что эта глава будет о том, как упорядочить и стандартизировать процесс написания программы.
Допустим, у вас родилась идея создать собственный "обработчик" "документов". Ну, предположим, текстовый редактор. В такой программе должны быть следующие возможности:
Дальше кто во что горазд. Даже этот минимальный набор возможностей, или, как говорят, функций программы, требует организации её кода. Можно, конечно, и всё подряд писать в одну кучу, но даже если вы сможете дописать хаотичную программу до полезного состояния, развивать новые возможности уже не получится, придётся переписать всё заново.
Обычно простое оконное приложение для форточек имеет примерно такую структуру:
вступление вызов главной функции завершение Главная функция: регистрация окна первая прорисовка окна цикл выборки сообщений Windows возврат из главной функции Оконная процедура: в зависимости от сообщения, переданного системой, идёт: - вызов функции открытия документа - вызов функции сохранения документа - вызов функции редактирования документа (если нажата буква, например) - вызов стандартной системной функции обработки окна - вызов системной функции завершения окна возврат из оконной процедуры Функция открытия документа: ... ... возврат из открытия документа Функция сохранения документа: ... ... возврат из сохранения документа Функция редактирования документа: ... ... возврат из редактирования документа
Если вместо точек подставить код самих функций, а словесное описание заменить на вызовы соответствующих API,
то получится программа =). Впрочем, не стоит забегать слишком далеко.
В языках высокого уровня понятия функция и процедура немножко отличаются (входом/выходом). Но насколько мне известно, на Ассемблере не принято различать процедуры и функции.
Однако не стоит путать функции с подпрограммами, и уж тем более с подпрограммами прерываний. Прерывания - особый вид подпрограмм (они имеют большую аппаратную поддержку: системная таблица, особые маш. коды и т.п.).
Подозреваю, что новичок не воспримет эту схему сразу же. Поэтому рекомендую взглянуть на неё еще раз после прочтения двух следующих статей.
Чтоб создавать такие приложения, лучше всего использовать команды:
Происхождение | От англ. слова call - звать, вызывать |
Формат | call адрес или [указатель на адрес функции] |
Действие | Сохраняет в стеке адрес следующей команды (для возврата) и переходит к выполнению указанной функции, то есть на адрес её первой команды. |
Примечание | Для возврата из функции используется команда RET |
Происхождение | От англ. слова return - вернуться |
Формат | ret (число) |
Действие | Извлекает из стека значение и совершает переход по нему. Если было число, то после этого ещё и увеличивает ESP, тем самым чистит стек на указанное количество байт (а не значений). |
Примечание | Следите за стеком, если вы не выровняли его перед выходом из функции, возврат будет ошибочным. |
Кроме того, что эти команды структурируют вашу программу, они позволяют ещё и подключаться к основным возможностям Windows.
Дело в том, что программисты - очень ленивый народ. А МелкоМягкие делают всё, чтоб мы были ещё более ленивые. Почти всё, что нужно сделать в стандартной программе, уже есть в функциях Windows. Они называются API (Application Programming Interface, программный интерфейс приложений). Мы уже познакомились с API-функцией MessageBox и знаем, что другие вызываются так же.
Конечно, про лень я пошутил, потому что суть этих функций в уменьшении:
Также API-функции являются частью механизма защиты. Ваша программа не может сама обращаться к общим устройствам, не остановив всех вокруг. А при вызове нужной функции Windows неявное обращение к экрану или диску не создаст конфликтов программ (по карйней мере так задумано).
И надо сказать, что API неплохо справляются с этими задачами. Нужно только знать, как с ними обращаться.
Параметры функциям Win32-API передаются через стек. А значения они возвращают в регистр EAX и различные переменные.
Есть хорошие справочники на этот счёт, и в следующих статьях я про них уже писал.
Для того чтоб отлаживать процедуры и API-функции (а также подпрограммы, прерывания, системные вызовы), нужно в отладчике выполнять пошаговую трассировку с заходом в функцию. В OllyDbg клавиша F7 как раз и назначена для этого (step into - шаг внутрь).
Вы уже должны были запомнить, что F8 выполняет одну строку кода программы. Когда в проге встретится команда "call", это значит, что будет вызвана функция. На подобной строке вы имеете возможность нажать F8, что приведёт к выполнению всех действий данной процедуры и возврату из неё. Если же вы нажмёте F7, то произойдёт прыжок на функцию (а точнее вход в неё) и отладка продолжится по одной машинной команде.
Нажатие F7 на простой команде в коде ничем не отличается от F8.
Olly не умеет отлаживать системные вызовы.
Вход в функцию с командой "sysenter" или "syscall" здесь невозможен (не больно-то вам и надо :).
Вы уже знаете почти всё о prax07 (на начальном уровне).
Давайте загрузим пример в Olly.
Эта программа извлекает аргументы, переданные через командную строку, и сообщает их через MessageBox на экран.
00401000 E8 59000000 call < jmp.kernel32.GetCommandLineA> ;[GetCommandLineA
В первой же строчке кода вызывается API-функция. Но в данном случае это звучит слишком гордо - API-функция :).
Выполните её с заходом (F7). Сначала произойдёт вызов последней строки кода программы (её при сборке exe подставил линковщик из связанного файла kernel32.lib). А затем произойдёт прыжок на саму функцию. Взгляните, из чего она состоит (F7). Всего две команды:
mov EAX, [адрес] ret
Предполагаю три вопроса:
Ну, на первый вопрос ответить очень просто. "A" - означает ANSI (текстовая кодировка Win).
После внедрения Unicode появились разные версии функций.
Юникод по-другому называют WideChar (worldwide character-encoding - всемирная кодировка символов).
Значит, если название функции заканчивается на W - это Unicode-версия API-функции.
В технологии WinNT все функции Unicode, а ANSI-версии всего лишь перемычки к W-функциям.
Второй вопрос чуть сложнее. Действительно, зачем вообще вызывать такую хрень,
почему нельзя сразу в нашей программе набрать одну-единственную строку из этой функции?
Дело в том, что Винды формируют адресное пространство процесса с разными смещениями (адресами).
Никто не знает, по какому адресу будет спроецирована та или иная внешняя функция,
и адрес размещения командной строки в памяти тоже заранее не определён.
Значит, нужен механизм исправления вызовов и перемещаемых адресов (relocation, fixup table - на слэнге релоки и фиксапы).
Так что лучше пока доверить выяснение адреса командной строки самой операционной системе.
Третий вопрос напрямую вытекает из второго ответа. Раз заранее неизвестно, куда прыгать на функцию такую-то, удобнее создать таблицу, которая будет заполняться правильными адресами в памяти (import table). В exe-файле хранится информация обо всех функциях, которые будут вызваны программой извне (в PE-директории import). Загрузчик программ в форточках выполняет много работы по построению виртуального адресного пространства процесса, в том числе и правки всех необходимых переходов вызовов и прыжков.
Строка "call < jmp.kernel32.GetCommandLineA> " вызвала строку: jmp dword ptr [< kernel32.GetCommandLineA> ]
Такие строки как раз и переключают нас на API-функции. Здесь Оли потрудилась вставить название функции, на которую нас перебросит. Но на самом деле строка выглядит так:
jmp dword ptr [00402000]
При команде jmp квадратные скобки (так же, как и при mov) означают, что операнд надо извлечь из указанного в них адреса.
Получается, что этот прыжок обращается к одной из ячеек таблицы импорта, заполненной после загрузки программы в память правильным адресом API-функции GetCommandLineA. Когда функция отработает, она вернёт нас командой ret к следующей строке после call (а не jmp).
Ещё вам нужно знать, что текстовые строки всегда оканчиваются нулевым байтом, это иногда называется ANSIz-строка (ANSI - кодировка Win, z - zero).
После выполнения этой функции: В EAX окажется адрес командной строки в кавычках и аргументы за пределами кавычек. Пример: "D:\tut\prax07.exe" аргументы |
00401005 B9 00020000 mov ecx, 200 0040100A 03C8 add ecx, eax
После выполнения этих инструкций: В ECX будет адрес командной строки + 512d |
0040100C 40 inc eax 0040100D 3BC1 cmp eax, ecx 0040100F 74 26 je short prax07.00401037 00401011 8038 22 cmp byte ptr [eax], 22 00401014 75 F6 jnz short prax07.0040100C
"short prax07." - к Ассемблеру не имеет отношения. Так Оли поясняет, что прыжок короткий (+128d -127d) и точка перехода - в рамках модуля "prax07".
В этом цикле находится адрес закрывающей кавычки. Допустим, при загрузке что-то произошло и командная строка была повреждена, такое вполне возможно, даже мы сейчас можем вписать туда что угодно. Тогда этот цикл мог бы работать до нахождения следующего байта 22h в памяти. Но его там может и не быть. В таком случае произошла бы ошибка чтения вне адресного пространства программы. Чтобы избежать этой случайности, я добавил аварийный выход из этого цикла. Благодаря строчкам 0040100D и 0040100F цикл не сможет повториться более 511 раз. Я предположил, что 511 символов в пути к файлу и его имени вполне достаточно (но, может быть, я и не прав).
Цикл может прерваться по следующим причинам: Текущий адрес стал на 512 байт больше начала командной строки. Тогда будет прыг на плохое сообщение. Найден байт со значением 22h (символ кавычки). Если это так, в строке 00401014 прыжка не будет. |
00401016 40 inc eax 00401017 8038 00 cmp byte ptr [eax], 0 0040101A 74 1B je short prax07.00401037 0040101C 8038 20 cmp byte ptr [eax], 20 0040101F 74 F5 je short prax07.00401016
В этом цикле выясняется, есть ли после кавычки байт 00 (конец строки) и байт 20h (символ пробела).
В зависимости от способа запуска примера могут быть разные комбинации. Если аргумента нет, может быть, строка закончится сразу нулём, а может, будет ещё пробел или два. Кроме того, нам всё равно нужно найти первый байт самого аргумента, и он может быть через несколько пробелов.
Учтите, prax07 - просто пример. В реальных условиях вероятны другие ситуации. Я за этот пример не отвечаю как за болванку для ваших программ. Но у меня на XP всё работало при любых обстоятельствах, которые я только мог придумать.
Цикл может прерваться по следующим причинам: Был найден байт 0. Тогда будет прыг на плохое сообщение. Не был найден байт 0 или 20h. В строке 0040101F прыжка не будет. |
00401021 6A 00 push 0 ; Style = MB_OK|MB_APPLMODAL 00401023 68 00304000 push prax07.00403000 ; Title = "It's My first command line for Win32" 00401028 50 push eax ; Text 00401029 6A 00 push 0 ; hOwner = NULL 0040102B E8 22000000 call < jmp.&user32.MessageBoxA> ; MessageBoxA
Эти строки будут выполняться только при условии, что был найден байт со значением не 20h и не 00h после найденной закрывающей кавычки.
Сначала в стек укладывается четвёртый параметр с точки зрения функции. Данный параметр задаёт стиль (внешний вид) сообщения. При значении 0 сообщение будет без иконки и с кнопкой OK по середине.
Затем третий параметр (строка 00401023). Данный параметр является адресом строки текста с нулевым байтом на конце. Эта строка будет титульной (на верхней полоске).
Второй параметр (00401028) - тоже адрес строки текста с нулевым байтом на конце (само сообщение внутри бокса).
Ну и последний параметр (первый с точки зрения функции). Нужно указать Хендл окна (уникальный номер), от которого вызывается сообщение. Это делается для того, чтоб прервать всякие действия с этим окном, пока не отработает функция сообщения.
Окна у нас нет, сообщение вызывается само по себе - значит, передаём нулевое значение (что такое handle, вы узнаете из последующих статей).
После того, как в стек будут уложены все необходимые параметры, произойдёт вызов самой функции. Пользователь нажмёт кнопку OK на сообщении, и функция вернёт управление программе со следующей строки. Из стека будут убраны 4 параметра, которые мы передали функции. |
Вы, возможно, забыли, но я, когда описывал директиву ".model", сказал, что о параметре "Stdcall" мы ещё поговорим. Вот теперь вы поймёте, что "уговор" Stdcall - это стандарт по вызову функций. По данному стандарту параметры передаются в обратном порядке (старший - высший, младший - ближний к вызову). И функция сама очищает из стека параметры, переданные ей. В форточках по этому уговору построены все API-функции, и мы будем использовать только его.
00401030 6A 00 push 0 ; ExitCode = 0 00401032 E8 21000000 call < jmp.kernel32.ExitProcess> ; ExitProcess
Сразу же после выполнения API MessageBox программа завершается вызовом ExitProcess с параметром 0.
После выполнения этих строк: Программа завершится... Совсем. |
00401037 6A 00 push 0 ; Style = MB_OK|MB_APPLMODAL 00401039 68 00304000 push prax07.00403000 ; Title = "It's My first command line for Win32" 0040103E 68 25304000 push prax07.00403025 ; Text = "Аргументы командной строки отсутствуют" 00401043 6A 00 push 0 ; hOwner = NULL 00401045 E8 08000000 call < jmp.user32.MessageBoxA> ; MessageBoxA
Если программа запускалась с ключом, то данное сообщение выводиться не будет. Сюда мы прыгнем, только если был превышен лимит в 512 символов или найден байт 0 без каких-либо полезных аргументов. Строка 00401037 в исходнике обозначается меткой "NO".
0040104A 6A 00 push 0 ; ExitCode = 0 0040104C E8 07000000 call < jmp.kernel32.ExitProcess> ; ExitProcess
Здесь точно такой же выход, как при первом сообщении.
После выполнения этих строк: Программа завершится. |
00401051 CC int3
Прерывание 3 - особое среди прерываний, оно вызывается одним байтом. Для того, чтоб простой отладчик мог останавливать программу после каждой строки кода, придумали такое прерывание. Оно также используется практически всеми отладчиками для установки Breakpoint'ов (BP - точка останова).
Здесь это прерывание используется для заполнения лишнего байта (чтоб выровнять код). Некоторые компиляторы заполняют такие байты командой nop (байт 90h). В принципе без разницы, что это будет за байт, так как это и не код, и не данные. Но в исключительных ситуациях (мало ли сбой какой) лучше, чтоб он был CCh. Допустим, произойдёт выполнение такого байта, тогда программа сразу же аварийно прекратится и не случится возможной серьёзной ошибки. В случае если загружен отладчик и в нём включена опция int3 breakpoint (или что-то в этом роде), отладчик перехватит программу, и мы увидим, из-за чего могла случиться такая ситуация.
Эта строка выполняться не должна. |
00401052 FF25 0C204000 jmp dword ptr [< user32.MessageBoxA> ] ; user32.MessageBoxA 00401058 FF25 04204000 jmp dword ptr [< kernel32.ExitProcess> ] ; kernel32.ExitProcess 0040105E FF25 00204000 jmp dword ptr [< kernel32.GetCommandLineA> ]; kernel32.GetCommandLineA
Это код, но мы его не писали. Он взялся из прикреплённых файлов импорта к нашему исходнику (user32.lib и kernel32.lib).
Повторюсь, здесь происходят прыжки к API-функциям, адрес которых извлекается из таблицы импорта.
Кроме API-функций, в Import Table указываются адреса всех внешних функций. Ведь наша программа может состоять из нескольких файлов. Чаще всего так вызываются функции из динамически подключаемых библиотек (dll).
User32 и kernel32 это как раз и есть dll-файлы, они находятся в системной папке форточек и содержат основные API-функции. Правда, есть ещё и другие библиотеки dll, которые мы тоже будем использовать, но об этом в следующий раз.
Вся теория изложена.
Вы сейчас, скорее всего, мало что уяснили. Но главная цель, я надеюсь, достигнута - у вас должна быть целая куча вопросов. Я постараюсь ответить на часть из них в следующем витке.
Ещё одна полезная команда.
Происхождение | От англ. слова TEST - проверять |
Формат | test приёмник, источник |
Действие | Производит логическое сравнение. Операнды не изменяет! Только флаги. Если оба из сравниваемых битов в операндах равны 1, то результат равен 1. Во всех остальных случаях результат - 0. ZF=1, если в результате получился ноль! ZF=0, если результат не ноль. |
Примечание | Очень часто команду test используют для выяснения, обнулён ли регистр, например: "test EAX,EAX" включает флаг нуля, если регистр был пуст. |
Опять же логические команды удобно представлять так:
Биты операнда: 0101 test Биты операнда: 0011 ---- мнимый результат:0001
Примеры использования команды.
Выяснить, содержит ли старший байт регистра EAX хоть что-нибудь, кроме нулей, можно так:
test EAX,0FF000000h
Если в EAX старший байт содержит не нулевое значение, то ZF=0.
Выяснить, включён ли БИТ номер 4 в восьмибитном регистре AL, можно так:
test AL, 00010000b
Флаг нуля выключится, только если в AL бит номер 4 будет в положении 1. Не забывайте: биты нумеруются от нуля, 000?0000 - потому и номер 4.
Обратите особое внимание, что флаг нуля меняется так:
Если результат не ноль, флаг нуля опускается.
Если результат ноль, флаг нуля поднимается.
Устали? Отдохните как следует, потому что сейчас будет экзамен за виток0. По себе знаю, что разбирать чужие закорючки значительно сложнее, чем писать свои, но вы всё-таки постарайтесь разобрать следующие два примера. Предполагаю, что у вас уйдёт на это несколько часов. Зато когда вы всё разберёте, вы почувствуете новые возможности в своей голове, я вам обещаю.
Матрос, беги, тебя вызывает Земля на удалённый экзамен. Удачи, малыш.
prax08.asm
(использование стека для хранения данных и передачи параметров)
Cкопируйте файл prax05.asm и назовите prax08.asm.
Измените секцию данных в исходнике:
.data MsgBoxCaption db "It's the first your program for Win32",0 MsgBoxText db "Assembler language for Windows is a fable!",0
Замените на:
.data MsgBoxCaption db "It's the first your debuging for Win32",0 MsgBoxText db "_ssembler language for Windows is a fable!",0
Удалите две строки в секции кода из того же исходника:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK invoke ExitProcess, NULL
И впишите на их место такой код:
mov EAX, offset MsgBoxCaption mov EBX, offset MsgBoxText mov byte ptr [EBX],41 ; Эта команда подменяет код символа "_" в строке MsgBoxText push EAX push EBX push 0 push EAX push EBX push 0 call MessageBox ; ------------------------ pop EAX pop EBX push 0 push EAX push EBX push 0 call MessageBox ; ------------------------ pop EBX pop EAX push 0 push EAX push EBX push 0 call MessageBox ; ------------------------ push 0 call ExitProcess
Цель программы - вывести три одинаковых сообщения.
С заголовком - "It's the first your debuging for Win32".
И текстом - "Assembler language for Windows is a fable!"
Задания:
prax09.asm
(программа с несколькими функциями)
.386 .model flat, stdcall option casemap :none ; case sensitive ;######################################################################### include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib ;######################################################################### .data MsgCaptionNO db "Нечего обрабатывать!",0 MsgTextNO db "Аргументы командной строки отсутствуют",0 MsgCaptionYES db "Обработанный результат:",0 Crypt_String byte 100h dup (00) ; Слева от dup - количество, справа - что повторять ;######################################################################### .code start: call Main ; вызов главной функции push 0 ; пустой параметр для процедуры выхода call ExitProcess ; вызов API-функции выхода ;========================================================================= Main proc ; начало главной функции call Take_Arguments ; вернёт в EAX указатель на ключ командной строки, если нет - EAX=0. test eax,eax ; проверить EAX на ноль jnz Next1 call Message ret Next1: call Crypt call Message ret Main endp ; конец главной функции ;========================================================================= Take_Arguments proc ; начало функции получения аргумента командной строки ; Вход - ничего. ; Выход - EAX= указатель на аргументы командной строки или 0. call GetCommandLine mov ECX,512d add ECX,EAX unquote: inc EAX cmp EAX,ECX jz NO_Arg cmp byte ptr[EAX],22h jnz unquote Arg_search: inc EAX cmp byte ptr[EAX],0 jz NO_Arg cmp byte ptr[EAX],20h jz Arg_search ret NO_Arg: xor eax,eax ret Take_Arguments endp ; конец функции получения аргумента командной строки ;------------------------------------------------------------------------- Crypt proc ; начало функции шифрования ; Вход - в EAX должен быть указатель на строку, ; заканчивающуюся нулём (это и есть параметр функции). ; Выход - в EAX будет указатель на шифрованную строку. ; Используется глобальная переменная Crypt_String. push EBX push ECX push EDX mov EBX, offset Crypt_String mov ECX,00001010b Crypt_Loop: movzx EDX, byte ptr [EAX] test EDX,EDX jz Fin_Crypt xor DL,CL mov byte ptr [EBX],DL inc EAX inc EBX xor CL, 00000101b jmp Crypt_Loop Fin_Crypt: pop EDX pop ECX pop EBX mov EAX, offset Crypt_String ret Crypt endp ; конец функции шифрования ;------------------------------------------------------------------------- Message proc ; начало функции вывода сообщений ; Вход - EAX=0 или указатель на строку текста, заканчивающуюся нулём. ; Выход - ничего. ; Используются глобальные переменные: ; MsgCaptionYES, Crypt_String, MsgCaptionNO, MsgTextNO. test EAX,EAX jz No_Arguments push 0 push offset MsgCaptionYES ; offset - директива push EAX push 0 call MessageBox ret No_Arguments: push 0 push offset MsgCaptionNO ; адрес заголовка push offset MsgTextNO ; адрес текста сообщения push 0 ; нет родительского окна call MessageBox ; вызов API-функции вывода сообщения на экран ret Message endp ; конец функции вывода сообщений ;------------------------------------------------------------------------- end start
Задания:
Собственно говоря, я надеюсь, вы сами справитесь со всеми заданиями, но для проверки скажу, что в prax08.asm были расставлены следующие ошибки:
Ну что тут скажешь, конечно, мы ещё не гуру низкоуровневого программирования, но если вы всё смогли выполнить, у вас есть все шансы стать свободным человеком. Свободным от запретов и ограничений в вашем собственном компьютере.
P.S. Дальше - больше.
Bitfry