Câmbio de moedas

Câmbio de moedas

Exercício de aprendizagem

Introdução

Em conceitos anteriores, foi mencionado que a diretiva global torna uma função ou variável definida num ficheiro de código-fonte visível a outros ficheiros. De forma semelhante, a diretiva extern informa o assembler de que uma função ou variável usada no ficheiro de código-fonte atual está definida noutro.

Estas duas diretivas são a principal forma de o código assembly interagir com outros ficheiros de código-fonte para alcançar a modularização.

Para efeitos destas diretivas, é indiferente se o código externo está definido ou é usado em assembly ou numa linguagem de alto nível. Assim, por exemplo, até uma função definida numa linguagem de alto nível pode ser usada em código x86-64 se for declarada extern.

Do mesmo modo, a convenção de chamada é partilhada por todas essas funções, definidas em assembly ou em linguagens de alto nível, e até com o sistema operativo (SO).

No entanto, como em assembly os dados são apenas uma sequência de bytes, é importante perceber como a linguagem de alto nível com que estamos a interagir gere a memória.

Tipos em C

A linguagem C é uma escolha comum de linguagem de alto nível para interagir com código assembly. Facilita a tarefa de interagir com o SO, oferecendo abstrações de alto nível para muitas tarefas importantes, como imprimir no ecrã, ler do teclado ou alocar memória dinâmica.

Neste track, os testes estão escritos em C, pelo que é importante dar uma visão geral da linguagem.

Note

Qualquer um dos tipos indicados abaixo pode ser qualificado com const. Isso torna-os apenas de leitura.

Tipos primitivos

Existem muitos tipos primitivos em C e o seu tamanho, em bytes, pode variar. Alguns deles estão resumidos na tabela seguinte, com o seu tamanho típico num sistema x86-64:

tipo número de bytes inteiro/vírgula flutuante
_Bool 1 inteiro
char 1 inteiro
short 2 inteiro
int 4 inteiro
long 4 ou 8 inteiro
long long 8 inteiro
float 4 vírgula flutuante
double 8 vírgula flutuante

Cada um desses tipos inteiros pode ser com sinal (o predefinido) ou sem sinal. Quando são sem sinal, isso tem de ser indicado, por exemplo: unsigned int.

Existem aliases para os tipos inteiros, definidos para permitir um controlo preciso do seu tamanho:

alias número de bytes
int8_t 1
int16_t 2
int32_t 4
int64_t 8

Esses aliases podem ter o prefixo u para indicar um tipo sem sinal, por exemplo: uint64_t.

Outros aliases que vale a pena conhecer são:

alias tipo
bool _Bool
size_t normalmente um inteiro sem sinal de 8 bytes

Um _Bool, ou o seu alias bool, tem a particularidade de, apesar de ocupar 1 byte inteiro, só poder assumir um de dois valores: true (1) e false (0).

Os valores desses tipos primitivos são passados às funções e devolvidos por elas de acordo com as regras habituais para inteiros e valores de vírgula flutuante.

Enums

Um enum é um tipo que pode assumir um de vários elementos indicados explicitamente:

enum example {
    example_1,
    example_2,
    example_3,
    example_4
};

Em C, os enums são convertidos implicitamente para um tipo inteiro, que normalmente é um int, ou seja, um inteiro de 4 bytes.

Por predefinição, o primeiro elemento possível de um enum é convertido em 0 e todos os elementos seguintes são convertidos no inteiro seguinte. Assim, no enum example definido acima, example_1 teria o valor 0, example_2 teria o valor 1, e assim sucessivamente.

É possível indicar o valor de qualquer elemento de um enum e, por predefinição, o elemento seguinte passa a ter como valor o inteiro seguinte:

enum example_2 {
    example_1 = 4,
    example_2 = 8,
    example_3
}; // example_3 is equal to 9

Endereços de memória

Em C, um endereço de memória é referenciado por um ponteiro para um tipo e representado com o operador *. Assim, por exemplo, int64_t * refere-se a uma localização de memória para um ou mais int64_t.

Os endereços são tratados como inteiros de 8 bytes, como habitualmente.

Arrays

Os arrays são passados às funções e devolvidos por elas como ponteiros para o seu primeiro elemento.

int64_t example_arr[] = {1, 2, 3}; // this is an array of 3 signed 8-byte integers
fn(example_arr); // this is a function that passes a pointer (int64_t *) to the beginning of the array as an argument

Cada elemento de um array tem o tamanho do tipo de elemento do array. O example_arr definido acima, por exemplo, tem 3 elementos de 8 bytes cada, o que dá 24 bytes no total.

Como os arrays não guardam o seu comprimento, normalmente é preciso um valor separado para indicar o número de elementos, a menos que um valor sentinela marque o fim.

Strings

Em C, as strings são um array de char, que é um tipo de 1 byte. A maioria das strings é composta por carateres ASCII e termina com NUL, ou seja, termina quando se encontra um byte com o valor 0. Isto significa que o comprimento de uma string normalmente não precisa de ser passado como um argumento separado.

Instruções

O teu amigo Chandler planeia visitar países exóticos por todo o mundo. Infelizmente, as competências matemáticas do Chandler não são boas. Ele está bastante preocupado por ser enganado nas casas de câmbio durante a viagem, e quer que lhe faças uma calculadora de câmbios.

Aqui estão as especificações dele para a aplicação.

Note

As funções deste exercício são declaradas num ficheiro fonte C com as assinaturas fornecidas. O teu código de assembly x86-64 tem de seguir essas especificações.

1. Acompanha as diferentes moedas

Criaste um enum currency_t para acompanhar as diferentes moedas que o Chandler usa nas suas viagens:

enum currency_t {
    GBP,
    EUR,
    JPY,
    AUD,
    BRL,
    CNY,
    CAD,
    INR
};

Às vezes precisas de imprimir essas moedas no ecrã para verificar se estão corretas.

Cria a função stringify_currency:

void stringify_currency(char *buffer, enum currency_t currency);

Esta função não tem valor devolvido e deve guardar uma string ao estilo de C no local indicado por buffer. A string deve representar visualmente que tipo de moeda foi passada à função:

char buffer[4];
stringify_currency(buffer, CAD);
// => "CAD"

2. Verifica a taxa de câmbio

Cria a função exchange_rate:

double exchange_rate(enum currency_t domestic_currency, enum currency_t foreign_currency, const double *value_in_US_dollars);

O argumento domestic_currency representa a moeda local do Chandler, que vai ser trocada por uma foreign_currency.

value_in_US_dollars é um ponteiro para um array de double que contém o valor de uma unidade de cada moeda em dólares norte-americanos. As moedas são índices neste array. Por isso, se EUR é um elemento válido de enum currency_t, então value_in_US_dollars[EUR] dá o valor de 1 EUR em dólares norte-americanos.

Esta função deve devolver o valor de uma unidade de moeda estrangeira na moeda local.

const double value_in_US_dollars[8] = {
    1.33329, 1.16443, 0.00654374, 0.650888,
    0.185577, 0.140420, 0.714142, 0.0113863
};

exchange_rate(JPY, GBP, value_in_US_dollars);
// => 203.750454633

3. Calcula o valor das notas

Cria a função get_value_of_bills:

uint64_t get_value_of_bills(unsigned long long denomination, unsigned short number_of_bills);

O argumento denomination é o valor de uma única nota, enquanto number_of_bills é o número total de notas.

Este balcão de câmbio só trabalha com dinheiro em determinados incrementos. O total que recebes tem de ser divisível pelo valor de uma "nota", ou unidade, o que pode deixar uma fração ou um resto. A tua função deve devolver apenas o valor total das notas (excluindo as quantias fracionárias) que o balcão devolveria. Infelizmente, o balcão fica com o resto/troco como bónus adicional.

get_value_of_bills(5, 128);
// => 640

4. Calcula o número de notas

Cria a função get_number_of_bills:

unsigned int get_number_of_bills(float amount, unsigned long long denomination);

Esta função deve devolver o número de notas que podes receber dentro do montante indicado. Por outras palavras: quantas notas inteiras cabem no montante inicial? Lembra-te: só podes receber notas inteiras, não frações de notas, por isso não te esqueças de dividir em conformidade. Na prática, estás a arredondar por defeito para a nota/denominação inteira mais próxima.

get_number_of_bills(127.5, 5);
// => 25

5. Calcula o valor após o câmbio

Cria a função exchangeable_value:

uint32_t exchangeable_value(float budget, double exchange_rate, uint8_t spread, unsigned long long denomination);

O parâmetro spread é a percentagem cobrada como taxa de câmbio, escrita como um número inteiro. É preciso convertê-la para decimal, dividindo-a por 100. Repara que a spread é adicionada à taxa de câmbio, por isso uma spread de 10 dá uma taxa de câmbio 10% mais alta.

Esta função deve devolver o valor máximo da nova moeda depois de calcular a taxa de câmbio ajustada pela spread. Lembra-te de que a denominação da moeda é um número inteiro e não pode ser subdividida.

exchangeable_value(127.25, 0,873350884, 10, 20);
// => 120
exchangeable_value(127.25, 0,873350884, 10, 5);
// => 130
Editar via GitHub A ligação abre numa nova janela ou separador
x86-64 Assembly Exercism

Estás pronto para começar Câmbio de moedas?

Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.