Занурення в assembler. Повний курс з програмування на асмі від ][

Написання ОС-завантажувачів, драйверів, переписування області пам'яті та інші завдання роботи з ЕОМ реалізуються з допомогою асемблера. Вибрані книги з асемблеру допоможуть зрозуміти принцип роботи машинно-орієнтованої мови та освоїти її.

1. Ревіч Ю. - Практичне програмування мікроконтролерів Atmel AVR мовою асемблера, 2014 р.

«Свіжа кров» у галузі програмування мікроконтролерів. Докладно викладено особливості Atmel AVR, є перелік команд та готові рецепти – асемблер на прикладах. Хороша річдля радіоаматорів та інженерно-технічних працівників, хоча підійде і початківцям кодерам: торкнулися історії, сімейства та можливості МК AVR. Варто відзначити, що введення лаконічне, що швидко перетікає в суть, тому нарікати на лірику не доведеться.

2. Калашніков О. – Асемблер – це просто. Вчимося програмувати, 2011 р.

Справжнє роздолля для новачків, які ще гуглять базову термінологію та шукають асемблер підручник. Це і є. Крім ознайомлення з мовою і перших програм, також порушені болючі точки - переривання: штука нескладна, але спочатку важка для сприйняття. З кожним розділом асемблер уроки ускладнюються, і на виході читач зможе писати програми на асемблері, оптимізувати їх, працювати з вірусами, антивірусами, пам'яттю та файловими системами.

3. Аблязов Р. - Програмування на асемблері на платформі x86-64, 2011

Акцент робиться на роботі процесора у захищеному режимі та long mode. Це незамінна база для програмування Win32 і Win64, яка зачіпає команди асемблера, переривання, механізми трансляції та захисту з урахуванням режимних відмінностей. Розглядається розробка віконних додатківта драйверів. Цей асемблерПідручник підійде кодерам-початківцям і тим, хто відразу перейшов до програмування на асемблері, але погано розібрався в апаратній платформі x86-64.

4. Столяров А. – Програмування мовою асемблера NASM для ОС Unix, 2011

Починаючи термінологією і закінчуючи взаємодією з ОС, це без перебільшень одне з найкращих навчальних посібників. Для тих, хто прагне освоїти програмування на асемблері, але не хоче перевантажувати книжкові полиці, достатньо цього підручника. Докладно розписано синтаксис мови асемблера NASM, порушено регістри та пам'ять, операції різної складності, команди, а також наведено приклади.

Для того, щоб машина могла виконати команди людини на апаратному рівні, необхідно задати певну послідовність дій мовою «нуликів і одиниць». Помічником у цій справі стане Асемблер. Це утиліта, яка працює з перекладом команд машинною мовою. Проте написання програми – дуже трудомісткий та складний процес. Ця мова не призначена для створення легень і простих дій. на Наразібудь-яка використовувана мова програмування (Ассемблер працює чудово) дозволяє написати спеціальні ефективні завданняякі сильно впливають на роботу апаратної частини. Основним призначенням є створення мікрокоманд та невеликих кодів. Ця мова дає більше можливостейніж, наприклад, Паскаль або С.

Короткий опис мов Ассемблера

Всі мови програмування поділяються на рівні: низький і високий. Будь-який із синтаксичної системи «сімейки» Асемблера відрізняється тим, що поєднує відразу деякі переваги найбільш поширених і сучасних мов. З іншими їх ріднить і те, що можна використовувати систему комп'ютера.

Відмінною рисою компілятора є простота використання. Цим він відрізняється від тих, що працюють лише з високими рівнями. Якщо взяти до уваги будь-яку таку мову програмування, Асемблер функціонує вдвічі швидше та краще. Для того, щоб написати в ньому легку програму, не знадобиться занадто багато часу.

Коротко про структуру мови

Якщо говорити про роботу і структуру функціонування мови, можна точно сказати, що його команди повністю відповідають командам процесора. Тобто Асемблер використовує мнемокоди, найбільш зручні людинідля запису.

На відміну від інших мов програмування, Асемблер використовує замість адрес для запису осередків пам'яті певні мітки. Вони з процесом виконання коду перетворюються на так звані директиви. Це відносні адреси, які не впливають на роботу процесора (не перекладаються в машинну мову), а необхідні для розпізнавання середовищем програмування.

Для кожної лінійки процесора існує своя При такому розкладі правильним буде будь-який процес, у тому числі перекладений

Мова асемблера має кілька синтаксисів, які будуть розглянуті в статті.

Плюси мови

Найбільш важливим та зручним пристосуванням мови Асемблера стане те, що на ній можна написати будь-яку програму для процесора, яка буде дуже компактною. Якщо код виявляється величезним, деякі процеси перенаправляє в оперативну пам'ять. При цьому всі вони виконують досить швидко і без збоїв, якщо звичайно, ними управляє кваліфікований програміст.

Драйвера, операційні системи, BIOS, компілятори, інтерпретатори і т. д. – це все програма мовою Асемблера.

При використанні дизассемблера, який здійснює переклад з машинного, можна запросто зрозуміти, як працює те чи інше системне завдання, навіть якщо до неї немає пояснень. Однак таке можливе лише в тому випадку, якщо програми є легкими. На жаль, у нетривіальних кодах розібратися досить складно.

Мінуси мови

На жаль, програмістам-початківцям (і найчастіше професіоналам) важко розібрати мову. Асемблер вимагає докладного опису необхідної команди. Через те, що потрібно використовувати машинні команди, зростає можливість помилкових дій і складність виконання.

Для того, щоб написати навіть саму просту програму, програміст має бути кваліфікованим, яке рівень знань досить високим. Середній фахівець, на жаль, найчастіше пише погані коди.

Якщо платформа, для якої створюється програма, оновлюється, всі команди необхідно переписувати вручну - цього вимагає сама мова. Асемблер не підтримує функцію автоматичного регулюванняпрацездатності процесів та заміну будь-яких елементів.

Команди мови

Як було зазначено вище, кожного процесора є свій набір команд. Найпростішими елементами, які розпізнаються будь-якими типами, є такі коди:


Використання директив

Програмування мікроконтролерів мовою (Ассемблер це дозволяє і чудово справляється з функціонуванням) самого низького рівняНайчастіше закінчується успішно. Найкраще використовувати процесори з обмеженим ресурсом. Для 32-розрядної техніки дана мовапідходить чудово. Часто в кодах можна побачити директиви. Що ж це таке? І навіщо використовується?

Для початку необхідно наголосити на тому, що директиви не перекладаються в машинну мову. Вони регулюють виконання компілятором. На відміну від команд, ці параметри мають різні функції, відрізняються не завдяки різним процесорам, а за рахунок іншого транслятора. Серед основних директив можна назвати такі:


походження назви

Завдяки чому дістала назву мова - "Ассемблер"? Йдеться про транслятор і компілятор, які і роблять зашифрування даних. З англійської Assembler означає не що інше, як збирач. Програма не була зібрана вручну, використано автоматичну структуру. Більше того, на даний момент вже у користувачів та фахівців стерлася різниця між термінами. Часто Асемблером називають мови програмування, хоча це лише утиліта.

Через загальноприйнятої збірної назви у деяких виникає помилкове рішення, що існує єдина мова низького рівня (або стандартні норми для неї). Щоб програміст зрозумів, про яку структуру йдеться, необхідно уточнювати, для якої платформи використовується та чи інша мова Асемблера.

Макрозасоби

Мови асемблера, які створені відносно недавно, мають макрозасоби. Вони полегшують як написання, і виконання програми. Завдяки їх наявності транслятор виконує написаний код в рази швидше. При створенні умовного вибору можна написати величезний блок команд, а простіше користуватися макрозасобами. Вони дозволять швидко перемикатися між діями у разі виконання умови або невиконання.

З використанням директив макромови програміст отримує макроси Асемблера. Іноді він може широко використовуватись, а іноді його функціональні особливостізнижуються до однієї команди. Їх наявність у коді полегшує роботу з ним, робить його більш зрозумілим та наочним. Однак слід все одно бути уважним – у деяких випадках макроси, навпаки, погіршують ситуацію.

Після багатьох років заняття, чим не потрапивши, вирішив повернутися до витоків. До програмування. Знову ж таки, зважаючи на безліч «сучасних досягнень» у цій галузі, було важко визначитися, чого насправді не вистачить, за що взятися було і приємно і корисно. Спробувавши багато чого потроху, вирішив повернутися туди, куди тягнуло з перших днів знайомства з комп'ютером (ще з копією творіння сера Сінклера) - до програмування на асемблері. Насправді, свого часу Асемблер я знав досить непогано (у даному випадкуговорю про x86), але майже 15 років нічого на ньому не писав. Таким чином, це своєрідне повернення «блудного сина».
Але тут чекало перше розчарування. Знайдені на просторах Інтернету книги, посібники та інші довідники з асемблеру, на мій глибокий жаль, містять мінімум інформації про те, як треба програмувати на асемблері, чому саме так, і що це дає.

Приклад з іншої області

Якщо брати як приклад бокс, то всі подібні посібники вчать виконувати удар, переміщатися стоячи на підлозі, але абсолютно відсутнє те, що робить бокс - боксом, а не «дозволеним мордобиттям». Тобто комбінаційна робота, особливості використання рингу, захисні дії, тактична побудова бою і тим більше стратегія бою не розглядаються взагалі. Навчили людину бити по «груші» і одразу на ринг. Це докорінно неправильно. Але саме так побудовано практично всі «підручники» та «керівництва» з програмування на асемблері.


Однак нормальні книги мають бути, швидше за все, під горою «шлаку» я їх просто не знайшов. Тому перш ніж заповнювати знання глобальним опис архітектури, мнемоніки та всіляких фокусів «як зліпити дулю з 2х пальців», підійдемо до питання програмування на асемблері з «ідеологічної» точки зору.

Ідилія?

Маленьке зауваження, далі за текстом буде використано класифікацію, яка відрізняється від поширеної в даний час. Однак це не є приводом для «суперечок про колір істини», просто в даному вигляді простіше пояснити думку автора на програмування.

Отже, сьогодні, здавалося б, для програмістів настала епоха щастя. Великий вибіркоштів на всі випадки життя та побажання. Тут тобі й мільйони «фреймворків»/«паттернів»/«шаблонів»/«бібліотек» та тисячі засобів, що «полегшують» програмування, сотні мов і діалектів, десятки методологій та різні підходи до програмування. Бери – не хочу. Але не «береться». І справа не в релігійних переконаннях, а в тому, що все це виглядає як спроба харчуватися чимось несмачним. За бажання і старанності можна пристосуватися і до цього, звісно. Але, повертаючись до програмування, здебільшого з запропонованого не видно технічної краси – видно лише безліч «милиць». Як результат, при використанні цих «досягнення», з-під «пензля художників» замість пейзажів, що зачаровують, виходить суцільна «абстракція», або лубки - якщо пощастить. Невже більшість програмістів такі бездарі, невчі і мають проблеми на рівні генетики? Ні, не думаю. То в чому ж причина?
На сьогоднішній день є безліч ідей та способів програмування. Розглянемо наймодніші з них.

  • Імперативне програмування – у цьому підході програміст задає послідовність дій, які призводять до вирішення завдання. У основі лежить поділ програми частини, виконують логічно незалежні операції (модулі, функції, процедури). Але на відміну від типізованого підходу (див. нижче) тут є важлива особливість- Відсутність «типізації» змінних. Іншими словами відсутнє поняття «тип змінної», замість нього використовується розуміння, що значення в одній і тій самій змінній можуть мати різний тип. Яскравими представником даного підходує Basic, REXX, MUMPS.
  • Типізоване програмування – модифікація імперативного програмуванняколи програміст і система обмежують можливі значення змінних. З найвідоміших мов – це Pascal, C.
  • Функціональне програмування – це більше математичний спосіброзв'язання задачі, коли рішення полягає у «конструюванні» ієрархії функцій (і відповідно створення відсутніх з них), що призводить до розв'язання задачі. Як приклади: Lisp, Forth.
  • Автоматне програмування – підхід, де програміст будує модель/мережу, що складається з об'єктів/виконавчих елементів, що обмінюються повідомленнями, як змінюють/зберігають свій внутрішній «стан» так і можуть взаємодіяти із зовнішнім світом. Іншими словами, це те, що зазвичай називають « об'єктне програмування»(Не об'єктно-орієнтоване). Цей спосіб програмування представлений Smalltalk.
А як багато інших мов? Як правило, це вже «мутанти». Наприклад, змішання типізованого та автоматного підходу дало «об'єктно-орієнтоване програмування».

Як бачимо, кожен із підходів (навіть без урахування обмежень конкретних реалізацій) накладає власні обмеження на саму техніку програмування. Але інакше й не може бути. На жаль, ці обмеження найчастіше створені штучно для «підтримання чистоти ідеї». У підсумку, програмісту доводиться «перекручувати» спочатку знайдене рішення у вигляд, хоч якось відповідний ідеології мови, що використовується або «шаблону», що використовується. Це навіть без урахування новомодних методик та способів проектування та розробки.

Здавалося б, програмуючи на асемблері, ми вільні робити все й так, що як побажаємо і дозволяє нам «залізо». Але як тільки нам захочеться використати універсальний драйвердля будь-якого типу обладнання, ми змушені змінювати свободу «творчості» на запропоновані (стандартизовані) підходи та способи використання драйвера. Як тільки нам знадобилася можливість використовувати напрацювання інших колег або дати їм можливість робити те саме з плодами нашої праці - ми змушені змінювати свободу вибору взаємодії між частинами програми на певні обговорені/стандартизовані способи.

Таким чином та «свобода», за якою часто рвуться в асемблер найчастіше виявляється «міфом». І цьому (розумінню обмежень і способам їх організації), на мій погляд, має приділятися підвищена увага. Програміст повинен розуміти причину обмежень, що вносяться, і, що відрізняє асемблер від багатьох мов високого рівня, мати можливість змінювати їх, при виникненні такої потреби. Однак зараз програміст на асемблері змушений миритися з обмеженнями, що вводяться мовами високого рівня, не маючи «пряників» доступних програмуючими на них. З одного боку, операційні системи надають безліч вже реалізованих функцій, є готові бібліотеки та багато іншого. Але способи їх використання, як спеціально, реалізовані без урахування виклику їх із програм, написаних на асемблері, а то й взагалі наперекір логіці програмування для x86 архітектури. В результаті зараз програмування на асемблері з викликом функцій ОС або зовнішніх бібліотек мов високого рівня – це «страх» і «жах».

Чим далі в ліс, тим товщі

Отже, ми усвідомили, що хоч асемблер дуже простий, але користуватися ним треба вміти. І основне злагодження - це необхідність взаємодії з середовищем виконання, де запускається наша програма. Якщо програмісти мовами високого рівня вже мають доступ до необхідним бібліотекам, функцій, підпрограм на багато випадків життя і їм доступні способи взаємодії із зовнішнім світом, у вигляді, узгодженому з ідеєю мови, то програмісту на асемблері доводиться продиратися крізь хащі всіляких перепон, поставлених на порожньому місці. Коли дивишся на те, що генерують мови високого рівня при компіляції, то складає відчуття, що ті, хто писав компілятори, або не мають поняття, як працює процесор з архітектурою x86, «або одне з двох» (ц).

Тож давайте по-порядку. Програмування – це насамперед інженерія, тобто наукова творчість, спрямована на ефективне (за показниками надійності, використання доступних ресурсів, термінів реалізації та зручності застосування) рішення практичних завдань. І в основі будь-якої інженерії лежить системний підхід. Тобто не можна розглядати будь-яке рішення як «нерозбірний» чорний ящик, що функціонує в повному і ідеальному вакуумі.

Ще один приклад з іншої галузі

Як яскравий приклад системного підходуможна навести виробництво вантажівок у США. В даному випадку, виробник вантажівки – це просто виробник рами та кабіни + збирач конструктора. Решта (двигун, трансмісія, підвіска, електрообладнання тощо) береться виходячи з побажань замовника. Захотів один замовник вийти собі якийсь Kenworth із двигуном від Detroit Diesel, ручною коробкою Fuller, ресорною підвіскою від якоїсь Dana – будь ласка. Знадобилася другу цього замовника та сама модель Kenworth, але з «рідним» двигуном Paccar, коробкою-автоматом Allison і пневмопідвіскою від іншого виробника – легко! І так роблять усі збирачі вантажівок у США. Тобто вантажівка - це система, в якій кожен модуль може бути замінений на інший, того ж призначення і безпроблемно зістикований з наявними. Причому спосіб стикування модулів зроблений з максимально доступною універсальністю та зручністю подальшого розширенняфункціоналу. Ось чого має прагнути інженер.

На жаль, нам доведеться жити з тим, що є, але надалі такого слід уникати. Отже, програма - це, по суті, набір модулів (неважко як вони називаються, і як себе «ведуть»), компонуючи які ми домагаємося вирішення завдання. Для ефективності вкрай бажано, щоб ці модулі можна було використовувати повторно. Причому не просто використовувати за будь-яку ціну, а використовувати зручним способом. І ось тут на нас чекає черговий неприємний «сюрприз». Більшість мов високого рівня оперують такими структурними одиницями як «функція» та «процедура». І як спосіб взаємодії з ними застосовується «передача параметрів». Це цілком логічно, і тут жодних питань не виникає. Але як завжди, «важливо не те, що робиться – важливо, як робиться» (ц). І ось тут починається найнезрозуміліше. На сьогодні поширені 3 способи організації передачі параметрів: cdecl, stdcall, fastcall. Так ось, жоден із цих способів не є «рідним» для x86. Більше того, всі вони неповні з точки зору розширення функціоналу підпрограм, що викликаються. Тобто, збільшивши кількість параметрів, що передаються, ми змушені змінювати всі точки виклику цієї функції/підпрограми, або ж плодити нову підпрограму зі схожим функціоналом, яка буде викликатися трохи іншим способом.

Вказані вище методи передачі параметрів відносно непогано працюють на процесорах з 2-ма роздільними стеками (стеком даних, і стеком адрес/управління) та розвиненими командами маніпулювання стеком (хоча б індексне звернення до елементів стека). Але при програмуванні на x86 доводиться спочатку перекручуватися при передачі/отриманні параметрів, а потім не забути структурне їх видалення зі стека. Принагідно намагаючись вгадати/розрахувати максимальну глибину стека. Нагадаємо, що x86 (16/32 бітний режим), це процесор, у якого:

  • спеціалізовані регістри (РОНи – регістри загального призначення – як такі відсутні: тобто, ми не можемо однією командою помножити вміст регістру GS на значення з EDI і результат отримати в парі EDX:ECX, або розділити значення з пари регістрів EDI:ESI на вміст регістру EAX);
  • регістрів мало;
  • один стек;
  • осередок пам'яті не дає ніякої інформації від типу значення, що зберігається там.
Інакше кажучи, методи програмування, що використовуються для процесорів з великим реєстровим файлом, за допомогою декількох незалежних стеків і т.д. здебільшого не застосовуються при програмуванні на x86.

Наступна особливість взаємодії з готовими модулями, написаними на «мовах високого рівня» - це «боротьба» з «типами змінних». З одного боку, причина появи типів змінних ясна – програміст знає, які значення використовуються всередині його підпрограми/модуля. Виходячи з цього, бачиться цілком логічним, що, задавши тип значень змінної, ми можемо спростити написання програми, поклавши контроль типів/меж меж значень на транслятор мови. Але й тут із водою виплеснули немовля. Тому що будь-яка програма пишеться не для генерації сферичних коней у вакуумі, а для практичної роботиз даними користувача. Тобто очевидне порушення системного підходу – начебто розробники мов високого рівня розглядали свої системи без урахування взаємодії із зовнішнім світом. У результаті, програмуючи типізованою мовою, розробник повинен передбачати всі можливі види «неправильних» вхідних даних, і шукати способи обходу невизначеностей. І ось тут на сцену виходять монструозні системи підтримки регулярних виразів, обробки виняткових ситуацій, сигнатури методів/процедур для різних типівзначень та інша генерація милиць.

Як було вже зазначено вище, для архітектури x86 саме значення, що зберігається в комірці пам'яті, не має жодного типу. Програміст на асемблері отримує привілей та відповідальністьза визначення способу обробки цього значення. А яким чином визначати тип значення і як його обробляти – тут на вибір безліч варіантів. Але, підкреслимо ще раз, усі вони стосуються лише значень, які отримують від користувача. Як чітко помітили розробники типізованих мов: типи значень внутрішніх та службових змінних майже завжди відомі заздалегідь.

Ця причина (перекручена передача параметрів у модулі, написані на мовах вищого рівня і необхідність суворо стежити за типами параметрів, що передаються в самі модулі) бачиться основний, через яку програмування на асемблері невиправдано утруднено. І більшість воліє розбиратися в нетрях «мов високого рівня», щоб скористатися тим, що вже напрацьовано іншими, ніж мучитися, вставляю одні й ті ж «типові» милиці для виправлення того, чого вони не робили. І рідкісний транслятор асемблера хоч якось розвантажує програміста від цієї рутини.

Що робити?

Попередні висновки з урахуванням 15-річного перервиу програмуванні на асемблері.
По-перше, щодо модулів чи частин програми. У випадку варто виділити два види виконавчих модулів програми мовою асемблера – «операція» і «підпрограма».
  • «Операцією» називатимемо модуль, який виконує «атомарну» дію і не вимагає для виконання безлічі параметрів (наприклад, операція очищення всього екрана, або операція розрахунку медіани числового рядуі т.п.).
  • «Підпрограмою» варто назвати функціональний модуль, що вимагає, для коректного функціонування, безліч вхідних параметрів (більше 2х-3х).
І тут варто оцінити досвід імперативних і функціональних мов. Вони нам подарували 2 цінні інструменти, якими варто скористатися: «структура даних» (або, на прикладі REXX – складові/доповнювані змінні) та «немутабельність даних».

Корисно також дотримуватися правила немутабельності - тобто незмінності параметрів, що передаються. Підпрограма не може (не повинна) змінювати значення в структурі, що їй передається, і результат повертає або в регістрах (не більше 2х-3х параметрів), або також у новій, створюваній структурі. Таким чином ми позбавлені необхідності робити копії структур, на випадок «забутої» зміни даних підпрограмами, і можемо використовувати вже створену структуру цілком або основну її частину для виклику кількох підпрограм, що оперують одним/подібним набором параметрів. Більше того, практично «автоматом» приходимо до чергового «функціонального» правила – внутрішньої контекстно-незалежності підпрограм та операцій. Іншими словами - до розподілу стану/даних від методу/підпрограми їх обробки (на відміну від автоматної моделі). У випадках паралельного програмування, а також спільного використанняоднієї підпрограми ми позбавляємося як необхідності плодити безліч контекстів виконання і стежити за їх «неперетином», так і від створення безлічі екземлярів однієї підпрограм з різними «станами», у разі кількох її викликів.

Що стосується «типів» даних, то тут можна залишити «все як є», а можна також не винаходити велосипеда і скористатися тим, що давно використовують розробники трансляторів імперативних мов – «ідентифікатор типу значення». Тобто всі дані, що надходять з зовнішнього світуаналізуються і кожному отриманому значенню присвоюється ідентифікатор типу, що обробляється (ціле, з плаваючою точкою, упаковане BCD, код символу і т.д.) і розмір поля/значення. Маючи цю інформацію, програміст, з одного боку, не заганяє користувача надмірно вузькі рамки «правил» введення значень, з другого - має можливість у процесі роботи вибрати найбільш ефективний спосіб обробки даних користувача. Але, повторюся ще раз, це стосується тільки роботи з даними користувача.

Це були загальні міркуванняпро програмування на асемблері, що не стосуються питань проектування, налагодження та обробки помилок. Сподіваюся, що розробникам ОС, які пишуть їх з 0-ля (а тим більше на асемблері), буде про що подумати і вони оберуть (нехай не описані вище, а будь-які інші) способи зробити програмування на асемблері більш систематизованим, зручним і приємним, а не сліпо копіювати чужі, часто безнадійно «криві» варіанти.

На сьогоднішній день існує безліч мов програмування високого рівня. На їх тлі програмування низькорівневою мовою- асемблері - може на перший погляд здатися чимось застарілим та нераціональним. Однак це лише здається. Слід визнати, що асемблер фактично є мовою процесора, а значить, без нього не обійтися, доки існують процесори. Основними перевагами програмування на асемблері є максимальна швидкодіята мінімальний розмір одержуваних програм.

Недоліки часто обумовлені лише схильністю сучасного ринкудо переваг кількості якості. Сучасні комп'ютериздатні легко впоратися з нагромадженням команд високорівневих функцій, а якщо нелегко – будьте ласкаві, оновіть апаратну частину вашої машини! Таким є закон комерційного програмування. Якщо ж мова йдепро програмування для душі, то компактна і спритна програма, написана на асемблері, залишить набагато приємніше враження, ніж високорівневий гігант, обтяжений купою зайвих операцій. Існує думка, що програмувати на асемблері можуть лише обрані. Це не правда. Звичайно, талановитих програмістів-ассемблерів можна перерахувати на пальцях, але така справа практично в будь-якій сфері людської діяльності. Не так багато знайдеться водіїв-асів, але навчитися керувати автомобілем зуміє кожен - було б бажання. Ознайомившись із цим циклом статей, ви не станете крутим хакером. Однак ви отримаєте загальні відомостіі навчіться простим способампрограмування на асемблері для Windows, використовуючи її вбудовані функції та макроінструкції компілятора. Звичайно, для того, щоб освоїти програмування для Windows, вам необхідно мати навички та досвід роботи у Windows. Спочатку вам буде багато чого незрозуміло, але не засмучуйтесь через це і читайте далі: згодом усе стане на свої місця.

Отже, щоб почати програмувати, нам як мінімум знадобиться компілятор. Компілятор - це програма, яка перекладає вихідний текст, написаний програмістом, виконуваний процесором машинний код. Основна маса підручників з асемблеру наголошує на використання пакета MASM32 (Microsoft Macro Assembler). Але я у вигляді різноманітності і з низки інших причин знайомитиму вас з молодим компілятором FASM (Flat Assembler), що стрімко набирає популярності. Цей компілятор досить простий у встановленні та використанні, відрізняється компактністю та швидкістю роботи, має багатий та ємний макросинтаксис, що дозволяє автоматизувати безліч рутинних завдань. Його останню версіюви можете завантажити на адресу: сайт вибравши flat assembler for Windows. Щоб встановити FASM, створіть папку, наприклад, "D:\FASM" і розпакуйте вміст завантаженого zip-архіву. Запустіть FASMW.EXE та закрийте, нічого не змінюючи. До речі, якщо ви користуєтесь стандартним провідником, і у вас не відображається розширення файлу (наприклад. Після першого запуску компілятора в нашій папці має з'явитися конфігураційний файл - FASMW.INI. Відкрийте його за допомогою стандартного блокнота та допишіть у самому низу 3 рядки:

Fasminc=D:\FASM\INCLUDE
Include=D:\FASM\INCLUDE

Якщо ви розпакували FASM в інше місце – замініть "D:\FASM\" на свій шлях. Збережіть та закрийте FASMW.INI. Забігаючи вперед, коротко поясню, як ми користуватимемося компілятором:
1. Пишемо текст програми або відкриваємо раніше написаний текст, збережений у файлі.asm, або вставляємо текст програми з буфера обміну комбінацією.
2. Тиснемо F9, щоб скомпілювати та запустити програму, або Ctrl+F9, щоб тільки скомпілювати. Якщо текст програми ще не збережено, компілятор попросить зберегти його перед компіляцією.
3. Якщо програма запустилася, тестуємо її на правильність роботи, якщо ні - шукаємо помилки, на грубі з яких компілятор нам вкаже або тонко натякне.
Ну, а тепер ми можемо розпочати довгоочікувану практику. Запускаємо наш FASMW.EXE та набираємо в ньому код нашої першої програми:

Include "%fasminc%/win32ax.inc"

Data
Caption db "Моя перша програма.",0
Text db "Всім привіт!",0

Code
start:

invoke ExitProcess,0

Тиснемо Run -> Run, або F9 на клавіатурі. У вікні збереження вказуємо ім'я файлу та папку для збереження. Бажано звикнути зберігати кожну програму в окрему папкущоб не плутатися в майбутньому, коли при кожній програмі може виявитися купа файлів: картинки, іконки, музика та інше. Якщо компілятор видав помилку, уважно перевірте ще раз вказаний ним рядок - може, кому пропустили або пробіл. Також необхідно знати, що компілятор чутливий до регістру, тому.data і.Data сприймаються як дві різні інструкції. Якщо ви все правильно зробили, то результатом буде найпростіший MessageBox (рис. 1). Тепер давайте розбиратися, що ми написали в тексті програми. У першому рядку директивою include ми включили до нашої програми великий текст із кількох файлів. Пам'ятаєте, при встановленні ми прописували в фасмівський іні-файл 3 рядки? Тепер %fasminc% у тексті програми означає D:\FASM\INCLUDE або той шлях, який ви вказали. Директива include як би вставляє у вказане місцетекст іншого файлу. Відкрийте файл WIN32AX.INC у папці include за допомогою блокнота або в самому фасмі та переконайтеся, що ми автоматично підключили (приєднали) до нашої програми ще й текст із win32a.inc, macro/if.inc, купу незрозумілих (поки що) макроінструкцій та загальний набір бібліотек функції Windows. У свою чергу, кожен з файлів, що підключаються може містити ще кілька файлів, що підключаються, і цей ланцюжок може йти за горизонт. За допомогою файлів, що підключаються, ми організуємо якусь подобу мови високого рівня: щоб уникнути рутини опису кожної функції вручну, ми підключаємо цілі бібліотеки опису. стандартних функцій Windows. Невже все це потрібно такій маленькій програмі? Ні, це щось на зразок "джентльменського набору на всі випадки життя". Справжні хакери, звичайно, не підключають все поспіль, але ж ми тільки вчимося, тому нам таке для першого разу можна пробачити.

Далі у нас позначено секцію даних - .data. У цій секції ми оголошуємо дві змінні - Caption та Text. Це не спеціальні команди, тому їх імена можна змінювати, як захочете, хоч a і b, аби без прогалин і не російською. Ну і не можна називати змінні зарезервованими словаминаприклад, code або data, зате можна code_ або data1. Команда db означає "визначити байт" (define byte). Звичайно, весь цей текст не поміститься в один байт, адже кожен окремий символ займає байт. Але в даному випадку цією командою ми визначаємо лише змінну-покажчик. Вона міститиме адресу, в якій зберігається перший символ рядка. У лапках вказується текст рядка, причому лапки за бажанням можна ставити і "такі", і "такі" - аби тільки початкова лапка була така ж, як і кінцева. Нулик після коми додає до кінця рядка нульовий байт, який позначає кінець рядка (null-terminator). Спробуйте прибрати в першому рядку цей нолик разом із комою і подивіться, що у вас вийде. У другому рядку в даному конкретному прикладі можна обійтися і без нуля (видаляємо разом з комою - інакше компілятор вкаже на помилку), але це спрацює лише тому, що в нашому прикладі відразу за другим рядком починається наступна секція, і перед її початком компілятор автоматично впише купу вирівнюють попередню секцію нулів. У загальних випадках нулі в кінці текстових рядківобов'язкові! Наступна секція – секція виконуваного кодупрограми - .code. На початку секції стоїть мітка start:. Вона означає, що саме з цього місця розпочнеться наша програма. Перша команда - це макроінструкція взв'язку. Вона викликає вбудовану у Windows API-функцію MessageBox. API-функції (application programming interface) помітно полегшують роботу в операційній системі. Ми просимо операційну систему виконати якусь стандартну дію, а вона виконує і по закінченні повертає нам результат виконаної роботи. Після імені функції через кому слідують її параметри. У функції MessageBox параметри такі:

1-й параметр повинен містити хендл вікна-власника. Хендл - це щось на зразок особистого номера, який видається операційною системоюкожному об'єкту (процесу, вікна та ін.). 0 в нашому прикладі означає, що у віконця немає власника, воно саме по собі і не залежить від якихось інших вікон.
2-й параметр - покажчик на адресу першої літери тексту повідомлення, що закінчується вищезгаданим нуль-термінатором. Щоб наочно зрозуміти, що це лише адреса, змістимо цю адресу на 2 байти прямо у виклику функції: invoke MessageBox,0,Text+2,Caption,MB_OK і переконаємося, що тепер текст буде виводитися без перших двох літер.
3-й – вказівник адреси першої літери заголовка повідомлення.
4-й – стиль повідомлення. Зі списком цих стилів ви можете ознайомитися, наприклад, в INCLUDE\EQUATES\USER32.INC. Для цього вам краще буде скористатися пошуком у Блокноті, щоб швидко знайти MB_OK та інші. Там, на жаль, немає опису, але з назви стилю зазвичай можна здогадатися про його призначення. До речі, всі ці стилі можна замінити числом, що означає той, інший стиль або їх сукупність, наприклад: MB_OK + MB_ICONEXCLAMATION. У USER32.INC вказані шістнадцяткові значення. Можете використовувати їх у такому вигляді або перекласти в десяткову системув інженерному режимістандартного Калькулятор Windows. Якщо ви не знайомі з системами числення і не знаєте, чим відрізняється десяткова від шістнадцяткової, то у вас є 2 виходи: або самостійно ознайомитися з цією справою в інтернеті/підручнику/запитати у товариша, або залишити цю витівку до кращих часів і спробувати обійтися без цієї інформації. Тут я не наводитиму навіть коротких відомостей по системах числення через те, що і без мене про них написано величезну кількість статей і сторінок будь-якого мислимого рівня.

Повернемось до наших баранів. Деякі стилі не можна використовувати одночасно - наприклад, MB_OKCANCEL і MB_YESNO. Причина в тому, що їх сума числових значень(1+4=5) відповідатиме значенню іншого стилю - MB_RETRYCANCEL. Тепер поекспериментуйте з параметрами функції практичного закріплення матеріалу, і ми йдемо далі. Функція MessageBox призупиняє виконання програми та очікує на дії користувача. Після завершення функція повертає програмі результат дії користувача і програма продовжує виконуватися. Виклик функції ExitProcess завершує процес нашої програми. Ця функція має лише один параметр – код завершення. Зазвичай, якщо програма нормально завершує свою роботу, цей код дорівнює нулю. Щоб краще зрозуміти останній рядокнашого коду – .end start, – уважно вивчіть еквівалентний код: format PE GUI 4.0

include "%fasminc%/win32a.inc"

section ".data" data readable writeable

Caption db "Наша перша програма.",0
Text db "Ассемблер на FASM - це просто!",0

section ".code" code readable executable
start:
invoke MessageBox,0,Text,Caption,MB_OK
invoke ExitProcess,0

section ".idata" import data readable writeable
library KERNEL32, "KERNEL32.DLL",\
USER32, "USER32.DLL"

import KERNEL32,\
ExitProcess, "ExitProcess"

import USER32,\
MessageBox, "MessageBoxA"

Для компілятора він практично ідентичний попередньому прикладу, але цей текст виглядає вже іншою програмою. Цей другий приклад я спеціально навів для того, щоб ви спочатку отримали уявлення про використання макроінструкцій і надалі могли, переходячи з одного підключеного файлу в інший, самостійно діставатися до справжнього коду програми, прихованої під покривалом макросів. Спробуємо розібратися на відмінностях. Найперше, що не сильно кидається в очі, але гідне особливої ​​уваги– це те, що ми підключаємо до тексту програми не win32ax, а лише win32a. Ми відмовилися від великого набору та обмежуємося малим. Ми постараємося обійтися без підключення всього підряд з win32ax, хоча дещо з нього нам все-таки поки що знадобиться. Тому відповідно до макросів з win32ax ми вручну записуємо деякі визначення. Наприклад, макрос із файлу win32ax:
macro .data ( section ".data" data readable writeable )

під час компіляції автоматично замінює.data на section ".data" data readable writeable. Якщо ми не включили цей макрос у текст програми, нам необхідно самим написати докладне визначеннясекції. За аналогією ви можете знайти причини інших змін тексту програми в другому прикладі. Макроси допомагають уникнути рутини під час написання великих програм. Тому вам необхідно відразу просто звикнути до них, а полюбіть їх вже потім =). Спробуйте самостійно розібратися з відмінностями першого та другого прикладу, за допомогою тексту макросів, що використовуються у файлі win32ax. Скажу ще, що в лапках можна вказати будь-яку іншу назву секції даних або коду - наприклад: section "virus" code readable executable. Це просто назва секції, і вона не є командою чи оператором. Якщо ви всі зрозуміли, то ви вже можете написати власний вірус. Повірте це дуже легко. Просто змініть заголовок та текст повідомлення:
Caption db "Небезпечний Вірус.",0

Text db "Доброго дня, я - особливо небезпечний вірус-троян і поширююсь по інтернету.",13,\
"Оскільки мій автор не вміє писати віруси, що завдають шкоди, ви повинні мені допомогти.",13, ​​\
"Зробіть, будь ласка, таке:",13, ​​\
"1.Зітріть у себе на диску каталоги C:\Windows і C:\Program files",13,\
"2. Надішліть цей файл всім своїм знайомим", 13, \
"Заздалегідь вдячний.",0

Число 13 - код символу "повернення каретки" в майкрософтовских системах. Знак \ використовується в синтаксисі FASM для об'єднання кількох рядків в один, без нього вийшов би занадто довгий рядок, що йде за край екрану. Наприклад, ми можемо написати start: а можемо - і st\
ar\
t:

Компілятор не помітить різниці між першим та другим варіантом.
Ну і для більшого куражу в нашому "вірусі" можна MB_OK замінити на MB_ICONHAND або просто на число 16. У цьому випадку вікно матиме стиль повідомлення про помилку і справить вражаючий ефект на жертву "зараження" (рис. 2).

Ось і все сьогодні. Бажаю вам успіхів та до нових зустрічей!
Всі наведені приклади були протестовані на правильність роботи під Windows XP і, швидше за все, працюватимуть під іншими версіями Windows, проте я не даю жодних гарантій їхньої правильної роботи на вашому комп'ютері. Вихідні текстипрограм можна знайти на форумі.

Програмування мовою асемблера

У цій частині курсу розглядаються основи програмування мовою асемблера для архітектури Win32.

Всі процеси в машині на найнижчому апаратному рівні приводяться в дію тільки командами (інструкціями) машинної мови. Мова асемблера – це символічна вистава машинної мови. Асемблер дозволяє писати короткі та швидкі програми. Однак цей процес надзвичайно трудомісткий. Для написання максимально ефективної програминеобхідно добрі знанняособливостей команд мови асемблера, увага та акуратність. Тому реально мовою асемблера пишуться переважно програми, які мають забезпечити ефективну роботуз апаратною частиною. Також мовою асемблера пишуться критичні за часом виконання або витрачання пам'яті ділянки програми. Згодом вони оформляються у вигляді підпрограм та поєднуються з кодом мовою високого рівня.

1. Реєстри

Регістри – це спеціальні осередки пам'яті, розташовані у процесорі. Робота з регістрами виконується набагато швидше, ніж із осередками оперативної пам'ятітому регістри активно використовуються як у програмах на мові асемблера, так і компіляторами мов високого рівня.

Регістри можна розділити на регістри загального призначення,покажчик команд,регістр прапорів та сегментні регістри.

1.1. Реєстри загального призначення

До регістрам загального призначення належить група з 8 регістрів, які можна використовувати у програмі мовою асемблера. Всі регістри мають розмір 32 біти і можуть бути поділені на 2 або більше частин.

Як видно з малюнка, регістри ESI, EDI, ESP і EBP дозволяють звертатися до молодших 16 біт за іменами SI, DI, SP і BP відповідно, а регістри EAX, EBX, ECX і EDX дозволяють звертатися як до молодших 16 біт (за іменами AX , BX, CX та DX), так і до двох молодших байтів окремо (за іменами AH/AL, BH/BL, CH/CL та

Назви регістрів походять від їхнього призначення:

EAX/AX/AH/AL (accumulator register) – акумулятор;

EBX/BX/BH/BL (base register) - регістр бази;

ECX/CX/CH/CL (counter register) – лічильник;

EDX/DX/DH/DL (data register) - регістр даних;

ESI/SI (source index register) - Індекс джерела;

EDI/DI (destination index register) - індекс приймача (одержувача);

ESP/SP (stack pointer register) – регістр покажчика стека;

EBP/BP (base pointer register) – регістр покажчика бази кадру стека.

Незважаючи на існуючу спеціалізацію, всі регістри можна використовувати у будь-яких машинних операціях. Однак треба враховувати той факт, що деякі команди працюють лише з певними регістрами. Наприклад, команди множення та поділу використовують регістри EAX та EDX для зберігання вихідних даних та результату операції. Команди управління циклом використовують регістр ECX як лічильник циклу.

Ще один аспект полягає у використанні регістрів як бази, тобто. сховища адреси оперативної пам'яті. Як регістри бази можна використовувати будь-які регістри, але бажано використовувати регістри EBX, ESI, EDI або EBP. У цьому випадку розмір машинної команди зазвичай менше.

На жаль, кількість регістрів катастрофічно мало, і найчастіше буває важко підібрати спосіб їхнього оптимального використання.

1.2. Покажчик команд

Реєстр EIP ( покажчик команд) містить зміщення наступної команди, що підлягає виконанню. Цей регістр безпосередньо недоступний програмісту, але завантаження та зміна його значення виконуються різними командами управління, до яких належать команди умовних та безумовних переходів, виклику процедур та повернення з процедур.

1.3. Реєстр прапорів

Прапор – це біт, що приймає значення 1 («прапор встановлений»), якщо виконано певну умову, і значення 0 («прапор скинутий») інакше. Процесор має регістр прапорів, що містить набір прапорів, що відбиває поточний стан процесора.

Позначення

Назва

Прапор переноче

Зарезервіро

Прапор парності

Зарезервіро

Auxiliary Carry Flag

Допоміжник

Зарезервіро

Прапор нуля

Прапор знак

Прапор трасир

Interrupt Enable Flag

Прапор дозволений

Прапор направ

Прапор перепо

I/O Privilege Level

Рівень при

Прапор вкладено

Зарезервіро

Прапор віднов

Virtual-8086 Mode

Режим вірту

Перевірка ви

Virtual Interrupt Flag

Віртуальні

Virtual Interrupt Pending

Очікувальне

Перевірка на

Зарезервіро

Значення прапорів CF, DF та IF можна змінювати безпосередньо в регістрі прапорів за допомогою спеціальних інструкцій(наприклад, CLD для скидання прапора напряму), але немає інструкцій, які дозволяють звернутися до регістру прапорів як звичайного регістру. Однак можна зберігати

регістр прапорів у стік або регістр AH і відновлювати регістр прапорів із них за допомогою інструкцій LAHF, SAHF, PUSHF, PUSHFD, POPF і POPFD.

1.3.1. Прапори стану

Прапори стану (біти 0, 2, 4, 6, 7 і 11) відображають результат виконання арифметичних інструкцій, таких як ADD, SUB, MUL, DIV.

Прапор переносу CF встановлюється при перенесенні зі старшого значущого біта/Позиці в старший значний біт і показує наявність переповнення в беззнаковій цілісній арифметиці. Також використовується у довгій арифметиці.

Прапор парності PF встановлюється, якщо молодший байт результату містить парне число одиничних бітів. Спочатку цей прапор був орієнтований на використання в комунікаційних програмах: при передачі даних лініях зв'язку для контролю міг також передаватися біт парності та інструкції для перевірки прапора парності полегшували перевірку цілісності даних.

Допоміжний прапор перенесення AF встановлюється при перенесенні з біта 3-го результату/позиці втретій біт результату. Цей прапор орієнтований на використання в двійково-десяткової (Binary coded decimal, BCD) арифметиці.

Прапор нуля ZF встановлюється, якщо результат дорівнює нулю.

Прапор знака SF дорівнює значенню старшого біта результату, який є знаковим бітом у знаковій арифметиці.

Прапор переповнення OF встановлюється, якщо цілочисленний результат занадто довгий для розміщення в цільовому операнді (реєстрі або осередку пам'яті). Цей прапор показує наявність переповнення у знаковій цілісній арифметиці.

З перелічених прапорів тільки прапор CF можна змінювати безпосередньо за допомогою інструкцій STC, CLC і CMC.

Прапори стану дозволяють одній і тій самій арифметичній інструкції видавати результат трьох різних типів: беззнакове, знакове та двійково-десяткове (BCD) ціле число. Якщо результат вважати беззнаковим числом, то прапор CF показує умову переповнення (перенесення або позику), для знакового результату перенесення або позику показує прапор OF, а для BCD-результату перенос/позику показує прапор AF. Прапор SF відображає знак знакового результату, прапор ZF відображає беззнаковий і знаковий нульовий результат.

У довгій цілочисленній арифметиці прапор CF використовується спільно з інструкціями додавання з перенесенням (ADC ) і віднімання із позикою (SBB ) для поширення перенесення або позики з одного обчислюваного розряду довгого числа до іншого.

Інструкції

умовного

переходу Jcc (перехід

умові cc), SETcc (установити

значення

байта-результату

залежності

умови cc), LOOPcc (організація

та CMOVcc (умовне

копіювання)

використовують

один чи кілька

прапори стану для перевірки умов. Наприклад, інструкція переходу JLE (jump if less or equal – перехід, якщо «менше чи одно») перевіряє умову «ZF = 1 чи SF ≠ OF».

Прапор PF був введений для сумісності з іншими мікропроцесорними архітектурами та за прямим призначенням використовується рідко. Найбільш поширене його використання спільно з іншими прапорами стану в арифметиці з плаваючою комою: інструкції порівняння (FCOM, FCOMP тощо) в математичному співпроцесорі встановлюють у ньому прапори-умови C0, C1, C2 і C3, і ці прапори можна скопіювати в регістр прапорів. Для цього рекомендується використовувати інструкцію FSTSW AX для збереження слова стану співпроцесора в регістрі AX і інструкцію SAHF для подальшого копіювання вмісту регістру AH в 8 бітів регістра прапорів, при цьому C0 потрапляє у прапор CF, C2 – у PF, а C3 – в ZF. Прапор C2 встановлюється, наприклад, у разі незрівнянних аргументів (NaN або формат, що не підтримується) в інструкції порівняння FUCOM .

1.3.2. Керуючий прапор

Прапор напряму DF (біт 10 у регістрі прапорів) керує рядковими інструкціями (MOVS ,CMPS ,SCAS ,LODS іSTOS ) – встановлення прапора змушує зменшувати адреси (обробляти рядки від старших адрес до молодших), обнулення змушує збільшувати адреси. Інструкції STD і CLD відповідно встановлюють і скидають прапор DF.

1.3.3. Системні прапори та поле IOPL

Системні прапори та поле IOPL керують операційним середовищем і не призначені для використання у прикладних програмах.

Прапор дозволу переривань IF – обнулення цього прапора забороняє відповідати на запити, що маскуються, на переривання.

Прапор трасування TF – встановлення цього прапора дозволяє покроковий режимналагодження, коли після кожної виконаної

інструкції відбувається переривання програми та виклик спеціального оброблювача переривання.

Поле IOPL показує рівень пріоритету введення-виводу виконуваної програмиабо завдання: щоб програма чи завдання могла виконувати інструкції вводу-виводу або змінювати прапор IF, її поточний рівеньпріоритету (CPL) має бути ≤ IOPL.

Прапор вкладеності завдань NT – цей прапор встановлюється, коли поточне завдання «вкладено» в інше, перерване завдання, і сегмент стану TSS поточного завдання забезпечує Зворотній зв'язокз TSS попереднього завдання. Прапор NT перевіряється інструкцією IRET визначення типу повернення – межзадачного чи внутризадачного.

Прапор поновлення RF використовується для маскування помилок налагодження.

VM – встановлення цього прапора у захищеному режимі викликає перемикання у режим віртуального 8086.

Прапор перевірки вирівнювання AC – установка цього прапора разом із бітом AM у регістрі CR0 включає контроль вирівнювання операндів при зверненнях до пам'яті: звернення до невирівняного операнда викликає виняткову ситуацію.

VIF – віртуальна копія прапора IF; використовується разом із прапором VIP.

VIP – встановлюється для вказівки відстроченого переривання.

ID – можливість програмно змінити цей прапор у регістрі прапорів вказує підтримку інструкції CPUID.

1.4. Сегментні регістри

Процесор має 6 про сегментних регістрів: CS, DS, SS, ES, FS і GS. Їх існування обумовлено специфікою організації та використання оперативної пам'яті.

16-бітові регістри могли адресувати лише 64 Кб оперативної пам'яті, що явно недостатньо для більш менш пристойної програми. Тому пам'ять програмі виділялася у вигляді кількох сегментів, які мали розмір 64 Кб. При цьому абсолютні адреси були 20-бітними, що дозволяло адресувати вже 1 Мб оперативної пам'яті. Виникає питання – як маючи 16-бітові регістри зберігати 20-бітові адреси? Для вирішення цього завдання адреса розбивалася на базу зміщення. База – це адреса початку сегмента, а усунення – це номер байта всередині сегмента. На адресу початку сегмента накладалося обмеження - він повинен був бути кратний 16. При цьому останні 4 біти дорівнювали 0 і не зберігалися, а малися на увазі. Таким чином, виходили дві 16-бітові частини адреси. Для отримання

абсолютної адреси до бази додавалися чотири нульових біта, та отримане значення складалося зі зміщенням.

Сегментні регістри використовувалися для зберігання адреси початку сегмента коду (CS - code segment), сегмента даних (DS - data segment) і сегмента стека (SS - stack segment). Регістри ES, FS та GS були додані пізніше. Існувало кілька моделей пам'яті, кожна з яких мала на увазі виділення програмі одного або декількох сегментів коду і одного або декількох сегментів даних: tiny, small, medium, compact, large і huge. Для команд мови асемблера існували певні угоди: адреси переходу сегментувалися за регістром CS, звернення до даних сегментувалися за регістром DS, а звернення до стеку – за регістром SS. Якщо програмі виділялося кілька сегментів для коду чи даних, то доводилося змінювати значення регістрах CS і DS звернення до іншого сегменту. Існували так звані «ближні» та «далекі» переходи. Якщо команда, яку треба зробити перехід, перебувала у тому сегменті, то переходу досить було змінити лише значення регістру IP. Такий перехід називався ближнім. Якщо ж команда, яку треба зробити перехід, перебувала у іншому сегменті, то переходу необхідно було змінити як значення регістру CS, і значення регістру IP. Такий перехід називався далеким і здійснювався довше.

32-бітові регістри дозволяють адресувати 4 Гб пам'яті, що вже достатньо для будь-якої програми. Кожну програму Win32 Windows запускає в окремому віртуальному просторі. Це означає, що кожна Win32-програма матиме 4-гігабайтовий адресний простір, але зовсім не означає, що кожна програма має 4 Гб фізичної пам'ятіа тільки те, що програма може звертатися за будь-якою адресою в цих межах. А Windows зробить все необхідне, щоб пам'ять, до якої програма звертається, «існувала». Звичайно, програма повинна дотримуватись правил, встановлених

Windows, інакше з'являється помилка General Protection Fault.

Під архітектурою Win32 відпала потреба у розділенні адреси на базу і усунення, і необхідність у моделях пам'яті. На 32-

використовуються по-іншому1. Раніше необхідно було зв'язувати окремі частини програми з тим чи іншим сегментним регістром і зберігати/відновлювати регістр DS під час переходу до іншого сегменту даних або явно сегментувати дані по іншому регістру. При 32-бітовій архітектурі потреба в цьому відпала, і в найпростішому випадку про сегментні регістри можна забути.

1.5. Використання стека

Кожна програма має область пам'яті, яка називається стеком . Стек використовується для передачі параметрів у процедури та зберігання локальних даних процедур. Як відомо, стек - це область пам'яті, при роботі з якою необхідно дотримуватися певних правил, а саме: дані, які потрапили в стек першими, витягуються звідти останніми. З іншого боку, якщо програмі виділено деяку пам'ять, то немає жодних фізичних обмежень на читання та запис. Як же поєднуються два ці суперечливі принципи?

Нехай у нас є функція f1, яка викликає функцію f2, а функція f2, у свою чергу, викликає функцію f3. При виклику функції f1 їй відводиться певне місце у стеку під локальні дані. Це місце відводиться шляхом віднімання з регістра ESP значення, що дорівнює розміру пам'яті. Мінімальний розмірвідведеної пам'яті дорівнює 4 байтам, тобто. навіть якщо процедурі потрібно 1 байт, вона повинна зайняти 4 байти.

Функція f1 виконує деякі дії, після чого викликає

функція f2 викликає функцію f3 яка також відводить собі місце в стеку. Функція f3 інших функцій не викликає і при завершенні роботи повинна звільнити місце в стеку, додавши до регістра ESP значення, які було віднято під час виклику функції. Якщо функція f3 не відновить значення регістру ESP, то функція f2, продовживши роботу, буде звертатися не до своїх даних, т.к. вона шукає

яка була до її виклику.

Таким чином, на рівні процедур необхідно дотримуватись правил роботи зі стеком – процедура, яка посіла місце в стеку останньої, повинна звільняти його першою. При недотриманні цього правила програма працюватиме некоректно. Але кожна процедура може звертатися до своєї області стеку довільним чином. Якби ми були змушені дотримуватись правил роботи зі стеком всередині кожної процедури, довелося б перекладати дані зі стека в іншу область пам'яті, а це було б дуже незручно і надзвичайно сповільнило виконання програми.

Кожна програма має область даних, де розміщуються глобальні змінні. Чому ж локальні дані зберігаються саме у стеку? Це робиться зменшення обсягу пам'яті займаного програмою. Якщо програма буде послідовновикликати кілька процедур, то кожен момент часу буде відведено місце лише під дані однієї процедури, т.к. стек займається та звільняється. Область даних існує постійно роботи програми. Якби локальні дані розміщувалися в області даних, довелося б відводити місце під локальні дані для всіх процедур програми.

Локальні дані автоматично не ініціалізуються. Якщо у наведеному вище прикладі функція f2 після функції f3 викличе функцію f4 , то функція f4 займе в стеку місце, яке до цього було зайнято функцією f3 , таким чином, функції f4 «у спадок» дістануться дані функції f3 . Тому кожна процедура обов'язково має дбати про ініціалізацію своїх локальних даних.

2. Основні поняття мови асемблера

2.1. Ідентифікатори

Поняття ідентифікатора у мові асемблера нічим не відрізняється від поняття ідентифікатора в інших мовах. Можна використовувати латинські літери, цифри та знаки _ . ? @ $ , причому точка може бути лише першим символом ідентифікатора. Великі та малі літери вважаються еквівалентними.

2.2. Цілі числа

У програмі на мові асемблера цілі числа можуть бути записані в двійковій, вісімковій, десятковій та шістнадцятковій системіобчислення. Для завдання системи числення в кінці числа