これまでの概念では、globalディレクティブを使うと、あるソースファイルで定義した関数や変数を他のファイルから参照できるようになると説明しました。
同じように、externディレクティブは、現在のソースファイルで使う関数や変数が別のファイルで定義されていることをアセンブラーに伝えます。
この2つのディレクティブは、モジュール化のためにアセンブリコードが他のソースファイルと連携する主な手段です。
これらのディレクティブでは、外部のコードがアセンブリで定義されていても、高水準言語で定義されていても、違いはありません。
たとえば、高水準言語で定義された関数であっても、externで宣言すればx86-64のコードから使えます。
同じように、呼び出し規約は、アセンブリで定義された関数でも高水準言語で定義された関数でも共通で、オペレーティングシステム(OS)との間でも同じものが使われます。
ただし、アセンブリではデータは単なるバイトの並びにすぎないので、連携する高水準言語がメモリをどのように管理するかを知っておくことが大切です。
アセンブリコードと連携する高水準言語としては、C言語がよく選ばれます。 C言語を使うとOSとのやり取りが簡単になります。画面への出力、キーボードからの入力、動的メモリの確保といった多くの重要な処理に対して、高水準のラッパーが用意されているからです。
このトラックでは、テストがC言語で書かれているので、この言語の概要をひととおり説明しておくことが大切です。
ここに挙げる型は、どれもconstを付けて修飾できます。
constを付けると読み取り専用になります。
Cには多くの基本型があり、バイト単位のサイズはそれぞれ異なります。 その一部を、x86-64システムでの一般的なサイズとともに次の表にまとめます。
| 型 | バイト数 | 整数・浮動小数点 |
|---|---|---|
| _Bool | 1 | 整数 |
| char | 1 | 整数 |
| short | 2 | 整数 |
| int | 4 | 整数 |
| long | 4 or 8 | 整数 |
| long long | 8 | 整数 |
| float | 4 | 浮動小数点 |
| double | 8 | 浮動小数点 |
これらの整数型はそれぞれ、符号付き(既定)または符号なしにできます。
符号なしにする場合は、たとえばunsigned intのように明示する必要があります。
整数型には、サイズを正確に制御するために定義された別名があります。
| 別名 | バイト数 |
|---|---|
| int8_t | 1 |
| int16_t | 2 |
| int32_t | 4 |
| int64_t | 8 |
これらの別名は、符号なし型であることを示すためにuを前に付けられます。たとえばuint64_tです。
他に注目すべき別名には次のようなものがあります。
| 別名 | 型 |
|---|---|
| bool | _Bool |
| size_t | 通常は符号なしの8バイト整数 |
_Bool(別名bool)が特別なのは、1バイト全体を占めるにもかかわらず、true(1)とfalse(0)の2つの値のどちらかしか取れないことです。
こうした基本型の値は、整数と浮動小数点値に関する通常のルールに従って、関数に渡されたり、関数から返されたりします。
enumは、明示的に示されたいくつかの要素のうちの1つを取ることのできる型です。
enum example {
example_1,
example_2,
example_3,
example_4
};
Cでは、enumは暗黙的に整数型に変換され、通常はint、つまり4バイトの整数になります。
既定では、enumの最初の要素は0に変換され、それに続く要素は順に次の整数に変換されます。
ですから、上で定義したenum exampleでは、example_1の値が0、example_2の値が1、というようになります。
enumの要素には値を指定することもでき、その場合、次の要素の値は既定でその次の整数になります。
enum example_2 {
example_1 = 4,
example_2 = 8,
example_3
}; // example_3 is equal to 9
Cでは、メモリアドレスは型へのポインターで参照し、*演算子で表します。
たとえばint64_t *は、1つ以上のint64_tを格納するメモリ位置を指します。
アドレスは通常どおり、8バイトの整数として扱われます。
配列は、最初の要素へのポインターとして関数に渡され、関数から返されます。
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
配列の各要素は、配列の要素型のサイズを持ちます。
たとえば上で定義したexample_arrは、8バイトの要素を3つ持つので、合計24バイトになります。
配列は長さを保持しないため、終端を示す番兵の値がない限り、通常は要素数を示す別の値が必要です。
Cの文字列はcharの配列で、charは1バイトの型です。
ほとんどの文字列はASCII文字からなり、NULで終端されるので、値0のバイトが見つかったところで終わります。
つまり、文字列の長さを別の引数として渡す必要は通常ありません。
友人のチャンドラーは、世界中のエキゾチックな国々を訪れる計画を立てています。 残念ながら、チャンドラーは数学が得意ではありません。 旅行中に両替でだまされてしまわないかかなり心配していて、自分のために通貨計算機を作ってほしいと思っています。
以下が、彼がアプリに求める仕様です。
この演習の関数は、Cのソースファイル内で、与えられたシグネチャで宣言されています。 x86-64アセンブリのコードは、その仕様に従う必要があります。
チャンドラーが旅先で使うさまざまな通貨を管理するために、enum currency_tを作成しました。
enum currency_t {
GBP,
EUR,
JPY,
AUD,
BRL,
CNY,
CAD,
INR
};
これらの通貨が正しいか確認できるよう、画面に表示する必要があることもあります。
stringify_currency関数を作成してください。
void stringify_currency(char *buffer, enum currency_t currency);
この関数は戻り値を返さず、bufferが示す場所にCスタイルの文字列を格納する必要があります。
この文字列は、関数に渡された通貨の種類を視覚的に表す必要があります。
char buffer[4];
stringify_currency(buffer, CAD);
// => "CAD"
exchange_rate関数を作成してください。
double exchange_rate(enum currency_t domestic_currency, enum currency_t foreign_currency, const double *value_in_US_dollars);
domestic_currency入力は、foreign_currencyと交換するチャンドラーの自国通貨を表します。
value_in_US_dollarsは、各通貨1単位の価値を米ドルで格納したdoubleの配列へのポインタです。
通貨は、この配列のインデックスになっています。
つまり、EURがenum currency_tの有効な要素であれば、value_in_US_dollars[EUR]は1 EURの価値を米ドルで返します。
この関数は、外国通貨1単位の価値を自国通貨で返す必要があります。
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
get_value_of_bills関数を作成してください。
uint64_t get_value_of_bills(unsigned long long denomination, unsigned short number_of_bills);
denomination入力は紙幣1枚の価値で、number_of_billsは紙幣の総枚数です。
この両替所は、決まった単位の現金しか扱いません。 受け取る合計は、1つの「紙幣」、つまり単位の価値で割り切れる必要があり、割り切れない端数や余りが出ることがあります。 関数は、両替所が返す紙幣の合計額(端数の金額を除く)だけを返す必要があります。 残念ながら、両替所は端数や釣り銭を、おまけとしてそのまま受け取ります。
get_value_of_bills(5, 128);
// => 640
get_number_of_bills関数を作成してください。
unsigned int get_number_of_bills(float amount, unsigned long long denomination);
この関数は、与えられた_金額_の中で受け取れる_紙幣の枚数_を返す必要があります。 言い換えると、元の金額の中に_1枚単位の紙幣_が何枚収まるかということです。 受け取れるのは_1枚単位の紙幣_だけで、紙幣の端数は受け取れません。そのつもりで割り算するのを忘れないでください。 実質的には、紙幣つまり単位のところで_切り捨て_ていることになります。
get_number_of_bills(127.5, 5);
// => 25
exchangeable_value関数を作成してください。
uint32_t exchangeable_value(float budget, double exchange_rate, uint8_t spread, unsigned long long denomination);
spread入力は、両替手数料として_取られる割合_を整数で表したものです。
100で割って小数に変換する必要があります。
spreadは為替レートに加算されるため、10のspreadでは為替レートが10%高くなる点に注意してください。
この関数は、spreadで調整した為替レートを計算したうえで、新しい通貨の最大値を返す必要があります。 通貨のdenominationは整数であり、分割できないことを覚えておいてください。
exchangeable_value(127.25, 0,873350884, 10, 20);
// => 120
exchangeable_value(127.25, 0,873350884, 10, 5);
// => 130
Exercismに登録すれば、22個のコンセプト130個の演習、そして本物の人間によるメンタリングとともに、x86-64 Assemblyを学んでマスターできます。すべて無料です。