13. Storage

지금까지 다룬 오브젝트는 대부분 변수복합 리터럴이였다. 이 오브젝트들은 프로그램의 문법적 구조에 의존하는 생애 주기를 갖는다. 이 객체 주기는 프로그램의 실행 시간 전체가 되기도 하고 (전역 변수, 전역 리터럴, static 변수) 함수 내 명령문으로 구성된 블록이 되기도 한다.

오브젝트는 동적 할당으로 생성할 수도 있는데 이렇게 만들어진 오브젝트들은 바이트 배열일 뿐 오브젝트로서의 표현을 갖지는 않는다. 그 곳에 무언가를 저장할 때에만 타입으로서의 의미를 갖는다. 그리고 오브젝트에는 초기화 시마다 저장소 기간, 오브젝트 생애 주기, 식별자 가시 범위가 존재한다. 이후에는 특정한 구현체에 따른 메모리 모델의 실제 구현 예도 살펴볼 것이다.

13.1. malloc and friends.

C에서는 직접 오브젝트를 위한 저장소 객체를 생성하고 필요 없어지면 삭제할 수 있는데 이를 동적 할당이라 한다. 인터페이스는 다음과 같다.

#include <stdlib.h>
void* malloc(size_t size);
void free(void* ptr);
void* calloc(size_t nmemb, size_t size);
void* realloc(void* ptr, size_t size);
void* aligned_alloc(size_t alignment, size_t size);

malloc은 저장소 객체를 생성한다. free는 그것을 소멸시킨다. 다른 3개는 malloc의 특수화된 버전이다. calloc은 생성된 저장소를 0으로 초기화시킨다. realloc은 저장소를 늘리거나 줄인다. aligned_alloc은 커스텀 정렬을 보장한다. 이 모든 함수들은 void*로 동작한다. 즉, 타입 정보는 별도로 필요 없이 모든 타입 포인터에 대해 동작한다.

size_t length = livingPeople();
double* largeVec = malloc(length * sizeof *largeVec);
// double* largeVec = malloc(sizeof(double[length]));
for (size_t i = 0; i < length; i++) {
    largeVec[i] = 0.0;
}
free(largeVec);

malloc은 타입 정보를 모르기 때문에 할당할 때 sizeof 으로 타입의 크기 정보를 전달해줘야 한다. 위의 2가지 활용법 중 편한 대로 쓰면 된다.

Takeaway 2.13.1.1. malloc과 그 부류의 리턴값을 형변환하지 말라.

이는 불필요할 뿐만 아니라 stdlib.h를 인클루드하지 않았을 때 오래된 컴파일러의 암묵적 함수 선언에 의해 재앙적 결과를 낳을 수 있다.

// If we forgot to include stdlib.h, many compilers still assume:
int malloc(); // wrong function interface!

double* largeVec = (void*) malloc(sizeof(double[length]));

Takeaway 2.13.1.2. malloc으로 할당된 저장소는 초기화되지 않는다.

13.1.1. A complete example with varying array size.

다음의 원형 버퍼에 대한 예제를 살펴 보자.

// circular.h

#ifndef CIRCULAR_H
#define CIRCULAR_H 1

#include <stddef.h>
#include <stdio.h>

typedef struct circular circular;

circular* circular_init(circular* c, size_t max_length);
void circular_destroy(circular* c);
circular* circular_new(size_t len);
void circular_delete(circular* c);
circular* circular_append(circular* c, double value);
double circular_pop(circular* c);
double* circular_element(circular* c, size_t pos);
size_t circular_getlength(circular* c);
circular* circular_resize(circular* c, size_t new_size);
void circular_fput(circular* c, FILE* s);

#endif
#include <stdlib.h>
#include <string.h>
#include "circular.h"

struct circular {
    size_t begin;
    size_t size;
    size_t capacity;
    double *buffer;
};

circular *circular_init(circular *c, size_t max_length) {
    if (c) {
        if (max_length) {
            *c = (circular) {
                    .capacity = max_length,
                    .buffer = malloc(sizeof(double[max_length])),
            };
            // Allocation failed.
            if (!c->buffer) c->capacity = 0;
        } else {
            *c = (circular) {0};
        }
    }
    return c;
}

void circular_destroy(circular *c) {
    if (c) {
        free(c->buffer);
        circular_init(c, 0);
    }
}

circular *circular_new(size_t len) {
    return circular_init(malloc(sizeof(circular)), len);
}

void circular_delete(circular *c) {
    circular_destroy(c);
    free(c);
}


size_t circular_getlength(circular *c) {
    return c ? c->size : 0;
}

static size_t circular_getpos(circular *c, size_t pos) {
    pos += c->begin;
    pos %= c->capacity;
    return pos;
}

circular *circular_append(circular *c, double value) {
    if (c) {
        double *where = circular_element(c, c->size);
        if (where) {
            *where = value;
            ++c->size;
            return c;
        }
    }
    return 0;
}

double *circular_element(circular *c, size_t pos) {
    double *ret = 0;
    if (c) {
        if (pos < c->capacity) {
            pos = circular_getpos(c, pos);
            ret = &c->buffer[pos];
        }
    }
    return ret;
}

double circular_pop(circular *c) {
    double ret = 0.0;
    if (c && c->size) {
        double *p = circular_element(c, 0);
        if (p) ret = *p;
        ++c->begin;
        --c->size;
    }
    return ret;
}


circular *circular_resize(circular *c, size_t new_size) {
    if (c) {
        size_t size = c->size;
        if (size > new_size) return 0;
        size_t orig_capacity = c->capacity;
        if (new_size != orig_capacity) {
            size_t orig_begin = circular_getpos(c, 0);
            size_t new_begin = orig_begin;
            double *orig_buffer = c->buffer;
            double *new_buffer;
            if (new_size > orig_capacity) {
                new_buffer = realloc(c->buffer, sizeof(double[new_size]));
                if (!new_buffer) return 0;
                // Two separate chunks
                if (orig_begin + size > orig_capacity) {
                    size_t upper_length = orig_capacity - orig_begin;
                    size_t lower_length = size - upper_length;
                    if (lower_length <= (new_size - orig_capacity)) {
                        /* Copy the lower one up after the old end. */
                        memcpy(new_buffer + orig_capacity, new_buffer,lower_length * sizeof(double));
                    } else {
                        /* Move the upper one up to the new end. */
                        new_begin = new_size - upper_length;
                        memmove(new_buffer + new_begin, new_buffer + orig_begin,upper_length * sizeof(double));
                    }
                }
            } else {
                if (orig_begin + size > orig_capacity) {
                    // Two separate chunks; mv the upper one down to the new end.
                    size_t upper_length = orig_capacity - orig_begin;
                    new_begin = new_size - upper_length;
                    memmove(orig_buffer + new_begin, orig_buffer + orig_begin, upper_length * sizeof(double));
                } else {
                    // A single chunk
                    if (orig_begin + size > new_size) {
                        // Reallocation cuts the existing chunk in two.
                        memmove(orig_buffer, orig_buffer + orig_begin, size * sizeof(double));
                        new_begin = 0;
                    }
                }
                // Now all data is saved in the conserved part of the array.
                new_buffer = realloc(c->buffer, sizeof(double[new_size]));
                // If realloc fails in this case (would be weird), just overrule it.
                if (!new_buffer) new_buffer = orig_buffer;
            }
            *c = (circular) {
                    .capacity = new_size,
                    .begin = new_begin,
                    .size = size,
                    .buffer = new_buffer,
            };
        }
    }
    return c;
}

void circular_fput(circular *c, FILE *s) {
    if (c) {
        size_t len = circular_getlength(c);
        double *tab = c->buffer;
        if (tab) {
            fprintf(s, "%p+%zu (%zu+%zu):", (void *) tab, c->capacity, c->begin, len);
            for (size_t i = 0; i < len; ++i) {
                fprintf(s, "\t%g", *circular_element(c, i));
            }
            fputc('\n', s);
            return;
        }
    }
    fputs("invalid circular\n", s);
}

Takeaway 2.13.1.3. malloc은 널 포인터 값을 반환함으로써 실패를 알린다.

realloc에는 흥미로운 특징이 있다.

  • 반환된 포인터는 인자와 같을 수도 다를 수도 있으며, 이는 런타임에 결정되지만, 오브젝트는 새로운 것으로 간주된다 (데이터가 같더라도). 그러므로 원 저장소로부터 유도된 포인터는 전부 무효화된다.
  • 반환된 포인터가 인자와 다르다면 (즉, 오브젝트가 복사되었다면) 이전 포인터에 대해 따로 조치를 할 필요는 없다.
  • 오브젝트의 내용은 보존된다: 오브젝트가 커지면 원래 크기에 해당하는 오브젝트의 첫 부분은 유지된다. 오브젝트가 작아지면 재할당된 오브젝트는 호출 이전 오브젝트의 앞 부분에 해당하는 내용을 갖는다.
  • 0이 반환되면 (런타임에 재할당 요청이 실패하면) 기존 오브젝트는 유지되고, 유실되지 않는다.

13.1.2. Ensuring consistency of dynamic allocations.

Takeaway 2.13.1.4. 모든 할당에는 대응되는 free가 있어야 한다.

그렇지 않으면 메모리 누수가 발생하여 할당된 오브젝트가 손실된다. 이는 리소스 소모로 인한 성능 저하 또는 프로그램 크래시를 낳는다.

Takeaway 2.13.1.5. 모든 free에 대해서는 대응되는 할당 (realloc 제외)이 있어야 한다.

realloc은 이전 오브젝트의 할당 해제와 새 오브젝트의 할당을 동시에 수행한다.

Takeaway 2.13.1.6. 오직 할당 (realloc 제외)이 반환한 포인터들만 free를 호출할 수 있다.

즉 다음 포인터들은 free해서는 안 된다.

  • 다른 방법으로 할당된 오브젝트에 대한 포인터 (변수, 복합 리터럴)
  • 이미 free된 포인터
  • 할당된 오브젝트의 일부분을 가리키는 포인터

이를 지키지 않으면 프로그램이 최악의 형태로 – 메모리를 완전히 오염시키면서 – 죽는다.

13.2. Storage duration, lifetime, and visibility.

void squareIt(double* p) {
    *p *= *p;
}
int main(void) {
    double x = 35.0;
    double* xp = &x;
    {
        squareIt(&x); // refers to double x
        int x = 0; // shadow double x
        squareIt(xp); // valid use of double x
    }
    squareIt(&x); // refers to double x
}

위의 예에서 안쪽 블록 안의 int x = 0; 정의문은 바깥쪽 블록에 선언된 double x = 35.0;의 가시 범위를 가리게 된다. 그렇다고 해서 그 오브젝트에 접근을 못하게 되는 것은 아니다.

Takeaway 2.13.2.1. 식별자들은 그들의 선언문으로부터 시작해서 속한 스코프 안쪽까지만 가시범위를 가진다.

Takeaway 2.13.2.2. 식별자의 가시 범위는 내부 스코프 내의 같은 이름을 가진 식별자에 의해 가려질 수 있다.

#include <stdio.h>

unsigned i = 1;

int main(void) {
    unsigned i = 2; // a new object
    if (i) {
        extern unsigned i; // an existing object
        printf("%u\n", i);
    } else {
        printf("%u\n", i);
    }
}

위의 예는 extern으로 내부 스코프에 같은 이름을 가진 식별자를 선언하는 예이다. 이 경우 extern으로 선언한 식별자는 파일 스코프 내 정의된 정적 저장소 기간을 갖는 오브젝트를 지정한다.

Takeaway 2.13.2.3. 변수의 정의문은 새로운, 별개의 오브젝트를 만든다.

그러므로 아래에서 A == B는 false가 된다.

char const A[] = {'e', 'n', 'd', '\0', };
char const B[] = {'e', 'n', 'd', '\0', };
char const* c = "end";
char const* d = "end";
char const* e = "friend";
char const* f = (char const[]){'e', 'n', 'd', '\0', };
char const* g = (char const[]){'e', 'n', 'd', '\0', };

그렇지만 위의 예에서 서로 다른 오브젝트는 총 몇 개인가? 이는 더 복잡하다.

Takeaway 2.13.2.4. 읽기 전용 오브젝트 리터럴은 겹칠 수 있다.

c, d, f, g는 같을 수도 있고 다를 수도 있다. 심지어는 e의 끝 부분에 c, d, f, g를 위치시킬 수도 있다. 이는 프로그램의 실행 상태에 의존한다. 오브젝트의 생애 주기는 시작점과 끝점이 있다.

Takeaway 2.13.2.5. 오브젝트의 생애 주기 밖에서는 오브젝트에 접근될 수 없다.

Takeaway 2.13.2.6. 생애 주기 밖에서 오브젝트에 접근하는 것은 정의되지 않은 행동이다.

C의 오브젝트는 4종류의 저장소 주기가 존재한다. 컴파일 타임에 결정되는 정적 주기, 런타임에 자동적으로 결정되는 자동 주기, malloc류에 의해 명시적으로 결정되는 할당 주기, 실행 스레드에 종속되는 스레드 주기.

저장소 클래스, 초기화, 링크, 저장소 주기, 생애 주기 간에는 복잡한 관계가 있다. 첫째로, 저장소 클래스 extern은 이름과는 다르게 외부 또는 내부 식별자를 모두 지정할 수 있다. 식별자의 연결은 컴파일러와는 별개로 링커라는 프로그램에 의해 관리된다. 이는 main이 시작되기 전에 초기화되는 것이 링커에 의해 보장된다. 다른 오브젝트 파일을 통해 접근되는 식별자는 링커에 의해 대응 관계가 성립되는 외부 링크가 필요하다.

외부 링크가 있는 주요 식별자는 C 시스템 라이브러리 등이 있다. 다른 오브젝트 파일과 링크 관계가 없는 전역, 파일 스코프의 오브젝트나 함수는 내부 링크가 있다고 한다. 이외에는 링크가 없다고 한다.

정적 저장소 기간은 저장소 클래스 static으로 변수를 선언하는 것과 같지 않다. static은 변수나 함수를 내부 링크로 강제시키는 역할을 한다. 이러한 변수는 전역 스코프에 정의될 수 있고 블록 스코프에 정의될 수도 있다. 할당 저장소 기간은 오브젝트가 할당되는 시점부터 소멸되는 시점까지의 생애 주기를 갖는다.

13.2.1. Static storage duration.

정적 저장소 주기를 갖는 오브젝트는 2가지 방법으로 정의되며, 전체 프로그램 실행 기간 동안의 생애 주기를 갖는다.

  • 파일 스코프 내에 정의된 오브젝트. 변수나 복합 리터럴.
  • 함수 블록 내에 static으로 정의된 변수.
double A = 37;
double* p = &(double){1.0, };
int main(void) {
    static double B;
}

위의 예에서는 A, p, B, 복합 리터럴 이렇게 4개의 정적 저장소 주기 오브젝트가 정의된다. 정적 저장소 주기 오브젝트는 컴파일 타임 내지는 프로세스 시작 과정에 의해 결정되는 표현식에 의해서만 명시적으로 초기화될 수 있다.

Takeaway 2.13.2.7. 정적 저장소 주기를 갖는 오브젝트는 항상 초기화된다.

p의 초기화는 다른 오브젝트의 주소를 사용하는데, 이런 주소는 실행이 시작되는 시점에만 계산될 수 있다. 이것이 대부분의 C 구현체가 링커가 필요한 이유이다. B의 예에서 보듯, 정적 저장소 주기를 갖는다고 해서 가시 범위가 프로그램 전체일 필요는 없다. 또한 extern 예에서 보듯, 다른 곳에 정의된 정적 저장소 주기 오브젝트를 참조할 수도 있다.

13.2.2. Automatic storage duration.

자동 저장소 주기를 갖는 오브젝트는 다음의 종류가 있다.

  • static으로 선언되지 않은, auto(기본) 또는 register로 선언된 블록 스코프 변수.
  • 블록 스코프 복합 리터럴.
  • 함수 호출에 의해 반환된 임시 오브젝트.

Takeaway 2.13.2.8. 가변 길이 배열이나 임시 오브젝트가 아니라면, 자동 주기 오브젝트는 정의된 블록에 대응하는 생애 주기를 갖는다.

즉 대부분의 지역 변수는 그들이 정의되는 스코프에 프로그램 실행이 진입할 시 생성되고 그 스코프를 빠져나갈 시 소멸한다. 재귀 호출로 인해 같은 오브젝트의 여러 인스턴스가 동시에 존재할 수 있다.

Takeaway 2.13.2.9. 재귀 호출은 자동 주기 오브젝트의 새 지역 인스턴스를 만든다.

자동 주기 오브젝트는 최적화에 도움이 된다. 컴파일러는 변수의 용례를 조사하고 에일리어싱되는지를 판단할 수 있다. 이것이 auto와 register의 차이를 결정한다.

Takeaway 2.13.2.10. register로 선언된 변수는 & 연산자로 주소를 취할 수 없다.

Takeaway 2.13.2.11. register로 선언된 변수는 에일리어싱할 수 없다.

이 최적화 기법은 배열이 아닌 변수들과 배열을 포함하지 않는 변수들에 대해 잘 적용된다.

Takeaway 2.13.2.12. 성능에 직결되는 배열이 아닌 지역 변수를 register로 선언하라.

Takeaway 2.13.2.13. 배열을 register로 선언하는 것은 의미가 없다.

struct demo {unsigned ory[1];};
struct demo mem(void);

printf("mem().ory[0] is %u\n", mem().ory[0]);

함수의 반환값은 일반적으로 주소값을 취할 수 없으나 리턴 타입이 배열을 포함하고 있으면 그 주소를 명시적으로 취할 수 있어야 하므로 [] 연산자가 잘 정의된다. 따라서 위의 예에서는 함수가 임시 오브젝트를 리턴함에도 불구하고 그 주소값을 취할 수 있다. C에서 임시 오브젝트가 존재하는 이유는 오직 이것 때문이다. 이외의 용도로 쓰지 말라.

Takeaway 2.13.2.14. 임시 주기 오브젝트는 읽기 전용이다.

Takeaway 2.13.2.15. 임시 주기는 그를 포함한 표현식이 끝날 때 끝난다.

13.3. Digression: using objects “before” their definition.

자동 주기 오브젝트의 생애 주기는 프로그램의 실행이 그 오브젝트의 정의문을 마주쳤을 때가 아니라 그 정의문이 속한 스코프의 시작점부터 시작된다.

void fgoto(unsigned n) {
    unsigned j = 0;
    unsigned* p = 0;
    unsigned* q;
AGAIN:
    if (p) printf("%u: p and q are %s, *p is %u\n", j, (q == p) ? "equal" : "unequal", *p);
    q = p;
    p = &((unsigned){j, });
    ++j;
    if (j <= n) goto AGAIN;
}

위의 코드의 goto 문은 라벨 AGAIN으로 점프하는 명령문이다. fgoto(2)를 호출하면 실행 결과는 다음과 같다.

1: p and q are unequal, *p is 0
2: p and q are equal, *p is 1

j == 2일 때 p와 q는 같은 복합 리터럴의 주소를 가리킨다. *p의 평가가 정의문 앞에 옴에도 불구하고.

Takeaway 2.13.3.1. 가변 길이 배열이 아닌 오브젝트에 대해, 생애 주기는 정의문의 스코프에 진입할 때 시작하고 그 스코프를 빠져나올 때 끝난다.

Takeaway 2.13.3.2. 자동 오브젝트와 복합 리터럴의 초기화자는 정의문이 실행될 때마다 평가된다.

Takeaway 2.13.3.3. 가변 길이 배열의 생애 주기는 정의문과 조우했을 때 시작하고 그 가시 스코프를 빠져나올 때 끝난다.

따라서 가변 길이 배열에 대해서는 위의 goto 트릭은 사용할 수 없다. 가변 길이 배열은 그 크기가 런타임에 결정되기 때문에 이런 특성을 가진다.

13.4. Initialization.

Takeaway 2.13.4.1. 정적 또는 스레드 저장 주기를 갖는 오브젝트는 0으로 초기화된다.

Takeaway 2.13.4.2. 자동 또는 할당 저장 주기를 갖는 오브젝트는 초기화되어야 한다.

초기화자를 사용하는 게 최선이지만, 초기화자를 사용할 수 없는 가변 길이 배열이나 동적 할당 배열은 함수를 통해 체계적으로 초기화하라.

Takeaway 2.13.4.3. 자료형에 대한 초기화 함수를 체계적으로 제공하라.

13.5. Digression: a machine model.

전통적인 컴퓨터 아키텍쳐는 폰 노이만 모델을 따른다. 프로세싱 단위는 유한 개의 하드웨어 레지스터가 있어 정수를 저장할 수 있고 선형으로 주소를 지정할 수 있는 메인 메모리가 있고 이 컴포넌트에 수행할 수 있는 연산인 명령 집합이 있다. CPU가 이해할 수 있는 명령문을 표현하는 중간 단계 프로그래밍 언어를 어셈블러라 한다. 어셈블러는 CPU, 컴파일러, OS에 따라 다르다.

.LC0:
        .string "equal"
.LC1:
        .string "unequal"
.LC2:
        .string "%u: p and q are %s, *p is %u\n"
fgoto:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $48, %rsp
        movl    %edi, -36(%rbp)
        movl    $0, -4(%rbp)
        movq    $0, -16(%rbp)
.L2:
        cmpq    $0, -16(%rbp)
        je      .L3
        movq    -16(%rbp), %rax
        movl    (%rax), %edx
        movq    -24(%rbp), %rax
        cmpq    -16(%rbp), %rax
        jne     .L4
        movl    $.LC0, %esi
        jmp     .L5
.L4:
        movl    $.LC1, %esi
.L5:
        movl    -4(%rbp), %eax
        movl    %edx, %ecx
        movq    %rsi, %rdx
        movl    %eax, %esi
        movl    $.LC2, %edi
        movl    $0, %eax
        call    printf
.L3:
        movq    -16(%rbp), %rax
        movq    %rax, -24(%rbp)
        movl    -4(%rbp), %eax
        movl    %eax, -28(%rbp)
        leaq    -28(%rbp), %rax
        movq    %rax, -16(%rbp)
        addl    $1, -4(%rbp)
        movl    -4(%rbp), %eax
        cmpl    -36(%rbp), %eax
        ja      .L7
        jmp     .L2
.L7:
        nop
        leave
        ret
main:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    $2, %edi
        call    fgoto
        movl    $0, %eax
        popq    %rbp
        ret

위의 코드는 fgoto 예제에 대한 어셈블러 출력의 예시이다. 이러한 어셈블러는 하드웨어 레지스터와 메모리 위치에 대한 명령문으로 동작한다. movl $0, -16(%rbp)는 %rbp 레지스터의 16바이트 아래에 0을 저장하라는 뜻이다. 어셈블러에는 프로그램의 특정 부분을 식별하는 라벨도 존재한다. fgoto:는 프로그램의 진입 지점이고 .L2:는 C 코드의 AGAIN: 부분이다.

하드웨어 레지스터를 여러 개 쓰고 있지만 %rbp (베이스 포인터)와 %rsp (스택 포인터)는 특별하다. 함수는 메모리의 지정된 구역 스택에 위치하고 이는 지역 변수와 복합 리터럴을 저장한다. 이 스택의 위쪽 끝은 %rbp 레지스터로 지정되어 있으며 변수는 이 레지스터에 대한 음수 오프셋으로 참조된다. 변수 n은 -36 오프셋으로 참조됨을 알 수 있다.

시작 시점에서 fgoto는 환경을 세팅하기 위한 3개의 명령문을 실행한다. %rbp를 세팅하고, %rsp의 값을 %rbp에 대입하고, %rsp를 48만큼 감소시킨다. 여기서 48은 컴파일러 판단 하에 필요한 모든 자동 주기 오브젝트를 저장하는 데 필요한 바이트 수이다. 이 공간들은 초기화되지 않고 쓰레기 값으로 차 있지만, n, j, p의 3개의 자동 주기 오브젝트는 명시적으로 초기화된다.

세팅 이후에 %rsp는 호출된 함수가 쓸 수 있는 새 메모리 지역을 가리키게 된다. 이는 printf를 호출하고 인자를 저장하기 위해 %edi, %esi, %ecx, %rdx 레지스터를 받고 %eax를 청소한 뒤 함수를 호출한다.

이로부터 결론지을 수 있는 것들은 다음과 같다:

  • 자동 주기 오브젝트는 함수나 스코프의 시작 지점부터 사용 가능하다.
  • 자동 주기 변수의 초기화는 강제되지 않는다.

위의 어셈블러 출력은 컴파일러 최적화가 적용되지 않은 것이다. 최적화할 경우 출력의 예는 다음과 같다.

.LC0:
        .string "unequal"
.LC1:
        .string "%u: p and q are %s, *p is %u\n"
.LC2:
        .string "equal"
fgoto:
        pushq   %r12
        pushq   %rbp
        pushq   %rbx
        subq    $16, %rsp
        movl    $0, 12(%rsp)
        testl   %edi, %edi
        je      .L1
        movl    %edi, %r12d
        xorl    %eax, %eax
        leaq    12(%rsp), %rbp
        movl    $1, %ebx
        jmp     .L3
.L10:
        movl    %ebx, %esi
        movl    $.LC1, %edi
        xorl    %eax, %eax
        call    printf
        movl    %ebx, 12(%rsp)
        addl    $1, %ebx
        cmpl    %r12d, %ebx
        ja      .L1
        movq    %rbp, %rax
.L3:
        leal    -1(%rbx), %ecx
        movl    $.LC0, %edx
        cmpq    %rbp, %rax
        jne     .L10
        movl    $.LC2, %edx
        jmp     .L10
.L1:
        addq    $16, %rsp
        popq    %rbx
        popq    %rbp
        popq    %r12
        ret
main:
        subq    $8, %rsp
        movl    $2, %edi
        call    fgoto
        xorl    %eax, %eax
        addq    $8, %rsp
        ret

컴파일러가 코드를 완전히 재구성했다. 이 코드는 원본 C 코드의 효과를 재현할 뿐이다. 그러나 포인터를 비교하지도 않고, 복합 리터럴의 주소를 추적하지도 않는다. j = 0인 경우는 실행되지 않기 때문에 아예 코드를 생성하지도 않는다. j = 1일 때에는 p, q가 다름이 확정되었으므로 이를 가정하고 printf를 실행한다. 그 이후부터 j를 증가시키고 일반적인 케이스의 printf를 실행한다.

위의 코드는 가변 인자 배열을 사용하지 않았다. 가변 인자 배열을 쓰면 이야기가 달라지는데, 이 때에는 가변 인자 배열을 저장할 메모리를 컴파일 타임이 아닌 런타임에 계산해야 하기 때문에 필요한 메모리를 미리 계산해 둘 수 없다. 이 경우 %rsp를 가변 인자 배열의 생성 시점에 재조정하고 가변 인자 배열의 스코프가 끝날 시 %rsp의 재조정이 원복된다.

요점 정리

  • 오브젝트는 동적으로 할당되고 소멸될 수 있다. 이 오브젝트의 할당/소멸은 직접 주의 깊게 추적해야 한다.
  • 식별자 가시성과 저장소 주기는 다르다.
  • 초기화는 타입에 맞게 체계적으로 이루어져야 한다.
  • C의 지역 변수 할당은 함수 스택의 저수준 핸들링에 잘 맞춰져 있다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중