货币兑换

货币兑换

学习练习

简介

在前面的概念中曾提到,global指令可以让在一个源文件中定义的函数或变量对其他文件可见。 同样,extern指令会告知汇编器,当前源文件中使用的某个函数或变量是在另一个源文件中定义的。

这两个指令是汇编代码与其他源文件交互、实现模块化的主要方式。

就这两个指令而言,外部代码是用汇编还是用高级语言定义或使用,并没有区别。 所以,举例来说,即使一个函数是用高级语言定义的,只要把它声明为extern,就可以在 x86-64 代码中使用。

同样,无论这些函数是用汇编还是用高级语言定义的,它们都共用同一套调用约定,与操作系统(OS)之间也是如此。

不过,由于在汇编中数据只是一串字节,因此有必要了解与之交互的高级语言是如何管理内存的。

C 数据类型

C 语言是让高级语言与汇编代码交互时常用的选择。 它让与操作系统交互这件事变得更容易,为许多重要任务提供了高级封装,比如向屏幕输出、从键盘读取输入或分配动态内存。

在本 track 中,测试是用 C 编写的,因此有必要对这门语言做一个总体介绍。

Note

下面列出的任何类型都可以用const限定。 这会让它们变成只读的。

基本类型

C 中的基本类型有很多,它们的字节大小可能各不相同。 下表总结了其中一部分,以及它们在 x86-64 系统中的典型大小:

类型 字节数 整数/浮点数
_Bool 1 整数
char 1 整数
short 2 整数
int 4 整数
long 4 或 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)。

这些基本类型的值按照整数和浮点数的一般规则传入函数、从函数返回。

枚举

enum是一种类型,它可以取若干个明确指出的元素之一:

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

在 C 中,枚举会被隐式转换为某种整数类型,通常是int,也就是一个 4 字节的整数。

默认情况下,枚举中第一个可能的元素会被转换为0,之后每个元素依次转换为下一个整数。 因此,在上面定义的enum example中,example_1的值是0,example_2的值是1,依此类推。

可以为枚举的任意元素指定值,之后默认情况下,下一个元素会取下一个整数作为值:

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

内存地址

在 C 中,内存地址通过指向某个类型的指针来引用,并用*运算符表示。 例如,int64_t *表示存放一个或多个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有 3 个元素,每个 8 字节,总共 24 字节。

由于数组不存储自己的长度,通常需要一个单独的值来指明元素个数,除非用哨兵值标记结尾。

字符串

C 中的字符串是char数组,而char是一种 1 字节类型。 大多数字符串由 ASCII 字符组成,并以 NUL 结尾,所以当遇到值为0的字节时字符串就结束了。 这意味着通常不需要把字符串的长度作为单独的参数传进去。

说明

你的朋友 Chandler 计划去世界各地那些充满异国风情的国家旅行。 可惜的是,Chandler 的数学不太好。 他很担心旅行途中被货币兑换处坑骗,于是想让你帮他做一个货币计算器。

以下是他对这款应用的具体要求。

Note

本练习中的函数已在 C 源文件中按给定的签名声明好了。 你的 x86-64 汇编代码必须遵循这些规范。

1. 记录不同的货币

你定义了一个enum currency_t,用来记录 Chandler 在旅行中用到的各种货币:

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

有时你需要把这些货币打印到屏幕上,好确认它们是否正确。

创建 stringify_currency 函数:

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

这个函数没有返回值,它应该把一个 C 风格字符串存到buffer所指示的位置。 这个字符串应该直观地表示传给函数的是哪一种货币:

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

2. 查询汇率

创建 exchange_rate 函数:

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

domestic_currency 实参表示 Chandler 的本地货币,它要被兑换成 foreign_currency。

value_in_US_dollars 是一个指向 double 数组的指针,这个数组保存着每种货币一单位以美元计的价值。 货币在这个数组里用下标表示。 所以,如果 EUR 是 enum currency_t 的一个有效元素,那么 value_in_US_dollars[EUR] 就给出 1 EUR 以美元计的价值。

这个函数应该返回一单位外币用本币表示的价值。

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. 计算钞票的价值

创建 get_value_of_bills 函数:

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

实参 denomination 是单张钞票的价值,而 number_of_bills 是钞票的总数。

这个兑换点只处理特定面额的现金。 你收到的总额必须能被一张“钞票”或一个单位的价值整除,除不尽的话会剩下小数或余数。 你的函数应该只返回兑换点会给你的钞票总价值(不含小数部分)。 遗憾的是,兑换点会把剩下的零头当作额外好处收归己有。

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

4. 计算钞票的数量

创建 get_number_of_bills 函数:

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

这个函数应该返回在给定 金额 之内你能拿到的_钞票数量_。 换句话说:起始金额里能装下多少_整张钞票_? 记住,你只能拿到_整张钞票_,不能拿到零头,所以记得相应地做除法。 实际上,你是在向_下_取整到最接近的整张钞票或面额。

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

5. 计算兑换后的价值

创建 exchangeable_value 函数:

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

形参 spread 是作为兑换手续费_收取的百分比_,用整数表示。 需要把它除以 100,换算成小数。 注意,spread 会加到汇率上,所以 spread 为 10 时,得到的汇率会高出 10%。

这个函数应该返回在算出经 spread 调整后的汇率之后,新货币的最大价值。 记住,货币的面额是整数,不能再细分。

exchangeable_value(127.25, 0,873350884, 10, 20);
// => 120
exchangeable_value(127.25, 0,873350884, 10, 5);
// => 130
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
x86-64 Assembly Exercism

准备好开始 货币兑换 了吗?

注册 Exercism,借助 22 个概念130 个练习 和真人导师指导,学习并掌握 x86-64 Assembly,全部免费。