Написаний вручну код асемблерною мовою рясніє повторами. Той самий короткий набір інструкцій повторюється знову й знову, майже без змін, і мало що дозволяє скоротити повторюваний код. І це ще не все: писати асемблерною мовою означає оперувати значеннями, в яких немає очевидної семантики. Іменованих параметрів немає, і більшість деталей роботи самої машини програміст мусить опрацьовувати вручну.
NASM, асемблер, який використовується в цьому треку, пропонує кілька інструментів, які допомагають програмісту з обома проблемами. Ці інструменти працюють під час асемблювання, перш ніж будь-яка інструкція потрапить до процесора. Вони підказують асемблеру, як переписати вихідний текст, ще до того, як його буде перекладено в машинний код.
Найважливіші з цих інструментів - макроси. Їх можна уявити як рецепт для асемблера. Вони розгортаються саме там, де використані, наче цей код було написано там вручну.
Макроси можуть приймати як параметри низку буквальних токенів, завдяки чому їх можна повторно використовувати в різних контекстах.
Однак це не функції: тут немає ні call, ні ret, ні передавання керування в іншу точку коду.
Найпростіше визначення дає імʼя значенню або короткому виразу за допомогою %define:
%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)
Кожне наступне входження WIDTH замінюється на 8 ще до асемблювання.
Заміна відбувається на рівні тексту, тож імʼя підставляється всюди, де воно трапляється: і в операнді, і всередині іншого визначення.
Для числових констант краще використовувати equ, а не %define.
Однорядковий макрос, оголошений через %define, розгортається лише тоді, коли його використано, і його можна перевизначити або скасувати.
А константа equ, своєю чергою, обчислюється в точці визначення і ніколи не змінюється.
%define може також приймати іменовані параметри й поводитися як невеличка вбудована функція:
%define double(x) ((x) + (x))
mov eax, double(WIDTH) ; assembled as mov eax, ((8) + (8))
І все тіло, і кожен параметр слід брати в дужки.
Із %define square(x) x*x виклик square(1+1) розгортається в 1+1*1+1, що дорівнює 3.
Якщо ж записати %define square(x) ((x)*(x)), той самий виклик розгортається в ((1+1)*(1+1)), що дорівнює 4.
Імена, визначені через %define, чутливі до регістру.
Варіант %idefine робить імʼя нечутливим до регістру, якщо це потрібно.
Однорядкові макроси допомагають надати сенсу різним частинам коду. Якщо користуватися ними обережно, це покращує читабельність і зменшує ймовірність одруківок. Наприклад, регістр, який усередині функції має окреме призначення, можна назвати за цим призначенням:
%define COUNTER rcx
...
.loop:
...
dec COUNTER
jnz .loop
...
Цілу послідовність коду, з будь-якою кількістю рядків, задають через %macro і закривають %endmacro.
Число після назви вказує, скільки аргументів приймає макрос; їх може бути й нуль.
Передані аргументи розділяються комою (,):
%macro example_macro 0
...
%endmacro
Аргументи в багаторядкових макросах не мають імен.
До них звертаються як до %1, %2 і так далі, у тому порядку, у якому їх передано:
; Multiplies two values, returning the result in the first argument
%macro mult_two 2 ; %1 = accumulator, %2 = value
imul %1, %2
%endmacro
mult_two eax, ecx ; multiplies eax and ecx and returns the result into eax
mult_two r8, r9 ; multiplies r8 and r9 and returns the result into r8
Зауважмо, що аргументи мають бути синтаксично придатними для операцій, які виконуються в тілі макроса.
Тут і mult_two eax, ecx, і mult_two r8, r9 успішно асемблюються, а mult_two eax, cl не буде асемблюватися, бо imul eax, cl не визначено.
Макрос, який визначає звичайну мітку, ламається відразу, щойно його використати двічі, бо тоді й мітку буде визначено двічі.
Щоб використовувати мітки всередині макроса, який може викликатися більше одного разу, перед такою міткою треба поставити %%.
Це вказує асемблеру давати цій мітці інше, унікальне імʼя при кожному розгортанні:
; clobbers rcx
%macro clear_n 2 ; %1 = base address, %2 = count of dwords to clear
xor ecx, ecx
%%loop:
mov dword [%1 + rcx*4], 0
inc ecx
cmp ecx, %2 ; %2 is compared with ecx: it is an immediate or a 32-bit operand
jb %%loop
%endmacro
clear_n rdi, 4 ; gets its own %%loop
clear_n rsi, 8 ; a different %%loop, no clash
Повторити однорядковий блок коду можна за допомогою times.
Це особливо зручно, коли треба оголосити в памʼяті повторювані значення:
section .data
negative_block: times 16 db -1
; this assembles to 16 db -1 in sequence, i.e., an array of 16 bytes, all equal to -1
Для повторення багаторядкового коду натомість можна скористатися %rep:
%rep 2 ; number of repetitions: 2
add rcx, 32
mov dword [rsi + rcx], 0
%endrep
Зауважмо, що кількість повторень указують одразу після %rep, а закриває повторюваний блок %endrep.
Усередині блоку %rep часто використовують лічильник часу асемблювання, так само як цикл під час виконання програми використовує лічильник.
Хоча цілочисельну константу часу асемблювання можна оголосити через equ, для цілочисельних змінних часу асемблювання використовують %assign.
На відміну від %define, %assign одразу обчислюється до числа і його можна перевизначити:
xor eax, eax
%assign i 0
%rep 4
add eax, i ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep
Після роботи препроцесора цикл зникає, і залишаються чотири інструкції add, у які вже вбудовано безпосередні значення 0, 1, 2 і 3.
Це розгортання циклу, яке виконує сам асемблер: жодного регістра-лічильника, жодного розгалуження, лише лінійний код.
Препроцесор має в розпорядженні різні умовні конструкції. Вони обирають один із кількох різних шляхів і розгортають своє тіло на місці, але лише тоді, коли їхня умова виконується.
%if ... %elif ... %else ... %endif обирають гілку за числовою перевіркою:
%if WIDTH > 4
mov eax, 2
%elif WIDTH > 2
mov eax, 1
%else
mov eax, 0
%endif
Макроси часто тримають в окремому файлі й долучають до кожного вихідного файлу, якому вони потрібні, за допомогою %include:
%include "patterns.mac"
Названий файл вставляється дослівно в цьому місці, ще до асемблювання, точно так, наче його вміст було набрано тут. Так визначення залишаються в одному місці, і кілька вихідних файлів можуть ними користуватися.
Якщо файл макросів може асемблюватися окремо, він повинен мати розширення .asm.
Зауважмо, що макроси не мають звʼязування.
Навіть якщо файл макросів буде асембловано, його макроси видимі лише тому файлу, який долучає його через %include.
Інакше файл, призначений лише для %include, може мати будь-яке розширення.
Поширеною практикою є використання або .asm, або .mac.
Ми підтримуємо бібліотеку макросів, на якій побудовано рушій верстки журналу. Сам рушій, тобто функції, що верстають кожну сторінку, фіксований, і ми його не пишемо. Ми пишемо лише той механізм часу асемблювання, з якого його побудовано.
У цій вправі не потрібно писати жодної функції, лише макроси. Ці макроси потім підключаються і викликаються у функціях, уже визначених в іншому файлі.
На нас чекають чотири завдання.
Кожен макрос викликається за точною назвою та порядком аргументів, які дає його завдання, тож напишіть їх саме так. Інакше файл не асемблюється.
Формат журналу задають кілька фіксованих чисел, а решта верстки звертається до них лише за назвою. Дайте кожному з них читабельну назву під час асемблювання:
COLUMN_COUNT дорівнює 4
COLUMN_STRIDE дорівнює 18
SPACE дорівнює 0x20, байту пробілуRULE дорівнює 0x3D, байту =
RULE_WIDTH дорівнює 4
PLAIN дорівнює 0x2E, байту .
RULED дорівнює 0x2D, байту -
BOXED дорівнює 0x23, байту #
Рушій видає їх у наведеному вище порядку.
out = {4, 18, 32, 61, 4, 46, 45, 35} // the eight named values, in order
fill_run
Рушій заповнює ґранку пробілами і викладає лінійки шапки, штампуючи послідовність одного байта. Оформіть цей крок як багаторядковий макрос, який рушій може викликати всюди, де потрібно заповнити послідовність.
Спершу визначте читабельну назву COUNTER для регістра rcx.
Потім визначте макрос fill_run, який має заповнити масив N копіями одного байта.
Він приймає три аргументи в такому порядку:
fill_run rdi, rax, SPACE
fill_run rdx, RULE_WIDTH, RULE
Макрос fill_run може вільно перезаписувати COUNTER, але не інші регістри.
Зауважте, що кількість байтів можна передати в 64-бітному регістрі загального призначення, наприклад rax або rsi, або як безпосереднє значення, наприклад 10 або 4.
Можна вважати, що ця кількість ніколи не буває відʼємною.
Цей макрос викликається тричі, у двох різних функціях.
length = 8
page = {32, 32, 32, 32, 32, 32, 32, 32}
width = 8
page = {61, 61, 61, 61, 32, 32, 32, 32} // '====' then spaces
lay_columns
Сітка фіксована для цього формату, тож початки стовпців можна обчислити під час асемблювання.
Визначте макрос lay_columns, який записує COLUMN_COUNT зсувів, кожен як 64-бітне ціле число.
Він приймає два аргументи в такому порядку:
lay_columns rsi, rax
Кількість зсувів, які треба записати за адресою призначення, фіксована й дорівнює COLUMN_COUNT.
Кожен зсув обчислюється як 64-бітне значення за формулою offset[i] = margin + i * COLUMN_STRIDE, де i позначає індекс зсуву, починаючи з 0, а margin є другим аргументом.
Цей макрос може перезаписувати свої два аргументи-регістри, але не інші. Він викликається один раз.
margin = 3
out = {3, 21, 39, 57}
set_header
Кожен із трьох постійних розділів має стиль заголовка, заданий форматом, тож байт відомий під час асемблювання.
Визначте set_header, який приймає два аргументи в такому порядку:
0 означає простий, 1 - з лінійкою, а 2 - у рамці.Адреса призначення задається виразом ефективної адреси памʼяті.
Він приймає базовий регістр, який можна додати до індексного регістра, масштабованого на 1, 2, 4 або 8, та безпосереднє значення:
set_header rdx+8*rcx+2, 1
Виберіть байт заголовка (PLAIN, RULED або BOXED) за допомогою умовного асемблювання (%if / %elif / %else), а не розгалуження під час виконання.
Цей макрос не повинен перезаписувати жоден регістр. Він викликається тричі в одній і тій самій функції.
pitch = 4
out = {46, 0, 0, 0, 45, 0, 0, 0, 35} // '.', '-', '#'
Зареєструйтеся на Exercism, щоб вивчати й опановувати x86-64 Assembly, а також 22 концепції130 вправ та справжнє наставництво від людей, і все це безкоштовно.