[리눅스] 단번에 이해하는 setjmp, longjmp를 활용하는 방법
아래의 내용과 더불어 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.
https://reakwon.tistory.com/233
비국소(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를 호출했을때의 상황입니다.
즉, 스택을 복원하면서 점프하는 것입니다. 여기서 정적변수, 전역변수, 휘발성 변수는 복원되지 않습니다. 정적변수, 전역변수는 스택 메모리에 기억되는 값이 아니니까요. 메모리의 구조를 확인하려면 아래의 포스팅을 확인해보세요.
https://reakwon.tistory.com/20
지금까지 setjmp, longjmp를 통해서 비국소적 분기를 이해해보았습니다.