메뉴 여닫기
환경 설정 메뉴 여닫기
개인 메뉴 여닫기
로그인하지 않음
지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.

Nonlocal Jump: 두 판 사이의 차이

noriwiki
Pinkgo (토론 | 기여)
새 문서: 상위 문서: Exceptional Control Flow ==개요== ==각주== 분류:컴퓨터 시스템
 
Pinkgo (토론 | 기여)
 
(같은 사용자의 중간 판 11개는 보이지 않습니다)
2번째 줄: 2번째 줄:


==개요==
==개요==
'''Nonlocal jump'''는 사용자 수준의 exceptional control flow을 제공한다. 이는 '''정상적인 호출이나 반환 순서를 거치지 않고 현재 실행 중인 한 함수에서 다른 함수로 직접 control을 전이'''시키는 역할을 한다.
==Nolocal functions==
Nonlocal jump는 <code>setjmp()</code>, <code>longjmp()</code> 함수들에 의해 구현된다.
<syntaxhighlight lang="cpp">
#include <setjmp.h>
//Returns: 0 from setjmp, nonzero from longjmps
int setjmp(jmp_buf env); //세이브 포인트 찍기
int sigsetjmp(sigjmp_buf env, int savesigs); //setjmp()의 시그널 핸들러용 버전
</syntaxhighlight>
<code>setjmp()</code> 함수는 현재의 호출 환경(calling environment)를 env 버퍼에 저장하여 <code>longjmp()</code> 함수에서 사용하도록 한 후, 0을 반환한다. 이때 호출 환경은 프로그램 카운터, 스택 포인터, 범용 레지스터(general-purpose registers)들을 포함한다. 이때, <code>setjmp()</code>의 반환값을 변수에 할당해서는 안된다.
<syntaxhighlight lang="cpp">
#include <setjmp.h>
//never returns
void longjmp(jmp_buf env, int retval); //문제가 생겼으니 세이브 포인트로 후퇴
void siglongjmp(sigjmp_buf env, int retval); //longjmp()의 시그널 핸들러용 버전
</syntaxhighlight>
<code>longjmp()</code> 함수는 env 버퍼로부터 호출 환경을 복원한 다음 해당 env를 초기화한 다음, 해당 env를 초기화한 최신의 <code>setjmp()</code> 함수의 return을 야기한다. 이때 <code>setjmp()</code> 함수는 0이 아닌 retval 값을 return하며 복귀한다.<br>
즉, <code>setjmp()</code> 함수는 한 번 호출되지만 여러 번 반환된다. 한 번은 <code>setjmp()</code> 함수가 처음 호출되어 호출 환경이 env 버퍼에 저장될 때이고, 나머지는 각각의 <code>longjmp()</code> 함수 호출에 대응하여 반환될 때이다. 반면, <code>longjmp()</code> 함수는 한 번 호출되지만 절대 반환되지 않는다.
비유를 하자면, <code>setjmp()</code>, <code>longjmp()</code> 함수들은 닥터 스트레인지의 마법과 비슷하다. <code>setjmp()</code>는 닥터 스트레인지가 도르마무와 맞서기 위해서 스폰 포인트를 찍어두는 곳이고, <code>longjmp()</code>는 닥터 스트레인지가 도르마무에게 죽는 곳이다. 즉, 닥터 스트레인지가 도르마무에게 죽으면 특정한 시간대로 돌아 가듯이, 코드가 <code>longjmp()</code>에 이르면 control이 강제로 <code>setjmp()</code> 지점으로 이동되며, 이를 통해서 <code>setjmp()</code>는 두 번 반환될 수 있다.
Nonlocal jump의 중요한 용도 중 하나는, 깊이 중첩된 함수 호출 내부에서 어떤 오류 조건이 감지되었을 때 즉시 return하도록 하는 것이다. 중첩된 함수 호출 내부 깊은 곳에서 오류 조건이 발생한 경우, 호출 스택(call stack)을 힘겹게 하나하나 풀어내는 대신, nonlocal jump를 이용해 common localized error handler로 직접 return할 수 있다.<br>
Nonlocal jump의 또 다른 중요한 용도는, 시그널 핸들러(signal handler)로부터 인터럽트(interruted)된 명령어로 복귀하는 대신, 특정 코드 위치로 분기(branch out)하는 것이다.
==Nonlocal examples==
===Nonlocal example 1===
<syntaxhighlight lang="cpp">
#include "csapp.h"
jmp_buf buf; //호출 환경을 저장하는 버퍼
int error1 = 0;
int error2 = 1;
void foo(void), bar(void);
int main() {
    switch(setjmp(buf)) {
        case 0:  foo();  break;
        case 1:  printf("Detected an error1 condition in foo\n");  break;
        case 2:  printf("Detected an error2 condition in foo\n");  break;
        default:  printf("Unknown error condition in foo\n");
    }
    exit(0);
}
/* Deeply nested function foo */
void foo(void) {
    if (error1) longjmp(buf, 1);
    bar();
}
void bar(void) {
    if (error2) longjmp(buf, 2);
}
</syntaxhighlight>
위는 어떻게 <code>setjmp()</code>, <code>longjmp()</code> 함수들이 어떻게 깊이 중첩된 함수 호출 내부에서 어떤 오류 조건이 감지되었을 때 즉시 return하도록 하는지 보여준다. 위 코드의 main 함수는 먼저 <code>setjmp()</code>를 호출하여 현재의 호출 환경을 저장하고, 이후 <code>foo</code>함수를 호출한다.(처음 return값은 0) <code>foo()</code> 함수는 <code>bar()</code> 함수를 다시 호출한다. 만약 <code>foo()</code>나 <code>bar()</code>에서 오류가 발생하면, <code>longjmp()</code> 함수 호출을 통해 곧바로 <code>setjmp()</code> 함수로 복귀하게 된다. 이때 <code>setjmp()</code> 함수의 반환값은 오류의 종류를 나타내며, 이를 decoding하여 하나의 코드 위치에서 처리할 수 있다.
===Nonlocal example 2===
<syntaxhighlight lang="cpp">
#include "csapp.h"
sigjmp_buf buf; //호출 환경을 저장하는 버퍼
void handler(int sig) {
    siglongjmp(buf, 1);
}
int main() {
    if (!sigsetjmp(buf, 1)) { //sigsetjmp()가 0을 반환하는 초기 경우
        Signal(SIGINT, handler); //sigsetjmp() 함수가 호출된 이후 시그널 핸들러 설정
        Sio_puts("starting\n");
    } else
        Sio_puts("restarting\n");
    while(1) {
        Sleep(1);
        Sio_puts("processing...\n");
    }
    exit(0); //Control never reaches here
}
</syntaxhighlight>
위 코드는 어떻게 <code>sigsetjmp()</code>, <code>siglongjmp()</code> 함수들이 시그널 핸들러로부터 인터럽트된 명령어로 복귀하는 대신, 특정 코드 위치로 분기하는지 잘 보여준다. 위 프로그램은 기본적으로 시그널과 nonlocal jump를 사용하여 사용자가 Ctrl+C를 누를 때마다 가벼운 재시작을 한다. 해당 코드에서 처음의 <code>sigsetjmp()</code> 함수 호출은 호출 환결과 시그널 컨텍스트(signal context)<ref>Pending signal vector와 blocking signal vector를 포함한다.</ref> 메인 함수는 이후 무한 루프에 진입한다. 이때 사용자가 Ctrl+C를 누르면, 커널은 프로세스에 SIGINT를 보내고, 해당 시그널에 대한 시그널 핸들러가 호출된다. 해당 시그널 핸들러에는 <code>siglongjmp()</code> 함수가 있어, 시그널 핸들러는 return하지 못하고 <code>sigsetjmp()</code>로 nonlocal jump를 수행한다. 이때, 위 프로그램을 실행하면 다음과 같은 결과를 얻는다.
<syntaxhighlight lang="cpp">
linux> ./restart
starting processing...
processing...
Ctrl+C
restarting processing...
Ctrl+C
restarting processing...
</syntaxhighlight>
이 프로그램에는 몇 가지 주의할 점이 있다. 먼저, race condtion을 피하기 위해서는 <code>sigsetjmp()</code> 함수 호출 이후에 핸들러를 설치하여야 한다. 그렇지 않으면 <code>sigsetjmp()</code> 함수가 호출되기 전에 커널이 SIGINT 시그널을 보내어 시그널 핸들러 내부의 <code>siglongjmp()</code> 함수가 먼저 호출될 수 있기 때문이다.<br>
그리고 <code>sigsetjmp()</code>, <code>siglongjmp()</code> 함수들은 async-signal-safe functions에 속하지 않는다. 그 이유는 일반적으로 <code>siglongjmp()</code> 함수가 임의의 코드로 점프할 수 있기 때문이다. 따라서 <code>siglongjmp()</code>로부터 도달할 수 있는 모든 코드에서는 반드시 async-signal-safe functions만 호출해야 한다. 이 예제에서도 <code>sio_puts</code>와 <code>sleep</code> 같은 안전한 함수들만 호출하며, <code>exit()</code>과 같이 위험한 함수는 도달할 수 없는 위치에 있다.


==각주==
==각주==
[[분류:컴퓨터 시스템]]
[[분류:컴퓨터 시스템]]

2025년 4월 10일 (목) 17:43 기준 최신판

상위 문서: Exceptional Control Flow

개요

Nonlocal jump는 사용자 수준의 exceptional control flow을 제공한다. 이는 정상적인 호출이나 반환 순서를 거치지 않고 현재 실행 중인 한 함수에서 다른 함수로 직접 control을 전이시키는 역할을 한다.

Nolocal functions

Nonlocal jump는 setjmp(), longjmp() 함수들에 의해 구현된다.

#include <setjmp.h>
//Returns: 0 from setjmp, nonzero from longjmps
int setjmp(jmp_buf env); //세이브 포인트 찍기
int sigsetjmp(sigjmp_buf env, int savesigs); //setjmp()의 시그널 핸들러용 버전

setjmp() 함수는 현재의 호출 환경(calling environment)를 env 버퍼에 저장하여 longjmp() 함수에서 사용하도록 한 후, 0을 반환한다. 이때 호출 환경은 프로그램 카운터, 스택 포인터, 범용 레지스터(general-purpose registers)들을 포함한다. 이때, setjmp()의 반환값을 변수에 할당해서는 안된다.

#include <setjmp.h>
//never returns
void longjmp(jmp_buf env, int retval); //문제가 생겼으니 세이브 포인트로 후퇴
void siglongjmp(sigjmp_buf env, int retval); //longjmp()의 시그널 핸들러용 버전

longjmp() 함수는 env 버퍼로부터 호출 환경을 복원한 다음 해당 env를 초기화한 다음, 해당 env를 초기화한 최신의 setjmp() 함수의 return을 야기한다. 이때 setjmp() 함수는 0이 아닌 retval 값을 return하며 복귀한다.
즉, setjmp() 함수는 한 번 호출되지만 여러 번 반환된다. 한 번은 setjmp() 함수가 처음 호출되어 호출 환경이 env 버퍼에 저장될 때이고, 나머지는 각각의 longjmp() 함수 호출에 대응하여 반환될 때이다. 반면, longjmp() 함수는 한 번 호출되지만 절대 반환되지 않는다.

비유를 하자면, setjmp(), longjmp() 함수들은 닥터 스트레인지의 마법과 비슷하다. setjmp()는 닥터 스트레인지가 도르마무와 맞서기 위해서 스폰 포인트를 찍어두는 곳이고, longjmp()는 닥터 스트레인지가 도르마무에게 죽는 곳이다. 즉, 닥터 스트레인지가 도르마무에게 죽으면 특정한 시간대로 돌아 가듯이, 코드가 longjmp()에 이르면 control이 강제로 setjmp() 지점으로 이동되며, 이를 통해서 setjmp()는 두 번 반환될 수 있다.

Nonlocal jump의 중요한 용도 중 하나는, 깊이 중첩된 함수 호출 내부에서 어떤 오류 조건이 감지되었을 때 즉시 return하도록 하는 것이다. 중첩된 함수 호출 내부 깊은 곳에서 오류 조건이 발생한 경우, 호출 스택(call stack)을 힘겹게 하나하나 풀어내는 대신, nonlocal jump를 이용해 common localized error handler로 직접 return할 수 있다.
Nonlocal jump의 또 다른 중요한 용도는, 시그널 핸들러(signal handler)로부터 인터럽트(interruted)된 명령어로 복귀하는 대신, 특정 코드 위치로 분기(branch out)하는 것이다.

Nonlocal examples

Nonlocal example 1

#include "csapp.h"
jmp_buf buf; //호출 환경을 저장하는 버퍼
int error1 = 0;
int error2 = 1;

void foo(void), bar(void);
int main() { 
    switch(setjmp(buf)) { 
        case 0:  foo();  break;
        case 1:  printf("Detected an error1 condition in foo\n");  break;
        case 2:  printf("Detected an error2 condition in foo\n");  break;
        default:  printf("Unknown error condition in foo\n");
    }
    exit(0);
}
/* Deeply nested function foo */
void foo(void) {
    if (error1) longjmp(buf, 1);
    bar();
}
void bar(void) {
    if (error2) longjmp(buf, 2);
}

위는 어떻게 setjmp(), longjmp() 함수들이 어떻게 깊이 중첩된 함수 호출 내부에서 어떤 오류 조건이 감지되었을 때 즉시 return하도록 하는지 보여준다. 위 코드의 main 함수는 먼저 setjmp()를 호출하여 현재의 호출 환경을 저장하고, 이후 foo함수를 호출한다.(처음 return값은 0) foo() 함수는 bar() 함수를 다시 호출한다. 만약 foo()bar()에서 오류가 발생하면, longjmp() 함수 호출을 통해 곧바로 setjmp() 함수로 복귀하게 된다. 이때 setjmp() 함수의 반환값은 오류의 종류를 나타내며, 이를 decoding하여 하나의 코드 위치에서 처리할 수 있다.

Nonlocal example 2

#include "csapp.h"
sigjmp_buf buf; //호출 환경을 저장하는 버퍼
void handler(int sig) {
    siglongjmp(buf, 1);
}
int main() {
    if (!sigsetjmp(buf, 1)) { //sigsetjmp()가 0을 반환하는 초기 경우
        Signal(SIGINT, handler); //sigsetjmp() 함수가 호출된 이후 시그널 핸들러 설정
        Sio_puts("starting\n");
    } else
        Sio_puts("restarting\n");

    while(1) {
        Sleep(1);
        Sio_puts("processing...\n");
    }
    exit(0); //Control never reaches here
}

위 코드는 어떻게 sigsetjmp(), siglongjmp() 함수들이 시그널 핸들러로부터 인터럽트된 명령어로 복귀하는 대신, 특정 코드 위치로 분기하는지 잘 보여준다. 위 프로그램은 기본적으로 시그널과 nonlocal jump를 사용하여 사용자가 Ctrl+C를 누를 때마다 가벼운 재시작을 한다. 해당 코드에서 처음의 sigsetjmp() 함수 호출은 호출 환결과 시그널 컨텍스트(signal context)[1] 메인 함수는 이후 무한 루프에 진입한다. 이때 사용자가 Ctrl+C를 누르면, 커널은 프로세스에 SIGINT를 보내고, 해당 시그널에 대한 시그널 핸들러가 호출된다. 해당 시그널 핸들러에는 siglongjmp() 함수가 있어, 시그널 핸들러는 return하지 못하고 sigsetjmp()로 nonlocal jump를 수행한다. 이때, 위 프로그램을 실행하면 다음과 같은 결과를 얻는다.

linux> ./restart
starting processing...
processing...
Ctrl+C
restarting processing...
Ctrl+C
restarting processing...

이 프로그램에는 몇 가지 주의할 점이 있다. 먼저, race condtion을 피하기 위해서는 sigsetjmp() 함수 호출 이후에 핸들러를 설치하여야 한다. 그렇지 않으면 sigsetjmp() 함수가 호출되기 전에 커널이 SIGINT 시그널을 보내어 시그널 핸들러 내부의 siglongjmp() 함수가 먼저 호출될 수 있기 때문이다.
그리고 sigsetjmp(), siglongjmp() 함수들은 async-signal-safe functions에 속하지 않는다. 그 이유는 일반적으로 siglongjmp() 함수가 임의의 코드로 점프할 수 있기 때문이다. 따라서 siglongjmp()로부터 도달할 수 있는 모든 코드에서는 반드시 async-signal-safe functions만 호출해야 한다. 이 예제에서도 sio_putssleep 같은 안전한 함수들만 호출하며, exit()과 같이 위험한 함수는 도달할 수 없는 위치에 있다.

각주

  1. Pending signal vector와 blocking signal vector를 포함한다.