У попередньому концепті згадувалося, що і локальні мітки, і функції - це просто адреси в секції з виконуваним кодом, наприклад 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, який між викликами читає або оновлює якусь постійну памʼять, може поводитися по-різному залежно від того, що було раніше. Його результат може залежати не лише від його аргументів.
Наприклад, лічильник, який приймає функцію і викликає її з поточним значенням лічильника, щоразу збільшуючи його:
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-ах у поєднанні зі станом, що зберігається між викликами.
Ми — бухгалтер у невеликому сільському банку. У кожного клієнта є рахунок, а його баланс ми тримаємо в книзі обліку. Упродовж року до цих балансів застосовуються транзакції: нараховуються відсотки, відраховуються комісії, виплачуються бонуси, стягуються штрафи. Кожна транзакція приймає баланс і повертає новий.
На нас чекають чотири завдання.
Можемо припустити, що кожен thunk (транзакції та захисні функції) у цій вправі - це функція, яка:
Касир дізнається про нову транзакцію на початку дня і записує її, щоб пізніше застосувати, коли прийде клієнт.
Визначте дві функції:
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:
У посібнику банку є список частих транзакцій, збережений у таблиці диспетчеризації. Кожне відділення веде власну копію цього списку й може реєструвати різні транзакції залежно від місцевих правил.
Визначте дві функції, які працюють із таблицею диспетчеризації, наданою тим, хто викликає:
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:
Для select_transaction:
Наприкінці місяця рахунок клієнта звіряють. Кожну транзакцію, що відбулася за місяць, застосовують до початкового балансу одну за одною, і результат стає новим балансом.
Визначте функцію 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-бітне невідʼємне ціле число.
Правила банку вимагають перевіряти деякі транзакції, перш ніж їх застосувати. Захисна функція переглядає запропонований баланс і вирішує, чи його можна прийняти. Вона повертає ненульове значення, щоб схвалити транзакцію, або нуль, щоб її відхилити.
Визначте process_with_guard, яка приймає початковий баланс, адресу памʼяті масиву транзакцій, кількість транзакцій у масиві та захисну функцію.
Для кожної транзакції по черзі:
Коли всі транзакції оброблено, повертаємо підсумковий баланс разом із кількістю схвалених транзакцій.
У псевдокоді 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
Наприклад, припустімо, що:
add_interest - це транзакція, яка додає 5, а service_fee - інша транзакція, яка віднімає 2at_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:
rax і кількість схвалених транзакцій у rdx.Зареєструйтеся на Exercism, щоб вивчати й опановувати x86-64 Assembly, а також 22 концепції130 вправ та справжнє наставництво від людей, і все це безкоштовно.