在前面的概念中曾提到,global指令可以让在一个源文件中定义的函数或变量对其他文件可见。
同样,extern指令会告知汇编器,当前源文件中使用的某个函数或变量是在另一个源文件中定义的。
这两个指令是汇编代码与其他源文件交互、实现模块化的主要方式。
就这两个指令而言,外部代码是用汇编还是用高级语言定义或使用,并没有区别。
所以,举例来说,即使一个函数是用高级语言定义的,只要把它声明为extern,就可以在 x86-64 代码中使用。
同样,无论这些函数是用汇编还是用高级语言定义的,它们都共用同一套调用约定,与操作系统(OS)之间也是如此。
不过,由于在汇编中数据只是一串字节,因此有必要了解与之交互的高级语言是如何管理内存的。
C 语言是让高级语言与汇编代码交互时常用的选择。 它让与操作系统交互这件事变得更容易,为许多重要任务提供了高级封装,比如向屏幕输出、从键盘读取输入或分配动态内存。
在本 track 中,测试是用 C 编写的,因此有必要对这门语言做一个总体介绍。
下面列出的任何类型都可以用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 的数学不太好。 他很担心旅行途中被货币兑换处坑骗,于是想让你帮他做一个货币计算器。
以下是他对这款应用的具体要求。
本练习中的函数已在 C 源文件中按给定的签名声明好了。 你的 x86-64 汇编代码必须遵循这些规范。
你定义了一个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"
创建 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
创建 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
创建 get_number_of_bills 函数:
unsigned int get_number_of_bills(float amount, unsigned long long denomination);
这个函数应该返回在给定 金额 之内你能拿到的_钞票数量_。 换句话说:起始金额里能装下多少_整张钞票_? 记住,你只能拿到_整张钞票_,不能拿到零头,所以记得相应地做除法。 实际上,你是在向_下_取整到最接近的整张钞票或面额。
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 会加到汇率上,所以 spread 为 10 时,得到的汇率会高出 10%。
这个函数应该返回在算出经 spread 调整后的汇率之后,新货币的最大价值。 记住,货币的面额是整数,不能再细分。
exchangeable_value(127.25, 0,873350884, 10, 20);
// => 120
exchangeable_value(127.25, 0,873350884, 10, 5);
// => 130