Статии - Програмиране на ниско ниво за гражданите
Асемблер Асемблер Статии Програмиране на ниско ниво за старите Уроци Кракване DirectX/OpenGL   Оптимизация   Компилатори   Вирусология   Работа в мрежа   Процесори   Инструмент за изследване на софтуера източници на комплекти
#1. С леката лява ръка на Денис Ричи беше обичайно да започнете да овладявате нов език за програмиране със създаването на най-простата програма „Здравей, свят“. Нищо човешко не ни е чуждо - нека извършим този грях. В предпоследния брой вече говорих за това как се работи с apish функции в асемблер, но вероятно не сте разбрали ;). Това е нормално и не е нужно да се тревожите за това. Всичко ще стане повече от ясно, след като напишем една-две прости програми и ги анализираме ред по ред.
Прочетете отново „Минимално приложение“ и въведете следния изходен код:
.386 .model flat,stdcall опция casemap:няма
SetConsoleTitleA PROTO :DWORD GetStdHandle PROTO :DWORD WriteConsoleA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD ExitProcess PROTO :DWORD Sleep PROTO :DWORD
sConsoleTitle db 'Моето първо конзолно приложение',0 sWriteText db 'hEILo, Wo(R)LD!!'
Главен PROC ЛОКАЛЕН hStdout :DWORD ;(1)
;заглавие на конзолата натискане на отместване sConsoleTitle ;(2) извикване на SetConsoleTitleA
;получаване на манипулатор на изхода ;(3) push -11 извикване GetStdHandle mov hStdout,EAX
; изход HELLO, WORLD! ;(4) push 0 push 0 push 16d push offset sWriteText push hStdout call WriteConsoleA
;закъснение за възхищение ;(5) натисни 2000d повикване Sleep
;изход ;(6) натискане 0 извикване на ExitProcess
Ето два реда от моя пакетен файл (*.bat), който ви позволява да не "парите" скоманден ред: c:\tools\masm32\bin\ml /c /coff hello.asm c:\tools\masm32\bin\link /SUBSYSTEM:CONSOLE /LIBPATH:c:\masm32\lib hello.obj
Обръщам внимание на факта, че за да създадете конзолно приложение, трябва да използвате ключа /SUBSYSTEM:CONSOLE. Въпреки факта, че прозорецът, в който се стартира, болезнено прилича на "MS-DOS сесия", получената програма е пълноценно 32-битово приложение на Windows във формат PE. Сглобяваме, свързваме, стартираме, наслаждаваме се.
#2. А сега нека организираме това разглобяване на източника. Крак 1. Така че дефинираме локална променлива с име hStdout с размер DWORD. Защо местни? Но тъй като тя съществува само в основната процедура и ако се опитаме да получим достъп до променливата hStdout извън тази процедура, асемблерът ще ни се скара с всякакви лоши думи - за разлика, да речем, от константата sWriteText, чието име е "познато" навсякъде в нашата програма.
Обърнете внимание на префикса h в името на променливата. Просто оставих напомняне за себе си, че променливата е поставена под манипулатора.
Crack 2. API функция SetConsoleTitleA - задайте заглавието (title) за прозореца на нашата конзола. Ето извадка от MSDN: BOOL SetConsoleTitle( LPCTSTR lpConsoleTitle // ново заглавие на конзолата );
Както можете да видите, функцията изисква един единствен параметър - указател към символния низ, който искаме да изведем в заглавието на прозореца. Низът трябва да завършва с нула.
Тук трябва да имате напълно логичен въпрос - защо добавихме буквата А в края на функцията? В MSDN няма буква А. Ще отговоря на този въпрос малко по-късно.
Прекъсване 3. Можем да използваме конзолата като входно устройство (входно устройство), изходно устройство (изходно устройство), устройствоза съобщаване на грешки (устройство за грешка). За да работим с това "устройство", трябва да получим манипулатора му, като използваме следната функция: HANDLE GetStdHandle( DWORD nStdHandle // устройство за вход, изход или грешка );
Единственият параметър, който изисква от нас, е индикация за кое устройство искаме да получим ръкохватката "ticket". Ето таблицата: Стандартен манипулатор за въвеждане -10 Манипулатор Stdout -11 Манипулатор за грешки -12
Какво ни трябва? Изведете линията! И така - изискваме манипулатор за стандартния изход, тоест преди да извикаме функцията "pop" в стека -11. След като функцията се изпълни, EAX регистърът съдържа така желаната "стандартна изходна манипулация". Поставяме този манипулатор в променливата hStdout (която толкова благоразумно дефинирахме в прекъсване 1) за по-късна употреба.
- Що за позор е това? - възкликвате вие. - Каква маса е толкова нездравословна? Някои отрицателни числа, които никога не помните! Искаме таблица като в MSDN! Така че не -10, -11, -12, а дълги мнемонични STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE!
Спокойно! Източникът, който сега разглеждаме, много точно отразява реалните процеси, протичащи в програмата. Малко по-късно ще го пренесем във вариант в стил C и ще видим как някои конструкции от високо ниво могат да бъдат използвани, за да направят живота много по-лесен за програмист от ниско ниво.
Crack 4. И накрая, най-важното нещо е функцията, която всъщност извежда низ от знаци на конзолата. Ето описанието му: BOOL WriteConsole( HANDLE hConsoleOutput, // манипулатор на екранния буфер CONST VOID *lpBuffer, // буфер за запис DWORD nNumberOfCharsToWrite, // брой знаци за запис LPDWORD lpNumberOfCharsWritten, // брой записани знаци LPVOIDlpReserved //резервирано );
Ние дешифрираме. Преди да извикаме функцията WriteConsole, трябва да поставим най-много пет параметъра в стека:
Дръжка. Какви проблеми? Вече го получихме и благоразумно го запазихме в променливата hStdout. Поставяме го в стека с командата push hStdout и това е всичко.
Броят знаци, които искаме да отпечатаме. В смисъл - броя на "буквите" от низа sWriteText. Колко знака има в низа "hEILo, Wo(R)LD!!"? Включително интервали - 16г. Пишем - натиснете 16d. Имайте предвид, че функцията WriteConsole не изисква нула в края на буфера!
Указател към променлива, която ще върне броя на отпечатаните знаци. Функцията любезно ни казва колко от шестнадесетте знака е успяла да отпечата. Също така изисква променлива, в която да пренесе тази информация към него. Нека се преструваме, че не ни трябва, тоест да напишем 0. Нищо ужасно няма да се случи, но ще видим погрешността на този вид невежество в следващата глава. Пишем - натиснете 0, но за себе си оставяме бележка, че функцията все още иска нещо от нас.
резерва. Така да се каже, запазено за бъдещи версии. Чувствайте се свободни да пишете - натиснете 0.
Сега, след като покрихме всички параметри, забележете, че редът на параметрите на MSDN не съвпада с реда, в който ги поставяме в стека в нашия изходен код. Върнете се към Минималното приложение, елемент 12, и внимателно прочетете клаузите на конвенцията за stdcall. Сега ясно ли е?
Пауза 5. За да имаме време да се възхищаваме на резултата от работата на нашите праведници, използвайки функцията Sleep, ние предизвикваме софтуерно забавяне от 2 секунди. Мисля, че лесно можете да разберете параметрите.
И накрая, почивка 6 - изход от програмата.
Всъщност правилният стил включва изрично освобождаване на всички заети ресурси след преминаване на необходимостта оттях, включително манипулатори, въпреки факта, че те се затварят автоматично от ExitProcess. Но да се надяваме, че ако не го направим в малка програма като нашата, няма да се случи нищо лошо. Естествено, "ce форматът" не се брои.
#3. Сега правим първата стъпка, за да приведем суровините си в по-разбираема форма. И така, първото нещо, което ще разгледаме, са еквивалентите, посочени във файла /MASM32/windows.inc. Вече срещнахме етикет на MSDN: Стойност Значение STD_INPUT_HANDLE Стандартен манипулатор на въвеждане STD_OUTPUT_HANDLE Стандартен манипулатор на изход STD_ERROR_HANDLE Стандартен манипулатор на грешка
Въпреки това, вместо мнемоничния интуитивен аргумент STD_OUTPUT_HANDLE, стойността -11 беше избутана в стека, никой не знае откъде идва. Нека напишем следния ред веднага след директивата includelib: STD_OUTPUT_HANDLE equ -11d
И ние ще заменим линията push -11 с push STD_OUTPUT_HANDLE. Какво се случи? Програмата се компилира без проблеми, защото в самото начало на листинга написахме equ[valence]. Просто казано, казахме на асемблера: "ако срещнете STD_OUTPUT_HANDLE в текста на програмата, имайте предвид, че това е същото като -11." С други думи, те донесоха нещо като константа (не променлива!) С име STD_OUTPUT_HANDLE и стойност -11.
Сега отворете файла windows.inc и се полюбувайте на съдържанието му. Има цял куп "еквиваленти", като горния! И за да се възползвате от това безплатно, изобщо не е необходимо да копирате тази или онази константа през клипборда. Можете да го направите много по-лесно - добавете директивата include [път към файла] windows.inc към източника
В отговор на това самият асемблер ще извлече цялата информация в този файл от windows.inc и ще я представи на компилатора на сребърна чиния.
#4. Второто безплатно, което ще използваме, е "включва" (нека наречем *.inc файлове по този начин) с функционални прототипи. Вече разгледахме какво представляват прототипите и каква роля играят при свързването на нашата програма с библиотеките за импортиране. Разбира се, ние самите, въз основа на MSDN описанието на функция, извличаме нейния прототип, но защо трябва да умножаваме обекти извън необходимото? В крайна сметка в MASM32 за всяка от библиотеките за импортиране има файл със същото име с прототипи. В нашия пример използвахме функциите на kernel32 и за това го свързахме с библиотеката kernel32.lib? Е, съответният прототип файл се нарича kernel32.inc!
Какво може да бъде по-лесно? От нашия изходен код изрязваме блока с прототипи и на негово място извайваме директивата include [path] kernel32.inc. Нека компилираме и, както казват по телевизията, „сега можете да забравите за тези неудобни мокри:“ (упс, бруталните фантазии се завръщат; време е да започнете нов параграф.).
Сега може би е време да спазим обещанието си и да обясним - защо, по дяволите, залепихме буквата "А" в края на функцията WriteConsole. Обяснявам - но защото в Windows няма функция WriteConsole!
#5. . но има функции WriteConsoleA и WriteConsoleW. "A" е, ако искате да отпечатате низ в ASCII формат (т.е. всеки знак заема един байт), а "W" - ако е в Unicode (W - от широк, широк. В Unicode символите не са 8-битови, а 16-битови и заемат два байта). Само онези функции, които по някакъв начин работят с низови стойности, имат такива окончания. Функцията ExitProcess например няма такова буквено окончание - преценете сами, има ли значение на какъв национален език се прекратява приложението?
Нека отворим файла kernel32.inc и да разгледаме по-отблизо съдържанието му, по-специално,към следното: WriteConsoleA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD WriteConsole equ
Както можете да видите, екипът за разработка на MASM32 се погрижи не само за прототипите, но и за "независимостта" на нашия източник от избраното кодиране. Тоест, за да "пренаточим" програмата за UNICODE, изобщо не е необходимо да заменяме окончанието A с W в името на функцията. Просто включете друг файл с прототипи и еквиваленти като
WriteConsoleW PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
а не да "паря" с пренаписване на изходния код. Трябва да се отбележи, че MASM32 няма такова активиране на "unicode", но можете лесно да го направите сами.
#6. И накрая, третият, най-голям "безплатен" е малка дрънкулка, чието използване незабавно превръща асемблера на макроси от кодиращ език в език за програмиране! С помощта на тези "дреболии" цял блок от инструкции: push 0 push 0 push 16d push offset sWriteText push hStdout call WriteConsoleA
можем лесно да заменим с един ред: извикване WriteConsoleA, hStdout, отместване sWriteText, 16d, 0, 0
Моля, имайте предвид, че когато използвате тази команда, ние предаваме параметрите от ляво на дясно, в същия ред, в който MSDN ни излъчва. За разлика от листа "push" с "call" в края.
#7. Сега най-важният момент. Задръжте дъха си! В светлината на горното, по-горе и по-горе, нашият изходен код придобива много красива форма на "високо ниво": .386 .model flat,stdcall option casemap:none
includelib kernel32.lib включете windows.inc включете kernel32.inc
sConsoleTitle db 'Моето първо конзолно приложение',0 sWriteText db 'hEILo, Wo(R)LD!!'
Главен PROC ЛОКАЛЕН hStdout :DWORD
извиквамSetConsoleTitle, отместване sConsoleTitle извикване GetStdHandle, STD_OUTPUT_HANDLE mov hStdout,EAX извикване WriteConsole, hStdout, отместване sWriteText, 16d, NULL, NULL извикване на Sleep, 2000d извикване на ExitProcess, NULL Главен ENDP
И какво? Време е да изпием бутилка бира ;).