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.
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.
Qualquer um dos tipos indicados abaixo pode ser qualificado com const.
Isso torna-os apenas de leitura.
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.
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
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.
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.
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.
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.
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.
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"
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
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
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
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
Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.