Операційна система (ОС) зазвичай відображає памʼять програми за такою загальною схемою:
| адреса | ділянка памʼяті |
|---|---|
| висока | стек |
| ... | |
| купа | |
| сегмент для читання й запису | |
| сегмент коду / лише для читання | |
| низька | зарезервовано |
Памʼять у сегментах організовано в секції з різними правами доступу.
Усі функції, які ми визначили досі, були в секції .text. Ця секція містить виконувані дані, призначені лише для читання. Інші секції використовують для оголошення змінних із даними, які можуть бути призначені лише для читання або для читання й запису, але не є виконуваними.
Ініціалізовані дані оголошують у секції .data.
У NASM (The Netwide Assembler, асемблер, який використовує цей трек) ініціалізована змінна має імʼя, директиву, яка вказує розмір даних, і список значень, розділених комами.
Кожен із цих елементів відокремлено від іншого пробілом, а після мітки необовʼязково може стояти :.
Основні директиви та відповідні їм розміри даних:
| директива | розмір |
|---|---|
| db | 1 байт |
| dw | 2 байти |
| dd | 4 байти |
| dq | 8 байтів |
Наприклад, це оголошує байтову змінну з іменем space і значенням 10:
section .data
space db 10
Змінні, оголошені в section .data, є змінними, тобто доступними для читання й запису.
Вони також мають статичну тривалість зберігання, а це означає, що вони існують протягом усього часу виконання програми.
Секція .rodata схожа на section .data.
Обидві секції містять ініціалізовані дані, які оголошують однаково, і мають однакову тривалість зберігання.
Головна відмінність між ними в тому, що дані в section .rodata є незмінними, тобто доступними лише для читання.
Константи, визначені за допомогою 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]
Хоча 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é викладає в місцевій школі. Він задумав кілька цікавих експериментів, які покажуть, як можна поєднувати кольори, щоб отримувати різні.
Він попросив нашої допомоги з цими експериментами.
У цій вправі колір представлено 32-бітним (4-байтовим) числом, яке кодує його значення RGB.
Значення RGB складається з 3 каналів, Red, Green і Blue, кожен з яких займає 8 бітів (1 байт).
Четвертий байт зазвичай зарезервовано для каналу Alpha, але в цій вправі його значення буде порожнім (0).
Значення кожного кольору вже збережено в таблиці, визначеній в іншому вихідному файлі. Кожен колір визначається унікальною адресою в цій таблиці.
Визначте функцію get_color_value, яка повертає 32-бітне значення кольору.
Ця функція приймає як параметр дійсну адресу цього кольору в таблиці кольорів.
get_color_value(black)
// => 0
Підказка - 32 біти еквівалентні 4 байтам.
Щоб змішувати різні кольори, José спершу фіксує базовий, а змінює лише додатковий колір, який із ним поєднується.
Визначте функцію add_base_color, яка зберігає 32-бітне значення кольору у змінній base_color, щоб його можна було використати пізніше.
Ця функція не повертає значення і приймає як параметр адресу кольору в таблиці кольорів.
Змінну base_color потрібно визначити самостійно, і вона має бути доступною з інших вихідних файлів.
Одночасно може існувати не більше одного базового кольору. Якщо додати новий базовий колір, старий відкидається.
Типово, на початку програми, base_color має бути ініціалізовано 32-бітним значенням white, тобто 0xFFFFFF00.
Підказка - NASM приймає числа, записані в шістнадцятковій системі, якщо на початку стоїть 0x, наприклад 0xFFFFFF00.
José планує робити багато комбінацій з основними кольорами, тож хоче мати їх окремо для швидкого доступу.
Оскільки він використовує RGB для представлення кольорів, основними кольорами є:
RED, зі значенням 0xFF000000.GREEN, зі значенням 0x00FF0000.BLUE, зі значенням 0x0000FF00.Визначте по одній константі для кожного з цих кольорів. Ці константи мають бути доступними з інших вихідних файлів.
Кольори потрібно поєднувати згідно з combining_function, визначеною в іншому вихідному файлі.
Ця функція приймає як параметри 32-бітні значення base_color і додаткового кольору, який з ним змішується.
Вона повертає 32-бітне значення поєднаного кольору.
Визначте функцію make_color_combination, яка поєднує два кольори й зберігає результат у памʼяті.
Ця функція не повертає значення і приймає як параметри, у такому порядку:
Зверніть увагу, що combining_function може змінити значення в регістрах, які ми використовуємо.
Перед викликом цієї функції збережіть у памʼяті всі потрібні змінні.
Зареєструйтеся на Exercism, щоб вивчати й опановувати x86-64 Assembly, а також 22 концепції130 вправ та справжнє наставництво від людей, і все це безкоштовно.