Egloos | Log-in
F/OSS study
F/OSS study
[Linux] signal handler 실행 과정
linux: 2.6.31
arch: x86


signal은 특정 프로세스에게 어떤 메시지를 전달할 수 있는 가장 기본적인 수단이다.
signal은 다른 (user-level) 프로세스로부터 직접적으로 받거나
혹은 (주로 문제가 될 만한 동작으로인해) 커널로부터 받을 수 있다.

이러한  signal은 kernel-mode에서 처리가 되는데
주로 시스템 콜이나 인터럽트 처리 등을 마치고 user-mode로 돌아오기 직전에
해당 프로세스에게 전달된 signal이 있는지 검사하여 실행된다.
(SMP 커널에서는 user-mode에서 실행 중인 프로세스가 signal을 처리해야하면
강제로 scheduling하도록 IPI를 보내서 kernel-mode로 들어오게 만들기도 한다.)

signal을 받은 프로세스의 기본적인 반응 (동작?)은
거의 대부분 해당 프로세스의 실행을 종료하는 것이며,
이 밖에 signal의 종류에 따라 실행을 중지하거나 그냥 무시하는 경우도 있다.

응용 프로그램은 커널에서 제공하는 몇 가지 시스템 콜을 이용하여
특정한 signal을 받았을 때 기본 동작을 수행하는 대신
사용자가 원하는 동작을 수행하는 signal handler를 등록해 둘 수 있다.
(물론 이런 식으로 처리할 수 없는 강제적인 signal도 있다.)

우선 다음과 같은 예제를 살펴보기로 하자.

sighandler.c:
#include <stdio.h>
#include <signal.h>

static void unused_func(void)
{
  printf("%s\n", __FUNCTION__);
}

static void sighandler(int sig)
{
  printf("%s\n", __FUNCTION__);
}

int main(void)
{
  struct sigaction sa;

  /* set up signal handler */
  sa.sa_handler = sighandler;
  sigaction(SIGUSR1, &sa, NULL);

  /* send signal to myself */
  printf("before raise()\n");
  raise(SIGUSR1);
  printf("after  raise()\n");
 
  return 0;
}

하지만 이러한 signal handler은 user-mode에서 실행되어야 한다는 문제가 있다.
앞서 말했다시피 signal에 대한 처리를 수행하는 것은 커널인데
signal handler는 잠시 user-mode에서 실행하고 실행이 끝나면 다시 커널로 돌아와야 하는 것이다.
리눅스는 kernel-mode로 진입 시 kernel stack에 user-mode에서 실행 중이던 context를 저장하는데
일단 kernel-mode를 벗어나면 kernel stack은 초기화되어버리기 때문에
signal handler를 마치고 다시 kernel-mode로 돌아가게되면
원래 돌아가야 할 user-mode에 대한 정보를 잃어버리게 된다!!

이를 해결하기 위해서는 signal handler를 실행하기 전에
원래의 kernel-stack에 있는 user context 정보를 (frame이라고 부른다.)
signal handler를 실행할 user stack에 임시로 저장해 두었다가
signal handler가 마치고 kernel mode로 돌아오면 임시로 저장해 둔 정보를 이용하여
kernel stack을 다시 복구하는 방법을 사용한다. (linux/arch/x86/kernel/signal.c::__setup_frame() 함수 참조)
이제 모든 signal을 처리하고 user mode로 돌아가게 되면
원래 signal이 발생했던 시점부터 다시 실행을 시작할 수 있게 된다.

실제로 이러한 frame  정보는 커널 내에 다음과 같이 정의되어 있다.
(알아보기 쉽도록 약간 정리하였다.)

linux/arch/x86/include/asm/sigframe.h:
struct sigframe
{
    char *pretcode;
    int sig;
    struct sigcontext sc;
    struct _fpstate fpstate;
    unsigned long extramask[1];
    char retcode[8];
};

여기서 sigcontext 구조체에 각종 레지스터들의 현재 값을 저장해둔다.
sigcontext 및 _fpstate 구조체는 /usr/include/signal.h 파일 어딘가?에 정의되어 있다.

그렇다면 signal handler가 실행을 마치고 kernel로 돌아간다는 것을 kernel이 알아야 한다.
이것이 어떻게 가능할까??

user-mode에서 kernel-mode로 전환하기 위해서는 system call을 이용해야 한다.
따라서 signal handler의 복귀를 위한 특별한 system call이 존재하며 (sigreturn과 rt_sigreturn)
커널은 signal handler를 실행하기 전에 return address가
해당 system call을 호출하는 코드(__kernel_sigreturn)를 가리키도록 미리 설정한다.
(여기서 vdso 방식의 vsyscall 페이지를 이용하는데, 이는 나중에 자세히 다루도록 하겠다.
간단히 커널과 응용 프로그램이 공유하는 user-level 코드라고 생각해도 될 것이다.)
따라서 signal handler에서 명시적으로 커널로 복귀하는 코드가 없어도
수행을 마치면 커널로 돌아갈 수가 있는 것이다.

__kernel_sigreturn의 코드는 아주 단순하다.
stack에서 4byte를 pop하고 sigreturn 시스템 콜을 호출하는 것이 전부다.
(참고로 __NR_sigreturn은 x86에서 119로 정의되어 있다.)

linux/arch/x86/vdso/vdso32/sigreturn.S:
...
__kernel_sigreturn:
    popl %eax        /* XXX does this mean it needs unwind info? */
    movl $__NR_sigreturn, %eax
    int $0x80
...

이 sigreturn이라는 시스템 콜은 커널이 signal handler를 수행한 후에 간접적으로 호출하도록 만들어진 것이므로
user-level에서는 직접적인 사용을 금지하고 있다.
예를 들어 signal handler에서 직접 sigreturn()을 호출하도록 프로그램을 작성해도
libc가 이를 무시하고 실제 시스템 콜을 호출하지 않는다.
실제로 glibc-2.9의 sigreturn() 구현은 아래와 같다.

glibc/signal/sigreturn.c:
#include <signal.h>
#include <errno.h>

int
__sigreturn (context)
     struct sigcontext *context;
{
  __set_errno (ENOSYS);
  return -1;
}
stub_warning (sigreturn)

weak_alias (__sigreturn, sigreturn)
#include <stub-tag.h>

위의 예제에서 sighandler() 함수 내에 sigreturn((void *) 0); 을 추가한 후 컴파일하면 다음과 같이 출력된다.

$ gcc sighandler.c
/tmp/ccEOGoWm.o: In function `sighandler':
sighandler.c:(.text+0x50): warning: warning: sigreturn is not implemented and will always fail

한 마디로 sigreturn은 쓰지 말라는 얘기이다.
하지만 (포기하지 말자!) __kernel_sigreturn에서와 같이
asm 코드로 직접 시스템 콜을 호출하면 동일한 효과를 얻을 수 있다.

한 가지 주의할 것은 (위의 sigreturn 함수의 prototype으로부터 얻을 수 있는 정보이기도 하다!)
sigreturn 시스템 콜이 호출되는 시점에는 esp 레지스터가
sigframe의 sigcontext 구조체를 가리키고 있어야 한다는 점이다. (frame + 8)
커널의 sigreturn 서비스 루틴은 esp에서 8을 빼서 sigframe의 위치를 찾는다.
(offsetof(struct sigframe, sc) = 8이다!)

이제 대강 얘기를 풀어놓았으니 실제 예제를 가지고 몇가지 장난을 좀 쳐보자.
먼저 위의 예제를 그냥 컴파일 후 실행하면 다음과 같은 결과를 얻는다.

$ ./a.out
before raise()
sighandler
after  raise()

이제 sighandler에서 sigframe 정보를 추출하고,
(sigframe은 함수의 return address부분부터 시작하므로 parameter 바로 아래의 주소에서 시작한다.)
위에서 호출하지 않았던 unused_func으로 eip를 설정하면
signal handler가 수행된 후에 커널로 제어가 넘어가고 다시 user-mode로 복귀할 때
unused_func()이 호출되는 것을 볼 수 있다.

static void sighandler(int sig)
{
  struct sigframe *frame = (struct sigframe *) (&sig - 1);
  printf("%s\n", __FUNCTION__);
  frame->sc.eip = (unsigned long) unused_func;
}

다음은 위의 실행 결과이다.
$ gcc sighandler.c
$ ./a.out
before raise()
sighandler
<--------------------------- 여기서 user-mode로 return됨
unused_func
after  raise()

이번에는 sigreturn() 시스템 콜을 직접 호출하여 커널로 복귀해 보자.
먼저 sighandler() 함수에서는 기존의 return address를 unused_func()의 주소로 바꾼다.

static void sighandler(int sig)
{
  struct sigframe *frame = (struct sigframe *) (&sig - 1);
  printf("%s\n", __FUNCTION__);
  /* frame->sc.eip = (unsigned long) unused_func; */
  frame->sc.pretcode = (void *) unused_func;
}

unused_func에서는 esp (stack pointer)을 앞서 말한대로 &frame->sc와 맞춰야한다.
이제 esp 값에 대해서 한 번 살펴보자.
우선 signal handler가 호출되는 순간 커널은 esp가 frame을 가리키도록 설정한다.
frame의 처음 두 필드는 return address와 parameter로 사용되는 signal 번호이므로
이는 일반적인 함수 호출 시의 스택 구성과 완전히 동일하다.
signal handler가 수행을 마치고 ret instruction을 수행하면 스택에서 return address를 pop하므로
이제 esp는 &frame->sig 값을 가진다. (= frame + 4)

다음으로는 바로 unused_func() 함수가 수행되는데
(다른 함수들과 마찬가지로) 이 함수가 제일 먼저 수행하는 일은
ebp를 스택에 push, esp를 ebp에 저장, 로컬 변수 및 함수 호출에 필요한 스택 영역 확보 순이다.

$ objdump -d a.out | grep -A 5 unused
08048484 <unused_func>:
 8048484:    55                       push   %ebp
 8048485:    89 e5                    mov    %esp,%ebp
 8048487:    83 ec 18                 sub    $0x18,%esp
 804848a:    c7 04 24 39 86 04 08     movl   $0x8048639,(%esp)
 8048491:    e8 26 ff ff ff           call   80483bc <puts@plt>

즉 ebp에 (이전의 esp 값 - 4) 값이 들어있다는 것을 알 수 있다.
따라서 ebp 값 + 8하면 &frame->sc 값을 얻을 수 있다.
이제 unused_func()을 다음과 같이 수정한다.

static void unused_func(void)
{
  printf("%s\n", __FUNCTION__);
  asm volatile("leal 8(%ebp), %esp; movl $119, %eax; int $0x80");
}

"leal 8(%ebp)" 부분은 "movl %ebp, %esp; addl $8, %esp" 명령과 동일하다.
이제 sigreturn의 시스템 콜 번호인 119를 eax에 저장하고 시스템 콜을 호출한다. (int $0x80)
아쉽게도? 출력 결과는 앞의 프로그램과 동일하다. (추가한 설명 부분의 위치만 약간 바뀌었다.)

$ gcc sighandler.c
$ ./a.out
before raise()
sighandler
unused_func
<--------------------------- 여기서 user-mode로 return됨
after  raise()

signal handler 등록 시 SA_INFO flag를 설정하여 sa_sigaction 핸들러를 이용하는 경우에도
sigframe의 구성과 sigreturn 대신 rt_sigreturn이 사용되는 몇 가지 차이 만 있을 뿐
동작하는 방식은 동일하므로 약간만 변형하여 같은 결과를 얻을 수 있다.
by namhyung | 2009/11/27 23:41 | System | 트랙백 | 덧글(14)
트랙백 주소 : http://studyfoss.egloos.com/tb/5182475
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Commented by SY Kim at 2009/11/28 17:34
좋은글 잘 봤습니다.
Commented by namhyung at 2009/12/01 19:08
덧글 남겨주셔서 감사합니다.. ^^
Commented by 산사랑 at 2009/12/02 00:05
음, 조금 오래된 애기지만
처음 DOS가 나왔을때 역어셈블을 하여 보던 기억이 나네요.

참, 재미있게 사시네요.
Commented by namhyung at 2009/12/02 10:35
넵.. 나름 재미있게 살고 있습니다.. ^^
Commented by Avan at 2009/12/21 01:34
시스템 프로그래밍 시간에 매우 중요하게 강조 된 내용인데, 이해가 안됐는데 여기서 이해하고 갑니다. 정말 감사합니다
Commented by namhyung at 2009/12/21 11:11
도움이 되셨다니 다행입니다.. ^^
Commented by TheName at 2011/04/07 19:16
frame->sc.eip = (unsigned long) unused_func;
이 부분에서 dereferencing pointer to incomplete type error가 발생합니다.
ㅠ.ㅠ 이 부분에서 왜 에러가 나는지 도저히 못찾겠어요....
혹시 그 이유를 알고 계시면 알려주시면 정말 감사하겠습니다.
Commented by namhyung at 2011/04/08 01:30
해당 에러 메시지는 정의되지 않은 자료형 (보통 구조체)에 대한 포인터를 이용할 때 나는 것입니다.
아마도 frame이 가리키는 자료형 (struct sigframe)이 정의되어 있지 않은 것 같습니다.

그런데 무엇 때문에 이 코드를 사용하시려고 하는지 궁금하군요..
이 코드는 일반적인 목적으로 이용하기에는 적절하지 않습니다.
혹시 KLDP와 iamroot에 질문 올리신 분이라면 제가 iamroot에 남긴 답글을 확인해 보시기 바랍니다.
Commented by seopan at 2011/12/22 13:57
안녕하세요 좋은 글 감사합니다.

궁금한게 있는데요.
그렇다면 커널 드라이버에서 인터럽트를 인지하고 그걸 시그널로 user-process에 알려주는 용도로는 시그널을 사용할 수 있나요? 사실 그렇게 사용하는 커널 api라던가 있는지 궁금하네요.
있다면 좀 알려주셨으면 감사하겠습니다. ^^ iamroot에다 올리려다 여기 더 많이 오실거 같아서 여기다 올려요! ^^
Commented by namhyung at 2011/12/24 19:40
fcntl을 통해 파일에 SETOWN 및 FASYNC 플래그를 설정하면 SIGPOLL (SIGIO) 시그널을 통해 원하시는 작업을 할 수 있을 것입니다.
무료로 공개된 LDD3의 6.4절 Asynchronous Notification 부분을 살펴보시기 바랍니다.

http://lwn.net/Kernel/LDD3/
Commented by andy at 2012/04/29 21:09
잘 봤습니다. 방대한 내용이라 자세히는 보지 못하지만, 레포트 작성에 참고가 되었습니다.
Commented by namhyung at 2012/05/03 22:44
답글 남겨주셔서 감사합니다.
Commented by 몽상가 at 2012/07/03 18:06
고수의 향기가 나네요.. ^^

요번에 맡은 업무가 TCP 스택을 수정하는 일이라 시그널 관련 처리와 기타 처리를 해야되서 자주 들렀습니다만, 늦게나마 감사의 글을 남깁니다.
Commented by namhyung at 2012/07/16 01:36
저도 늦었지만 답글 남겨주셔서 감사합니다.

:         :

:

비공개 덧글

◀ 이전 페이지 다음 페이지 ▶

카테고리
General
Application
System
Kernel
Book
Tips
태그
linux computer-architecture memory algorithm gcc git build sed glibc emacs vcs bash elf CARM compiler documentation script CAaQA3 C blktrace binutils patch perf SMP awk x86 synchronization block-layer scm kernel
전체보기
이글루 파인더

최근 등록된 덧글
http://serbaserbiinfoterkini56..
by cakra at 09/22
informsi yang bagus dan b..
by cakra at 09/16
informsi yang bagus dan be..
by pordanaia at 08/05
최근 등록된 트랙백
Tod's Ferrari Homme
by Tods Pas Cher,Kodak did ..
Mocassin Femme
by Mocassins Homme, I got so..
natural garcinia cambogia
by
rss

skin by jiinny


X