Треки
/
x86-64 Assembly
x86-64 Assembly
/
Вправи
/
Палітра кольорів
Палітра кольорів

Палітра кольорів

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

Вступ

Памʼять

Операційна система (ОС) зазвичай відображає памʼять програми за такою загальною схемою:

адреса ділянка памʼяті
висока стек
...
купа
сегмент для читання й запису
сегмент коду / лише для читання
низька зарезервовано

Памʼять у сегментах організовано в секції з різними правами доступу.

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

Секція .data

Ініціалізовані дані оголошують у секції .data.

У NASM (The Netwide Assembler, асемблер, який використовує цей трек) ініціалізована змінна має імʼя, директиву, яка вказує розмір даних, і список значень, розділених комами. Кожен із цих елементів відокремлено від іншого пробілом, а після мітки необовʼязково може стояти :.

Основні директиви та відповідні їм розміри даних:

директива розмір
db 1 байт
dw 2 байти
dd 4 байти
dq 8 байтів

Наприклад, це оголошує байтову змінну з іменем space і значенням 10:

section .data
    space db 10

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

Секція .rodata

Секція .rodata схожа на section .data. Обидві секції містять ініціалізовані дані, які оголошують однаково, і мають однакову тривалість зберігання.

Головна відмінність між ними в тому, що дані в section .rodata є незмінними, тобто доступними лише для читання.

Note

Константи, визначені за допомогою equ, відрізняються від тих, що визначені в section .rodata.

Константа, визначена за допомогою equ, не займає місця в памʼяті, і асемблер безпосередньо підставляє її значення замість неї. Насправді вона є заповнювачем для цього значення.

З іншого боку, константи, визначені в section .rodata, насправді зберігаються в памʼяті й мають адресу.

Доступ до даних

Мітки та непряме звернення

Оголошені дані повинні мати повʼязане з ними імʼя. Це імʼя називають міткою.

Мітка - це символ, який кодує конкретну адресу даних у памʼяті. Адреси в x86-64 - це 64-бітні значення.

У NASM спроба звернутися до даних безпосередньо за їхньою міткою дає не виділену памʼять, а її адресу:

section .data
    example dq 27 ; this declares a 8-byte variable initialized with 27

section .text
fn:
    mov rax, example ; this stores the address of the declared variable in rax, not its contents
    ...

Щоб отримати вміст, що зберігається за адресою памʼяті, потрібно її розіменувати. Це називають непрямим зверненням.

У NASM це роблять за допомогою []:

section .data
    example dq -27 ; this declares a 8-byte variable initialized with -27

section .text
fn:
    mov rax, [example] ; this dereferences example and access the value stored in memory (-27)
    ...

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

Ось найважливіші префікси та їхні розміри в типовій програмі для x86-64:

префікс розмір
byte 1 байт
word 2 байти
dword 4 байти
qword 8 байтів

Те саме завантаження можна записати із явно зазначеним розміром:

    mov rax, qword [example] ; same dereference, size stated explicitly

Це хороша практика: завжди використовувати префікс під час розіменування памʼяті.

Запис у памʼять

Запис у памʼять виконують так само, розіменовуючи адресу:

section .data
    example1 db 10            ; example1 is a 1-byte memory location initialized with value 10
    example2 dq -456          ; example2 is a 8-byte memory location initialized with value -456
    example3 dd 54            ; example3 is a 4-byte memory location initialized with value 54

section .text
fn:
    mov byte [example1], 20   ; example1 now has value 20
    mov qword [example2], rdx ; example2 now has value equal to the contents in rdx
    mov dword [example3], eax ; example3 now has value equal to the contents in eax

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

section .data
    example4 dw 4
    example5 dq -8
    example6 dd 15

section .text
fn:
    add word [example4], 5     ; example4 is now a 2-byte memory location with the value 4 + 5 = 9
    imul rax, qword [example5] ; rax = rax * (-8)
    ; this is not possible -> sub dword [example6], dword [example6]
Інструкція LEA

Хоча mov можна використати, щоб зберегти адресу змінної в регістрі, для цього є спеціальна інструкція: lea.

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

lea rax, [example] ; this stores the address of 'example' in rax

Обчислювати й зберігати адреси памʼяті в регістрах ідіоматичніше за допомогою lea.

Відносна адресація

Коли ми звертаємося до комірок памʼяті, NASM типово генерує абсолютні адреси, тобто фіксовані адреси памʼяті.

З міркувань безпеки виконувані файли часто збирають як PIE (Position Independent Executable), де ділянки памʼяті розміщуються за випадковими адресами. У PIE остаточна адреса змінної невідома на етапі компонування. Тож код натомість обчислює адреси як зсув від значення в спеціальному регістрі rip, який указує на наступну інструкцію для виконання.

Це зазвичай називають адресацією відносно RIP.

У NASM можна запросити доступ відносно RIP за допомогою оператора rel:

mov rax, qword [rel variable]

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

Усі вправи в цьому треку компілюють і компонують як PIE, тому для генерування відносних адрес варто використовувати rel.

Видимість

Мітки (функції та дані), визначені в будь-якій секції (наприклад, .text, .data, .rodata), видимі в межах одного вихідного файлу. Якщо їх оголосити як global, вони стають видимими й для інших вихідних файлів.

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

default rel

section .data

global number1 ; 'number1' is a variable visible to other source files
number1 db 200

extern number2 ; 'number2' is a variable visible to the current source file, but defined in another

section .text

extern sum ; sum is a function visible to the current source file, but defined in another

fn:
    mov dil, byte [number1]
    mov sil, byte [number2]
    call sum
    ...

Вказівки

Наш друг José викладає в місцевій школі. Він задумав кілька цікавих експериментів, які покажуть, як можна поєднувати кольори, щоб отримувати різні.

Він попросив нашої допомоги з цими експериментами.

Note

У цій вправі колір представлено 32-бітним (4-байтовим) числом, яке кодує його значення RGB.

Значення RGB складається з 3 каналів, Red, Green і Blue, кожен з яких займає 8 бітів (1 байт). Четвертий байт зазвичай зарезервовано для каналу Alpha, але в цій вправі його значення буде порожнім (0).

1. Отримати значення RGB для кольору

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

Визначте функцію get_color_value, яка повертає 32-бітне значення кольору. Ця функція приймає як параметр дійсну адресу цього кольору в таблиці кольорів.

get_color_value(black)
// => 0

Підказка - 32 біти еквівалентні 4 байтам.

2. Додати базовий колір

Щоб змішувати різні кольори, José спершу фіксує базовий, а змінює лише додатковий колір, який із ним поєднується.

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

Змінну base_color потрібно визначити самостійно, і вона має бути доступною з інших вихідних файлів.

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

Типово, на початку програми, base_color має бути ініціалізовано 32-бітним значенням white, тобто 0xFFFFFF00.

Підказка - NASM приймає числа, записані в шістнадцятковій системі, якщо на початку стоїть 0x, наприклад 0xFFFFFF00.

3. Визначити константи для основних кольорів

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

  • RED, зі значенням 0xFF000000.
  • GREEN, зі значенням 0x00FF0000.
  • BLUE, зі значенням 0x0000FF00.

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

4. Поєднати кольори

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

Визначте функцію make_color_combination, яка поєднує два кольори й зберігає результат у памʼяті. Ця функція не повертає значення і приймає як параметри, у такому порядку:

  • Адресу, за якою слід зберегти 32-бітне значення поєднаного кольору.
  • Адресу додаткового кольору в таблиці кольорів, який поєднується з основним кольором.
Caution

Зверніть увагу, що combining_function може змінити значення в регістрах, які ми використовуємо. Перед викликом цієї функції збережіть у памʼяті всі потрібні змінні.

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
x86-64 Assembly Exercism

Час розпочати Палітра кольорів?

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