Rust에서 스택 오버플로우 알아보기

Rust의 스마트 포인터가 애플리케이션을崩溃시키는 방법에 대해 알아보겠습니다.

스택 오버플로우가 발생하는 코드 예제:

struct Node<T> {
    val: T,
    next: Option<Box<Node<T>>>,
}

struct List<T> {
    head: Option<Box<Node<T>>>,
}

impl<T> List<T> {
    fn new() -> Self {
        Self { head: None }
    }

    fn push_front(&mut self, val: T) {
        let next = self.head.take();
        self.head = Some(Box::new(Node { val, next }));
    }
}

fn main() {
    let mut list = List::new();
    for i in 0..1000000 {
        list.push_front(i);
    }
}

실행 결과:

thread 'main' has overflowed its stack
fatal runtime error: stack overflow
timeout: the monitored command dumped core
/playground/tools/entrypoint.sh: line 11:     8 Aborted                 timeout --signal=KILL ${timeout} "$@"

원문에서의 설명은 다음과 같습니다:

애플리케이션이崩溃하는 이유는 List의 스마트 포인터 헤드에서의 기본적인 해제 과정 중 다음 노드에 대한 재귀적인 호출이 발생하며, 이는 테일 재귀가 아니기 때문에 최적화가 불가능합니다. 해결 방법은 List 데이터 구조체의 드랍 메서드를 수동적으로 재정의하여 각 노드를 반복적으로 해제하는 방법입니다. 이는 스마트 포인터의 목적이었던 프로그래머로부터의 수동 메모리 관리를 덜어내는 데 반대합니다.

이해를돕기 위해 Node와 List에 Drop 트레이트를 추가합니다:

struct Node<T> {
    val: T,
    next: Option<Box<Node<T>>>,
}

struct List<T> {
    head: Option<Box<Node<T>>>,
}

impl<T> List<T> {
    fn new() -> Self {
        Self { head: None }
    }

    fn push_front(&mut self, val: T) {
        let next = self.head.take();
        self.head = Some(Box::new(Node { val, next }));
    }
}

impl<T> Drop for Node<T> {
    fn drop(&mut self) {
        println!("드랍 노드 시작");
        let _ = self.next.take();
        println!("드랍 노드 완료");
    }
}

impl<T> Drop for List<T> {
    fn drop(&mut self) {
        println!("드랍 리스트 시작");
        let _ = self.head.take();
        println!("드랍 리스트 완료");
    }
}

fn main() {
    let mut list = List::new();
    for i in 0..1000000 {
        list.push_front(i);
    }
    println!("프로그램 종료");
}

실행 결과:

프로그램 종료
드랍 리스트 시작
드랍 노드 시작
드랍 노드 완료
드랍 노드 시작
드랍 노드 완료
...
드랍 리스트 완료

이 문제를 해결하기 위해 수동적으로 노드를 해제하는 방법을 사용합니다:

impl<T> Drop for Node<T> {
    fn drop(&mut self) {
        println!("드랍 노드 시작");
        let _ = self.next.take();
        println!("드랍 노드 완료");
    }
}

impl<T> Drop for List<T> {
    fn drop(&mut self) {
        println!("드랍 리스트 시작");
        let mut node = self.head.take();
        while let Some(mut inner) = node {
            node = inner.next.take();
        }
        println!("드랍 리스트 완료");
    }
}

실행 결과:

프로그램 종료
드랍 리스트 시작
드랍 노드 시작
드랍 노드 완료
드랍 노드 시작
드랍 노드 완료
...
드랍 리스트 완료

실제 애플리케이션에서 이런 문제를 발견할 때, 스택 오버플로우가 일어난 함수 호출 스택을 확인할 수 있는 방법도 알아두면 유용합니다. 이를 위해 backtrace-on-stack-overflow crate를 사용할 수 있습니다:

fn main() {
    unsafe { backtrace_on_stack_overflow::enable() };
    f(92)
}

fn f(x: u64) {
    f(x)
}

실행 결과 예시:

스택 오버플로우:
   0: backtrace_on_stack_overflow::handle_sigsegv
             at /home/matklad/p/backtrace-on-stack-overflow/src/lib.rs:33:40
   1: <unknown>
   2: main::f
             at src/main.rs:6
   3: main::f
             at src/main.rs:7:5
   4: main::f
             at src/main.rs:7:5
   5: main::f
             at src/main.rs:7:5
   6: main::f
             at src/main.rs:7:5
   7: main::f
             at src/main.rs:7:5
   8: main::f
             at src/main.rs:7:5
   9: main::f
             at src/main.rs:7:5
  10: main::f
             at src/main.rs:7:5

태그: Rust 리자로 백트레이스

6월 9일 20:35에 게시됨