
labs&konspeckts / SP_ukr / asm1
.docОбробка асемблерних програм Одним з найбільш значних компонентів процесора є регістри. Процесори 8088, 8086, 80188 і 80186 мають однакові набори регістрів, тому їх всі ми об'єднаємо під терміном «8086». Процесори більш нових моделей лінійки x86 розширюють кількість використовуваних регістрів, зберігаючи всі регістри попередників для зворотної сумісності. Розробники фірми Intel поділяють регістри на 3 групи: регістри загального призначення (РОН або робочі регістри), сегментні регістри і спеціальні регістри. Робочі регістри - це регістри, які можуть виступати в ролі операндів арифметичних, логічних і інших інструкцій процесора. Сегментні регістри використовуються для доступу до блоків пам'яті, так званими «сегментами». Спеціальні регістри відображають стан системи, і до них немає прямого доступу. Регістри загального призначення процесора 8086 Існує вісім 16-бітних робочих регістрів. Це AX, BX, CX, DX, SI, DI, BP і SP. Будь-які з них можуть бути задіяні в обчислювальному процесі, але необхідно враховувати, що все ж багато інструкції процесора для роботи вимагають використання строго певних регістрів з цієї групи. Регістр AX (Accumulator) доцільно використовувати для тимчасового зберігання даних (наприклад в обчисленнях); BX (Base) - для задавання адрес в пам'яті; CX (Count) - для підрахунку (кількість повторів циклу); DX (Data) - для зберігання даних в деяких арифметичних операціях а також адрес для доступу до шини введення / виведення.
SI
(Source Index) і DI (Destination Index) так само
мають спеціальне призначення. З
їх допомогою можуть задаватися адреси
для здійснення непрямого доступу до
пам'яті, а також для виконання спеціальних
«стрічкових» інструкцій
процесора 8086.
Регістр BP (Base
Pointer) схожий з регістром BX, але
в основному використовується для
доступу до переданих через стек
параметрам з підпрограми.
Серед
всіх регістрів виділяється SP (Stack
Pointer), за допомогою якого задається
адреса верхівки програмного стека. Це
дуже важливо в багатьох випадках,
тому не рекомендується використовувати SP в
іншій ролі.
Крім 16-бітних регістрів, в процесорі 8086 присутні ще вісім 8-бітних регістрів - AL, AH, BL, BH, CL, CH, DL і DH. Але ці регістри не реалізовані окремо від 16-бітних на фізичному рівні. Кожен з них - це певна частина 16-бітного регістру. Наприклад, AL означає «Ax Low» - молодший байт AX, а CH - «Cx High» - старший байт CX. Важливо запам'ятати, що зміна будь-якого з 8-бітнихрегістрів спричинить за собою зміну і відповідного йому 16-бітного регістру, як і навпаки.
Регістри спеціального
призначення процесора8086
В процесорі 8086 існує два регістри спеціального призначення - IP (Instruction Pointer) і FLAGS. Процесор особливим чином маніпулює цими регістрами,тому до них немає прямого доступу, як до РОН або сегментних регістрів. IP - це 16-бітний регістр, який використовується для завдання адреси інструкції процесора, яка виконується в даний момент часу. Регістр FLAGS не схожий на інші регістри процесора - він не зберігає числових значень, а є «набором» однобітних значень - прапорців, які характеризують стан процесора. Хоч FLAGS має довжину 16 біт, реально використовуються тільки 9 з них. Найбільш часто використовувані прапорці -zero (нуль), carry (перенесення), sign (знак) і overflow (переповнення). Це так звані прапорці умови.
Сегменти і сегментні регістри в 8086
Оперативна пам'ять комп'ютера складається з комірок розміром в один байт. Загальна кількість байт = 2 в степені кількість розрядів шини адреси (ША). Процесор 8086 16-розрядний, але ША 20-розрядна, таким чином можна адресувати 1 Мбайт пам'яті. Це досягається завдяки сегментації – розбивкою пам'яті на блоки розміром 64кілобайти, так звані сегменти. Положення сегмента в пам'яті описується16-бітовим значенням, яке, для отримання доступу до цього сегменту, повинно зберігатися в одному з сегментних регістрів. А всередині кожного з сегментів дані адресуються через 16 – розрядне зміщення (offset) відносно початку сегмента.
Існують чотири сегментних регістра – це CS (Code Segment), DS (Data Segment), ES (Extra Segment) і SS (Stack Segment). Всі ці регістри можуть використовуватися для роботи з будь-якими сегментами пам'яті, але є деякі обмеження. CS повинен бути пов'язаний з сегментом, який зберігає виконувані в даний момент команди процесора (code). Щоб виконати інструкції, що знаходяться в інших сегментах, просто необхідно змінити вміст CS. Регістр SS використовується для операцій зі стеком програми. Стек працює за принципом FILO (First Input Last Output). Це означає, що при читанні ми забираємо з стека останні записані в нього дані. У більшості випадків стек використовується для збереження стану процесора (наприклад, вмісту регістрів), передачі параметрів у підпрограми тощо, тому змінювати SS рекомендується тільки в рідкісних випадках і одночасно з SP
DS і ES можуть бути пов'язані з будь-якими сегментами пам'яті, хоча найзручніше було б DS застосовувати для доступу часто використовуваних даних програми (наприклад, змінним), а ES - в якості додаткового регістра,коли інші сегментні регістри задіяні, і небажано їх зміну. Хоча, в принципі, сегментні регістри, як і регістри загального призначення, можна використовувати для тимчасового зберігання даних, але в будь-якому випадку навряд чи це буде гарною ідеєю, оскільки будь-яка допущена помилка (наприклад, якщо забути відновити старий вміст регістра) може привести до «зависання » системи. Регістри процесора 80386 В процесорі 80386 всі регістри загального призначення, а також IP і FLAGS розширилися до 32 біт. Для сумісності з 8086, збереглися 16-бітові регістри, які тепер є молодшими словами розширених регістрів, і доступні під старими іменами. Для доступу до розширених регістрів, додається префікс «E» (Extended). Наприклад, 32-бітний аналог AX - EAX.
Сегментні регістри залишилися 16-бітними, але до них додалося два нових регістри FS і GS.
Структура програми на асемблері Програма на асемблері являє собою набір операторів, кожний з яких займає окремий рядок коду програми, в них можуть бути записані: - Інструкції процесора, які транслюються в машинний код програми; - Директиви, які вказують транслятору на необхідність виконання деяких дій, і при трансляції не створюють машинний код. - Макрокоманди - символьні позначення, які під час трансляції програми заміщуються одним або кількома асемблерними пропозиціями; Також, кожна інструкція програми може мати свою мітку - ім'я, яке буде однозначно описувати місцезнаходження цієї інструкції в програмі. Ім'я мітки пишеться на самому початку рядка і відділяється двокрапкою. При необхідності, в кінці будь-якої пропозиції, програміст може помістити коментар, відокремивши його крапкою з комою. При написанні програми можна використовувати такі символи: - Всі латинські літери: A-Z, a-z. При цьому великі і малі літери вважаються еквівалентними; - Цифри від 0 до 9; - Знаки?, @, $, _, &; - Роздільники,. [] () <> {} + / *%! '"? \ = # ^. Ідентифікатори (імена змінних, констант, міток) можуть складатися з одного або декількох символів. В якості символів можна використовувати букви латинського алфавіту, цифри і деякі спеціальні знаки - _,?, $, @. Ідентифікатор не може починатися з цифри. Довжина ідентифікатора може бути до 255 символів, хоча транслятор сприймає лише перші 32, а решту ігнорує. Числа у вихідній програмі записуються відповідними даній системі числення цифрами і закінчуються буквою, визначальною систему обчислення: Десяткові числа не вимагають для свого запису будь-яких додаткових символів. Для того, щоб вказати, що число записано в двійковій системі, в кінці числа додається «b», а в шістнадцятковій - «h». Якщо число в шістнадцятковій системі починається з букви, то спочатку числа дописується нуль. Наприклад, число 161 можна записати як: 161, 0a1h, 10100001b.
Набір інструкцій процесора Процесори Intel x86 можуть виконувати різноманітні команди, кількість яких в сучасних моделях перевищує кілька сотень . Ці команди,так звані інструкції процесора, виконують різні дії над регістрами, пам'яттю або системою вцілому. Вони зчитуються процесором з пам'яті, у вигляді машинного коду. Інструкції можуть мати один або два операнди, а можуть і зовсім не мати операндів. Будь-який рядок програми, яка визначає виконання команди процесора може бути записана за такою схемою:
Кожна команда процесора представляється своєї мнемонікою (ім'ям), за якою слідують через кому операнди. Наприклад, команда MOV копіює дані з одного місця в інше.
MOV <куди »,« звідки> В якості першого операнда може виступати будь-який регістр або адреса в пам'яті, куди необхідно зробити запис значення, а другим операндом може бути регістр, адреса в пам'яті або константа, які визначають це значення.
Важливо зауважити, що командою MOV неможливо записати дані з пам'яті в пам'ять.
Приклад фрагмента програми, що використовує MOV:
MOV AX, BX ; Копіювання вмісту BX в AX MOV DH, CL ; Копіювання вмісту CL в DH MOV EAX, 50 ; Запис в EAX числа 50 MOV [100h], DI ; Кладемо в пам'ять за адресою 256 вміст DI Цікава ситуація виникає, при записі в пам'ять константи. Процесору необхідно знати розмір цієї константи (у байтах), а транслятор сам не може його визначити. Дійсно, наприклад, число 10 може займати як один, так і два чи навіть чотири байти. Тому необхідно в тексті програми вручну вказати цей розмір, використовуючи директиву вказівки типу PTR. <byte/word/dword> PTR константа Слова byte, word або dword визначають розмір константи. Вони означають один, два або чотири байти відповідно. MOV [10h], byte PTR 10 ; Запис 10 в один байт за адресою 16 MOV [10h], word PTR 10 ; Запис 10 в два байти за адресою 16 MOV [10h], dword PTR 10 ; Запис 10 в чотири байти за адресою 16 Найпоширеніші помилки, пов'язані з використанням команди MOV: MOV AX, BH ; Копіювання в 16-бітний регістр вмісту 8-бітного регістра MOV CL, SI ; І навпаки MOV DH, 10000 ; Запис в регістр числа, що перевищує його розмір MOV [600], 87 ; При запису в пам'ять не вказано розмір константи MOV [30], [90] ; Копіювання з пам'яті в пам'ять MOV 10, AX ; Не можна писати в константу Крім інструкції MOV існує корисна інструкція XCHG, яка має такий же синтаксис, як і MOV, і здійснює не копіювання, а обмін даними. XCHG AX, BX; Міняємо місцями вміст AX і BX Також, при написанні програм широко застосовуються так звані арифметичні і логічні команди. Ці команди в основному мають два операнда, правила завдання яких аналогічні правилам для інструкції MOV. Над цими операндами виконується деякаарифметична або логічна дія, а потім, як правило, результат зберігається в першому з них. Розглянемо арифметичні і логічні команди
Арифметичні інструкції |
|
ADD A, B |
A = A + B |
SUB A, B |
A = A – B |
INC A |
A = A + 1 |
DEC A |
A = A – 1 |
Логічні Інструкції |
|
AND A, B |
A = логічне «і» (A, B) |
OR A, B |
A = логічне «або» (A, B) |
XOR A, B |
A = виключне «або» (A, B) |
NOT A |
A = логічне «не» A |
Наприклад, для обчислення виразу AX = (30 - SI) «І» (BH + 1): MOV AX, 30; Кладемо в AX 30 SUB AX, SI; Віднімаємо з AX SI INC BH; Додаємо до BH 1 ; Для того, щоб виконати «І», необхідно, щоб другий операнд теж мав розмір ; Два байти. Для цього, переписуємо BH в BL, а в BH записуємо нуль. XOR BL, BL ; Більш швидкий варіант MOV BL, 0 XCHG BL, BH ; Міняємо місцями BH з BL ; Тепер в BX лежить потрібне значення AND AX, BX ; В AX отримуємо результат.
Визначення даних Для опису простих типів даних в програмі використовуються спеціальні директиви DEFINE резервування й ініціалізації (установки початкового значення) даних. Транслятор, обробляючи таку директиву, виділяє необхідну кількість байт в пам'яті і при необхідності ініціалізує виділену область значенням. Якщо число займає більше одного байта, то в пам'яті ці байти будуть слідувати за старшинством, починаючи з самого молодшого. Наприклад, число 12345678h в пам'яті буде записано як 78h, 56h, 34h, 12h. При завданні строкових констант, всі символи константи беруться в одинарні або подвійні лапки. У пам'яті символи зберігаються у вигляді числового коду, який визначається за спеціальною таблицею. В операційній системі MSDOS використовується кодування ASCII:
# |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
A |
B |
C |
D |
E |
F |
0 |
|
☺ |
☻ |
♥ |
♦ |
♣ |
♠ |
• |
◘ |
◘ |
◙ |
♂ |
♀ |
♪ |
♫ |
☼ |
1 |
► |
◄ |
↕ |
‼ |
¶ |
§ |
▬ |
↨ |
↑ |
↓ |
→ |
← |
└ |
↔ |
▲ |
▼ |
2 |
|
! |
" |
# |
$ |
% |
& |
' |
( |
) |
* |
+ |
, |
- |
. |
/ |
3 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
: |
; |
< |
= |
> |
? |
4 |
@ |
A |
B |
C |
D |
E |
F |
G |
H |
I |
J |
K |
L |
M |
N |
O |
5 |
P |
Q |
R |
S |
T |
U |
V |
W |
X |
Y |
Z |
[ |
\ |
] |
^ |
_ |
6 |
` |
a |
b |
c |
d |
e |
f |
g |
h |
i |
j |
k |
l |
m |
n |
o |
7 |
p |
q |
r |
s |
t |
u |
v |
w |
x |
y |
z |
{ |
| |
} |
~ |
€ |
8 |
А |
Б |
В |
Г |
Д |
Е |
Ж |
З |
И |
Й |
К |
Л |
М |
Н |
О |
П |
9 |
Р |
С |
Т |
У |
Ф |
Х |
Ц |
Ч |
Ш |
Щ |
Ъ |
Ы |
Ь |
Э |
Ю |
Я |
A |
а |
б |
в |
Г |
д |
е |
ж |
з |
и |
й |
к |
л |
м |
н |
о |
п |
B |
░ |
▒ |
▓ |
│ |
┤ |
╡ |
╢ |
╖ |
╕ |
╣ |
║ |
╗ |
╝ |
╜ |
╛ |
┐ |
C |
└ |
┴ |
┬ |
├ |
─ |
┼ |
╞ |
╟ |
╚ |
╔ |
╩ |
╦ |
╠ |
═ |
╬ |
╧ |
D |
╨ |
╤ |
╥ |
╙ |
╘ |
╒ |
╓ |
╫ |
╪ |
┘ |
┌ |
█ |
▄ |
▌ |
▐ |
▀ |
E |
р |
с |
т |
у |
ф |
х |
ц |
ч |
ш |
щ |
ъ |
ы |
ь |
э |
ю |
я |
F |
Ё |
ё |
Є |
Є |
Ї |
ї |
Ў |
ў |
º |
∙ |
∙ |
√ |
№ |
¤ |
■ |
|
Шістнадцятковий код символу формується як рядок * 16 + стовпець. Наприклад, код символу «S» - 53h. Загальний синтаксис директиви для визначення даних: [<ім'я змінної>] <директива> <значення> [, <значення> ...] Директива визначає кількість виділених байтів:
Директива |
К-сть байт |
Діапазон для числа зі знаком |
Діапазон для числа без знака |
||
DB (байт) |
1 |
-128 |
127 |
0 |
255 |
DW (слово) |
2 |
-32 768 |
32 767 |
0 |
65 535 |
DD (подвійне слово) |
4 |
-2 147 483 648 |
2 147 483 647 |
0 |
4 294 967 295 |
DF, DP |
6 |
–247 |
247 – 1 |
0 |
248 |
DQ |
8 |
–263 |
263 – 1 |
0 |
264 |
DT |
10 |
–279 |
279 – 1 |
0 |
280 |
Значення може бути будь-яким виразом, що містить числові або рядкові константи, а також може бути знаком «?», Це буде означати, що воно невідоме. Якщо в директиві DB поспіль йдуть кілька символів, їх можна об'єднати в одні лапки. Так само, якщо поспіль йдуть кілька однакових значень, їх можна замінити конструкцією <кількість повторів> dup(<значення>)
Приклад визначення даних: A DB? ; Значення A не задане при ініціалізації B DD 1, 2, 3, 4, 40h ; B – масив подвійних слів C DB 'X' ; символ D DW 12 'XY' ; вираз str DB 'Рядок' ; еквівалентно str DB 'Р', 'я', 'д', 'о', 'к' buf DD 5 dup (0) ; еквівалентно buf DD 0, 0, 0, 0, 0
Виклик функцій BIOS і операційної системи Практично будь-яка програма потребує взаємодії з BIOS або операційною системою (ОС) - вони пропонують програмісту набір вже готових функцій, що дуже зручно, особливо для програмування на асемблері. Програма «спілкується» з операційною системою MSDOS і BIOS за допомогою інструкції виклику переривання: INT <номер переривання> У ДОС номер програмного переривання - 21h. Багато з переривань багатофункціональні, номер функції завжди записується в регістрі AH. Також дуже важливо запам'ятати, що під час виклику переривань, вони можуть змінити вміст певних регістрів, тому потрібні дані завбачливо буде зберегти в стеці. Для виходу в операційну систему використовується переривання ДОС. Номер цієї функції - 4ch - повинен бути в AH, а в AL записується код виходу програми (зазвичай - нуль, якщо програма виконалася успішно, інакше - код помилки). MOV AX, 4c00h ; Кладемо в AH 4ch, а в AL - нуль INT 21h ; Викликаємо переривання ДОС Після цих інструкцій програма передасть управління операційній системі, і всі наступні команди виконуватися не будуть. Розглянемо ще дві корисні функції ДОС - зчитування символу з клавіатури і виведення символу на екран. Для читання, кладемо 1 (номер функції) в AH і викликаємо переривання 21h. Після цього в AL буде зберігатися код зчитаного символу. Для виводу, в AH записуємо 2, а в DL - код символу, який потрібно вивести, і потім викликаємо переривання. Наприклад, щоб ввести символ і тут же вивести його на екран: MOV AH, 1 ; Код функції зчитування символу INT 21h ; Викликаємо переривання ДОС MOV DL, AL ; Кладемо в DL код ліченого символу MOV AH, 2 ; Код функції виведення символу INT 21h ; Викликаємо переривання ДОС Директиви SEGMENT, ASSUME, END і .386 Будь-яка програма складається з набору машинних команд і даних, які під час виконання знаходяться в пам'яті. Іноді для роботи програми необхідно також додаткове місце, наприклад під стек. В принципі, можливо спільне розміщення команд, даних і стека в одному блоці пам'яті, але тоді з'являється небезпека при записі в стек або область даних пошкодити команди, а також ускладниться налагодження програми. Тому, доцільніше буде використовувати різні області пам'яті (сегменти) для зберігання команд процесора, даних, і для реалізації стека. Операційна система повинна знати, скільки пам'яті необхідно виділяти під програму, які сегменти потрібно визначити при запуску, і чим їх потрібно заповнити, а також місцезнаходження команди, починаючи з якої відбувається виконання програми - так званої «точки входу». Вся ця та інша корисна інформація знаходиться в виконуваному файлі програми. Для того щоб транслятор асемблера знав, в який сегмент потрібно покласти певні дані або команди, використовується директива SEGMENT: <Ім'я сегмента> SEGMENT [параметри] . . . команди / дані . . . <Ім'я сегмента> ENDS Як параметри можна вказати додаткову інформацію про сегмент. Наприклад, параметр STACK вказує операційній системі, що цей сегмент буде використовуватися, як системний стек. Приклад визначення сегмента: DATA SEGMENT X DB? ; Байт пам'яті, початкове значення якого не визначено Y DW 100 ; два байти, що містять числа 100 DD 10 dup (0) ; 40 обнулений байт DATA ENDS Для того, щоб транслятор міг більш розумно працювати з мітками і змінними (наприклад, сам враховував розмір даних), а також знав, які значення потрібно записати в сегментні регістри при запуску програми, необхідно «зв'язати» певні сегменти і сегментні регістри. Це робиться за допомогою директиви ASSUME. ASSUME <сегментний регістр>: <ім'я сегмента> [, <сегментний регістр>: <ім'я сегмента> ...] Наприклад, якщо програма використовує роздільні сегмент коду і сегмент даних з іменами CODE і DATA: ASSUME cs: CODE, ds: DATA Існує ще одна спеціальна директива, без якої програма не буде працездатною - це директива END. Вона має відразу два призначення - позначення кінця програми та визначення точки входу в неї. END <адреса точки входу> Наприклад, якщо при запуску програма повинна перейти на мітку beg: ... beg: MOV AX, 10 ... END beg З директивою .386 все здавалося б просто - вона говорить транслятору, що програма написана для процесора 80386. Але справа в тому, що після зазначення цієї директиви транслятор автоматично настроюється на 32-розрядну адресацію, що не підтримується програмою tlink.exe. Для запобігання цьому, необхідно вказати, що ми використовуємо 16-бітове зміщення всередині сегменту за допомогою параметра USE16 директиви SEGMENT. DATA SEGMENT USE16 Тепер явно вказано, що сегмент DATA - 16-бітний. Компіляція програми Для того, щоб створити з тексту програми працездатний виконуваний EXE-файл, необхідно виконати дві дії - створити з тексту об'єктний файл (OBJ), а потім з об'єктного - виконуваний. Для цього, в командному рядку ДОС необхідно виконати дві команди: tasm prog.asm - для трансляції програми в OBJ-файл tlink prog.obj - для створення з OBJ-файла EXE-файлу Тут prog.asm - ім'я вихідного файлу з текстом програми. Також, необхідна наявність в поточному каталозі файлів tasm.exe і tlink.exe. Завдання на лабораторну роботу Написати програму для процесора 8086, яка виводить на екран заданий у варіанті символ, виконує над його кодом вказане дію, а потім виводить на екран отриманий символ. Результат операції повинен зберігатися в сегменті даних. Перед виходом в ДОС необхідно викликати функцію читання символу з клавіатури, щоб забезпечити затримку для перегляду результату. Відстежити роботу програми в відладчику: