이전 개념에서, global 지시자는 한 소스 파일에 정의된 함수나 변수를 다른 파일에서도 볼 수 있게 해 준다고 했어요.
비슷하게, extern 지시자는 현재 소스 파일에서 사용하는 함수나 변수가 다른 파일에 정의되어 있다는 것을 어셈블러에게 알려줘요.
이 두 지시자는 어셈블리 코드가 모듈화를 이루기 위해 다른 소스 파일과 상호작용하는 주된 방법이에요.
이 지시자들의 목적상, 외부 코드가 어셈블리로 정의되거나 사용되는지, 아니면 고급 언어로 정의되거나 사용되는지는 상관없어요.
그래서 예를 들어, 고급 언어로 정의된 함수도 extern으로 선언하기만 하면 x86-64 코드에서 사용할 수 있어요.
같은 방식으로, 호출 규약은 어셈블리로 정의되었든 고급 언어로 정의되었든 그 모든 함수들 사이에서, 심지어 운영체제(OS)와도 공유돼요.
그런데 어셈블리에서 데이터는 그저 바이트의 나열일 뿐이기 때문에, 함께 사용하는 고급 언어가 메모리를 어떻게 관리하는지 알아 두는 것이 중요해요.
C 언어는 어셈블리 코드와 인터페이스할 고급 언어로 흔히 선택돼요. 화면에 출력하기, 키보드 입력 읽기, 동적 메모리 할당처럼 중요한 작업을 위한 고급 래퍼를 제공해서, OS와 상호작용하는 일을 더 쉽게 만들어 줘요.
이 트랙에서는 테스트가 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에서 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 *는 하나 이상의 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에서 문자열은 1바이트 타입인 char의 배열이에요.
대부분의 문자열은 ASCII 문자로 이루어지고 NUL로 종료되기 때문에, 값이 0인 바이트를 만나면 끝나요.
그래서 문자열의 길이는 보통 별도의 인자로 전달할 필요가 없어요.
친구 Chandler는 전 세계의 이국적인 나라들을 여행할 계획이에요. 안타깝게도 Chandler는 수학 실력이 좋지 않아요. 여행 중에 환전소에서 바가지를 쓸까 봐 꽤 걱정하고 있고, 여러분에게 환전 계산기를 만들어 달라고 부탁했어요.
다음은 그가 앱에 대해 요구한 사항이에요.
이 연습 문제의 함수들은 주어진 시그니처와 함께 C 소스 파일에 선언되어 있어요. 여러분의 x86-64 어셈블리 코드는 그 명세를 따라야 해요.
Chandler가 여행 중 사용하는 여러 통화를 구분하기 위해 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와 교환할 Chandler의 현지 통화를 나타내요.
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
Exercism에 가입하고 x86-64 Assembly 트랙을 개념 22개연습 문제 130개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.