Кожен біт цілого числа можна використати, щоб зберегти двійкове значення. Оскільки багато ситуацій повʼязані з двійковою інформацією, як-от правда чи неправда, включення чи виключення, увімкнено чи вимкнено, двійкове подання N-бітного цілого числа дає компактний спосіб закодувати двійковий стан N елементів. Саме тому вміння працювати з бітами й байтами є необхідним в асемблері. Набір інструкцій x86-64 пропонує широкий вибір інструкцій для побітових операцій.
Ці інструкції працюють з окремими бітами в операнді.
Усі вони приймають два операнди, другий указує індекс біта, з яким працюють у першому операнді. Усі вони копіюють вибраний біт у прапорець перенесення (CF).
| Назва | Опис |
|---|---|
bt |
копіює біт у CF, не змінюючи жодного операнда |
bts |
копіює біт у CF і встановлює його в операнді призначення |
btr |
копіює біт у CF і скидає його в операнді призначення |
btc |
копіює біт у CF і доповнює (інвертує) його в операнді призначення |
Побітові операції виконуються над усіма бітами операнда.
Для кожної з них є інструкція з тією ж назвою, що й виконувана побітова операція:
| Назва | Опис |
|---|---|
and |
1, якщо обидва біти дорівнюють 1 |
or |
1, якщо принаймні один із бітів дорівнює 1 |
xor |
1, якщо біти різняться |
not |
1, якщо біт був 0; 0, якщо біт був 1 |
Більшість із них приймають два операнди, виконують побітову операцію над обома й зберігають результат в операнді призначення.
Виняток - not, яка приймає лише один операнд призначення.
Коли ми тлумачимо одиницю та нуль як включення та виключення відповідно, ціле число називають бітовою маскою (або просто маскою).
Бітова маска «відсіює» елементи, бо нуль у i-му біті виключає i-й елемент, а одиниця включає його.
Також ми часто використовуємо бітову маску, щоб включити певні біти цілого числа й виключити інші.
Наприклад, нехай A буде цілим числом, двійкове подання якого таке:
| індекс | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| біти | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 |
Так само нехай M буде цілим числом, двійкове подання якого таке:
| індекс | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| біти | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
Обидва - 8-бітні цілі числа.
У цьому випадку можна сказати, що M вибирає біти 0, 2 і 3 числа A, а решту виключає.
Побітові інструкції, розглянуті раніше, стають у пригоді, коли ми працюємо з цілими числами за допомогою масок. Наприклад:
A, які не вибрані маскою M, виконаймо побітове AND: A AND M.A, вибрані маскою M, виконаймо побітове OR: A OR M.Інструкція test виконує побітове AND між обома операндами й встановлює прапорці відповідно до результату.
Нехай A буде першим операндом, а B - другим:
| прапорець | встановлюється, коли |
|---|---|
CF |
завжди скидається |
ZF |
A AND B == 0 |
SF |
знаковий біт A AND B встановлено |
OF |
завжди скидається |
Ця інструкція приймає два операнди й оновлює прапорці, але не змінює свої операнди.
Ці інструкції переміщують біти в операнді призначення на кількість позицій, указану другим операндом.
Другий операнд має бути сталим числом (immediate) або регістром cl (наймолодші 8 бітів rcx).
| Назва | Опис |
|---|---|
shl/sal
|
Зсуває біти ліворуч |
shr/sar
|
Зсуває біти праворуч |
Зауважмо, що лічильник у другому операнді маскується до 5 бітів, або 6 бітів, якщо операнд призначення 64-бітний.
Будь-які біти після цього фактично ігноруються.
Це означає, що максимальний зсув - 31, або 63 з 64-бітним операндом.
І shl, і sal виконують ту саму операцію, одна з них - псевдонім іншої.
Щоразу, коли виконується зсув ліворуч, біти, розташовані ближче до кінця послідовності, ніж довжина зсуву, спершу переміщуються в CF, а потім відкидаються.
Натомість на початок додається стільки нових скинутих бітів, скільки становить довжина зсуву.
Оскільки кожен біт у цілому числі представляє степінь 2, зсув ліворуч на n позицій приводить до множення цілого числа на 2ⁿ.
Є дві інструкції для переміщення бітів праворуч: shr і sar.
Коли використовується будь-яка з цих двох інструкцій, біти, розташовані ближче до початку послідовності, ніж довжина зсуву, спершу переміщуються в CF, а потім відкидаються.
Натомість у кінець додається стільки нових бітів, скільки становить довжина зсуву.
Різниця між ними в тому, що shr додає 0 бітів на лівому кінці, а sar додає 1, якщо найстарший біт був встановлений, і 0 в іншому разі.
Це означає, що sar зберігає знак під час зсуву знакового цілого числа.
Оскільки кожен біт у цілому числі представляє степінь 2, зсув праворуч на n позицій за допомогою shr приводить до беззнакового ділення на 2ⁿ.
Так само зсув праворуч на n позицій за допомогою sar приводить до знакового ділення на 2ⁿ.
Ці інструкції переміщують біти в операнді призначення на кількість позицій, указану другим операндом.
Другий операнд має бути сталим числом (immediate) або регістром cl (наймолодші 8 бітів rcx).
Різниця між обертанням і зсувом у тому, що обертання не відкидає й не додає жодного біта. Біти, які були б відкинуті під час зсуву, натомість переміщуються на протилежний кінець. Отже, усі біти залишаються, але всі вони змінюють свої місця.
| Назва | Опис |
|---|---|
rol |
Обертає біти ліворуч |
ror |
Обертає біти праворуч |
Зауважмо, що лічильник у другому операнді маскується до 5 бітів, або 6 бітів, якщо операнд призначення 64-бітний.
Будь-які біти після цього фактично ігноруються.
Це означає, що максимальне обертання - 31, або 63 з 64-бітним операндом.
Є й інші корисні інструкції для роботи з бітами:
| Назва | Опис |
|---|---|
popcnt |
Підраховує кількість встановлених бітів |
bsr |
Повертає індекс найстаршого встановленого біта. Якщо жоден біт не встановлено, результат невизначений |
bsf |
Повертає індекс наймолодшого встановленого біта. Якщо жоден біт не встановлено, результат невизначений |
Усі ці інструкції працюють із двома 16-бітними, 32-бітними або 64-бітними операндами.
Їх не можна використовувати з 8-бітними операндами.
Наш друг щойно надіслав нам повідомлення з важливою таємницею. Щоб його не змогли легко прочитати інші, повідомлення зашифрували, виконавши низку операцій над бітами. Нам потрібно буде написати методи, які допоможуть розшифрувати повідомлення.
Ось інструкції для одного біта, згадані в цьому концепті:
| Name | Description |
|---|---|
| bt | копіює біт у CF, не змінюючи жодного операнда |
| bts | копіює біт у CF і встановлює його в операнді призначення |
| btr | копіює біт у CF і скидає його в операнді призначення |
| btc | копіює біт у CF і доповнює (перевертає) його в операнді призначення |
Ось побітові інструкції, згадані в цьому концепті:
| Name | Description |
|---|---|
| and | 1, якщо обидва біти дорівнюють 1 |
| or | 1, якщо принаймні один із бітів дорівнює 1 |
| xor | 1, якщо біти різняться |
| not | 1, якщо біт був 0; 0, якщо біт був 1 |
Ось інструкції зсуву, згадані в цьому концепті:
| Name | Description |
|---|---|
| shl/sal | зсуває біти вліво |
| shr/sar | зсуває біти вправо |
Ось інструкції обертання, згадані в цьому концепті:
| Name | Description |
|---|---|
| rol | обертає біти вліво |
| ror | обертає біти вправо |
Ось різні інструкції, згадані в цьому концепті:
| Name | Description |
|---|---|
| popcnt | рахує кількість установлених бітів |
| bsr | отримує індекс найстаршого встановленого біта. Якщо жоден біт не встановлено, результат не визначено |
| bsf | отримує індекс наймолодшого встановленого біта. Якщо жоден біт не встановлено, результат не визначено |
Повідомлення закодовано в 16-бітному цілому числі. Однак із них 8 старших бітів насправді не є частиною повідомлення, а маскою, яку потрібно використати для розшифрування.
Реалізуйте функцію extract_higher_bits, яка приймає 16-бітне ціле число і повертає його 8 старших бітів.
extract_higher_bits(0b1010010011000101)
// => 0b10100100
Мало вміти видобути маску, слід також відокремити саме повідомлення.
Реалізуйте функцію extract_lower_bits, яка приймає 16-бітне ціле число і повертає його 8 молодших бітів.
extract_lower_bits(0b1010010011000101);
// => 0b11000101
Деякі біти встановлені як у повідомленні, так і в масці. Це дуже важлива інформація, яка знадобиться пізніше.
Реалізуйте функцію extract_redundant_bits, яка приймає 16-бітне ціле число, що кодує і повідомлення, і маску, і повертає 8-бітне ціле число, у якому встановлено лише надлишкові біти.
Біт у поверненому числі має бути встановлений в 1 там, де він також дорівнює 1 і в повідомленні, і в масці.
Усі інші біти мають бути скинуті.
extract_redundant_bits(0b1010010011000101);
// => 0b10000100
Далі, деякі біти потрібно встановити в 1 в повідомленні, відповідно до маски.
Реалізуйте функцію set_message_bits, яка приймає 16-бітне ціле число, що кодує і повідомлення, і маску, і повертає результат установлення бітів повідомлення в 1.
Біт повідомлення має бути встановлений в 1 там, де біт у масці дорівнює 1.
Усі інші біти мають залишитися без змін, тобто встановленими, якщо вони вже були встановлені, і скинутими, якщо вже були скинуті.
set_message_bits(0b1010010011000101);
// => 0b11100101
Одна частина головоломки в повідомленні явно не вказана: 16-бітне число 0b1011001100111100.
Це число - спільний приватний ключ, і його потрібно використати, щоб допомогти розшифрувати повідомлення.
Щоб це зробити, спершу потрібно обернути біти приватного ключа вліво на певну кількість позицій. Кількість позицій дорівнює кількості надлишкових бітів, установлених і в повідомленні, і в масці.
Реалізуйте функцію rotate_private_key, яка приймає 16-бітне ціле число, що кодує і повідомлення, і маску, і повертає результат обертання приватного ключа.
Цей результат - 16-бітне ціле число.
rotate_private_key(0b1010010011000101);
// => 0b1100110011110010
NASM (The Netwide Assembler, асемблер, який використовує цей трек) підтримує константи у двійковому форматі з префіксом 0b.
Він також дозволяє використовувати символ підкреслення (_) як розділювач у константі для кращої читабельності:
PRIVATE_KEY equ 0b1011_0011_0011_1100
Щоб його можна було використати для розшифрування, приватний ключ потрібно відформатувати так, щоб відокремити потрібні біти.
Щоб повністю відформатувати приватний ключ, потрібно:
Перевернутий біт дорівнює 1, якщо він був 0, і 0, якщо він був 1.
Реалізуйте функцію format_private_key, яка приймає 16-бітне ціле число, що кодує і повідомлення, і маску, і повертає повністю відформатований 8-бітний приватний ключ.
format_private_key(0b1010010011000101);
// => 0b11000001
Коли в нас уже є повідомлення з усіма потрібними встановленими бітами та відформатований приватний ключ, настав час поєднати їх, щоб отримати підсумкове повідомлення.
Підсумкове повідомлення - це 16-бітне ціле число, у якому:
Реалізуйте функцію decrypt_message, яка приймає 16-бітне ціле число, що кодує і повідомлення, і маску, і повертає 16-бітне ціле число з повністю розшифрованим повідомленням.
Ця функція має використовувати відформатований приватний ключ, який ми отримуємо за допомогою format_private_key, а також повідомлення з усіма потрібними встановленими бітами, отримане за допомогою set_message_bits.
decrypt_message(0b1010010011000101);
// => 0b1100000111100101
Зареєструйтеся на Exercism, щоб вивчати й опановувати x86-64 Assembly, а також 22 концепції130 вправ та справжнє наставництво від людей, і все це безкоштовно.