Könyvelés

Könyvelés

Tanulófeladat

Bevezetés

Thunkok

Egy korábbi fogalomnál már szó volt róla, hogy a helyi címkék és a függvények valójában nem mások, mint címek egy futtatható kódot tartalmazó szakaszban, például a section .text-ben.

Valójában a függvényekkel ugyanúgy bánhatunk, mint bármely memóriacímmel, vagyis betölthetők regiszterekbe, átadhatók és memóriában tárolhatók. Az is lehetséges, hogy a call vagy a jmp utasítással átadjuk a vezérlést egy regiszterben vagy memóriában tárolt függvénynek:

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

Az értékként átadott függvénycím neve thunk. A thunkok a magasabb rendű programozás építőkövei assemblyben: olyan kódé, amely más kóddal dolgozik.

A kód mint adat

A függvénycímeket memóriában is eltárolhatjuk, hogy később elővegyük őket:

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

A save_op a kapott függvénycímet a cached_fn-be írja. Az érték megmarad, miután a save_op visszatér, így az apply_op minden későbbi hívása arra a címre ugrik, amelyet legutóbb eltároltunk. Így futásidőben megváltoztatható, hogy az apply_op melyik függvényt hívja meg.

Diszpécsertáblák

Ha a függvénycímeket egy tömbben tároljuk, akkor valamilyen index alapján különböző függvényeket választhatunk ki, amely index akár futásidejű feltételtől is függhet. Ezt diszpécsertáblának nevezzük:

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

Állapottal rendelkező thunkok

Egy olyan thunk, amely a hívások között valamilyen perzisztens memóriát olvas vagy frissít, másképp viselkedhet attól függően, mi történt korábban. Az eredménye nem csak az argumentumaitól függhet.

Például egy számláló, amely átvesz egy függvényt, és meghívja azt az aktuális számlálóértékkel, minden alkalommal növelve a számlálót:

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

A tick meghívja a kapott függvényt az aktuális számlálóértékkel argumentumként, majd növeli a számlálót. Így az első tick(square) hívás a square(0)-t hívja meg, a következő tick(square) a square(1)-et, az azt követő pedig a square(2)-t, és így tovább.

Egy másik példa a késleltetett számítás:

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

A delay átvesz egy függvényt és egy értéket, eltárolja őket, majd visszaadja az invoke-ot. Amikor meghívjuk az invoke-ot, az lefuttatja a rögzített függvényt a mentett argumentummal.

A magasabb szintű nyelvekben gyakori minták közül sok, például a visszahívások, a virtuális metódusok, a generátorok, a currying, a függvénykompozíció és még sok más, a perzisztens állapottal párosított thunkokra épül.

Utasítások

Egy kis falusi bank könyvelője vagy. Minden ügyfélnek van egy számlája, és te a főkönyvedben vezeted az egyenlegét. Az év során tranzakciók módosítják ezeket az egyenlegeket: kamatot írunk jóvá, díjakat vonunk le, bónuszokat fizetünk ki, büntetéseket számítunk fel. Minden tranzakció egy egyenleget kap, és egy újat ad vissza.

Négy részfeladat vár rád.

Note

Feltételezheted, hogy ebben a feladatban minden thunk (a tranzakciók és a guardok) olyan függvény, amely:

  1. egy 64 bites, nem negatív egész számot kap argumentumként,
  2. és szintén egy 64 bites, nem negatív egész számot ad vissza.

1. Jegyezz meg egy tranzakciót

A pénztáros a nap elején megismer egy új tranzakciót, és leírja, hogy később alkalmazni lehessen, amikor megérkezik egy ügyfél.

Definiálj két függvényt:

  • A remember_transaction kap egy tranzakciót, és eltárolja a memóriában.
  • Az apply_remembered kap egy egyenleget, és alkalmazza rá a korábban eltárolt tranzakciót.

Példa, feltéve hogy az add_interest egy olyan tranzakció, amely öt egység kamatot ír jóvá:

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

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

A remember_transaction függvénynél:

  • Az argumentum egy tranzakció, amelyet későbbi használatra el kell menteni.
  • Nincs visszatérési értéke.

Az apply_remembered függvénynél:

  • Az argumentum egy 64 bites, nem negatív egész szám.
  • A visszatérési érték egy 64 bites, nem negatív egész szám.

2. A bank kézikönyve

A bank kézikönyvében a gyakori tranzakciók listája egy dispatch táblában található. Minden fiók a lista saját példányát tartja nyilván, és a helyi szabályzattól függően máshogy is regisztrálhat tranzakciókat.

Definiálj két függvényt, amelyek a hívó által átadott dispatch táblán működnek:

  • A register_transaction kap egy dispatch tábla memóriacímét, egy indexet és egy tranzakciót. Ezt a tranzakciót a tábla megadott indexére menti.
  • A select_transaction kap egy dispatch tábla memóriacímét, egy indexet és egy egyenleget. Kikeresi a megadott indexen lévő tranzakciót, alkalmazza az egyenlegre, és visszaadja az új egyenleget.

A select_transaction egyetlen indirekt farokhívással érje el a kikeresett tranzakciót.

Példa, feltéve hogy a manual egy négy üres hellyel rendelkező dispatch tábla memóriacíme:

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

A register_transaction függvénynél:

  • Az első argumentum egy dispatch tábla memóriacíme.
  • A második argumentum egy 64 bites, nem negatív egész szám (az index).
  • A harmadik argumentum egy tranzakció.
  • Nincs visszatérési értéke.

A select_transaction függvénynél:

  • Az első argumentum egy dispatch tábla memóriacíme.
  • A második argumentum egy 64 bites, nem negatív egész szám (az index).
  • A harmadik argumentum egy 64 bites, nem negatív egész szám (az egyenleg).
  • A visszatérési érték egy 64 bites, nem negatív egész szám.

3. Dolgozz fel egy havi kimutatást

A hónap végén az ügyfél számláját egyeztetjük. A hónap során történt minden tranzakciót sorban alkalmazunk a kezdő egyenlegre, és az eredmény lesz az új egyenleg.

Definiálj egy process_statement függvényt, amely kap egy kezdő egyenleget, egy tranzakciótömb memóriacímét és a tömbben lévő tranzakciók számát. Sorban minden tranzakcióra alkalmazza a tranzakciót a futó egyenlegre, majd az eredményt használja a következő tranzakció egyenlegeként. A végső egyenleget adja vissza.

Pszeudokódban a process_statement(balance, transactions, n) ezt számítja ki:

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

Példa, feltéve hogy a transactions egy olyan tömb memóriacíme, amely ebben a sorrendben az add_interest, service_fee és add_interest tranzakciókat tartalmazza, ahol az add_interest 5-öt ad hozzá, a service_fee pedig 2-t von le:

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

Az első argumentum egy 64 bites, nem negatív egész szám. A második argumentum egy tranzakciótömb memóriacíme. A harmadik argumentum egy 64 bites, nem negatív egész szám (a tömb hossza). A visszatérési érték egy 64 bites, nem negatív egész szám.

4. Feldolgozás guarddal

A bank szabályzata megköveteli, hogy bizonyos tranzakciókat a jóváhagyásuk előtt ellenőrizni kell. A guard egy olyan függvény, amely megvizsgál egy javasolt egyenleget, és eldönti, hogy elfogadható-e. Ez a guard függvény nullától különböző értékkel hagyja jóvá, vagy nullával utasítja el.

Definiálj egy process_with_guard függvényt, amely kap egy kezdő egyenleget, egy tranzakciótömb memóriacímét, a tömbben lévő tranzakciók számát és egy guard függvényt. Sorban minden tranzakcióra:

  1. Alkalmazd a tranzakciót a futó egyenlegre, hogy kiszámítsd az ideiglenes új egyenleget.
  2. Hívd meg a guardot az ideiglenes egyenleggel.
  3. Ha a guard nullától különböző értéket ad vissza, véglegesítsd: a futó egyenleg az ideiglenes egyenleg lesz.
  4. Ha a guard nullát ad vissza, a futó egyenleg változatlan marad, és a tranzakciót kihagyjuk.

Miután az összes tranzakciót feldolgoztad, add vissza a végső egyenleget és a jóváhagyott tranzakciók számát.

Pszeudokódban a process_with_guard(balance, transactions, n, guard) ezt számítja ki:

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

Például feltételezzük, hogy:

  1. az add_interest egy olyan tranzakció, amely 5-öt ad hozzá, a service_fee pedig egy másik tranzakció, amely 2-t von le
  2. az at_least_10 egy olyan guard, amely nullától különböző értéket ad vissza, ha az egyenleg >= 10

Ekkor:

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

A process_with_guard függvénynél:

  • Az első argumentum egy 64 bites, nem negatív egész szám (a kezdő egyenleg).
  • A második argumentum egy tranzakciótömb memóriacíme.
  • A harmadik argumentum egy 64 bites, nem negatív egész szám (a tömb hossza).
  • A negyedik argumentum egy guard függvény, amely egy 64 bites, nem negatív egész számot kap, és egy 64 bites, nem negatív egész számot ad vissza.
  • A visszatérési értékek két 64 bites, nem negatív egész szám: a végső egyenleg a rax-ban, a jóváhagyott tranzakciók száma pedig az rdx-ben.
Szerkesztés GitHubon A hivatkozás új ablakban vagy lapon nyílik meg
x86-64 Assembly Exercism

Készen állsz elkezdeni a(z) Könyvelés feladatot?

Iratkozz fel az Exercism-re, hogy megtanuld és elsajátítsd a(z) x86-64 Assembly nyelvet 22 fogalom130 feladat segítségével, valódi emberi mentorálással, mindez ingyen.