Num conceito anterior, foi referido que tanto os rótulos locais como as funções são apenas endereços numa secção com código executável, como a section .text.
Com efeito, as funções podem ser manipuladas da mesma forma que qualquer endereço de memória, ou seja, podem ser carregadas para registos, passadas adiante e guardadas na memória.
Também é possível usar call ou jmp para transferir a execução para uma função guardada num registo ou na memória:
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
Um endereço de função que é passado adiante como um valor chama-se thunk. Os thunks são um bloco de construção da programação de ordem superior em assembly: código que opera sobre outro código.
Os endereços de funções também podem ser guardados na memória e recuperados mais tarde:
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
O save_op escreve o endereço de função que recebe em cached_fn.
O valor persiste depois de o save_op devolver, por isso qualquer chamada posterior ao apply_op salta em cauda para o último endereço que ficou guardado.
Isto torna possível mudar qual a função que o apply_op invoca em tempo de execução.
Guardar endereços de funções num array torna possível selecionar funções diferentes de acordo com um índice, possivelmente dependente de uma condição de tempo de execução. A isto chama-se uma tabela de despacho:
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
Um thunk que lê ou atualiza alguma memória persistente entre chamadas pode comportar-se de forma diferente consoante o que aconteceu antes. O seu resultado pode depender de mais do que apenas os seus argumentos.
Por exemplo, um contador que recebe uma função e a invoca com a contagem atual, avançando a contagem de cada vez:
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
O tick invoca a função dada com a contagem atual como argumento e depois avança a contagem.
Assim, uma primeira chamada tick(square) invoca square(0), a chamada seguinte tick(square) invoca square(1), a seguinte square(2), e por aí adiante.
Outro exemplo seria uma computação diferida:
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
O delay recebe uma função e um valor, guarda-os e devolve o invoke.
Quando o invoke é chamado, executa a função capturada com o argumento guardado.
Muitos dos padrões comuns em linguagens de nível superior, como callbacks, métodos virtuais, geradores, currying, composição de funções e muitos outros, assentam em thunks emparelhados com estado persistente.
És o guarda-livros de um pequeno banco de aldeia. Cada cliente tem uma conta e tu guardas o respetivo saldo no teu livro-razão. Ao longo do ano, são aplicadas transações a esses saldos: creditam-se juros, deduzem-se comissões, pagam-se bónus, cobram-se penalizações. Cada transação recebe um saldo e produz um novo.
Tens quatro tarefas.
Podes assumir que cada thunk (transações e guardas) deste exercício é uma função que:
No início do dia, o caixa fica a conhecer uma nova transação e anota-a, para que possa ser aplicada mais tarde, quando chegar um cliente.
Define duas funções:
remember_transaction recebe uma transação e guarda-a em memória.apply_remembered recebe um saldo e aplica-lhe a transação guardada anteriormente.Exemplo, assumindo que add_interest é uma transação que credita cinco unidades de juros:
remember_transaction(add_interest);
apply_remembered(100);
// => 105
remember_transaction(service_fee);
apply_remembered(100);
// => 98 (assuming service_fee deducts 2)
Para remember_transaction:
Para apply_remembered:
O manual do banco tem uma lista de transações frequentes guardada numa tabela de despacho. Cada agência mantém a sua própria cópia da lista e pode registar transações diferentes, consoante a política local.
Define duas funções que operam sobre uma tabela de despacho fornecida por quem chama:
register_transaction recebe o endereço de memória de uma tabela de despacho, um índice e uma transação.
Guarda essa transação no índice indicado da tabela.select_transaction recebe o endereço de memória de uma tabela de despacho, um índice e um saldo.
Procura a transação no índice indicado, aplica-a ao saldo e devolve o novo saldo.select_transaction deve chegar à transação encontrada com uma única chamada de cauda indireta.
Exemplo, assumindo que manual é o endereço de memória de uma tabela de despacho com quatro fendas vazias:
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
Para register_transaction:
Para select_transaction:
No final do mês, a conta de um cliente é reconciliada. Todas as transações que ocorreram durante o mês são aplicadas ao saldo inicial, uma a seguir à outra, e o resultado é o novo saldo.
Define uma função process_statement que recebe um saldo inicial, o endereço de memória de um array de transações e o número de transações do array.
Para cada transação, por ordem, deve aplicar a transação ao saldo corrente e usar depois o resultado como saldo da transação seguinte.
O saldo final é devolvido.
Em pseudocódigo, process_statement(balance, transactions, n) calcula:
for each transaction in transactions:
balance = transaction(balance)
return balance
Exemplo, assumindo que transactions é o endereço de memória de um array que contém as transações add_interest, service_fee e add_interest, por essa ordem, em que add_interest acrescenta 5 e service_fee deduz 2:
process_statement(100, transactions, 3);
// add_interest(100) = 105
// service_fee(105) = 103
// add_interest(103) = 108
// => 108
O primeiro argumento é um inteiro não negativo de 64 bits. O segundo argumento é o endereço de memória de um array de transações. O terceiro argumento é um inteiro não negativo de 64 bits (o comprimento do array). O valor devolvido é um inteiro não negativo de 64 bits.
A política do banco exige que certas transações sejam verificadas antes de serem efetivadas. Uma guarda é uma função que inspeciona um saldo proposto e decide se é aceitável. Essa função de guarda devolve um valor diferente de zero para aprovar, ou zero para rejeitar.
Define process_with_guard, que recebe um saldo inicial, o endereço de memória de um array de transações, o número de transações do array e uma função de guarda.
Para cada transação, por ordem:
Depois de processares todas as transações, devolve o saldo final juntamente com o número de transações aprovadas.
Em pseudocódigo, process_with_guard(balance, transactions, n, guard) calcula:
approved = 0
for each transaction in transactions:
tentative = transaction(balance)
if guard(tentative) is non-zero:
balance = tentative
approved = approved + 1
return balance, approved
Por exemplo, assume que:
add_interest é uma transação que acrescenta 5 e service_fee é outra transação que deduz 2at_least_10 é uma guarda que devolve um valor diferente de zero quando o saldo é >= 10Então:
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
Para process_with_guard:
rax e o número de transações aprovadas em rdx.Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.