👩‍💻Developer/Language

[C/C++] C언어의 Memory model

블루누들 2022. 11. 29. 03:04

* 해당 글은 학과 수업으로 배운 내용과 코드가 포함되어 있으며 개인적 공부 목적으로 업로드하였습니다.

C언어에서의 array

5개의 element를 가진 int array를 제작한다고 할때 우리는 아래와 같이 쓸수있다.

int* A = malloc(sizeof(int)*5);

C언어에서 array와 pointer는 같은것이다.

실제 array는 5개의 int로 heap에 space가 존재하며 이것의 address를 return하게 된다.

array를 access하는 것은 c0과 동일하다.

A[1] = 7;

A[2] = A[1]+5;

 

Pointer Arithmetic

만약 A가 포인터라면 *A는 valid하다.

이때 A가 int*이기 때문에 *A는 int type이다. 

따라서 *A는 A[0]와 같다.

ex) *A = 42 는 A[0]=42와 같다.

 

A는 첫 element의 주소가 된다.

다음 element의 주소는 어떻게 알수있을까?

그것은 int하나만큼 다음 위치 즉 A+1이 될것이다.

이것을 우리는 pointer arithmetic이라고 한다.

 

A+i는 A[i]의 address가 된다. 따라서 *(A+i)는 A[i]의 element이다. 

ex) 우리가 A[1]을 7로 지정했다면 printf("A[1] is %d\n", *(A+1))에서 7을 print할것이다.

 

Pointer arithmetic은 C언어에서 에러가 생기기 가장 쉬운 것중 하나이다. 

 

Initializing memory

(x)malloc은 default value를 메모리에 initialize하지 않기 때문에 우리가 default value로 initialize하고싶다면 우리는 calloc을 써야한다.

int* A = calloc(5, sizeof(int));

-calloc은 가능한 memory가 없다면 NULL을 return한다.

 

이렇게 A가 allocated memory로 형성되었다면 프로그램이 끝나기전에 우리는 이것을 free해야한다

 

C0에서는 오직 contract에서만 array의 길이를 알수 있다. 하지만 C언어에서는 array의 길이를 알수가 없다. 

하지만 free를 통해서 OS에 얼마만큼의 memory를 줘야하는지 알수 있다. 

 

Array에 대해서 총 정리하자면

Arrays in C Arrays in C0
Array는 pointer이다. Array는 특별한 타입을 가진다.
(x)malloc(elements가 initialize되지 않는다)이나 (x)calloc(elements가 initialize된다)으로 만들어진다. alloc_array를 통해서 만들어진다.
free되어야 한다. garbage collected 된다.
길이를 알 수 없다. contracts에서 길이를 알수 있다. 

 

Out of Bound Accesses

만약 5개의 elements를 가진 array에서 A[5]를 아래와 같이 부른다면 어떻게 될까

printf("A[5] is %d\n", A[5]);

C0에서는 safety violation이지만 C에서는 *(A+5)가 된다.

이는 매번 compile할때마다 다른 결과를 가져올 수 있다. 

이는 valgrind에서 invalid write/read로 에러가 뜨게 된다. 

만약 access한 index가 heap memory 밖이라면 segmentation fault가 발생한다.

 

따라서 out of bound accesses를 우리는 undefined behavior라고 부른다.

-compile할때마다 다른 결과를 가져온다.

 

C0에서 우리가 safety violation이라고 여겼던 것들이 C에서는 undefined behavior가 된다. 

-out of bound인 element를 access하는 것

-NULL을 dereference하는 것 

뿐만 아니라 C언어에서는 더 많은 undefined behavior가 있다.

-unintialized memory를 읽으려고 할때

-이미 free 시킨 memory를 사용하는 것 

등등

 

Aliasing 

int* B = A+2;

-> B는 A[2]의 주소를 가지고 있다. 

하지만 B는 int*이다. 즉 int array라는 뜻이다. 

위 사진을 보면 새롭게 aliasing이 된것을 볼 수가 있다. 

하지만 우리는 B를 free할 수는 없다. 

-(x)malloc이나 (x)calloc으로 만들어지지 않았기때문에 B를 free하는 것은 undefined behavior가 될것이다.

 

Casting Pointers

C1에서 우리는 어떠한 pointer도 void*로 cast할 수 있었다. 

C에서는 어떠한 pointer도 어떠한 pointer로 cast할 수 있다. 

예를 들어 위의 int* A를 아래와 같이 cast할 수 있다.

char*C = (char*)A;

-이때 char가 1 byte이고 int가 4 bytes이기 때문에 한 int에 char 4개가 들어가게 된다.

예를 들어 C[16]이 A[4]의 첫 byte가 된다. 

만약 A[4]가 1이라면 

1 == 0x00000001에서 

첫 두 0이기 때문에 C[16]이 0이 된다.

 

단 casting할때 undefined behavior가 생길 수 있어 void* 혹은 char*로 cast 하는 것이 좋다.

 

C1에서 void*는 어떤 타입의 pointer도 나타낼 수 있다.

C에서는 void*는 void타입의 array이다. 

-하지만 void type은 실제로 존재하지 않기 때문에 이것은 어떠한 종류의 array의 첫 element의 주소를 나타낸다고 볼 수 있다.

 

우리는 이를 이용해서 generic array를 작성할 수 있다.

-void* casting 하고

-element size를 정하고

-element 갯수를 정한다.

ex) generic sort function

void sort(void* A, int elem_size, int num_elem, compare_fn*cmp);

 

Stack Allocation

C0에서는 array는 오직 heap에서만 지낼 수 있다.

하지만 C에서는 stack에 array를 만드는 것도 가능하다.

-이것을 stack-allocated arrays라고 부른다.

 

예를 들어 int E[8]은 8 element int array를 stack에 만드는 것이다.

안의 element들은 일반적인 array notation을 통해 접근 가능하다

 

또한 stack-allocated array는 array literals로도 나타낼 수 있다.

int F[] = {2,4,6,8,3};

-이것은 5개의 int element array를 stack에 정의한것이다.

 

비슷하게도 stack에 struct 을 allocate하는 것도 가능하다

struct point q = {.x = 15, .y=122};

 

stack-allocated struct은 pointer가 아니기 때문에 그 안의 fields들은 무조건 dot notation을 이용해서 접근해야한다.

-p.x = 9;

p.y = 7;

printf("p is (%d, %d)\n", p.x, p.y);

 

stack-allocated array와 struct의 공간은 이들을 처음 allocate 한 함수를 exit할때 reclaim하기 때문에 free할 필요가 없다.

이때문에 일반적인 data structure는 사용될 수 없다.

 

 

Memory Addresses

C1에서는 &이 오직 함수 이름에서만 사용될 수 있지만 C에서는 memory address를 가진 모든 것에서 사용할 수 있다.

-함수도 될수있고 일반 변수도 될수있고 struct의 field도 될수있고 array element도 될 수 있다. 

아래 함수에 따라 memory address를 어떻게 활용하는지 살펴보자

void increment(int* p){
  REQUIRES(p!=NULL);
  *p = *p+1;
}

 

local variable

ex) int i = 11;

 increment(&i);

-> 이때 i는 이제 12가 되었다.

 

struct의 field

ex) increment(&p.y);

struct point *q = calloc(1, sizeof(struct point));

increment(&(q->y));

-> 이때 p.y는 1 올라가고 q->y도 1만큼 올라가게 된다.

 

array elements

ex) increment(&A[3]);

increment(&F[2]);

->A[3]와 F[2]의 value를 1씩 올린다.

 

pointer arithmetic을 사용한 모든 코드는 이렇게 아래와 같이 바꿀 수있다.

*(A+i) -> A[i]

A+i -> &A[i]

 

단 &을 아래와 같이 쓰면 안된다.

- &(i+2)

-&(A+3)

-&&i

 

int* bad(){
  int a = 1;
  return &a;
}

 

위 코드의 경우 return할시에 deallocate될 stack value의 주소를 가져오고 있다.

따라서 이것은 큰 문제가 될것이므로 이 점 역시 주의해야한다.

 

Strings

C에는 string type이 없고 string은 char*타입의 character array이다

만약 char* s1 = "hello"가 있다면

printf("%c%c%c%c%c\n", s1[0],s1[1],s1[2],s1[3],s1[4]);
printf("%s\n", s1);

은 같은 결과를 가져온다.

 

하지만 printf에게 character print를 언제 멈추라고 말할 수 있을까?

string의 끝은 NUL character로 나타낼 수 있으며 '\0'로 쓰인다.

 

따라서 s1은 6개의 character를 가진 array로써 s1[5]는 '\0'이 된다.

 

C의 <string> library에 이러한 string을 활용할 수 있는 유용한 함수들이 포함되어있다.

-strlen은 string의 character 개수를 나타낸다.

ex) strlen(s1)==5;

-strcpy(dst, src)함수는 src에 있는 모든 character들을 dst로 옮긴다.

-NUL character가 포함되어있고 dst가 src+ NUL을 모두 저장할 수 있을만큼 커야한다.

 

 

String은 총 3가지 곳에 머무를 수 있다. 

1) DATA segment

-이곳에 머무르는 string은 read-only이다.

따라서 s1이 DATA segment에 있다면 s1[0] = 'm';은 undefined behavior가 된다.

또한 free하는 것 역시 undefined behavior이다.

 

2) heap

char* s2 = xmalloc(strlen(s1)+1);

strcpy(s2, s1)

s2[0]='Y';

free(s2)

 

우리는 NUL terminator를 위해서 extra character를 allocate해야하고 이것을 또한 free할 필요가 있다.

 

3) stack

char s3[] = "world";

char s4[] = {'s','k','y', '\0'};

-이렇게 array literals가 stack에 있을 경우 우리는 꼭 nul terminator를 포함시켜야 하며 free할 필요가 없다.

 

정리하자면

  Writable? Allocation Deallocation
DATA No Automatic N/A
Stack Yes Automatic Automatic
Heap Yes Manual Manual

Undefined behavior 정리

-non-allocated memory에 읽거나쓰려고 하는것

-uninitialized memory를 읽는것

-free한 이후 사용하는것

-double free

-malloc/calloc으로 return되지 않은 memory를 free하는 것

-read-only memory에 새로 쓰려고 하는것