Little Jay

[CS] Structure&Union. 구조체와 공용체 based on Intel_x86-64 Architecture - Computer System 3rd Edition by Bryant 본문

Univ/System Programming

[CS] Structure&Union. 구조체와 공용체 based on Intel_x86-64 Architecture - Computer System 3rd Edition by Bryant

Jay, Lee 2022. 3. 7. 18:28

구조체(Sturcture)

구조체는 간단하게 말해서 기본 데이터 타입들을 가지고 새롭게 사용자가 정의할 수 있는 타입이다.

기본타입뿐만 아니라 복잡한 타입들을 묶어 구조체로 선언할 수 있다.

 

조금 더 자세히 설명을 하자면, 구조체는 객체를 그룹화하는 데이터 형식을 생성할 수 있다.

서로 다른 유형을 단일 객체로 그룹화 시킨다. 구조체의 다른 구성요소는 이름으로 참조될 수 있는 것이다.

구조체의 구현은 구조체의 모든 구성 요소가 메모리의 연속적인 영역에 저장되고, 구조체에 대한 포인터가 첫 번째 바이트의 주소라는 점에서 배열의 구현과 유사하다고 할 수 있다.

컴파일러는 각 필드의 바이트 오프셋을 나타내는 각 구조 유형에 대한 정보를 관리하게 되는데, 이러한 오프셋을 참조 명령어의 변위로 사용하여 구조 요소에 대한 참조를 생성하게 된다.

struct rec {
    int i;
    int j;
    int a[2];
    int *p;
};

위의 예시를 잠깐 살펴보자. 구조체도 배열과 마찬가지로 메모리의 연속적인(continuous)한 공간에 생성되므로, 아래와같은 구조를 생각해볼 수 있다. (앞에서 포인터는 8byte를 가진다고 언급하였다.)

이 rec 이라는 구조체에 접근하기 위해서는 일반적으로 . (dot 연산)을 통해 멤버함수에 접근할 수 있지만, 만약 이 구조체가 포인터로 선언이 되었다면 -> 로 접근해야 한다. 

int main(int argc, char* argv[]) {
    rec r1;
    r1.i = 0;
    
    rec* r2;
    r2 = (rec*) malloc(sizeof(rec));
    r2->j = 4;
    
    free(r2);
}

malloc을 통해 동적으로 메모리 할당이 되었으므로 r2는 heap 영역에 저장이 되어 사용할 수 있을 것이다.

동적할당을 했을때 c 에서는 free를, c++에서는 delete를 해주는 것을 까먹지 말자.

메모리 누수관련해서 정말 중요한 문제이다.

 

그리고 구조체는 특이하게도 선언과 동시에 다른 변수에 그 내용을 선언해줄 수 있다. 

stuct rec {
	int a[4];
    long i;
    struct rec *next;
};

struct rec r; //32yte 할당
struct rec *p; //8byte 할당

stuct rec {
	int a[4];
    long i;
    struct rec *next;
} r, *p;

위쪽 부분의 코드와 아래쪽 부분의 코드는 동치관계이다.

 

구조체는 앞서서 말했다싶이 사용자가 정의할 수 있는 새로운 타입의 집합체이다. 그런만큼 사이즈도 사용자가 정의한 객체의 순서대로 그 크기자 결정이 된다. 위의 그림을 다시한번 떠올려보면, i, j, a[0], a[1], p가 순서대로 메모리 공간에 예약되어 있는 것을 볼 수 있다. 이 구조체의 멤버들은 sequentially in order하게 선언된 순서대로 저장이 되는 것이다. 메모리 주소는 우리가 일반적으로 생각할 수 있들이 첫 번째 멤버가 가장 낮은 메모리 주소를 가지게 되고, 마지막 멤버가 가장 높은 메모리 주소 번지를 가지게 된다. 

 

이제 문제는 컴파일러로 넘어가게 된다. 사실 Machine-Level 즉 Low Level에서는 struct라는 개념이 존재하지 않는다. 이는 배열도 마찬가지이다. 컴파일러가 이런 메모리에 접근을 하게 될때는 배열, 구조체, 이런 기능들이 정의되어 있지 않고 단지 메모리의 주소를 가지고 컨트롤한다. 이게 어떻게 가능하냐고 물어볼 수 있겠지만, 우리는 배열이나 구조체가 연속적인 메모리공간을 가지고 있다는 것을 유의해야한다. 저번 포스팅에서도 assembly어를 통해 간단히 살펴보았지만, 결국 assembly언어는 배열이나 구조체의 시작점을 기준으로하여 index를 컨트롤하면서 멤버에 접근할 수 있는 것이다.

assembly언어의 ISA는 Inter_x86 Architecture을 사용하고 있다. 

int *get_a(struct temp *t, size_t idx) {
	return &(t->a[idx]);
}

struct temp {
    int a[2];
    size_t s;
    struct temp *some;
};

//%rdi가 r, %rsi가 idx라고 가정
leaq (%rdi, %rsi, 4), %rax
ret

위의 간단한 예시를 보면 그 사용을 이해할 수 있을것이다. 

결국 a는 t + 4*idx로 접근이 가능하다. temp라는 구조체의 시작번지는 당연하게도 a의 시작번지가 되는 것이고, int 자료형의 배열이기 때문에 idx에 4를 곱해 내가 컨트롤 할 수 있는 것이다.

이를 짤막하게 assembly어로 나타내면 위와 같다. 

 

구조체를 선언할때 지켜야 하는 몇가지 Principles들이 있다. 

그 전에 앞서서 Intel_x86-64 에서 정의하는 Alignment에 대해 짤막히 알아보자.

이 아키텍쳐에서 정의하는 Aligned란, K bytes를 크기를 지닌 primitive object의 주소는 K 의 배수의 주소를 가져야한다.

시스템과 HW마다 다르겠지만, 기본적으로 메모리는 aligned된 byte의 단위로 끊어서 읽게 된다. 

quad word, 4byte 단위로 읽는 경우가 많은데 만약 메모리에서 4byte대로 메모리가 저장되지 않는다면 컴파일러가 메모리를 읽는데 어려움을 겪을 수 있다. 

 

예를 들어보자, memory 안에 한 5개의 page가 있다고 가정을 해보자. 우리는 4byte씩 끊어읽기로 했는데 시스템에서 메모리에 char 배열을 저장할 때 4의 배수로 끝마치는것이 아닌 0x85 이렇게 끝났다고 하고 다음 페이지는 0x88에서 시작된다고 해보자. 그렇게 되면 메모리를 읽을 때 0x85에서 4byte를 읽게 되면 페이지를 두 개나 읽어버리고, 메모리를 읽고 fetch, decode, excute하는 과정에서 다양한 faults로 인해 interrupt가 발생이 되어버릴 수 있다. 그렇기 때문에 memory에 어떤 구조체를 저장할 때는 다양한 ISA에서 규약한 메모리 저장방식을 따라야한다. 

그렇기 때문에 구조체를 선언을 하면 본인이 생각했던 것 보다는 사이즈가 크게 나올 수 있다. 결국 Intel x86의 규약을 따라야 하기 때문에 이를 맞춰주기 위한 보정작업이 들어간다고 볼 수 있다. (그렇다고 메모리를 작게하면 데이터를 저장할 수 없게되니.....)  아래의 예시를 보자. S1이라는 구조체는 처음에 봤을 때 17바이트를 가지는 것을 생각해볼 수 있다. 그러나 내부에서는 보정작업을 통해 이 메모리의 주소를 primitive data type의 배수로 맞춰주어야 한다. 컴파일러가 메모리를 읽을 때 idx로 컨트롤 해야하기 때문에 우리가 생각했던 것과는 차이가 발생한다. 왼쪽에 그림을 보면 알겠지만 실제 저 코드를 실행했을때 우리가 17byte로 예상한 구조체의 크기는 24byte이다. 이것이 바로 보정작업이 들어가서 구조체를 컴파일러가 읽기 편하도록 맞춤작업이 진행되었으며, padding byte를 집어넣어 그 공간을 메꿔주고 있다고 할 수 있다. 아래의 그림같이 padding이 들어가게 된다. 

 

조금 설명이 들어간다면 i[0]은 int이다. int의 주소는 4바이트의 배수가 되야한다. 그렇기 때문에 3byte의 패딩이 붙게 되고, v도 마찬가지로 8byte의 배수가 와야 하므로, 4byte의 보정작업이 추가적으로 들어가야한다. 

 

이런식의 보정이 들어가는 한편 다른 보정작업이 들어갈 수 있는데, 구조체의 전체 사이즈는 구조체의 가장 큰 멤버의 정수배의 크기를 가져야 한다는 것이다. 일련의 구조체가 정상적으로 문제없이 연속적으로 잘 배치가 되었다고해도 구조체의 크기는 결국 구조체 멤버중 가장 큰 사이즈의 배수가 되야 한다. 아래의 코드를 잠깐 보면 17byte로 구조체가 padding없이 잘 정의되었다고 생각할 수 있다. 그러나 이 구조체에서 가장 큰 멤버의 크기는 v, 즉 8byte이다. 구조체 자체가 이 8byte의 배수가 되어야 하므로 8byte의 배수인 24를 맞춰주기 위해서는 7byte의 padding이 들어가야 한다. 

struct s2{
    double  v;
    int i[2];
    char c;
} *p;

결과적으로 구조체 내에서는 각각의 Architecture가 규약하고 있는 그 사이즈를 맞춰주어야 한다. 다시 이 내용들을 정리하자면

  • 각각의 구조체의 멤버들의 시작주소과 구조체의 길이는 구조체 내의 가장 큰 사이즈의 멤버의 정수배 형태가 되어야 하며,
  • 구조체의 멤버들의 시작주소는 각 멤버의 사이즈의 정수배 형태로 시작해야한다

라고 정리를 해볼 수 있을 것이다. 

 

그러나 이렇게 구조체의 사이즈를 조절을 하다보면 memory leak까지는 아니지만 결국 memory 내에서 낭비되는 메모리 공간들이 발생이 된다. 예를 들어 위의 s2의 구조체가 100개의 배열로 있다고 가장해보자. 그러면 배열 내부의 한 인덱스마다 7byte씩 padding이 생겨 7*100, 즉 700byte나 낭비가 되고 있는 것이다. 그렇기 때문에 프로그래머의 입장이 아닌 컴퓨터에 입장에서 볼 때 이 공간의 낭비를 최소화해주는 것이 바람직할 것이다. 

따라서 프로그래머가 임의적으로 구조체를 생성을 할 때 충분히 고민을 해보고 구조체를 선언해야 할 것이다. 

아래의 코드처럼 s4는 12byte의 크기를 가지지만, 이를 optimizing 해준다면 8byte로 줄일 수 있다. 

대부분의 구조체에서 각 멤버의 크기가 작은 것부터 앞에서 선언을 해주는 스타일로 코드를 작성한다면 메모리를 조금 더 효율적으로 사용할 수 있을 것이다. 

struct s4{
    char c;
    int i;
    char a;
} *p;

struct s5{
    char a;
    char c;
    int i;
} *p;

공용체

잘 사용되지는 않지만, 공용체라는 타입도 존재한다.

공용체 선언의 문법은 구조에 대한 구문과 동일하지만 그 의미는 다르다.

구조체는 서로 다른 멤버가 서로 다른 메모리 블록을 참조하도록 하는 대신 공용체는 모두 동일한 블록을 참조한다.

공용체는 가장 큰 멤버의 크기의 사이즈를 할당하고 그 내부에서 사용한다.

그리고 참조가 잘못 되는 것을 막기 위해 한번에 한번만 그 멤버를 사용할 수 있게 해준다.

결국 해석하는 주체는 컴파일러이기에 컴파일러에게 동시에 다른 참조를 맡길 수 없다. 

struct s {
    char c;
    int i[2];
    double v;
} *sp;

union u {
    char c;
    int i[2];
    double v;
} *up;

이 외에도 메모리 공간을 더욱 더 효율적으로 사용하기 위한 bit field라는 개념도 존재한다. 

bit field는 bit 단위로 구조체를 allocate하는 방식인데, 이렇게 되면 공간은 매우 압축이 되지만, 사용자 입장에서는 해당 비트만큼의 공간을 컨트롤 해야하므로 생각하면서 size를 초과하지는 않는지 생각해야한다.

임베디드 시스템적으로 많이 사용이 되며, 여기에 대해서는 많은 설명은 하지는 않겠다.

필자도 이를 사용해본 경험은 없다.

struct b {
    unsigned int size: 10; //10bit 사용
    unsigned int color: 16; //16bit 사용
    unsigned int style: 6; //6bit 사용
};

출처

http://www.tcpschool.com/c/c_struct_intro

Randal E. Bryant, & David R. O’Hallaron. (2016). Computer Systems A Programmer’s Perspective Third edition (pp. 120-144). Carnegie Mellon University: Pearson.

Comments