Stack Unwinding

Gunjoo Ahn·2022년 8월 18일
0

예외 처리 코스트가 무거운 이유는 stack unwinding과 같은 작업이 있기 때문이다.

혼자 연구하는 C/C++ 함수와 예외 처리 - 링크 내용 복사입니다.

예외를 던지는 throw 는 보통 try 블록 내부에 있어야 한다. 그러나 함수 안에서는 try 블록없이 throw 만 있을 수도 있다. 이때는 함수를 호출하는 호출원이 try 블록을 가져야 한다. 다음 예제는 0 으로 나누는 함수 divide 를 작성하고 이 함수에서 인수로 전달된 d0 일 때 throw 로 예외를 던진다.

ex. throwfunc

#include <iostream>

void divide(int a, int d){
     if (d == 0) throw "0으로는 나눌 수 없습니다.";
     std::cout << "나누기 결과 = " << a/d << "입니다." << std::endl;
}

void main(){
     try {
          divide(10,0); // #1 Stack unwinding
     }
     catch(const char *message) {
          std::cout << message << std::endl;
     }
     divide(10,5); // #2 Normal
//  divide(2,0); // #3 No try catch, process terminated by default
/*
     try {
          divide(20,0); // #4 Cannot catch, process terminated by default
     }
     catch(int code) {
          printf("%d번 에러가 발생했습니다.\n",code);
     }
*/
}

함수 실행중에 throw 를 만나면 대응되는 catch 를 찾기 위해 자신을 호출한 호출원을 거슬러 올라가야 한다. 첫 번째 divide 호출문에서 예외가 발생하면 divide 함수는 자신을 호출한 main 으로 돌아와서 대응되는 catch 문을 찾아 이 코드를 실행한다. catchthrow 가 던진 에러 메시지 문자열을 화면으로 그대로 출력할 것이다. 만약 maindivide 사이에 다른 함수들이 있더라도 마찬가지로 main 까지 복귀한 후 예외가 처리된다.

함수가 호출될 때는 스택에 각 함수의 스택 프레임이 생성되며 스택 프레임에는 함수 실행에 필요한 여러 가지 정보들이 저장된다. 함수가 리턴할 때 스택 프레임은 정확하게 호출 전의 상태로 돌아가도록 되어 있다. 예외가 발생했을 때 호출원의 catch 로 곧바로 점프해 버리면 스택이 항상성을 잃어 버리므로 이후 프로그램이 제대로 실행될 수 없을 것이다.

그래서 throw 는 호출원으로 돌아가기 전에 자신과 자신을 호출한 함수의 스택을 모두 정리하고 돌아가는데 이를 스택 되감기(Stack Unwinding) 라고 한다.

  1. 첫 번째 divide 호출문이 예외를 던질 때 maincatch 가 이 예외를 처리한 후 그 다음 문장을 아무 이상없이 실행할 수 있는 이유는 throw 가 스택 되감기를 하여 main 의 스택 프레임을 divide 호출 전의 상태로 복구하기 때문이다.

  2. 두 번째 divide(10,5) 는 올바른 인수를 전달했으므로 예외가 발생되지 않으며 호출 후 정상적인 절차대로 리턴한다.

  3. 세 번째 divide(2,0) 호출은 두 번째 인수가 0 이므로 예외가 발생하는데 이때 이 예외를 받아줄 catch 문이 없다. 함수 호출부가 try 블록에 있지 않기 때문인데 이때는 예외를 처리할 수 없으므로 디폴트 처리되어 프로그램이 강제로 종료된다.

  4. 설사 try 안에 있더라도 예외를 받아줄 catch 가 없으면 이때도 처리되지 않는데 네 번째 호출문 divide(20,0) 의 경우 try 안에 있고 catch 도 있지만 divide 가 던지는 char * 타입의 catch 는 없으므로 역시 처리되지 않고 프로그램은 종료된다.

throw 는 대응되는 try 블록의 catch 를 찾기 위해 스택에서 위쪽 함수를 찾아 올라가면서 호출 스택을 차례대로 정리하는데 이때 각 함수들이 지역적으로 선언한 객체들도 정상적으로 파괴된다. 다음 예제를 통해 스택을 되감는 절차를 연구해 보자.

ex. stackunwinding

#include <iostream>

class C {
public:
    int a;
    C() { std::cout << "생성자 호출" << std::endl; }
    ~C() { std::cout << "파괴자 호출" << std::endl; }
};

void divide(int a, int d){
    if (d == 0) throw "0으로는 나눌 수 없습니다.";
    std::cout << "나누기 결과 = " << a/d << "입니다." << std::endl;
}

void calc(int t,const char *m){
    C c; // #2
    divide(10,0);
    // throw 로 인하여 바로 stack unwinding 을 하기에 불리지 않음
    std::cout << "This is not called" << std::endl;
}

int main(){
    C* test = new C(); // #1
    test->a = 10;
    try {
        test->a=11;
        calc(1,"계산");
    }
    catch(const char *message) {
         std::cout << message << std::endl; // #3
    }
    std::cout << "프로그램이 종료됩니다. -- " << test->a << std::endl;
    return 0;
}

/*
생성자 호출 // #1
생성자 호출 // #2
파괴자 호출 // stack unwinding
0으로는 나눌 수 없습니다. // #3
프로그램이 종료됩니다. -- 11 // try 블록 내에서 throw 발생전에 변경된 사항은 반영, 롤백되지 않음
*/

maintry 블록에서 calc 를 부르고 calc 는 지역 객체 C 를 선언한다. 그리고 예외를 일으키는 divide(10,0) 을 호출하는데 이 함수에서 throw 에 의해 문자열 예외가 던져진다. 이 때의 스택 상황은 다음과 같을 것이다.

divide 에서 예외가 발생했으므로 이 함수는 더 이상 실행할 수 없다. 그래서 이 예외를 처리할 catch 문을 찾는데 함수 내부에서는 catch 가 없으므로 일단 자신을 호출한 calc 함수로 돌아간다. 이 과정에서 자신의 스택 프레임은 정리하는데 이렇게 하지 않으면 호출원이 예외를 처리하더라도 제대로 실행될 수 없기 때문이다.

calc 에서 다시 catch 를 찾는데 이 함수도 catch 를 가지고 있지 않으므로 같은 방식으로 스택을 정리한다. 이때 calc 의 인수 tm, 지역변수 C가 파괴되는데 C 는 객체이므로 정상적인 파괴를 위해 파괴자가 호출된다. calcmain 으로 리턴하면 maincatch(char *) 로 점프하여 예외를 처리한다. 스택 되감기를 하면서 리턴되는 함수의 모든 지역 객체를 파괴하는데 만약 파괴자를 호출하지 않는다면 예외만 처리될 뿐 생성된 객체들이 제대로 해제되지 않아 프로그램의 상태는 여전히 불안해질 것이다. 파괴자는 단순히 메모리만 정리하는 것이 아니라 때로는 DB 연결 해제, 프로그램 상태 변경 등의 중요한 일을 할 수도 있으므로 반드시 호출해야 한다.

throw 가 대응되는 catch 를 찾기 위해 스택 되감기를 해야 하는 이유는 아주 명백하다. throwcatch 로의 점프 동작을 하는데 함수간에 아무렇게나 점프를 해 버리면 스택의 호출 정보는 엉망이 되어 버린다. 호출원으로 돌아갈 때는 스택도 호출원의 것으로 정확하게 복구해야 하며 그러기 위해서는 자신을 호출한 모든 함수의 스택을 일일이 정리해야 하는 것이다. 위 예에서 main의 마지막에 있는 출력이 제대로 실행되려면 catch 가 예외를 처리한 후 스택의 최상단에는 main의 스택 프레임이 있어야 하며 그러기 위해서는 dividethrowdividecalc 의 스택을 정리해야 하는 것이다.

Reference

https://banaba.tistory.com/42

profile
Backend Developer

0개의 댓글