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
태그
glibc git binutils linux blktrace kernel synchronization script documentation vcs CAaQA3 x86 emacs awk scm algorithm elf sed build compiler memory patch SMP perf CARM bash block-layer gcc computer-architecture C
전체보기
이글루 파인더

최근 등록된 덧글
http://indoessen.blogspo..
by indo essen at 09/06
info bagus dan menarik http:/..
by indo essen at 03/24
http://obatuntukpromil.xyz/ h..
by zahraherba at 03/09
최근 등록된 트랙백
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