Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

176162
807
85244


[라즈베리파이] 시그널 - 시그널 핸들러 실행 및 커널 복귀 과정 12장. 시그널

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을 다시 복구하는 방법을 사용합니다. 
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/signal.c]
static int
setup_frame(struct ksignal *ksig, sigset_t *set, struct pt_regs *regs)
{
struct sigframe __user *frame = get_sigframe(ksig, regs, sizeof(*frame));
int err = 0;

if (!frame)
return 1;

/*
 * Set uc.uc_flags to a value which sc.trap_no would never have.
 */
__put_user_error(0x5ac3c35a, &frame->uc.uc_flags, err);

err |= setup_sigframe(frame, regs, set);
if (err == 0)
err = setup_return(regs, ksig, frame->retcode, frame);

return err;
}

이제 모든 signal을 처리하고 user mode로 돌아가게 되면 원래 signal이 발생했던 시점부터 다시 실행을 시작할 수 있게 됩니다.

실제로 이러한 frame  정보는 커널 내에 다음과 같이 정의되어 있습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/signal.c]
struct sigframe {
struct ucontext uc;
unsigned long retcode[2];
};

여기서 ucontext 구조체에 각종 레지스터들의 현재 값을 저장해둡니다.

ucontext 구조체 정의는 다음 코드에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/asm/ucontext.h]
struct ucontext {
unsigned long   uc_flags;
struct ucontext  *uc_link;
stack_t   uc_stack;
struct sigcontext uc_mcontext;
sigset_t   uc_sigmask;
/* Allow for uc_sigmask growth.  Glibc uses a 1024-bit sigset_t.  */
int   __unused[32 - (sizeof (sigset_t) / sizeof (int))];
/* Last for extensibility.  Eight byte aligned because some
   coprocessors require eight byte alignment.  */
  unsigned long   uc_regspace[128] __attribute__((__aligned__(8)));
};

그렇다면 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)를 가리키도록 미리 설정합니다.

따라서 signal handler에서 명시적으로 커널로 복귀하는 코드가 없어도 수행을 마치면 커널로 돌아갈 수 있습니다.

__kernel_sigreturn의 코드는 아주 단순합니다.
stack에서 4byte를 pop하고 sigreturn 시스템 콜을 호출하는 것이 전부입니다.
(참고로 __NR_sigreturn은 ARM에서 119로 정의되어 있다.)
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/sigreturn_codes.S]
sigreturn_codes:

/* ARM sigreturn syscall code snippet */
arm_slot 0
ARM_OK( mov r7, #(__NR_sigreturn - __NR_SYSCALL_BASE) )
ARM_OK( swi #(__NR_sigreturn)|(__NR_OABI_SYSCALL_BASE) )

/* Thumb sigreturn syscall code snippet */
thumb_slot 0
movs r7, #(__NR_sigreturn - __NR_SYSCALL_BASE)
swi #0

/* ARM sigreturn_rt syscall code snippet */
arm_slot 1
ARM_OK( mov r7, #(__NR_rt_sigreturn - __NR_SYSCALL_BASE) )
ARM_OK( swi #(__NR_rt_sigreturn)|(__NR_OABI_SYSCALL_BASE) )

/* Thumb sigreturn_rt syscall code snippet */
thumb_slot 1
movs r7, #(__NR_rt_sigreturn - __NR_SYSCALL_BASE)
swi #0

이 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에서는 sp (stack pointer)을 앞서 말한대로 &frame->sc와 맞춰야합니다..
이제 sp 값에 대해서 한 번 살펴봅시다.

우선 signal handler가 호출되는 순간 커널은 sp가 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이 사용되는 몇 가지 차이 만 있을 뿐
동작하는 방식은 동일하므로 약간만 변형하여 같은 결과를 얻을 수 있습니다.



덧글

댓글 입력 영역