Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

80258
1323
114582


[리눅스커널][시그널] 시그널 전달과 처리는 어떻게 할까? 12. Signal

이번 소절에서 분석할 소스 코드를 보면 리눅스 커널이 배경 작업으로 시그널 처리를 위해 얼마나 정교하게 코드 구현이 됐는지 알 수 있습니다.

slow_work_pending 레이블에서 do_work_pending() 함수를 브랜치하는 코드를 이전 소절에서 알아봤습니다. 이번에는 do_work_pending() 함수부터 시그널을 처리하는 handle_signal() 함수까지 살펴봅니다.

get_signal() 함수 분석

get_signal() 함수를 분석하기 앞서 do_work_pending() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/kernel/signal.c]
1 asmlinkage int
2 do_work_pending(struct pt_regs *regs, unsigned int thread_flags, int syscall)
3 {
4 trace_hardirqs_off();
5 do {
6 if (likely(thread_flags & _TIF_NEED_RESCHED)) {
7 schedule();
8 } else {
9 if (unlikely(!user_mode(regs)))
10 return 0;
11 local_irq_enable();
12 if (thread_flags & _TIF_SIGPENDING) {
13 int restart = do_signal(regs, syscall);

다시 한번 current_thread_info() 로 thread_flags 멤버를 읽어 _TIF_SIGPENDING이면 do_signal() 함수를 호출합니다. 프로세스가 시그널을 받을 때 가장 먼저 실행하는 함수는 do_signal() 입니다.

do_signal() 함수 코드를 읽기 전에 이 함수에 전달하는 인자를 점검합시다.
struct pt_regs *regs: 프로세스 최하단 스택 공간에 푸시한 유저 프로세스 레지스터 세트
int syscall: 시스템 콜 테이블 주소

만약 시스템 콜 핸들러를 실행한 후 ret_fast_syscall 레이블로 복귀해서 do_signal() 함수를 실행할 경우, syscall 시스템 콜 테이블 주소를 갖고 있습니다.

다음은 do_signal() 함수 구현부입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/kernel/signal.c]
1 static int do_signal(struct pt_regs *regs, int syscall)
2 {
3 unsigned int retval = 0, continue_addr = 0, restart_addr = 0;
4 struct ksignal ksig;
5 int restart = 0;
6
7 if (syscall) {
8 continue_addr = regs->ARM_pc;
9 restart_addr = continue_addr - (thumb_mode(regs) ? 2 : 4);
10 retval = regs->ARM_r0;
11
12 switch (retval) {
13 case -ERESTART_RESTARTBLOCK:
14 restart -= 2;
15 case -ERESTARTNOHAND:
16 case -ERESTARTSYS:
17 case -ERESTARTNOINTR:
18 restart++;
19 regs->ARM_r0 = regs->ARM_ORIG_r0;
20 regs->ARM_pc = restart_addr;
21 break;
22 }
23 }
24
25 if (get_signal(&ksig)) {
26 /* handler */
27 if (unlikely(restart) && regs->ARM_pc == restart_addr) {
28 if (retval == -ERESTARTNOHAND ||
29     retval == -ERESTART_RESTARTBLOCK
30     || (retval == -ERESTARTSYS
31 && !(ksig.ka.sa.sa_flags & SA_RESTART))) {
32 regs->ARM_r0 = -EINTR;
33 regs->ARM_pc = continue_addr;
34 }
35 }
36 handle_signal(&ksig, regs);
37 } else {
38 /* no handler */
39 restore_saved_sigmask();
40 if (unlikely(restart) && regs->ARM_pc == restart_addr) {
41 regs->ARM_pc = continue_addr;
42 return restart;
43 }
44 }
45 return 0;
46}

먼저 7번째 줄 코드를 보겠습니다.
7 if (syscall) {
8 continue_addr = regs->ARM_pc;
9 restart_addr = continue_addr - (thumb_mode(regs) ? 2 : 4);
10 retval = regs->ARM_r0;
11
12 switch (retval) {
13 case -ERESTART_RESTARTBLOCK:
14 restart -= 2;
15 case -ERESTARTNOHAND:
16 case -ERESTARTSYS:
17 case -ERESTARTNOINTR:
18 restart++;
19 regs->ARM_r0 = regs->ARM_ORIG_r0;
20 regs->ARM_pc = restart_addr;
21 break;
22 }
23 }

유저 공간에서 실행 중인 레지스터 중 ARM_pc 레지스터를 로딩해서 continue_addr 변수에 저장한 후, restart_addr 지역 변수에 다시 저장합니다.

유저 공간에서 실행 중인 프로그램 카운터 주소 정보는 ARM_pc 멤버에 저장돼 있습니다.
이 주소에서 ARM 모드에 따라 2 혹은 4를 빼서 유저 공간에서 다시 실행할 프로그램 카운터 주소를 보정합니다.

ARM 프로세서는 파이프 라인을 적용한 아키텍처라 프로그램 카운터는 실제 실행 중인 함수 주소에서 +4만큼 큽니다. 그래서 다시 실행할 함수 주소에서 4를 빼서 보정하는 것입니다.

25~46번째 줄 코드를 보겠습니다. 이 코드는 크게 2단계로 나눌 수 있습니다.
25 if (get_signal(&ksig)) {
26 /* handler */
27 if (unlikely(restart) && regs->ARM_pc == restart_addr) {
28 if (retval == -ERESTARTNOHAND ||
29     retval == -ERESTART_RESTARTBLOCK
30     || (retval == -ERESTARTSYS
31 && !(ksig.ka.sa.sa_flags & SA_RESTART))) {
32 regs->ARM_r0 = -EINTR;
33 regs->ARM_pc = continue_addr;
34 }
35 }
36 handle_signal(&ksig, regs);
37 } else {
38 /* no handler */
39 restore_saved_sigmask();
40 if (unlikely(restart) && regs->ARM_pc == restart_addr) {
41 regs->ARM_pc = continue_addr;
42 return restart;
43 }
44 }
45 return 0;
46}

25번째 줄 코드에서 get_signal() 함수를 호출한 후 반화하는 값에 따라 실행 흐름이 나뉘게 됩니다. 27~36번째 줄 코드는 시그널 핸들러를 지정했을때 실행하고, 37~44번째 줄 코드는 시그널 핸들러를 지정하지 않았을 때 실행합니다.

다음 get_signal() 함수를 보겠습니다. get_signal() 함수 앞부분과 중간에 수 많은 예외 처리 코드가 있는데, 시그널을 핵심 코드 조각을 모아 봤습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 int get_signal(struct ksignal *ksig)
2 {
3 struct sighand_struct *sighand = current->sighand;
4 struct signal_struct *signal = current->signal;
5 int signr;
...
6 for (;;) {
7 struct k_sigaction *ka;
...
8 signr = dequeue_signal(current, &current->blocked, &ksig->info);
9
10 if (!signr)
11 break; /* will return 0 */
...
12 ka = &sighand->action[signr-1];
13
14 /* Trace actually delivered signals. */
15 trace_signal_deliver(signr, &ksig->info, ka);
16
17 if (ka->sa.sa_handler == SIG_IGN) /* Do nothing.  */
18 continue;
19 if (ka->sa.sa_handler != SIG_DFL) {
20 /* Run the handler.  */
21 ksig->ka = *ka;
22
23 if (ka->sa.sa_flags & SA_ONESHOT)
24 ka->sa.sa_handler = SIG_DFL;
25
26 break; /* will return non-zero "signr" value */
27 }
28
29 if (sig_kernel_ignore(signr)) /* Default is nothing. */
30 continue;
...
31 current->flags |= PF_SIGNALED;
...
32 do_group_exit(ksig->info.si_signo);
33 /* NOTREACHED */
34 }
35 spin_unlock_irq(&sighand->siglock);
36
37 ksig->sig = signr;
38 return ksig->sig > 0;
39 }

8번째 줄 코드를 보겠습니다.
8 signr = dequeue_signal(current, &current->blocked, &ksig->info);
9
10 if (!signr)
11 break; /* will return 0 */

프로세스 태스크 디스크립터 구조체 struct task_struct 멤버 중 pending 혹은 signal->shared_pending로 펜딩된 시그널 정보를 &ksig->info로 저장하고 시그널 번호를 반환합니다.

만약 시그널 번호가 0이면 10~11번째 줄 코드와 같이 break 문을 실행해서 실행을 중단합니다.

12번째 줄 코드를 보겠습니다.
12 ka = &sighand->action[signr-1];

시그널 타입에 따라 시그널 핸들링 정보를 저장한 &sighand->action[] 배열에 접근해서 ka 지역 변수로 반환합니다.

다음은 ftrace 로그를 출력하는 코드입니다.
15 trace_signal_deliver(signr, &ksig->info, ka);

signal_deliver 이란 ftrace 이벤트를 켰을 때 실행하는 코드입니다.

signal_deliver 이벤트를 키면 다음과 같은 ftrace 로그를 확인할 수 있습니다.
signal_handle-12151 [001] d..1  6207.473891: signal_deliver: sig=2 errno=0 code=128 sa_handler=400398 sa_flags=10000000

시그널 핸들러 함수 주소와 전달하는 시그널 종류를 출력합니다.

19~26번째 줄 코드는 시그널 핸들러를 지정했으면 실행하는 코드입니다.
19 if (ka->sa.sa_handler != SIG_DFL) {
20 /* Run the handler.  */
21 ksig->ka = *ka;
22
23 if (ka->sa.sa_flags & SA_ONESHOT)
24 ka->sa.sa_handler = SIG_DFL;
25
26 break; /* will return non-zero "signr" value */
27 }
..
35 spin_unlock_irq(&sighand->siglock);
36
37 ksig->sig = signr;
38 return ksig->sig > 0;

21번째 줄 코드를 실행해서 시그널 정보를 저장한 다음 26번째 줄 코드를 실행해서 for(;;) 루프에서 빠져나와 37~38번째 줄 코드를 실행해서 1을 반환합니다. 보통 시그널 번호가 0보다 크니 1을 반환합니다.

유저 어플리케이션에서 시그널 별로 설정한 시그널 핸들러를 실행하기 위한 동작은 handle_signal() 함수에서 살펴보겠습니다.

다음 31번째 줄 코드를 보겠습니다.
31 current->flags |= PF_SIGNALED;
...
32 do_group_exit(ksig->info.si_signo);
33 /* NOTREACHED */

실행 중인 프로세스 태스크 디스크립터 flags 멤버에 PF_SIGNALED 매크로를 저장합니다. 
“|=” 연산자를 썼으니 이미 저장된 flags 멤버 값은 그대로 유지합니다.

32번째 줄 코드를 보면 do_group_exit() 함수를 호출합니다. 이 코드로 대부분 시그널 처리 결과는 프로세스 종료란 사실을 알 수 있습니다. do_group_exit() 함수를 호출해서 현재 실행 중인 프로세스와 프로세스가 속한 스레드 그룹 내 다른 프로세스들을 종료시킵니다.

리눅스 커널에서는 유저 프로세스 관점으로 생성된 스레드도 프로세스로 간주합니다. 그래서 커널 입장에서 위와 같이 표현한 것입니다. 유저 프로세스 관점으로는 해당 스레드와 스레그 그룹에 속한 스레드들을 종료한다라고 보면 됩니다.

33번째 줄 코드를 보면 “NOTREACHED” 이란 주석문을 볼 수 있습니다. do_group_exit() 함수는 해당 프로세스와 스레드 그룹에 소속한 다른 프로세스도 종료했으니 33번째 줄 코드는 다시 실행될 수 없기 때문입니다.

handle_signal() 함수 분석

handle_signal() 함수 분석에 앞서 어떤 조건으로 handle_signal() 함수가 호출하는지 코드로 살펴보겠습니다.
1 static int do_signal(struct pt_regs *regs, int syscall)
2 {
3 unsigned int retval = 0, continue_addr = 0, restart_addr = 0;
4 struct ksignal ksig;
...
5 if (get_signal(&ksig)) {
6 /* handler */
7 if (unlikely(restart) && regs->ARM_pc == restart_addr) {
8 if (retval == -ERESTARTNOHAND ||
9     retval == -ERESTART_RESTARTBLOCK
10     || (retval == -ERESTARTSYS
11 && !(ksig.ka.sa.sa_flags & SA_RESTART))) {
12 regs->ARM_r0 = -EINTR;
13 regs->ARM_pc = continue_addr;
14 }
15 }
16 handle_signal(&ksig, regs);

get_signal() 함수에서 시그널 정보에서 시그널 핸들러가 등록됐다는 정보를 확인 후 1을 반환합니다. 이후 16번째 줄 코드를 실행해서 handle_signal() 함수를 실행합니다.

handle_signal() 함수를 시작으로 유저 프로세스가 등록한 시그널 핸들러를 실행하는 함수 흐름은 다음과 같습니다.
 

[1] 단계
시그널을 처리하기 전에 해당 프로세스는 sys_pause() 함수에서 schedule() 함수를 호출해서 실행 준비 상태 였습니다. 시그널을 생성하면 시그널을 받을 프로세스를 깨우니 schedule() 함수에서 ret_fast_syscall() 함수로 실행합니다.

[2] 단계
프로세스는 시그널이 자신에게 전달됐다는 사실을 알아채고 시그널에 대한 처리를 수행합니다. get_signal() 함수에서 시그널 핸들러가 등록됐다는 사실을 파악한 후 handle_signal() 함수를 호출합니다.

setup_return() 함수에서는 시그널 정보에서 시그널 핸들러 함수 주소를 읽어서 유저 공간에서 복귀하면 바로 시그널 핸들러를 실행할 수 있게 ARM PC 레지스터에 시그널 핸들러 함수 주소를 써줍니다.

또한 시그널 핸들러 함수 실행 이후 다시 커널 공간으로 복귀해야 하니 R14 레지스터에 시스템 콜을 바로 실행하는 코드를 지정합니다.

[3] 단계
유저 모드로 복귀한 다음 시그널 핸들러를 실행합니다.

[4] 단계
시그널 핸들러를 실행하고 난 후 R14에 저장된 주소 코드를 실행합니다. 시스템 콜 번호 117를 r7 레지스터에 저장하고 시스템 콜을 발생해서 다시 커널 공간으로 이동합니다.

[5] 단계
커널 공간으로 다시 진입 한 후 sys_sigreturn() 함수를 실행해서 커널 공간 스택 프레임 정보를 복원합니다.

[6] 단계
시그널이 생성하기 전 상태 콜스택으로 되돌아가 다시 시그널을 기다립니다.

우리는 유저 어플리케이션에서 시그널 핸들러를 등록하면 당연히 시그널 핸들러가 제대로 실행된다고 가정하고 프로그램을 작성합니다. 커널이 유저 어플리케이션에서 등록한 시그널 핸들러를 실행하기 위해 배경 작업으로 많은 동작을 합니다.

무엇보다 이 과정에서 커널 스택 공간에 저장된 프레임 정보를 유저 공간에 백업한 후 다시 커널 공간으로 이동 후 복사하는 과정을 거칩니다. 그래서 ARM 아키텍처 관련 코드가 상당히 많아 이 동작을 이해하기 어렵습니다.

코드를 자세히 분석하기 전 전체 흐름을 먼저 파악할 필요가 있습니다.  

전체 흐름을 눈에 그리면서 소스 코드를 봅시다. 다음은 handle_signal() 함수 코드입니다.
1 static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
2 {
3 sigset_t *oldset = sigmask_to_save();
4 int ret;
5
6 /*
7  * Set up the stack frame
8  */
9 if (ksig->ka.sa.sa_flags & SA_SIGINFO)
10 ret = setup_rt_frame(ksig, oldset, regs);
11 else
12 ret = setup_frame(ksig, oldset, regs);
13
14 /*
15  * Check that the resulting registers are actually sane.
16  */
17 ret |= !valid_user_regs(regs);
18
19 signal_setup_done(ret, ksig, 0);
20 }

9~12번째 줄 코드를 보면 시그널 설정 상태에 따라 두 개 함수로 호출 흐름이 나뉩니다.
9 if (ksig->ka.sa.sa_flags & SA_SIGINFO)
10 ret = setup_rt_frame(ksig, oldset, regs);
11 else
12 ret = setup_frame(ksig, oldset, regs);

시그널 설정 플래그가 SA_SIGINFO이면 setup_rt_frame() 함수 이외에는 setup_frame() 함수를 호출합니다. 일반적으로 setup_frame() 함수를 호출하므로 setup_frame() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/signal.c]
1 static int
2 setup_frame(struct ksignal *ksig, sigset_t *set, struct pt_regs *regs)
3 {
4 struct sigframe __user *frame = get_sigframe(ksig, regs, sizeof(*frame));
5 int err = 0;
6
7 if (!frame)
8 return 1;
9
10 __put_user_error(0x5ac3c35a, &frame->uc.uc_flags, err);
11
12 err |= setup_sigframe(frame, regs, set);
13 if (err == 0)
14 err = setup_return(regs, ksig, frame->retcode, frame);
15
16 return err;
17 }

먼저 4~12번째 줄 코드를 분석하겠습니다.
4 struct sigframe __user *frame = get_sigframe(ksig, regs, sizeof(*frame));
5 int err = 0;
6
7 if (!frame)
8 return 1;
9
10 __put_user_error(0x5ac3c35a, &frame->uc.uc_flags, err);
11
12 err |= setup_sigframe(frame, regs, set);

4번째 줄 코드는 유저 공간 스택에 접근해서 프레임 정보를 읽습니다.

이후 12번째 줄 코드와 같이 setup_sigframe() 함수를 호출해서,
프로세스 최하단 스택 근처에 저장된 유저 모드 레지스터 세트(struct pt_regs *regs)를 유저모드 스택에 추가 공간을 할당합니다.

이는 기존 유저모드 실행 흐름으로 복귀하는 대신에 유저모드로 복귀하면 시그널 핸들러를 호출하기 위해서 입니다. 보통 시스템 콜 핸들러 처리 이 후 유저 모드로 복귀할 때 프로세스 최하단 스택에 푸시된 유저 모드에서 실행했던 레지스터 세트를 로딩해서 실행합니다.

이 동작을 마무리 한 후 14번째 줄 코드와 같이 setup_return() 함수를 호출해서 시그널 핸들러 함수를 설정합니다.

다음으로 볼 함수는 setup_return() 입니다.
1 static int
2 setup_return(struct pt_regs *regs, struct ksignal *ksig,
3      unsigned long __user *rc, void __user *frame)
4 {
...
5 regs->ARM_r0 = ksig->sig;
6 regs->ARM_sp = (unsigned long)frame;
7 regs->ARM_lr = retcode;
8 regs->ARM_pc = handler;
9 regs->ARM_cpsr = cpsr;
10
11 return 0;
12 }

유저 공간에 복귀하면 바로 실행할 레지스터 정보를 업데이트 합니다.
이는 유저 모드 레지스터 세트로 원래 실행 흐름으로 복귀하지 않고 시그널 핸들러를 실행하기 위함입니다.

ARM_lr 멤버에는 시그널 핸들러를 처리한 후 실행할 코드를 저장합니다. sigreturn 시스템 콜을 실행하게 준비된 코드를 가리키게 합니다. 시그널 핸들러로부터 복귀할 때 다음 어셈블리 코드가 실행하는 순간 시스템 콜을 유발해서 해당 프로세스가 다시 커널 공간으로 진입하도록 합니다.
mov pc, lr

시그널 핸들러 실행 후 유저 모드로 복귀할 코드는 다음과 같은데, sigreturn_codes를 배열로 관리해서 ARM 프로세스 모드에 따라 시스템 콜을 실행합니다.
extern const unsigned long sigreturn_codes[7];

1  80800414 <sigreturn_codes>:
2  80800414: e3a07077  mov r7, #119 ; 0x77
3  80800418: ef900077  svc 0x00900077
4  8080041c: 2777       movs r7, #119 ; 0x77
5  8080041e: df00       svc 0
6  80800420: e3a070ad  mov r7, #173 ; 0xad
7  80800424: ef9000ad  svc 0x009000ad
8  80800428: 27ad       movs r7, #173 ; 0xad
9  8080042a: df00       svc 0
10 8080042c: 00000000  andeq r0, r0, r0

일반적인 상황에서는 4~5번째 줄 코드를 실행해서 r7 레지스터에 시스템 콜 번호를 저장하고 "svc 0x0" 명령어를 실행해서 커널 모드로 진입합니다.

119번에 해당하는 시스템 콜은 sigreturn() 함수이며 커널 공간에서 sys_sigreturn() 함수를 실행합니다.
1 asmlinkage int sys_sigreturn(struct pt_regs *regs)
2 {
3 struct sigframe __user *frame;
...
4
5 frame = (struct sigframe __user *)regs->ARM_sp;
6
7 if (!access_ok(VERIFY_READ, frame, sizeof (*frame)))
8 goto badframe;
9
10 if (restore_sigframe(regs, frame))

10번째 줄 코드와 같이 restore_sigframe() 함수에서 실행합니다.

이전에 유저 모드 스택에 저장해놓은 (원래 프로세스 스택 최하단 주소에 저장했던) 유저 모드 레지스터 세트를 다시 프로세스 스택 최하단 주소에 다시 복구합니다. 이 동작은 restore_sigframe() 함수에서 실행합니다.


"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
혹시 글을 읽고 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려 드리겠습니다!"

덧글

댓글 입력 영역