[C] 지역 변수는 스택 프레임에.

JM·2026년 9월 20일

디버깅 랩

목록 보기
2/2

문제 상황

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;
}

코드 분석

  1. text = "alpha\nbeta\ngamma"배열에 저장.
  2. split_lines(&v, text)함수로 LineView타입 구조체 v에 alpha, beta, gamma각각 저장.
    (warm_stack)함수 일단 제외
  3. v를 순회하면서 각 라인의 첫 번째 글자v.lines[i][0]을 checksum에 더함.
    3-1. 첫 번째 글자는 각각 a, b, c이므로 ASCII코드를 참고하면
    97 + 98 + 103 = 298

예상 결과

실제 결과


배경 지식

C언어 메모리 관리

스택 프레임


코드 흐름 상 문제?

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의 첫 번째 인덱스의 주소다.

디버깅으로 한 단계씩 알아보자.

  1. main 함수에 선언된 text배열의 주소는 deb0이다.

나중에 view_set함수에 저장되는 값도 위와 같아야 한다.

  1. split_lines함수에 선언된 parts배열의 주소는 de30이다.

  2. 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언어에 조금은 더 가까워진 것 같아서 만족!

profile
개발자 지망생

0개의 댓글