문제
split_lines() 는 줄 포인터들을 '지역 배열' parts[] 에 모은 뒤, 그 배열의 주소를 LineView.lines 에 담아 돌려준다. 함수가 끝나면 parts[] 가 있던 스택 프레임은 무효가 되고, 이어서 호출되는 warm_stack() 이 그 자리를 다른 값으로 덮는다. 그 뒤 v.lines[i] 를 읽으면 '덮인 쓰레기'를 포인터로 해석해 역참조 → SIGSEGV. (v.lines 자체는 유효한 스택 주소지만, 그 안의 내용이 이미 오염됐다는 점이 함정)
Jungle-C-DebuggingLab/challenges/07_stack_use_after_return/bug.c at main · yeooonee/Jungle-C-DebuggingLab
Krafton jungle week 5 debugging lab. Contribute to yeooonee/Jungle-C-DebuggingLab development by creating an account on GitHub.
github.com
문제풀이
문제는 주석 아예 없이, 너무 막힐 때 claude 도움 조금 받아서 풀었다.
먼저 run 후 bt 를 찍어봤을 때, 아래 코드에서 Program received signal SIGSEGV, Segmentation fault. 에러가 발생했다. 해당 에러는 허용되지 않은 메모리에 접근했을 때 발생하는 에러이므로, 메모리 관련 문제가 일어났구나 짐작했다.
checksum += (unsigned char)v.lines[i][0];

일단 gdb 로 계속 디버깅해서 어떻게 오류가 발생하는지 확인했다.
일단 코드를 살펴봤는데, checksum += ... 코드 위에 있던건 split_lines 와 warm_stack 이었다. warm_stack 을 지우면 정상 동작하는 것 같아서 이 코드의 역할이 무엇인가 계속 생각했는데, 그냥 오류를 명시적으로 보여주려고 넣은거더라.
split_lines 에서 실질적인 역할을 진행한다. 과거에 정리했던 내용을 보면, 함수의 호출순서와 수명이 맞지 않는 데이터들이 존재한다. 현재의 이 parts 데이터가 그러한 데이터이다. 이에 대해 생각한 방법은 2가지이다.
- 함수 바깥으로 꺼내서 값을 준 다음 return 하거나
- malloc 을 사용해 함수가 끝나고도 살아있게 만들기.
2번을 선택해 진행하기로 했다. 근데 원래는 split_lines 함수 안에서 호출하고 바깥에서 free 해주려고 했는데, main 에 함수를 전달하는 방법을 몰라서 main 에서 malloc 을 만들고 전달했다.
malloc을 main 에서 선언하고 malloc에 값을 넣도록 인자를 하나 추가했다.
static void split_lines(LineView *out, char *text, char **parts) { // split_lines 내부에서 스택이 사용돼서 반환이 되지 않고 있다.
// ....
}
int main(void) {
// ...
char **parts = malloc(sizeof(char *) * MAX_LINES);
// ...
free(parts);
// ...
}
이 변수 생명주기에서는 큰 어려움이 없었는데.. 타입 관련부분에서 너무 어려웠다 .
char **parts = malloc(sizeof(char *) * MAX_LINES);
처음에는 char *parts 로 선언했는데, 계속 에러가 나서 이유를 몰랐다. 여기서는 고민해보다가 안되겠어서 claude 도움을 받았는데, 타입의 문제라고 했다. 대충 짐작가는건 내가 추가한 parts 뿐이고, 여전히 이유를 모르겠고 ...
암튼 이것도 열심히 때려맞추고 사후적으로 원인을 파악했다.
char **parts = malloc(sizeof(char *) * MAX_LINES);
- **parts : split_lines 를 보면 각 parts 칸에 주소를 넣고 있기 때문에 ** 로 선언함
- MAX_LINES: 위에서 선언해준 값 8
- sizeof (char *) : char * 의 사이즈/ 칸 하나의 크기
결론은 parts 의 각 칸의 값이 *ln (주소값) 으로 들어가기 때문에 그것을 가리키는 parts 는 ** 로 선언해줘야한다는 것이었다.
static void split_lines(LineView *out, char *text, char **parts) { // split_lines 내부에서 스택이 사용돼서 반환이 되지 않고 있다.
// char *parts[MAX_LINES];
for (char *ln = strtok(text, "\n"); ln && n < MAX_LINES; ln = strtok(NULL, "\n"))
parts[n++] = ln;
view_set(out, parts, n);
}
int main(void) {
char text[] = "alpha\nbeta\ngamma";
LineView v;
char **parts = malloc(sizeof(char *) * MAX_LINES);
split_lines(&v, text, parts);
warm_stack();
long checksum = 0;
for (int i = 0; i < v.count; i++){
// if (*v.lines == NULL) continue;
checksum += (unsigned char)v.lines[i][0];
}
free(parts);
printf("lines = %d, checksum = %ld\n", v.count, checksum);
return 0;
}
어려웠던 점
- 스택/힙의 생명주기
- 포인터 타입