Strings, Structs, and Gotcha's


So what's a string?

C에서는 역사적인 이유로 길이가 고정된 문자열보다 Null로 끝나는 문자열을 사용합니다. 즉, 프로그래밍에서 사용하는 모든 문자열에는 널 문자가 포함되어 있다는 것으 ㄹ기억해야 합니다. C에서 string이란 '\0'나 null byte를 만나기 전까지 바이트의 묶음입니다.

Two places for strings

constant string을 선언하면(char* str = "constant"와 같이) 이 문자열은 읽기만 가능한 code(data) segment에 저장됩니다. 이러한 문자열을 수정하려고 시도하면 segmentation fualt가 발생합니다.
또 다른 방식으로는 malloc으로 선언해 heap에 저장할 수가 있는데 heap 영역은 수정이 가능합니다.

Memory Mismanagement

다음과 같은 프로그램을 쓸 때 흔히 일어나는 실수를 살펴보겠습니다.

char* hello_string = malloc(14);
                       ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
// hello_string ----> | g | a | r | b | a | g | e | g | a | r | b | a | g | e |
                       ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾
hello_string = "Hello Bhuvan!";
// (constant string in the text segment)
// hello_string ----> [ "H" , "e" , "l" , "l" , "o" , " " , "B" , "h" , "u" , "v" , "a" , "n" , "!" , "\0" ]
                       ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
// memory_leak -----> | g | a | r | b | a | g | e | g | a | r | b | a | g | e |
                       ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾
hello_string[9] = 't'; //segfault!!


위 코드를 잘 살펴보면, 우선 맨 처음 라인에서 heap에 14바이트를 할당해 그 주소값을 hello_string으로 가리키고 있습니다.

하지만 그 다음 라인에서 hello_string이 가리키는 주소값은 code segment에 read-only로 존재하는 "Hello Bhuvan!"이라는 문자열의 시작 주소값이 됩니다. 또한 이전에 heap에 할당한 14바이트는 더이상 접근이 불가능해지므로 memory leak이 발생하게 됩니다.

세번째 라인에서는 이 read-only 메모리에 접근해 write를 시도하고 있기 때문에 segmentation falut가 발생하게 됩니다.


이러한 경우를 방지하려면 두 번째 라인에서 주소값을 바꾸는 것이 아니라 다음과 같이 메모리를 복사해 와야 합니다.


strcpy(hello_string, "Hello Bhuvan!");


Remember the NULL byte!

String의 끝이 항상 null byte라는 것을 잊지 맙시다. 항상 boundary check는 중요합니다!! wiki book에 있는 heart bleed bug는 부분적으로 이러한 일 때문에 일어났습니다.



Where can I find an In-Depth and Assignment-Comprehensive explanation of all of these functions?

string에 관련된 Linux man page를 통해 알아봅시다
https://linux.die.net/man/3/string

String Information/Comparison: strlen strcmp

int strlen(const char *s)는 null byte를 제외한 문자열의 길이를 반환해줍니다.

int strcmp(const char *s1, const char *s2)는 두 문자열의 사전적 정의 순서에 따라 정수를 반환합니다. 만약 s1이 s2보다 사전에서 먼저 나온다면  -1이 반환됩니다. 두 문자열이 일치한다면 0이 반환됩니다. 사전에서 s1이 s2보다 늦게 나온다면 1이 반환됩니다.

이러한 대부분의 함수들은 문자열이 읽을 수 있고 NULL이 아니라고 가정하기 때문에 NULL을 parameter로 넘겨주면 예상되지 않은 동작이 발생할 수 있습니다.


String Alteration: strcpy strcat strdup

char *strcpy(char *dest, const char *src)는 src의 문자열을 dest로 복사합니다. 이때 dest는 src를 복사하기에 충분한 사이즈라고 가정합니다.

char *strcat(char *dest, const char *src)는 dest 문자열끝에 src를 붙여넣습니다. 이 때 dest의 맨 뒤에는 src의 NULL byte를 포함해 붙여넣기에 충분한 크기가 있다고 가정합니다.

char *strdup(const char *dest)는 malloc으로 할당한 곳에 dest를 복사하여 할당된 주소값을 반환합니다. 반환된 값을 free시켜주는 것을 잊지 맙시다.

String Search: strchr strstr

char *strchr(const char *haystack, int needle)은 처음으로 needle이 발견되는 포인터를 반환해줍니다. 만약 needle이 없다면 NULL이 반환됩니다.

char *strstr(const char* haystack, const char *needle)은 위와 같지만 이번엔 needle이 문자가 아니라 string입니다.


String Tokenize: strtok

string을 tokenize해주는 strtok 함수는 매우 유용하지만 위험한 함수입니다. tokenize는 문자열을 적절하게 잘라주는 것을 의미합니다. 이 함수는 많은 사양을 가지고 있으므로 man page를 읽어보십시오. 


#include <stdio.h>
#include <string.h>

int main(){
    char* upped = strdup("strtok,is,tricky,!!");
    char* start = strtok(upped, ",");
    do{
        printf("%s\n", start);
    }while((start = strtok(NULL, ",")));
    return 0;
}

위와 같은 코드는 다음과 같은 결과를 출력할 것입니다.

strtok
is
tricky
!!

strtok는 upped 문자열을 ,를 기준으로 잘라 반환하고 있습니다.

upped가 다음과 같은 경우에도 같은 결과값을 출력합니다.
char* upped = strdup("strtok,is,tricky,,,!!");


Memory Movement: memcpy and memmove

memcpy와 memmove 모두 string.h에 존재할까요? 사실 string은 끝이 null byte인 메모리들이기 때문입니다. 
void *memcpy(void *dest, const void *src, size_t n)은 src에서 시작해서 n바이트만큼 dest에 복사합니다. 이 때 src와 dest의 메모리 영역이 중복되면 의도치 않은 동작이 일어날 수 있습니다. 이것은 machine에서 일어나는 클래식한 동작중 하나이므로 valgrind로는 잡아낼 수 없습니다. 이것의 더 안전한 버전을 고려해보십시오.


- This is one of the classic works on my machine examples because many times valgrind won't be able to pick it up because it will look like it works on your machine. When the autograder hits, fail. Consider the safer version which is.- 해석이 어렵습니다..

void memmove(void *dest, const void *src, size_t n)은 위와 같은 동작을 하지만 메모리 영역이 겹쳤을 때에도 모든 바이트가 정확히 복사되는 것을 보장합니다.













'Angrave System Programming > Learning C' 카테고리의 다른 글

Debugging(1)  (0) 2019.01.09
Strings and Structs(2)  (0) 2019.01.09
Common Gotchas(3)  (0) 2019.01.09
Common Gotchas(2)  (0) 2019.01.09
Common Gotchas(1)  (0) 2019.01.09
Posted by 몰랑&봉봉
,