rflags
Існує спеціальний регістр під назвою rflags.
Його біти поводяться як прапорці для різних умов.
Деякі з них наведено нижче:
| назва | символ | біт |
|---|---|---|
| перенесення | CF | 0 |
| нуль | ZF | 6 |
| знак | SF | 7 |
| переповнення | OF | 11 |
Повний перелік шукайте в посібнику Intel.
Прапорці в rflags не змінюються напряму.
Натомість їх встановлюють багато різних інструкцій.
Наприклад, ZF встановлюють багато арифметичних або порозрядних операцій, коли результат дорівнює нулю.
Одна з найпоширеніших інструкцій для перевірки умов - cmp.
Вона приймає два операнди й оновлює прапорці, але не змінює своїх операндів.
cmp
Інструкція cmp віднімає другий операнд від першого і встановлює прапорці відповідно до результату.
Якщо A - перший операнд, а B - другий:
| прапорець | встановлюється, коли |
|---|---|
| CF | A < B (беззнакове) |
| ZF | A == B |
| SF | A < B (знакове, без переповнення) |
| OF | переповнення під час знакового віднімання |
За замовчуванням код у x86-64 виконується послідовно, згори вниз.
Але є багато ситуацій, коли цю поведінку потрібно змінити. Наприклад, щоб виконати інший набір інструкцій у відповідь на умову.
У мовах вищого рівня це зазвичай роблять за допомогою абстракцій, як-от умовні конструкції if...else.
Однак у x86-64 їх немає.
Натомість x86-64 надає інструкції, які фактично передають виконання в інше місце коду.
Це називається branching.
Ми вже бачили дві такі інструкції: call і ret.
Коли викликають функцію, виконання передається від функції, що викликає, до викликаної функції. А під час повернення виконання передається назад до функції, що викликала.
Якщо ret не знайдено, виконання «провалюється» з однієї функції в наступну.
Іноді це можна використати, щоб оптимізувати потік виконання коду.
Інструкція jmp безумовно передає виконання програми в іншу точку коду. Її єдиний операнд - це мітка, яка містить адресу точки, з якої виконання продовжиться.
Розгляньмо, наприклад, таку функцію:
fn:
mov rax, 5
jmp end
add rax, 10
end:
ret
Коли викликають fn, виконання починається з mov rax, 5.
На цьому етапі значення в rax стає рівним 5.
Наступна інструкція - jmp end, яка передає виконання на мітку end.
Після end наступна інструкція - ret, яка передає виконання назад до функції, що викликала.
Зауважмо, що оскільки add rax, 10 розташована після jmp end і перед end, вона ніколи не виконується.
Значення rax на момент повернення з fn дорівнює 5.
Сімейство інструкцій jcc передає виконання програми в іншу точку лише за виконання певної умови. Інакше виконання продовжується послідовно.
Кожна умова відповідає одному або кільком прапорцям у rflags.
Деякі варіанти jcc перевіряють, що прапорець встановлено, інші - що його скинуто.
cc у jcc - не буквальне позначення, а вказівка на конкретний суфікс, повʼязаний із прапорцем, який перевіряють.
Суфіксів багато, і чимало з них перевіряють ту саму умову, що й інший суфікс. Деякі з них посилаються безпосередньо на прапорець, тож інструкція переходить на мітку, якщо цей прапорець встановлено:
| суфікс | переходить, якщо |
|---|---|
| z | ZF == 1 |
| c | CF == 1 |
| s | SF == 1 |
| o | OF == 1 |
Багато інших обрано так, щоб відображати їхнє значення в інструкції cmp.
Наприклад:
| інструкція | суфікс | переходить, якщо |
|---|---|---|
| cmp A, B | e | A == B |
| cmp A, B | l | A < B (знакове) |
| cmp A, B | b | A < B (беззнакове) |
| cmp A, B | g | A > B (знакове) |
| cmp A, B | a | A > B (беззнакове) |
Можна додати e після l, b, g або a, щоб урахувати рівність в умові:
cmp rcx, r8
jge two ; this jumps to 'two' if rcx is greater than, or equal to, r8 in a signed comparison
jbe two ; this jumps to 'two' if rcx is lesser than, or equal to, r8 in an unsigned comparison
Для всіх суфіксів є варіанти, які перевіряють протилежну поведінку.
Вони мають той самий синтаксис, але з n перед суфіксом.
Наприклад, jnz переходить, коли ZF не встановлено.
Так само jnae переходить, коли A не >= B (A і B розглядаються як беззнакові цілі числа).
Деякі суфікси є синонімами для тих самих умов.
Наприклад, jz (суфікс z, для ZF) і je (суфікс e, для рівності) обидва переходять, коли ZF встановлено.
Це тому, що в cmp ZF встановлюється, коли віднімання дає нуль, а це відповідає рівності двох операндів.
Однак інші суфікси перевіряють комбінацію прапорців, і їх не можна безпосередньо замінити суфіксом одного прапорця.
Краще обирати той суфікс, який точніше описує семантику порівняння.
Мітки видно в усьому вихідному файлі, вони не є локальними для функції. Тому повторно використати назву мітки неможливо.
Щоб відтворити поведінку локальної мітки, NASM має спеціальний синтаксис для мітки, оголошеної з крапкою (.) перед нею.
Цей синтаксис задає мітку, яка неявно містить назву попередньої мітки без крапки:
section .text
fn1:
...
.example: ; this is fn1.example
...
ret
fn2:
...
.example: ; this is fn2.example
...
ret
Усе ще можна перейти на цю мітку з будь-якого місця коду, використавши повну назву мітки, наприклад jmp fn1.example.
Однак перехід, який використовує частину назви мітки, починаючи з крапки, відбудеться на мітку всередині зовнішньої функції.
Наприклад, .example поводиться так, ніби є локальною для функції:
section .text
fn1:
...
.example:
...
jmp .example ; this jumps to fn1.example
fn2:
...
.example:
...
jmp .example ; this jumps to fn2.example
Зауважмо, що мітка без крапки всередині функції на практиці задає ще одну функцію:
section .text
fn1:
...
.example: ; this is 'fn1.example'
...
non_dotted:
...
.example: ; this is 'non_dotted.example'
...
ret