Memory Mistakes
String Constants are Read-Only
char array[] = "Hi!"; // array contains a mutable copy strcpy(array, "OK"); char *ptr = "Can't change me"; // ptr points to some immutable memory strcpy(ptr, "Will not work");
문자열은 프로그램의 변경이 불가능한 Code segment에 저장되어 있는 Character array입니다.
위 코드에서 array는 Code segment에 존재하는 "Hi!"라는 문자열을 stack에 복사합니다. 즉 이 array는 code segment에 존재하는 것이 아니라 stack에 존재하므로 변경이 가능합니다.
반면 "Can't change me"라는 문자열의 주소를 가리키는 ptr은 현재 code segment를 가리키는 주소이므로 변경이 불가능합니다.
같은 문자열은 메모리의 같은 공간을 가리킵니다. 다음의 예로 이 사실을 확인할 수 있습니다.
char *str1 = "Brandon Chong is the best TA"; char *str2 = "Brandon Chong is the best TA"; str1 == str2; // true
str1과 str2가 가리키는 주소가 같은 것으로 보아 같은 문자열은 메모리의 같은 곳에 존재한다는 것을 알 수 있습니다.
반면 char array는 같은 문자열을 stack에 복사해 오는 것이므로 서로 다른 array는 같은 문자열을 저장하고 있더라도 서로 다른 메모리를 차지하고 있을 것입니다.
char arr1[] = "Brandon Chong didn't write this"; char arr2[] = "Brandon Chong didn't write this"; arr1 == arr2; // false &arr1[0] == &arr2[0]; // false
위 코드에서 보듯이 arr1과 arr2는 stack에 각각 잡히는 값이므로 두 주소값은 서로 다릅니다. array의 시작 주소값을 확인해 보면 두 값이 서로 다르다는 것을 확인할 수 있습니다.
Buffer Overflow / Underflow
C에서는 array 접근시에 boundary check가 이루어지지 않습니다.
int i = 10, array[10]; for (; i >= 0; i--) array[i] = i;
위와 같은 코드는 array의 범위를 벗어난 array[10]에 10을 쓰려고 하고 있습니다. 이러한 접근은 stack에 다른 변수를 침범하거나 stack 호출의 수행을 손상시켜 프로그램이 해커의 공격에 노출될 수 있습니다. 사실 이러한 overflow는 아래와 같이 안전하지 않은 library call이나 안전한 library call이라도 크기 제한을 잘못 주어 사용할 경우 발생하기 쉽습니다.
gets(array); // Let's hope the input is shorter than my array! (NEVER use gets) fgets(array, 4096, stdin); // Whoops
위 코드는 array가 10인데 입력받는 길이가 null string을 포함해 10을 넘는다면(9개 이상의 input) buffer overflow가 발생할 것입니다.
Handling Pointers to Out-of-Scope Automatic Variables
int *f() { int result = 42; static int imok; int *p; { int x = result; p = &x; } //imok = *p; // Not OK: x has already gone out of scope //return &result; // Not OK: result will go out of scope after the function returns return &imok; // OK - static variables are not on the stack }
Automatic variables는 변수들이 scope 안에 존재하는 동안만 stack memory에 바운딩되어 있습니다. 이 변수들은 scope 밖으로 벗어나게 되면 그 메모리 주소에 저장되어 있던 데이터는 undefine됩니다. 반면 static variable은 data segment에 남아있으므로 variable들이 scope안에 존재하지 않더라도 안전하게 보호됩니다.
sizeof(type *) vesus sizeof(type)
struct User { char name[100]; }; typedef struct User user_t; user_t *user = (user_t *) malloc(sizeof (user_t *));
위와 같은 코드에서 User 구조체가 저장되기에 충분한 메모리를 할당해야 합니다. 하지만 위이 예에서는 pointer를 저장하기에 충분한 크기만큼의 메모리만 할당되고 있습니다.즉, struct의 크기인 100만큼이 할당되는 것이 아니라 포인터의 크기(32비트라면 4, 64비트라면 8바이트)만큼만 할당되고 있습니다. 이런 동적 할당은 나중에 user 포인터에 data를 썼을 때 할당한 heap의 범위를 넘어가 다른 메모리를 침범할 수 있습니다. 정확한 코드를 짜기 위해서는 아래와 같이 할당해야 합니다.
struct User { char name[100]; }; typedef struct User user_t; user_t *user = (user_t *) malloc(sizeof (user_t));
String Require strlen(s)+1 Byte
모든 문자열은 마지막에 null byte를 포함해야 합니다. 즉, Hi라는 문자열을 저장하기 위해서는 [H] [i] [\0] 라는 세개의 바이트가 필요합니다. 문자열을 저장하기 위한 길이는 실제 문자열의 길이를 반환해주는 strlen에 null character를 위한 1 바이트입니다.
char *strdup(const char *input) { /* return a copy of 'input' */ char *copy; copy = malloc(sizeof (char *)); /* nope! this allocates space for a pointer, not a string */ copy = malloc(strlen(input)); /* Almost...but what about the null terminator? */ copy = malloc(strlen(input) + 1); /* That's right. */ strcpy(copy, input); /* strcpy will provide the null terminator */ return copy; }
Failing to Initialize Memory
stack 변수와 malloc을 통해 할당받은 heap memory는 항상 0으로 초기화되어 있는 것이 아닙니다. 아래와 같이 초기화하지 않고 사용하는 것은 비정상적인 동작을 일으킬 수 있습니다.
void myfunct() { char array[10]; char *p = malloc(10); printf("%s %s\n", array, p); }
Double-free
double free error는 같은 heap memory를 두번 free 시켜줄 때 발생합니다.
char *p = malloc(10); free(p); // .. later ... free(p);
Dangling Pointers
free 되어진 heap memory에 접근해 사용하는 것은 프로그래머가 원하지 않은 동작을 일으킬 수 있습니다.
char *p = malloc(10); strcpy(p, "Hello"); free(p); // .. later ... strcpy(p,"World");
이러한 pointer를 dangling pointer라고 하는데, 이러한 접근을 막기위해서는 free시켜준 뒤 그 포인터가 NULL을 가리키게 하는 습관을 들이는 것이 좋습니다. 포인터가 적절한 위치를 가리키고 있는지 확인할 적절한 다른 방법이 없기 때문입니다. 아래와 같은 매크로 함수를 사용하는 것도 좋습니다.
#define safer_free(p) {free(p); (p) = NULL;}
Forgetting to Copy getline Buffer
#include <stdio.h> int main(void){ char *line = NULL; size_t linecap = 0; char *strings[3]; // assume stdin contains "1\n2\n\3\n" for (size_t i = 0; i < 3; ++i) strings[i] = getline(&line, &linecap, stdin) >= 0 ? line : ""; // this prints out "3\n3\n\3" instead of "3\n\2\n1\n" for (size_t i = 3; i--;) // i=2,1,0 printf("%s", strings[i]); }
위 코드에서, getline은 항상 buffer를 재사용하기 때문에 string array는 모두 같은 메모리를 가리키고 있을 것입니다. 즉 위와 같은 경우 1 2 3 을 연속으로 입력하면 strings는 모두 line이라는 주소값을 가리킬 것입니다. 하지만 이 line은 for문을 반복하면서 처음엔 "1\n" 두번째는 "2\n" 마지막으로 "3\n"이 되므로 결국 line이 가리키는 값은 "3\n"이 되어 strings는 모두 "3\n"을 가리키게 됩니다. 이러한 작업을 방지하려면 아래 코드와 같이 주소값을 가리키지 않고 strings에 직접 값을 복사해 오면 의도한 대로 3 2 1 이 출력될 것입니다.
strings[i] = getline(&line, &linecap, stdin) >= 0 ? strdup(line) : "";
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
※ getline함수를 호출하기 전에 lineptr이 NULL, n이 0으로 세팅되어 있으면 getline은 한 줄 입력을 받기위한 buffer를 할당해줍니다. 이렇게 생성된 버퍼는 getline이 실패하더라도 사용자가 반드시 free해줘야 합니다.(By linux man page)
만약 lineptr이 사용자에 의해 malloc된 메모리이고 n이 할당된 메모리의 크기일 때 입력받은 크기가 할당받은 크기보다 작다면 realloc을 통해 크기를 다시 조절하고 두 인자를 적절하게 업데이트 해줍니다.
두 경우 모두 lineptr과 n은 적절한 주소값과 크기로 각각 업데이트 됩니다.
getline의 리턴값은 입력받은 문자열의 길이입니다. 이 길이는 구분문자를 포함합니다.
Fun fact : 고치기 전 프로그램에서 1\n2\n3\n 대신에 1\n123456789abcdef\n3\n을 입력해 보면 3\n3\n3\n대신에 3\n3\n1\n이 출력될 것입니다. 왜 그럴까요?
실제로 gcc에서 실행해 보니 getline은 두번째 인자인 n의 값이 120씩 증가하고 있었습니다. 그래서 위와 같은 경우에도 정상적으로 3\n3\n3\n이 출력되었습니다. ???문제의 의도는 뭘까요??
문제의 의도는 getline이 1\n을 입력받으면 line은 "1\n"로, linecap은 2로 업데이트 되어 그 다음 123456789abcdef를 받을 때 buffer overflow가 발생한다는 것을 보여주려는 것 같은데... 왜 3\n3\n1\n인지 모르겠습니다...gigacity6/jmjung/angrave/getsline으로 확인
'Angrave System Programming > Learning C' 카테고리의 다른 글
Common Gotchas(3) (0) | 2019.01.09 |
---|---|
Common Gotchas(2) (0) | 2019.01.09 |
Text Input And Output(2) (0) | 2019.01.08 |
Text Input And Output(1) (0) | 2019.01.08 |
Intoduction (0) | 2019.01.04 |