Nei concetti precedenti è stato detto che la direttiva global rende visibile agli altri file una funzione o una variabile definita in un file sorgente.
Allo stesso modo, la direttiva extern segnala all'assembler che una funzione o una variabile usata nel file sorgente corrente è definita in un altro.
Queste due direttive sono il modo principale con cui il codice assembly interagisce con gli altri file sorgente per ottenere la modularizzazione.
Ai fini di queste direttive, non fa differenza se il codice esterno è definito o usato in assembly o in un linguaggio di alto livello.
Così, per esempio, anche una funzione definita in un linguaggio di alto livello può essere usata in codice x86-64 se dichiarata extern.
Allo stesso modo, la convenzione di chiamata è condivisa da tutte queste funzioni, definite in assembly o in linguaggi di alto livello, e persino con il sistema operativo (OS).
Tuttavia, dato che in assembly i dati sono solo una sequenza di byte, è importante sapere come il linguaggio di alto livello con cui ci si interfaccia gestisce la memoria.
Il linguaggio C è una scelta comune come linguaggio di alto livello per interfacciarsi con il codice assembly. Rende più semplice interagire con il sistema operativo, offrendo wrapper di alto livello per molte attività importanti, come stampare sullo schermo, leggere da tastiera o allocare memoria dinamica.
In questo percorso i test sono scritti in C, quindi è importante offrire una panoramica generale del linguaggio.
Ognuno dei tipi indicati di seguito può essere qualificato con const.
Questo li rende di sola lettura.
In C ci sono molti tipi primitivi e la loro dimensione, in byte, può variare. Alcuni di essi sono riassunti nella tabella seguente, con la loro dimensione tipica in un sistema x86-64:
| tipo | numero di byte | intero/virgola mobile |
|---|---|---|
| _Bool | 1 | intero |
| char | 1 | intero |
| short | 2 | intero |
| int | 4 | intero |
| long | 4 o 8 | intero |
| long long | 8 | intero |
| float | 4 | virgola mobile |
| double | 8 | virgola mobile |
Ognuno di questi tipi interi può essere con segno (il valore predefinito) o senza segno.
Se sono senza segno, questo va specificato, per esempio: unsigned int.
Esistono alias per i tipi interi, definiti per offrire un controllo preciso sulla loro dimensione:
| alias | numero di byte |
|---|---|
| int8_t | 1 |
| int16_t | 2 |
| int32_t | 4 |
| int64_t | 8 |
Questi alias possono essere preceduti da u per indicare un tipo senza segno, per esempio: uint64_t.
Altri alias degni di nota sono:
| alias | tipo |
|---|---|
| bool | _Bool |
| size_t | di solito un intero senza segno di 8 byte |
Un _Bool, o il suo alias bool, ha una particolarità: pur occupando 1 byte intero, può assumere solo uno di due valori: true (1) e false (0).
I valori di questi tipi primitivi vengono passati alle funzioni e restituiti da esse secondo le regole abituali per gli interi e i valori in virgola mobile.
Un enum è un tipo che può assumere uno tra diversi elementi indicati esplicitamente:
enum example {
example_1,
example_2,
example_3,
example_4
};
In C, gli enum vengono convertiti implicitamente in un tipo intero, che di solito è un int, cioè un intero di 4 byte.
Per impostazione predefinita, il primo elemento possibile di un enum viene convertito in 0 e tutti gli elementi successivi vengono convertiti nell'intero successivo.
Quindi, nell'enum example definito sopra, example_1 avrebbe valore 0, example_2 avrebbe valore 1 e così via.
È possibile indicare il valore di un qualsiasi elemento di un enum e poi, per impostazione predefinita, l'elemento successivo avrebbe come valore l'intero successivo:
enum example_2 {
example_1 = 4,
example_2 = 8,
example_3
}; // example_3 is equal to 9
In C, un indirizzo di memoria è indicato da un puntatore a un tipo e si denota con l'operatore *.
Così, per esempio, int64_t * si riferisce a una posizione di memoria per uno o più int64_t.
Gli indirizzi sono trattati come interi di 8 byte, come al solito.
Gli array vengono passati alle funzioni e restituiti da esse come puntatori al loro primo 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
Ogni elemento di un array ha la dimensione del tipo di elemento dell'array.
L'example_arr definito sopra, per esempio, ha 3 elementi di 8 byte ciascuno, cioè 24 byte in totale.
Poiché gli array non memorizzano la loro lunghezza, di solito serve un valore separato per indicare il numero di elementi, a meno che un valore sentinella non ne segni la fine.
In C le stringhe sono un array di char, che è un tipo da 1 byte.
La maggior parte delle stringhe è composta da caratteri ASCII e termina con un NUL: finisce quando si incontra un byte con valore 0.
Questo significa che di solito la lunghezza di una stringa non deve essere passata come argomento separato.
Il tuo amico Chandler ha in programma di visitare paesi esotici in giro per il mondo. Purtroppo, Chandler non è bravo in matematica. È piuttosto preoccupato di essere truffato dagli uffici di cambio durante il viaggio, e vuole che tu gli crei un calcolatore di valuta.
Queste sono le sue specifiche per l'app.
Le funzioni di questo esercizio sono dichiarate in un file sorgente C con le firme fornite. Il codice assembly x86-64 deve seguire queste specifiche.
Hai creato un enum currency_t per tenere traccia delle diverse valute che Chandler usa nei suoi viaggi:
enum currency_t {
GBP,
EUR,
JPY,
AUD,
BRL,
CNY,
CAD,
INR
};
A volte ti capita di dover stampare quelle valute sullo schermo per controllare che siano corrette.
Crea la funzione stringify_currency:
void stringify_currency(char *buffer, enum currency_t currency);
Questa funzione non ha un valore restituito e deve memorizzare una stringa in stile C nella posizione indicata da buffer.
La stringa deve rappresentare visivamente quale tipo di valuta è stato passato alla funzione:
char buffer[4];
stringify_currency(buffer, CAD);
// => "CAD"
Crea la funzione exchange_rate:
double exchange_rate(enum currency_t domestic_currency, enum currency_t foreign_currency, const double *value_in_US_dollars);
L'argomento domestic_currency rappresenta la valuta locale di Chandler da cambiare con una foreign_currency.
value_in_US_dollars è un puntatore a un array di double che contiene il valore di un'unità di ciascuna valuta in dollari statunitensi.
Le valute sono indici di questo array.
Quindi, se EUR è un elemento valido per enum currency_t, allora value_in_US_dollars[EUR] restituisce il valore di 1 EUR in dollari statunitensi.
Questa funzione deve restituire il valore di un'unità di valuta estera nella valuta locale.
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
Crea la funzione get_value_of_bills:
uint64_t get_value_of_bills(unsigned long long denomination, unsigned short number_of_bills);
L'argomento denomination è il valore di una singola banconota, mentre number_of_bills è il numero totale di banconote.
Questo chiosco di cambio tratta solo contanti in determinati tagli. Il totale che ricevi deve essere divisibile per il valore di una «banconota» o unità, cosa che può lasciare una frazione o un resto. La funzione deve restituire solo il valore totale delle banconote (escludendo gli importi frazionari) che il chiosco restituirebbe. Purtroppo, il chiosco si tiene il resto come bonus aggiuntivo.
get_value_of_bills(5, 128);
// => 640
Crea la funzione get_number_of_bills:
unsigned int get_number_of_bills(float amount, unsigned long long denomination);
Questa funzione deve restituire il numero di banconote che puoi ricevere con l'importo dato. In altre parole: quante banconote intere entrano nell'importo di partenza? Ricorda: puoi ricevere solo banconote intere, non frazioni di banconota, quindi dividi di conseguenza. In pratica, stai arrotondando per difetto alla banconota intera più vicina.
get_number_of_bills(127.5, 5);
// => 25
Crea la funzione exchangeable_value:
uint32_t exchangeable_value(float budget, double exchange_rate, uint8_t spread, unsigned long long denomination);
Il parametro spread è la percentuale trattenuta come commissione di cambio, scritta come numero intero.
Va convertita in decimale dividendola per 100.
Nota che lo spread viene aggiunto al tasso di cambio, quindi uno spread di 10 produce un tasso di cambio più alto del 10%.
Questa funzione deve restituire il valore massimo della nuova valuta dopo aver calcolato il tasso di cambio corretto dallo spread. Ricorda che il taglio della valuta è un numero intero e non può essere suddiviso.
exchangeable_value(127.25, 0,873350884, 10, 20);
// => 120
exchangeable_value(127.25, 0,873350884, 10, 5);
// => 130
Iscriviti a Exercism per imparare e padroneggiare x86-64 Assembly con 22 concetti130 esercizi e il mentoring di persone reali, tutto gratis.