07_stack_use_after_return
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_LINES 8
typedef struct {
char **lines;
int count;
} LineView;
static void view_set(LineView *out, char **arr, int n) {
out->lines = arr;
out->count = n;
}
static void split_lines(LineView *out, char *text) {
char *parts[MAX_LINES];
int n = 0;
for (char *ln = strtok(text, "\n"); ln && n < MAX_LINES; ln = strtok(NULL, "\n"))
parts[n++] = ln;
view_set(out, parts, n);
}
static void warm_stack(void) {
char *scratch[MAX_LINES];
for (int i = 0; i < MAX_LINES; i++)
scratch[i] = (char *)0x4141414141414141ULL;
__asm__ volatile("" :: "r"(scratch) : "memory");
}
int main(void) {
char text[] = "alpha\nbeta\ngamma";
LineView v;
split_lines(&v, text);
warm_stack();
long checksum = 0;
for (int i = 0; i < v.count; i++)
checksum += (unsigned char)v.lines[i][0];
printf("lines = %d, checksum = %ld\n", v.count, checksum);
return 0;
}
text = "alpha\nbeta\ngamma"배열에 저장.split_lines(&v, text)함수로 LineView타입 구조체 v에 alpha, beta, gamma각각 저장.(warm_stack)함수 일단 제외v를 순회하면서 각 라인의 첫 번째 글자v.lines[i][0]을 checksum에 더함.a, b, c이므로 ASCII코드를 참고하면97 + 98 + 103 = 298
split_lines함수는 전달한 문자열을 strtok함수로 잘라 저장하고 있다.
strtok은 문자열을 반환하는 것이 아니라 구분 기준(문제에서는 '\n')으로 잘라 그 주소를 반환한다.
따라서 parts 배열에 저장된 값은 잘린 문자열 각각의 주소(ln에 저장)이고, view_set함수에 인자로 넘겨주고 있다.
v.lines
│
↓
┌──────────────────┐
│ parts[0] │
│ parts[1] │ ← 유효
│ parts[2] │
└──────────────────┘
view_set함수 내부에서는 LineView객체의 **lines변수에 자른 문자열의 주소를 각각 저장하고 있다.
여기까지만 보면 main함수 내에서 split_lines함수가 종료되었을 때 LineView객체에 적절하게 문자열이 저장되었을 것만 같다.
하지만 실제로는 v.lines를 들여다볼 때 SEGFAULT가 발생하고 있다.
split_lines함수에서 선언된 parts배열은 지역변수다.
스택 프레임 공부할 때 배웠던 내용대로라면, 지역변수가 차지하고 있던 공간은 함수가 종료될 때 회수된다. ( 데이터가 없어지는 건 아니지만 사용 권한이 없는 느낌 ).
그런데 view_set함수의 매개변수 arr은 이중 포인터로 선언되어 있고, 해당 함수를 호출할 때 parts자체를 전달하고 있다.
view_set함수에서는 전달받은 arr을 그대로 out->lines에 저장하게 하는데, 이 때 저장하는 것은 arr의 첫 번째 인덱스의 주소다.
디버깅으로 한 단계씩 알아보자.
main 함수에 선언된 text배열의 주소는 deb0이다.나중에 view_set함수에 저장되는 값도 위와 같아야 한다.
split_lines함수에 선언된 parts배열의 주소는 de30이다.
view_set함수에서 out->lines 에 저장한 주소는 de30이다!
이미 상황이 끝났다. 함수가 종료되면 반납할 주소를 저장했으니 out->lines가 dangling pointer가 되었다.
원인을 파악하고 가장 먼저 떠오른 생각은 "parts배열을 반납시키지 않기"였다.
parts변수를 간단히 정적 변수로 만든 것 만으로 쉽게 해결이 가능하다.
( 정적 변수는 Data영역에 저장됨. 위에 링크 참고 )
다음은 LineView구조체의 lines멤버를 배열로 변경하고 strtok의 결과인 주소를 직접 저장하는 방법이다.
함수가 종료되면 회수되는 arr매개변수를 그대로 저장하지 않고, 역참조하여 안에 저장되어 있는 strtok함수의 결과를 배열에 저장하는 방법이 있다.
첫 번째 방법보다는 조금 나아 보인다. static으로 해결하면 댕글링 포인터 문제는 해결되지만, parts배열이 누구의 것인가? 하는 질문이 생길 수 있다.
호출자인 main이 만든 것도 아니고, LineView구조체가 자체적으로 가진 것도 아니기 때문이다. 결국 static은 변수의 lifetime만 해결한 방법이다.
지역 변수 문제를 해결하는 또 다른 방법은 malloc을 이용하는 것이다.
메모리 소유권도 split_lines함수에 있어서 조금은 더 명확하다.
그런데 main함수에 free로 해제해 주어야 한다는 점이 조금 마음에 걸린다. 최대한 main함수는 수정하지 않으려 했기 때문에....
세 가지 방법 중에서는 두 번째 방법이 가장 좋아 보인다. 사실 처음에는 static 처리하는 방법이 깔끔해 보여서 만족했었는데, 메모리가 누구의 것인가? 하는 질문은 처음에 생각하지 못했던 것 같다.
지금까지 main함수는 아예 변경하지 않는 방향으로 문제를 파고 있는데... 이유는 잘 모르겠다. 굳이 말하자면 코드의 설계를 크게 뒤집는 것 같은 느낌이라 그런 것 같다.
이 문제도 풀이 자체는 단순한데, 깊게 파고 들어 이해하려고 하니 알아야 하는 지식도 많고 꽤나 복잡했다. 그래도 일단 C언어에 조금은 더 가까워진 것 같아서 만족!