컴퓨터/운영체제(주로 리눅스)

[리눅스] 단번에 이해하는 setjmp, longjmp를 활용하는 방법

REAKWON 2022. 4. 5. 20:04

아래의 내용과 더불어 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

비국소(nonlocal) 분기

setjmp나 longjmp는 이름에서도 알수 있듯이 jump하는 함수입니다. 실행부를 점프한다는 것입니다. 그전에 비국소(nonlocal)라는 단어를 설명할 필요가 있습니다. C언어에서 goto구문을 아시죠? goto도 역시 코드를 이동시킬 수 있지만, goto와는 다릅니다. 아래는 간단한 goto의 활용예입니다.

#include <setjmp.h>
#include <stdio.h>

int go_to(){
        int i = 1;

        if(i==1)
                goto here;

        here:{
                printf("here!\n");
        }
}
int main(){
        go_to();
        return 0;
}

실행해보면 here label이 있는곳으로 코드가 이동되어 실행이되는것을 볼 수 있습니다.

label은 항상 goto가 부르는 label과 같은 함수 내에 있어야합니다. 즉, 아래와 같은 코드는 컴파일되지 않습니다. 다른 함수로 건너뛸수가 없다는 말입니다.

#include <setjmp.h>
#include <stdio.h>

int go(){
    here :
        printf("here!\n");
    go_to();
}
int go_to(){
    int i = 1;
    if(i==1)
        goto here;

}
int main(){
    go();
    return 0;
}
$ gcc jump.c
jump.c: In function ‘go’:
jump.c:7:2: warning: implicit declaration of function ‘go_to’ [-Wimplicit-function-declaration]
  go_to();
  ^
jump.c: In function ‘go_to’:
jump.c:12:3: error: label ‘here’ used but not defined
   goto here;

 

setjmp와 longjmp는 비국소(nonlocal) 분기를 수행합니다. 비국소라는 같은 함수내부에서만 점프하지는 않는다는 것입니다. 함수가 호출된 경로 안에 있는 다른 어떤 함수로 점프하는 것이 가능하다는 얘기입니다.

 

setjmp, longjmp

#include <setjmp.h>

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

setjmp, longjmp 를 사용하기 위해서는 setjmp.h를 include시켜야합니다. 아래의 코드에서 보겠지만 이 함수들에 대해서 간단하게 설명하면 이렇습니다.

setjmp : 점프할 위치를 지정합니다. env는 나중에 longjmp가 호출되었을때 스택의 상태를 복원할때 필요한 모든 정보를 담는 배열입니다. setjmp의 반환값은 longjmp의 val입니다. 이 반환값으로 흐름을 분기시킬 수 있습니다.

longjmp : 실제로 점프하는 함수입니다. 이 함수를 통해서 setjmp위치로 돌아갑니다. 이때 env는 setjmp와 같은 env를 사용하며 val은 setjmp가 반환할 값입니다. 

이 함수들을 이용해서 다음의 프로그램을 수정할것인데요. 프로그램에 대해서 설명하자면 비밀번호를 입력받고 입력이 8자리인가, 포함되어서는 안될 문자가 있는가 검사하는 아주 간단한 프로그램입니다.

#include <stdio.h>
#include <string.h>
#define TRUE 1
#define FALSE 0
char password[32];

int check_size(){
        if(strlen(password)< 8) return FALSE;
}
int is_possible_word(){
        int i;
        for(i=0;i<strlen(password);i++)
                if(password[i] == '|' || password[i] == '%' || password[i] == '&') return FALSE;
        return TRUE;
}
int verify_input(){
        if(!check_size()) return 1;
        if(!is_possible_word()) return 2;
        return 3;
}
int main(){

        while(1){
                printf("비밀번호:");
                scanf("%s",password);
                int result = verify_input();
                if(result == 3) break;
                switch(result){
                        case 1:
                                printf("비밀번호는 8자 이상이어야합니다.\n");
                        break;
                        case 2:
                                printf("|, %%, &문자는 사용할 수 없습니다.\n");
                        break;
                }
        }
        return 0;
}

 

위 코드의 문제점을 파악해보도록 하지요.

1. 검증이 완료될때까지 while문으로 계속 password를 입력받습니다. 

2. verify_input에는 두가지 정도를 검증하는데 하나는 길이, 다른 하나는 포함불가한 문자를 거르는것인데요. 이때 하나의 조건이라도 맞지 않는다면 verify_input에 그에 맞는 error code를 주고, verify_input은 다시 main으로 error code를 반환하게 됩니다. 만약 검증해야될 조건이 많다면 더 복잡해지겠네요. 

이 프로그램의 특징은 비밀번호의 조건중 하나라도 맞지 않으면 처음부터 다시 수행합니다. 그냥 main 함수 밑에 점프할 곳을 딱 정해놓고 check_size()나 is_possible_word()에 그 조건이 맞지 않는다면 그쪽으로 점프시키는 쪽으로 변경하면 안될까요? 이렇게 되면 while을 쓰지 않아도 되고, verify_input()에 굳이 error code를 전달하지 않아도 되니까요. 만약 verify_mode()에서 is_possible_word()까지 들어가고 여기에 새로운 조건을 검사하는 함수가 추가되서 계속 호출되면 코드는 더 복잡해집니다. 이러한 상황은 곧 setjmp, longjmp를 통해서 간단하게 해결할 수 있습니다.

그 코드가 아래에 있습니다. 

#include <stdio.h>
#include <string.h>
#include <setjmp.h>

#define TRUE 1
#define FALSE 0

//setjmp와 longjmp가 같이 사용하는 jmp_buf로
//전역적으로 사용
jmp_buf jmpbuf;
char password[32];

void check_size(){
        if(strlen(password)< 8) 
                longjmp(jmpbuf, 1);
}

void is_possible_word(){
        int i;
        for(i=0;i<strlen(password);i++){
                if(password[i] == '|' || 
                                password[i] == '%' || password[i] == '&') 
                        longjmp(jmpbuf,2);
        }
}

void verify_input(){
        check_size();
        is_possible_word();
}

int main(){
        int ret = 0;
        //setjmp는 나중에 longjmp를 통해서 점프되었을때실행되는 부분
        if((ret = setjmp(jmpbuf)) > 2){
                printf("unknown option\n");
                return -1;
        }else{
                //longjmp에서 두번째 인자 val이 ret값이 됨.
                switch(ret){
                        case 1:
                                printf("비밀번호는 8자 이상이어야합니다.\n");
                                break;
                        case 2:
                                printf("|, %%, &문자는 사용할 수 없습니다.\n");
                                break;
                }
        }

        printf("비밀번호:");
        scanf("%s",password);

        verify_input();
        return 0;
}

 

메인의 while문이 빠졌고, check_size(), is_possible_word()에서 직접 longjmp를 통해서 main함수의 입력받는 부분으로 되돌아갑니다. setjmp의 반환값은 longjmp의 두번째로 전달되는 인자의 값인것을 알 수 있습니다.

 

만약 setjmp 이전에 longjmp를 호출하게 되면 Segmentation fault가 발생하게 됩니다. 그러니까 아래와 같은 상황에서는 점프할 수 없습니다. 점프는 함수가 호출된 순서에서 거꾸로 돌아가는 것(원복의 목적)만 가능합니다.

 

#include <stdio.h>
#include <setjmp.h>
jmp_buf jmpbuf;
int main(){
    int ret = 0;
    longjmp(jmpbuf, 1); //이 코드는 수행할수 없다.

    setjmp(jmpbuf);
    return 0;
}

 

사실 스택 프레임을 거꾸로 돌린다고 표현하는게 더 정확할 것 같네요. 여기에 위 코드의 동작과정을 그림으로 표현하였습니다. check_size()에서 longjmp를 호출했을때의 상황입니다.

check_size()에서 longjmp

 

즉, 스택을 복원하면서 점프하는 것입니다. 여기서 정적변수, 전역변수, 휘발성 변수는 복원되지 않습니다. 정적변수, 전역변수는 스택 메모리에 기억되는 값이 아니니까요.  메모리의 구조를 확인하려면 아래의 포스팅을 확인해보세요.

https://reakwon.tistory.com/20

 

[C언어] 동적 메모리 할당의 세가지 방법 malloc, calloc, realloc

동적 메모리 할당 우리는 이제껏 메모리를 할당할때 정적으로 할당했습니다. 어떤 것이냐면 int arr[100]; 이렇게 할당을 했었죠. 뭐 문제없습니다. 실행도 잘 되구요. 하지만 이런 상황은 조금 불

reakwon.tistory.com

 

지금까지 setjmp, longjmp를 통해서 비국소적 분기를 이해해보았습니다. 

반응형