Треки
/
x86-64 Assembly
x86-64 Assembly
/
Вправи
/
Управління запасами
Управління запасами

Управління запасами

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

Вступ

Цілі числа

Двійкове подання

Ціле число - це абстракція, яка представляє числа без дробової частини, як-от 4, -2, 0 або 64532.

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

Беззнакові числа

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

Беззнакові числа подають безпосередньо як суму степенів двійки, що відповідають усім встановленим бітам у їхній послідовності.

Діапазон невідʼємних цілих чисел, які можна представити в регістрі, простягається від 0 (жоден біт не встановлено) до 2⁶⁴ - 1 (сума всіх 64 встановлених бітів).

Щоб розширити беззнакове число до більшого розміру, усі старші біти заповнюють 0, тож жоден новий біт не додає нічого до значення. Це називають розширенням нулями.

Інструкція movzx (z означає zero) виконує розширення нулями 8-бітного або 16-бітного операнда-джерела до більшого операнда-призначення. А 32-бітний операнд-джерело завжди розширюється нулями до всіх 64 бітів операнда-призначення простим mov.

Знакові числа

Якщо ціле число може набувати додатних або відʼємних значень, його називають знаковим.

Щоб представити відʼємні числа, x86-64 використовує доповняльний код.

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

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

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

Інструкція movsx (s означає sign) виконує розширення знака 8-бітного або 16-бітного операнда-джерела до більшого операнда-призначення. Різновид movsx під назвою movsxd робить те саме з 32-бітного операнда-джерела до 64-бітного операнда-призначення.

Інструкцію neg можна використати, щоб змінити знак числа.

Caution

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

Коментарі можуть дуже допомогти в цьому.

Безпосередні значення

У попередньому концепті згадувалося, що стале число, як-от 4 або -15, можна використовувати як операнд-джерело для багатьох інструкцій. Такі числа називають безпосередніми значеннями.

Безпосереднє значення не зберігається в регістрі чи памʼяті: воно закодоване всередині самої інструкції. У більшості інструкцій для нього відведено лише 32 біти, хоч би яким великим був операнд-призначення.

Коли операнд-призначення має 64 біти, ці 32 біти розширюються зі збереженням знака, щоб заповнити його. Старша половина операнда повністю заповнюється копіями найстаршого біта безпосереднього значення, тож у такий спосіб можна записати лише число в діапазоні 32-бітного знакового цілого числа:

add rax, -1          ; the immediate is sign-extended, so all 64 bits of rax are affected
add rax, 2147483647  ; the largest immediate an instruction like this accepts

Число поза цим діапазоном не можна використати як безпосереднє значення. Виняток із цього правила - mov, який може приймати повне 64-бітне безпосереднє значення, коли операнд-призначення є регістром. Якщо потрібне 64-бітне безпосереднє значення, спершу використаймо mov, щоб завантажити його в регістр, а потім скористаймося цим регістром:

mov rax, 3435973837           ; this works, mov can take a 64-bit immediate
mov rdx, 18446744073709551615 ; the largest immediate mov accepts
sub rdx, rax

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

mov rax, -1                   ; rax = 18446744073709551615
mov rax, 18446744073709551615 ; rax = -1

Додавання

Додавання двох чисел можна обчислити за допомогою інструкції add.

Є також однооперандна інструкція inc, яка додає 1 до значення у своєму операнді:

inc rax ; rax = rax + 1

Додавання двох цілих чисел відбувається однаково і для беззнакових, і для знакових чисел.

Віднімання

Віднімання двох цілих чисел виконують за допомогою інструкції sub.

Є також однооперандна інструкція dec, яка віднімає 1 від значення у своєму операнді:

dec rax ; rax = rax - 1

Віднімання двох цілих чисел теж відбувається однаково і для беззнакових, і для знакових чисел.

Множення

У x86-64 є дві різні інструкції для множення двох чисел. Зазвичай для беззнакового множення використовують інструкцію mul, а для знакового - imul.

Інструкція mul має таку однооперандну форму, де src - це операнд-джерело:

mul src

Інструкція imul може мати однооперандну, двооперандну або триоперандну форму:

imul src
imul dest, src
imul dest, src1, src2
Однооперандне множення

В однооперандній формі для множення неявно використовують два регістри: rax і rdx. Якщо множаться два 64-бітні числа, то молодші 64 біти результату опиняться в rax, а старші 64 біти - в rdx.

Зазвичай це позначають як rdx:rax, щоб показати, що обидва регістри працюють у парі:

mul rcx ; rax = lower 64 bits of rax * rcx
        ; rdx = upper 64 bits of rax * rcx

Так само відбувається й для інших розмірів операндів. Наприклад, якщо множаться два 32-бітні числа, використовують eax і edx.

Виняток - множення двох байтів.

У цьому випадку замість dl:al використовують ax. Молодша частина ax (al) отримає молодші 8 бітів добутку, а старша частина (ah) - старші 8 бітів.

Caution

Регістри, які неявно використовуються в множенні, як-от rax і rdx, завжди перезаписуються. Якщо їхні значення знадобляться пізніше, їх слід зберегти перед операцією.

Двооперандне множення

Двооперандна форма imul має явний операнд-призначення і дотримується звичайного синтаксису. rdx не використовується. Натомість результат обрізають, щоб він умістився в операнд-призначення.

imul r8, r9 ; r8 = lower 64 bits of r8 * r9
Триоперандне множення

Триоперандна форма imul має два операнди-джерела, другий з яких завжди є безпосереднім значенням (сталим числом). Обидва операнди-джерела перемножують, а результат обрізають і записують в операнд-призначення:

imul r8, r9, 100 ; r8 = lower 64 bits of r9 * 100

Зауважмо, що операнд-призначення не бере участі в множенні. Він лише отримує результат.

Робота з переповненням

І двооперандне, і триоперандне множення обрізає результат, щоб він умістився в розмір операнда-призначення. Однооперандне множення зберігає повний діапазон, але результат зазвичай розподіляється між двома регістрами, rdx і rax.

Тож іноді корисно розширити операнди перед множенням, щоб умістити весь добуток в одному регістрі. Беззнаковий операнд розширюють нулями, а знаковий - зі збереженням знака:

movzx eax, di ; di and si hold unsigned 16-bit numbers
movzx ecx, si
mul ecx       ; the 32-bit product fits in eax, and edx is cleared

Ділення

Як і у випадку з множенням, для ділення двох чисел теж є дві інструкції. Для беззнакового ділення використовують інструкцію div, а для знакового - idiv.

Обидві інструкції працюють лише з одним операндом:

div src
idiv src

У 16-, 32- і 64-бітному діленні як ділене використовують dx:ax, edx:eax і rdx:rax відповідно. У цих випадках обидва регістри діють у парі, утворюючи 2N-бітне значення, де N - розмір операції (16, 32 або 64 біти). Потім це значення ділять на операнд-джерело. Частку записують у ax, eax або rax, а остачу - у dx, edx або rdx, залежно від розміру операції.

Ділення байтів особливе: замість dl:al використовують ax. Молодші 8 бітів ax (al) отримають частку від операції, а старші 8 бітів (ah) - остачу.

Зауважмо, що перед діленням усі біти діленого мають бути належно встановлені. Будь-який встановлений біт у rdx (або в ah для 8-бітного ділення) додається до значення, яке ділять.

У беззнаковому діленні, коли значення, яке ділять, уміщається в молодшій половині, старшу половину слід обнулити. Підійде будь-яка інструкція, яка обнуляє ці біти. Наприклад, mov edx, 0 обнуляє старші біти в 32-бітному діленні.

У знаковому діленні значення, навпаки, слід розширити зі збереженням знака. Є інструкції, які автоматизують цей процес: cbw, cwd, cdq і cqo. Перша встановлює біти в ah відповідно до знака al. Решта виконують розширення знака з ax до dx, з eax до edx і з rax до rdx відповідно.

Caution

Регістри, які неявно використовуються в діленні, як-от rax і rdx, завжди перезаписуються. Якщо їхні значення знадобляться пізніше, їх слід зберегти перед діленням.

Вказівки

Місцевий магазин перевозить свої товари до більшого складу. Нас найняли, щоб спакувати і перевезти все.

У нас є чотири завдання, усі повʼязані з організацією перевезення.

Note

Це інструкції, згадані в цій концепції:

Інструкція Опис
add a, b a = a + b
inc a a = a + 1
sub a, b a = a - b
dec a a = a - 1
imul a rdx:rax = a * rax (зі знаком)
imul a, b a = a * b (зі знаком, усічене)
imul a, b, c a = b * c (зі знаком, усічене)
mul a rdx:rax = a * rax (без знаку)
div a rax = частка, rdx = остача від rdx:rax / a (без знаку)
idiv a rax = частка, rdx = остача від rdx:rax / a (зі знаком)
movzx a, b a = b, додаючи 0 до додаткових бітів
movsx a, b a = b, додаючи 1 до додаткових бітів, якщо b < 0, інакше 0
Note

Памʼятаймо, що до одного й того самого регістра можна звертатися з різними розмірами, змінюючи назву операнда. Наприклад: rax (64-бітний), eax (32-бітний), ax (16-бітний), al (8-бітний).

Повну таблицю можна знайти в попередній концепції.

1. Обчисліть вагу кожної коробки

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

Щоб краще все організувати, у коробці лежать товари лише двох різних видів.

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

  • Кількість товарів першого виду в коробці
  • Вагу кожного товару першого виду в g
  • Кількість товарів другого виду в коробці
  • Вагу кожного товару другого виду в g

Врахуйте, що порожня коробка важить 500 г. Константу WEIGHT_OF_EMPTY_BOX визначено на початку файлу рішення.

Приклад:

get_box_weight(30, 40, 50, 20);
// => 2700

Усі аргументи - 16-бітні невідʼємні цілі числа, а повернене значення - 32-бітне невідʼємне ціле число.

2. Обчисліть, скільки коробок уміщається у вантажівку

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

Визначте функцію max_number_of_boxes, яка повертає, скільки коробок певної висоти можна скласти вертикально (одну на одну) у вантажівці.

Функція приймає як параметр висоту коробки в cm. Врахуйте, що внутрішня висота вантажівки становить 300 см. Константу TRUCK_HEIGHT визначено на початку файлу рішення.

Приклад:

max_number_of_boxes(30);
// => 10

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

3. Перевірте, чи всі товари враховано

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

Визначте функцію items_to_be_moved, яка повертає, скільки товарів ще залишилося перевезти на новий склад для заданого виду. Функція приймає такі параметри в такому порядку:

  • Кількість товарів, які ще не враховано для певного виду
  • Кількість товарів цього виду в коробці

Приклад:

items_to_be_moved(76532, 120);
// => 76412

Аргументи - 32-бітні невідʼємні цілі числа. Повернене значення - 32-бітне ціле число. У разі помилки в процесі результат може бути відʼємним числом.

4. Отримайте оплату

Наша оплата залежить від того, скільки коробок перевезено і скільки рейсів вантажівки знадобилося. За кожну коробку нам заплатять 5 доларів, а за кожен рейс - 220 доларів. Константи PAY_PER_BOX і PAY_PER_TRUCK_TRIP визначено на початку файлу рішення.

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

Отже, чиста сума, яку нам винні або яку винні ми, така:

net = boxes * PAY_PER_BOX + trips * PAY_PER_TRUCK_TRIP - up_front - broken_items * item_value

Цю оплату, або борг, поділять порівну між нами та кількома робітниками, яких ми найняли. Решта грошей або боргу залишається нам. Наприклад, якщо чиста сума становить 100 і її ділять між 6 людьми (нами і 5 робітниками), ми отримуємо 20 (100/(5 + 1) = 16 плюс залишок 4).

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

  • Скільки ми отримали авансом, як 64-бітне невідʼємне ціле число
  • Загальну кількість перевезених коробок, як 32-бітне невідʼємне ціле число
  • Кількість здійснених рейсів вантажівки, як 32-бітне невідʼємне ціле число
  • Кількість пошкоджених або загублених товарів, як 32-бітне невідʼємне ціле число
  • Вартість кожного втраченого товару, як 64-бітне невідʼємне ціле число
  • Кількість робітників, з якими ми ділимо оплату або борг, як 8-бітне додатне ціле число

Приклад:

calculate_payment(2000, 1000, 5, 21, 2, 1);
// => 2029

Повернене значення - 64-бітне ціле число.

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

Час розпочати Управління запасами?

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