Треки
/
x86-64 Assembly
x86-64 Assembly
/
Вправи
/
Верстка журналу
Верстка журналу

Верстка журналу

Навчальна вправа

Вступ

Макроси

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

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

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

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

Однорядкові макроси

Найпростіше визначення дає імʼя значенню або короткому виразу за допомогою %define:

%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)

Кожне наступне входження WIDTH замінюється на 8 ще до асемблювання. Заміна відбувається на рівні тексту, тож імʼя підставляється всюди, де воно трапляється: і в операнді, і всередині іншого визначення.

Note

Для числових констант краще використовувати equ, а не %define.

Однорядковий макрос, оголошений через %define, розгортається лише тоді, коли його використано, і його можна перевизначити або скасувати. А константа equ, своєю чергою, обчислюється в точці визначення і ніколи не змінюється.

%define може також приймати іменовані параметри й поводитися як невеличка вбудована функція:

%define double(x) ((x) + (x))

    mov eax, double(WIDTH)   ; assembled as mov eax, ((8) + (8))
Caution

І все тіло, і кожен параметр слід брати в дужки. Із %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.

Вказівки

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

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

На нас чекають чотири завдання.

Caution

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

1. Назвіть константи формату

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

  • 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

2. Напишіть макрос fill_run

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

Спершу визначте читабельну назву COUNTER для регістра rcx. Потім визначте макрос fill_run, який має заповнити масив N копіями одного байта. Він приймає три аргументи в такому порядку:

  1. адреса призначення, передана в 64-бітному регістрі загального призначення.
  2. кількість байтів (N) для запису, передана в 64-бітному регістрі загального призначення або як безпосереднє значення.
  3. байт для запису, переданий як безпосереднє значення.
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

3. Напишіть макрос lay_columns

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

Визначте макрос lay_columns, який записує COLUMN_COUNT зсувів, кожен як 64-бітне ціле число. Він приймає два аргументи в такому порядку:

  1. адреса призначення, передана в 64-бітному регістрі загального призначення
  2. лівий відступ, переданий у 64-бітному регістрі загального призначення
lay_columns rsi, rax

Кількість зсувів, які треба записати за адресою призначення, фіксована й дорівнює COLUMN_COUNT.

Кожен зсув обчислюється як 64-бітне значення за формулою offset[i] = margin + i * COLUMN_STRIDE, де i позначає індекс зсуву, починаючи з 0, а margin є другим аргументом.

Цей макрос може перезаписувати свої два аргументи-регістри, але не інші. Він викликається один раз.

margin = 3
out    = {3, 21, 39, 57}

4. Напишіть макрос set_header

Кожен із трьох постійних розділів має стиль заголовка, заданий форматом, тож байт відомий під час асемблювання.

Визначте set_header, який приймає два аргументи в такому порядку:

  1. адреса призначення.
  2. літеральний ідентифікатор стилю, переданий як безпосереднє значення, де 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} // '.', '-', '#'
Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
x86-64 Assembly Exercism

Час розпочати Верстка журналу?

Зареєструйтеся на Exercism, щоб вивчати й опановувати x86-64 Assembly, а також 22 концепції130 вправ та справжнє наставництво від людей, і все це безкоштовно.