C 언어에서 구조체는 서로 다른 필드로 이루어진 복합 타입이에요. 각 필드는 고유한 타입과 크기를 가져요.
예를 들어 다음은 정수 세 개, 즉 4바이트짜리 두 개와 8바이트짜리 하나로 이루어진 타입을 정의해요:
struct example {
int32_t first_four_byte_integer;
uint32_t second_four_byte_integer;
uint64_t eight_byte_integer;
};
구조체는 다른 구조체를 포함할 수도 있어요:
struct example_2 {
struct example first;
struct example second;
};
구조체의 필드 중 하나가 배열이라면, 그 배열을 위해 할당된 모든 바이트가 구조체의 일부예요. 하지만 그 필드가 배열을 가리키는 포인터일 뿐이라면, 배열은 다른 곳에 저장돼야 해요:
struct with_array {
char string[20];
};
// all 20 bytes of string are inside struct with_array
struct with_pointer {
const char *string;
};
// only the 8-byte pointer to the string is inside struct with_pointer
타입의 정렬은 음이 아닌 2의 거듭제곱(1, 2, 4, 8, ...)으로, 그 타입의 주소에 대한 요구 사항을 나타내요. 그 타입의 모든 객체는 자신의 정렬 값의 배수인 주소에 위치해야 해요.
원시 타입의 경우 정렬은 보통 크기와 같아서, 예를 들어 int32_t는 보통 4바이트 정렬을 가져요.
즉 int32_t는 4바이트 경계에 할당돼야 해요.
반면 구조체의 정렬은 필드들 중 가장 큰 정렬 값이에요.
예를 들어 위에서 정의한 struct example은 8바이트 정렬을 가져요. 이는 uint64_t의 정렬이기 때문이에요.
구조체 필드의 정렬이 구조체의 정렬보다 작으면, 그 필드 뒤에 패딩 바이트가 삽입될 수 있어요. 이렇게 해서 뒤따르는 필드들과 구조체 전체가 정렬 요구 사항을 만족하게 돼요. 실제로는 이 패딩 바이트들이 구조체의 크기에는 기여하지만, 구조체의 값에는 사용되지 않아요:
struct example_3 {
char one_byte; // alignment of 1
int four_byte; // alignment of 4
};
// example_3 has alignment of 4 (the same as int)
// it occupies 8 bytes in space:
// 1 byte for field 'one_byte'
// 3 padding bytes (with undefined value)
// 4 bytes for field 'four_byte'
위에서 정의한 구조체에서 필드 'one_byte'는 정렬이 1이라 어떤 주소에든 놓일 수 있어요. 필드 'four_byte'는 정렬이 4이므로, 구조체 안의 필드들도 4바이트 경계를 지켜야 해요. 즉 구조체 안의 임의의 두 필드 사이에는 바이트 수가 4의 배수여야 해요. 'one_byte'는 1바이트만 차지하므로, 이 경계를 지키기 위해 그 뒤에 패딩 바이트 3개가 삽입돼요.
필요한 패딩 바이트 수를 최소화하기 위해, 인접한 필드들은 가능한 한 묶여요:
struct two_padding {
char one; // alignment of 1
char two; // alignment of 1
int three; // alignment of 4
};
// struct two_padding has alignment of 4 (the same as int)
// it occupies 8 bytes in space:
// 1 byte for field 'one'
// 1 byte for field 'two'
// 2 padding bytes
// 4 bytes for field 'three'
이 묶음은 각 필드의 정렬도 지켜야 해요:
struct one_padding {
char one; // alignment of 1
int16_t two; // alignment of 2
int three; // alignment of 4
};
// struct one_padding has alignment of 4 (the same as int)
// it occupies 8 bytes in space:
// 1 byte for field 'one'
// 1 padding byte
// 2 bytes for field 'two'
// 4 bytes for field 'three'
// Since 'two' has alignment of 2, it must be placed in a 2-byte boundary
// So, there must be 1 padding byte between 'one' and 'two'
따라서 구조체에서 필드의 순서를 바꾸면 크기가 달라질 수 있어요:
struct example_4 {
int64_t one;
short two;
int32_t three;
};
// example_4 has alignment of 8 (the same as int64_t)
// it occupies 16 bytes in space:
// 8 bytes for field 'one'
// 2 bytes for field 'two'
// 2 padding bytes
// 4 bytes for field 'three'
// two' and 'three' are adjacent and can be grouped to sum up 8 bytes with only 2 bytes of padding
struct example_5 {
int16_t one;
int64_t two;
unsigned int three;
};
// example_5 also has alignment of 8 (the same as int64_t)
// however, it occupies 24 bytes in space:
// 2 bytes for field 'one'
// 6 bytes for padding
// 8 bytes for field 'two'
// 4 bytes for field 'three'
// 4 padding bytes
// 'one' can't be grouped with 'two' because their sum would be higher than the struct's alignment
// 'two' and 'three' also can't be grouped, for the same reason
// so padding bytes must be inserted in full after each field with less than 8 bytes in alignment
배열의 정렬은 기반 타입의 정렬과 같아요:
struct array_example {
char string[10]; // alignment of 1
};
// array_example has alignment of 1 (it can be placed at any address)
// it occupies 10 bytes in space, one for each char in 'string'
배열의 원소들은 배열에 속하지 않은 것처럼 같은 방식으로 묶일 수 있어요:
struct group_array {
int16_t arr[3];
int8_t one_byte;
int32_t four_byte;
};
// group_array has alignment of 4 because this is the alignment of 'four_byte'
// it occupies 12 bytes in space:
// 4 bytes for arr[0] and arr[1]
// 2 bytes for arr[2]
// 1 byte for 'one_byte'
// 1 padding byte (arr[2] is grouped with 'one_byte')
// 4 bytes for 'four_byte'
어셈블리에서 구조체에 접근할 때는 가능한 모든 패딩 바이트를 고려하는 것이 중요해요.
일반적으로 값은 누구나 예상할 수 있는 방식으로 함수에 전달되고 함수에서 반환돼요:
xmm 레지스터로 전달돼요.하지만 구조체는 특별한 경우예요. 경험적으로, 구조체의 모든 필드는 메모리에서 인접한 공간을 차지하며, 가능하면 하나의 레지스터에 넣어야 해요:
struct single_type {
int16_t first;
int16_t second;
int16_t third;
int16_t fourth;
};
// this struct occupies 8 bytes
// it is entirely passed in the same GPR register:
// the field 'first' is passed on the lowest 2 bytes
// each subsequent field is passed in the following 2 bytes
구조체에 정숫값을 가진 필드와 부동소수점 값을 가진 필드가 함께 있으면, 구조체의 연속된 8바이트마다 따로 분류해야 해요:
struct mixed_types {
double first;
int second;
int third;
};
// this struct occupies 16 bytes
// the field 'first' is passed in the first 8 bytes of a xmm register
// the fields 'second' and 'third' are passed in the same GPR register:
// the field 'second' is passed on the lowest 4 bytes
// the field 'third' is passed in the next 4 bytes
정수와 부동소수점 값이 같은 8바이트 안에 있으면 그 타입은 정수로 취급되고, 부동소수점 값을 포함한 8바이트 전체가 GPR에 들어가요:
struct mixed_types {
int32_t integer;
float floating_point;
};
// this struct occupies 8 bytes
// it is entirely passed in the same GPR register:
// the field 'integer' is passed on the lowest 4 bytes
// the field 'floating_point' is passed on the next 4 bytes
cvtsi2ss나 cvtsi2sd로 부동소수점에서 정수로 변환하면, 내부 바이트 구성이 바뀌어요.
movd(32비트 값)와 movq(64비트 값)를 사용하면 비트 표현을 바꾸지 않고 xmm 레지스터에서 GPR로 원시 바이트를 옮길 수 있어요.
하지만 크기가 16바이트를 넘는 구조체는 각 8바이트의 타입과 상관없이 보통 메모리에 놓여요:
struct big_struct {
uint8_t first;
double second;
int64_t third;
};
// this struct occupies 24 bytes, including 7 padding bytes between 'first' and 'second'
// it is entirely passed in memory
구조체가 함수의 인자로 전달될 때는 필요한 만큼 레지스터를 사용해요. 메모리에 놓이는 경우에는 대신 스택을 사용해요.
함수에서 반환되는 구조체에도 비슷한 규칙이 적용돼요.
보통 정숫값이면 rax와 rdx에, 부동소수점 값이면 xmm0과 xmm1에 먼저 반환돼요.
하지만 메모리에 놓이는 경우에는 호출자가 그 구조체를 위한 공간을 확보하고, 이 메모리 위치의 주소를 함수의 암시적 첫 번째 인자로(rdi에) 전달해야 해요.
그러면 함수는 그 공간에 구조체를 저장하고 rax에 그 주소를 반환해요.
이 연습 문제에서는 여러 종류의 무선 조종 자동차가 벌이는 경주를 조직해요. 각 자동차는 저마다의 특성을 가지고 있으며, 토너먼트 시즌 내내 다양한 트랙에서 열리는 경주를 달려요.
이 연습 문제의 함수들은 제공된 시그니처와 함께 C 소스 파일에 선언되어 있어요. 작성한 x86-64 어셈블리 코드는 그 명세를 따라야 해요.
무선 조종 자동차를 나타내기 위해 car_t 구조체를 정의했어요:
struct car_t {
char name[10];
int16_t speed;
float battery;
};
x86-64 어셈블리로 new_car 함수를 정의해서 무선 조종 자동차를 만들 수 있게 해요.
battery 필드는 항상 100.0 값으로 시작해요.
struct car_t new_car(short speed, const char name[]);
트랙은 track_t 구조체로 나타내요:
struct track_t {
enum surface_t surface;
size_t distance;
};
enum surface_t는 트랙의 여러 표면을 구분해요:
enum surface_t {
ASPHALT,
SAND,
ICE,
CLAY
};
x86-64 어셈블리로 new_track 함수를 정의해서 새 트랙을 만들 수 있게 해요.
struct track_t new_track(enum surface_t surface, size_t distance);
각 경주에는 최대 6대의 자동차가 참가해서 트랙을 정해진 횟수만큼 돌아요.
경주는 race_t 구조체로 나타내요:
struct race_t {
struct track_t track;
uint8_t num_of_laps;
struct car_t cars[6];
uint8_t num_of_running_cars;
};
x86-64 어셈블리로 new_race 함수를 정의해요.
경주가 시작될 때 참가가 확정된 자동차는 없어요:
struct race_t new_race(struct track_t track, uint8_t num_of_laps);
자동차 주인이 자기 차의 경주 참가를 요청하면, 목록이 아직 꽉 차지 않았을 때만 참가가 확정돼요.
x86-64 어셈블리로 add_participant 함수를 정의해요.
경주에 참가자를 한 명 더 받을 자리가 있으면, 자동차가 목록에 추가되고 카운터가 갱신되면서 참가가 확정돼요.
그렇지 않으면 경주 주최자는 자동차 주인에게 이번에는 참가할 수 없다는 것을 알려야 해요.
bool add_participant(struct race_t *race, struct car_t car);
시즌 동안 예정된 경주는 tournament_t 구조체로 관리해요:
struct tournament_t {
struct race_t races[20];
size_t num_of_races;
};
x86-64 어셈블리로 add_race 함수를 정의해서 토너먼트의 배열에 경주를 추가하고 카운터도 갱신해요.
races 배열에는 새 경주를 넣을 공간이 항상 충분하다고 생각해도 돼요.
void add_race(struct tournament_t *tournament, struct race_t race);
Exercism에 가입하고 x86-64 Assembly 트랙을 개념 22개연습 문제 130개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.