Треки
/
x86-64 Assembly
x86-64 Assembly
/
Вправи
/
Бухгалтерія
Бухгалтерія

Бухгалтерія

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

Вступ

Thunk-и

У попередньому концепті згадувалося, що і локальні мітки, і функції - це просто адреси в секції з виконуваним кодом, наприклад section .text.

Насправді, з функціями можна поводитися так само, як і з будь-якою адресою памʼяті, тобто їх можна завантажувати в регістри, передавати і зберігати в памʼяті. Також можна використати call або jmp, щоб передати керування функції, збереженій у регістрі чи памʼяті:

section .text
sum_op:
    lea rax, [rdi + rsi] ; loads the sum rdi + rsi into rax
    ret

apply_sum:
    lea rax, [rel sum_op]
    jmp rax   ; tail call

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

Код як дані

Адреси функцій також можна зберігати в памʼяті й діставати згодом:

section .bss
    cached_fn resq 1

section .text
save_op:
    mov qword [rel cached_fn], rdi
    ret

apply_op:
    ; arguments are already set up according to the ABI
    jmp qword [rel cached_fn] ; tail call

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

Таблиці диспетчеризації

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

section .data
    dispatch_table dq add_op, sub_op, mul_op

section .text
dispatch:
    ; this function takes two arguments in rdi and rsi, and an index in rdx
    ; it then applies the function corresponding to the index in rdx to the arguments
    lea rax, [rel dispatch_table]
    jmp qword [rax + 8*rdx]   ; tail-call the function address for the index

Thunk-и зі станом

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

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

section .data
    count dq 0

section .text
tick:
    mov rax, rdi               ; saves the function address
    mov rdi, [rel count]       ; loads the current count as the function's argument
    inc qword [rel count]      ; advances the count
    jmp rax                    ; tail-calls the function

tick викликає передану функцію, передаючи їй поточне значення лічильника як аргумент, а потім збільшує лічильник. Тож перший виклик tick(square) викликає square(0), наступний виклик tick(square) викликає square(1), далі square(2) і так далі.

Інший приклад - відкладене обчислення:

section .bss
    captured_fn resq 1
    argument resq 1

section .text
delay:
    mov qword [rel captured_fn], rdi ; saves the function
    mov qword [rel argument], rsi    ; saves the argument
    lea rax, [rel invoke]            ; returns the `invoke` function
    ret

invoke:
    mov rdi, qword [rel argument]    ; loads the saved argument into `rdi`
    jmp qword [rel captured_fn]      ; tail-calls the saved function

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

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

Вказівки

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

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

Note

Можемо припустити, що кожен thunk (транзакції та захисні функції) у цій вправі - це функція, яка:

  1. приймає як аргумент 64-бітне невідʼємне ціле число
  2. і так само повертає 64-бітне невідʼємне ціле число.

1. Запамʼятати транзакцію

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

Визначте дві функції:

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

Наприклад, припустімо, що add_interest - це транзакція, яка нараховує пʼять одиниць відсотків:

remember_transaction(add_interest);
apply_remembered(100);
// => 105

remember_transaction(service_fee);
apply_remembered(100);
// => 98   (assuming service_fee deducts 2)

Для remember_transaction:

  • Аргумент: транзакція, яку треба зберегти для подальшого використання.
  • Поверненого значення немає.

Для apply_remembered:

  • Аргумент: 64-бітне невідʼємне ціле число.
  • Повернене значення: 64-бітне невідʼємне ціле число.

2. Посібник банку

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

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

  • register_transaction приймає адресу памʼяті таблиці диспетчеризації, індекс і транзакцію. Вона зберігає цю транзакцію за вказаним індексом у таблиці.
  • select_transaction приймає адресу памʼяті таблиці диспетчеризації, індекс і баланс. Вона знаходить транзакцію за вказаним індексом, застосовує її до балансу й повертає новий баланс.

select_transaction повинна звертатися до знайденої транзакції єдиним непрямим хвостовим викликом.

Наприклад, припустімо, що manual - це адреса памʼяті таблиці диспетчеризації з чотирма порожніми комірками:

register_transaction(manual, 0, monthly_interest);
register_transaction(manual, 1, service_fee);

select_transaction(manual, 0, 100);
// applies monthly_interest to 100

select_transaction(manual, 1, 100);
// applies service_fee to 100

Для register_transaction:

  • Перший аргумент: адреса памʼяті таблиці диспетчеризації.
  • Другий аргумент: 64-бітне невідʼємне ціле число (індекс).
  • Третій аргумент: транзакція.
  • Поверненого значення немає.

Для select_transaction:

  • Перший аргумент: адреса памʼяті таблиці диспетчеризації.
  • Другий аргумент: 64-бітне невідʼємне ціле число (індекс).
  • Третій аргумент: 64-бітне невідʼємне ціле число (баланс).
  • Повернене значення: 64-бітне невідʼємне ціле число.

3. Обробити місячний звіт

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

Визначте функцію process_statement, яка приймає початковий баланс, адресу памʼяті масиву транзакцій і кількість транзакцій у масиві. Для кожної транзакції по черзі вона має застосувати транзакцію до поточного балансу, а потім узяти результат як баланс для наступної транзакції. Повертається підсумковий баланс.

У псевдокоді process_statement(balance, transactions, n) обчислює:

for each transaction in transactions:
    balance = transaction(balance)
return balance

Наприклад, припустімо, що transactions - це адреса памʼяті масиву, який містить транзакції add_interest, service_fee і add_interest саме в такому порядку, де add_interest додає 5, а service_fee віднімає 2:

process_statement(100, transactions, 3);
// add_interest(100) = 105
// service_fee(105)  = 103
// add_interest(103) = 108
// => 108

Перший аргумент: 64-бітне невідʼємне ціле число. Другий аргумент: адреса памʼяті масиву транзакцій. Третій аргумент: 64-бітне невідʼємне ціле число (довжина масиву). Повернене значення: 64-бітне невідʼємне ціле число.

4. Обробити із захисною функцією

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

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

  1. Застосуймо транзакцію до поточного балансу й обчислимо попередній новий баланс.
  2. Викличмо захисну функцію з попереднім балансом.
  3. Якщо захисна функція повертає ненульове значення, транзакцію зараховуємо: поточний баланс стає попереднім.
  4. Якщо захисна функція повертає нуль, поточний баланс не змінюється, а транзакцію пропускаємо.

Коли всі транзакції оброблено, повертаємо підсумковий баланс разом із кількістю схвалених транзакцій.

У псевдокоді process_with_guard(balance, transactions, n, guard) обчислює:

approved = 0
for each transaction in transactions:
    tentative = transaction(balance)
    if guard(tentative) is non-zero:
        balance = tentative
        approved = approved + 1
return balance, approved

Наприклад, припустімо, що:

  1. add_interest - це транзакція, яка додає 5, а service_fee - інша транзакція, яка віднімає 2
  2. at_least_10 - це захисна функція, яка повертає ненульове значення, коли баланс >= 10

Тоді:

process_with_guard(5, {add_interest, service_fee, add_interest}, 3, at_least_10);
// add_interest(5) = 10; at_least_10(10) != 0;
// => balance = 10, approved = 1
//
// service_fee(10) = 8; at_least_10(8) = 0;
// => balance = 10, approved = 1
//
// add_interest(10) = 15; at_least_10(15) != 0;
// => balance = 15, approved = 2
//
// final balance (15) is returned in rax
// number of approved transactions (2) is returned in rdx

Для process_with_guard:

  • Перший аргумент: 64-бітне невідʼємне ціле число (початковий баланс).
  • Другий аргумент: адреса памʼяті масиву транзакцій.
  • Третій аргумент: 64-бітне невідʼємне ціле число (довжина масиву).
  • Четвертий аргумент: захисна функція, яка приймає 64-бітне невідʼємне ціле число й повертає 64-бітне невідʼємне ціле число.
  • Повернених значень два, обидва - 64-бітні невідʼємні цілі числа: підсумковий баланс у rax і кількість схвалених транзакцій у rdx.
Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
x86-64 Assembly Exercism

Час розпочати Бухгалтерія?

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