Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

189162
807
85257


[라즈베리파이] 시그널 - 커널 공간 시그널 함수 분석 12장. 시그널

시그널과 연관된 동작을 변경하면 유저 공간에서 sigaction() 함수를 호출하면 됩니다. 이 함수를 호출하면 시스템 콜 핸들러인 sys_rt_sigaction() 함수가 실행하며 커널 공간에서 시그널을 설정합니다. 이 실행 흐름을 표현하면 다음 그림과 같습니다.
 

유저 공간에서 sigaction() 함수를 호출하면 커널 공간에서 sys_rt_sigaction() 함수를 호출합니다. 


유저 공간에서 sigaction() 함수를 호출하면 커널 공간에서 sys_rt_sigaction() 함수가 어떻게 실행하는지 바로 이해하기 어렵습니다. 이 과정을 조금 더 자세히 정리하면 다음과 같습니다.
[유저 공간]
1. r7에 sys_rt_sigaction() 함수에 해당하는 시스템 콜 번호인 174를 지정
2. “svc 0” 명령어로 시스템 콜 처리를 위해 소프트웨어 인터럽트를 발생해서 커널 공간 스위칭

[커널 공간]
3. vector_swi 레이블 실행
4. 유저 공간에서 저장한 시스템 콜 번호인 174를 r7 레지스터에서 읽음
5. sys_call_table에 접근해서 시스템 콜 핸들러인 sys_rq_sigaction 시스템 콜 핸들러로 분기

 시스템 콜을 실행하고 커널 공간으로 스위칭한 다음 시스템 콜 벡터인 vector_swi 레이블을 실행합니다. vector_swi 레이블에서 시스템 콜 테이블인 sys_call_table에 접근해서 시스템 콜 핸들러인 sys_rq_sigaction 시스템 콜 핸들러로 분기합니다.

Trace32로 sys_rq_sigaction 함수가 sys_call_table 이란 시스템 콜 테이블 어느 공간에 있는지 확인하면 다음과 같습니다.
data.view%symbol.long sys_call_table
________address|_data________|value_____________|symbol
0      NSD:80107FC4| 0x8012D4E0      \\vmlinux\kernel/signal\sys_restart_syscall
1      NSD:80107FC8| 0x80121E08      \\vmlinux\exit\sys_exit
2      NSD:80107FCC| 0x8011C6D0      \\vmlinux\fork\sys_fork
3      NSD:80107FD0| 0x802844FC      \\vmlinux\read_write\sys_read
...
173   NSD:80108278| 0x80108638      \\vmlinux\Global\sys_rt_sigreturn_wrapper
174   NSD:8010827C| 0x8012EB24      \\vmlinux\kernel/signal\sys_rt_sigaction
175   NSD:80108280| 0x8012D89C      \\vmlinux\kernel/signal\sys_rt_sigprocmask
176   NSD:80108284| 0x8012D9D4      \\vmlinux\kernel/signal\sys_rt_sigpending
177   NSD:80108288| 0x8012DDD0      \\vmlinux\kernel/signal\sys_rt_sigtimedwait
178   NSD:8010828C| 0x8012E3A8      \\vmlinux\kernel/signal\sys_rt_sigqueueinfo
179   NSD:80108290| 0x8012EDF8      \\vmlinux\kernel/signal\sys_rt_sigsuspend

가장 왼쪽 열에 보이는 174번호가 시스템 콜 번호이고 바로 오른쪽에 보이는 8010827C 주소가 
sys_rt_sigaction() 함수 주소입니다. 173~179번째 시스템 콜 번호는 시그널 처리 관련 다른 시스템 콜 핸들러 함수에 대응하며 이를 표로 정리하면 다음과 같습니다.
시스템 콜 번호 시스템 콜 핸들러 함수
173 sys_rt_sigreturn_wrapper
174 sys_rt_sigaction
175 sys_rt_sigprocmask
176 sys_rt_sigpending
177 sys_rt_sigtimedwait
178 sys_rt_sigqueueinfo
179 sys_rt_sigsuspend

라즈베리파이에서 다음 해더 파일에도 시스템 콜 번호를 확인할 수 있습니다.
[/usr/include/arm-linux-gnueabihf/asm/unistd.h]
#define __NR_rt_sigreturn (__NR_SYSCALL_BASE+173)
#define __NR_rt_sigaction (__NR_SYSCALL_BASE+174)
#define __NR_rt_sigprocmask (__NR_SYSCALL_BASE+175)
#define __NR_rt_sigpending (__NR_SYSCALL_BASE+176)
#define __NR_rt_sigtimedwait (__NR_SYSCALL_BASE+177)
#define __NR_rt_sigqueueinfo (__NR_SYSCALL_BASE+178)
#define __NR_rt_sigsuspend (__NR_SYSCALL_BASE+179)


다음 소절에서는 유저 공간에서 sigaction() 함수를 호출하면 실행하는 시스템 콜 핸들러 함수에 대해서 살펴보겠습니다.

sys_rt_sigaction() 함수 분석

먼저 sys_rt_sigaction() 함수 코드를 보기 전에 선언부 코드를 확인합시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/syscalls.h]
asmlinkage long sys_rt_sigaction(int,
const struct sigaction __user *,
struct sigaction __user *,
size_t);


함수 선언부 가장 왼쪽에 있는 asmlinkage 지시자는 어셈블리 코드에서 이 함수를 호출한다는 의미입니다. 조금 더 구체적으로 vector_swi 레이블에서 sys_rt_sigaction() 함수로 분기합니다.


sys_rt_sigaction() 함수에 전달하는 인자와 속성은 다음 테이블과 같습니다.
인자 속성
int sig 시그널 번호
struct k_sigaction *act 새롭게 설정하는 시그널 속성
struct k_sigaction *oact 이전에 설정했던 시그널 속성

sys_rt_sigaction() 함수 인자를 알아봤으니 코드를 살펴볼 차례입니다. 코드는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
01 SYSCALL_DEFINE4(rt_sigaction, int, sig,
02 const struct sigaction __user *, act,
03 struct sigaction __user *, oact,
04 size_t, sigsetsize)
05 {
06 struct k_sigaction new_sa, old_sa;
07 int ret;
08
09 /* XXX: Don't preclude handling different sized sigset_t's.  */
10 if (sigsetsize != sizeof(sigset_t))
11 return -EINVAL;
12
13 if (act && copy_from_user(&new_sa.sa, act, sizeof(new_sa.sa)))
14 return -EFAULT;
15
16 ret = do_sigaction(sig, act ? &new_sa : NULL, oact ? &old_sa : NULL);
17 if (ret)
18 return ret;
19
20 if (oact && copy_to_user(oact, &old_sa.sa, sizeof(old_sa.sa)))
21 return -EFAULT;
22
23 return 0;
24 }

먼저 13번째 줄 코드를 보겠습니다. 새롭게 설정하는 시그널 속성을 &new_sa.sa 이란 지역 변수에 저장합니다.
13 if (act && copy_from_user(&new_sa.sa, act, sizeof(new_sa.sa)))
14 return -EFAULT;

유저 공간에서 전달된 메모리 버퍼는 copy_from_user() 함수로 복사할 수 있습니다.

16번째 줄 코드는 do_sigaction() 함수를 실행합니다.
16 ret = do_sigaction(sig, act ? &new_sa : NULL, oact ? &old_sa : NULL);

다음 20~21 번째 줄 코드를 보겠습니다.
20 if (oact && copy_to_user(oact, &old_sa.sa, sizeof(old_sa.sa)))
21 return -EFAULT;

이전에 설정했던 시그널 속성을 담고 있는 &old_sa.sa 정보를 유저 공간 메모리 oact에 복사합니다.


do_sigaction() 함수 분석하기

다음으로 살펴볼 코드는 do_sigaction() 함수입니다. 먼저 do_sigaction() 함수는 선언부를 봅시다.
int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact);

do_sigaction() 함수에 전달하는 인자 속성은 다음과 같은데 sys_rt_sigaction() 함수에 전달된 인자 유형과 같습니다.
인자 속성
int sig 설정한 시그널 번호
struct k_sigaction *act 새롭게 설정하는 시그널 속성
struct k_sigaction *oact 이전에 설정했던 시그널 속성

제대로 시그널을 설정했으면 0, 시그널 설정 동작 중 오류를 확인했을때는 –EINVAL 이란 정수형 매크로를 반환합니다.

do_sigaction() 함수 인자를 알아봤으니 코드를 분석할 차례입니다.
1 int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact)
2 {
3 struct task_struct *p = current, *t;
4 struct k_sigaction *k;
5 sigset_t mask;
6
7 if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig)))
8 return -EINVAL;
9
10 k = &p->sighand->action[sig-1];
11
12 spin_lock_irq(&p->sighand->siglock);
13 if (oact)
14 *oact = *k;
15
16 sigaction_compat_abi(act, oact);
17
18 if (act) {
19 sigdelsetmask(&act->sa.sa_mask,
20       sigmask(SIGKILL) | sigmask(SIGSTOP));
21 *k = *act;
22
23 if (sig_handler_ignored(sig_handler(p, sig), sig)) {
24 sigemptyset(&mask);
25 sigaddset(&mask, sig);
26 flush_sigqueue_mask(&mask, &p->signal->shared_pending);
27 for_each_thread(p, t)
28 flush_sigqueue_mask(&mask, &t->pending);
29 }
30 }
31
32 spin_unlock_irq(&p->sighand->siglock);
33 return 0;
34 }

7번째 줄 코드를 보겠습니다. 인자로 전달된 정수형 시그널 번호에 오류가 있는지 점검하는 예외처리 코드입니다. 유저 공간에서 정의되어 있지 않은 시그널 번호를 함수 인자로 전달할 수 있기 때문입니다.
7 if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig)))
8 return -EINVAL;

이 조건을 만족하면 -EINVAL를 반환하고 함수 실행을 종료하면서 시그널 설정을 더 이상 진행하지 않습니다.

10번째 줄 코드를 보겠습니다.
3 struct task_struct *p = current, *t;
4 struct k_sigaction *k;
..
10 k = &p->sighand->action[sig-1];

현재 실행 중인 태스크 디스크립터 주소를 저장하고 있는 p이란 지역 변수로 시그널 번호인 sig에 해당하는 action 멤버 배열을 k이란 struct k_sigaction 구조체 지역 변수에 저장합니다.

struct task_struct 구조체 멤버 sighand->action[] 에는 시그널 종류 별로 어떤 동작을 처리할지에 대한 설정 정보가 저장돼 있습니다.

Trace32로 &p->sighand->action 멤버를 확인하면 다음과 같습니다.
1  (static struct task_struct) \Global\init_task = (
2    (long int) state = 0 = 0x0,
3    (void *) stack = 0x80C00000,
...
4    (struct sighand_struct *) sighand = 0x80C07B80 -> (
5      (atomic_t) count = ((int) counter = 3 = 0x3),
6      (struct k_sigaction [64]) action = (
7         [0] = ((struct sigaction) sa = ((__sighandler_t)sa_handler = 0x0, (long unsigned int) sa_flags = 0
8         [1] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x0, (long unsigned int) sa_flags = 0
9         [2] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x7AAE00B4, (long unsigned int) sa_flags = 0
10       [3] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x7AAE00B4, (long unsigned int) sa_fla
11       [4] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x0, (long unsigned int) sa_fla

...
12        [13] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x0, (long unsigned int) sa_flags = 0
12        [63] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x0, (long unsigned int) sa_flags = 0

action이란 배열의 크기는 64개이고4번째 줄 정보에 보이는 struct sighand_struct 구조체 멤버 중 하나입니다. 

다음과 같이 2~3번째 배열을 보면 sa_handler 멤버에서 주소를 볼 수 있습니다. 2번째 배열은 SIGINT, 3번째 배열은 SIGQUIT 시그널을 의미하며 유저 공간에서 설정된 시그널 핸들러 주소가 0x7AAE00B4임을 알 수 있습니다.
9 [2] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x7AAE00B4, (long unsigned int) sa_flags = 0
10 [3] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x7AAE00B4, (long unsigned int) sa_fla

다음 코드는 SIGINT와 SIGQUIT 시그널 번호 선언부입니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/uapi/asm/signal.h]
#define SIGINT 2
#define SIGQUIT 3


다음 13~14번째 줄 코드를 보겠습니다.
13 if (oact)
14 *oact = *k;

현재 실행 중인 프로세스에서 읽어온 k이란 포인터를 oact에 저장합니다.
k는 시그널 번호에 해당하는 struct k_sigaction 구조체인데 oact 포인터에 저장하는 동작은 기존에 설정된 시그널 속성을 oact 포인터로 저장한다는 의미입니다.

다음 18번째 줄 코드를 보겠습니다. 이 함수에서 가장 중요한 동작입니다.
18 if (act) {
19 sigdelsetmask(&act->sa.sa_mask,
20       sigmask(SIGKILL) | sigmask(SIGSTOP));
21 *k = *act;

do_sigaction() 함수로 전달된 act이란 인자는 새롭게 설정하는 시그널 속성을 의미합니다.
이를 k이란 포인터에 저장합니다. 이는 새롭게 설정한 struct k_action 구조체를 프로세스 태스크 디스크립터인 struct task_struct 구조체 내 sighand->action 배열에 저장한다는 의미입니다.

k이란 지역 변수 출처를 다시 확인하겠습니다.
3 struct task_struct *p = current, *t;
..
10 k = &p->sighand->action[sig-1];

가장 중요한 sys_rt_sigaction() 함수가 실행한 다음 변경되는 시그널 관련 자료 구조를 Trace32로 살펴봅시다.
1  (static struct task_struct) \Global\init_task = (
2    (long int) state = 0 = 0x0,
3    (void *) stack = 0x80C00000,
...
4    (struct sighand_struct *) sighand = 0x80C07B80 -> (
5      (atomic_t) count = ((int) counter = 3 = 0x3),
6      (struct k_sigaction [64]) action = (
7         [0] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x0, (long unsigned int) sa_flags = 0
8         [1] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x0, (long unsigned int) sa_flags = 0
9         [2] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x7AAE11AC, (long unsigned int) sa_flags = 0
10       [3] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x7AAE00B4, (long unsigned int) sa_fla

시그널을 설정하면 시그널을 처리할 프로세스의 태스크 디스크립터 struct task_struct sighand->action[] 배열이 업데이트됩니다. 프로세스 태스크 디스크립터 sighand->action[64] 배열 중 시그널 번호에 해당하는 멤버가 변경된다는 것입니다.

만약 2번 SIGINT 시그널을 설정했고 시그널 핸들러 주소가 0x7AAE11AC이면 위와 같이 9번째 줄 정보와 같이 업데이트됩니다.

이렇게 시그널을 설정하고 난 후 SIGINT 시그널이 발생하면 프로세스가 시그널을 받아서 struct task_struct sighand->action[] 배열에 저장된 속성에 따라 시그널을 처리합니다.

이번 소절에서 유저 공간에서 시그널을 설정하면 어떤 흐름으로 커널 코드가 실행하는지 살펴봤습니다. 다음 소절에서는 시그널을 기다릴 때 호출하는 pause() 함수를 호출하면 커널 공간에서 어떻게 실행하는지 살펴보겠습니다.

sys_pause() 함수 분석

다음은 유저 공간에서 pause() 함수를 호출하면 커널 공간에서 어떤 동작을 하는지 살펴보겠습니다. 다음 시그널 설정 흐름도에서 가장 하단에 표시된 박스를 확인합시다.
 

pause() 함수는 시그널을 기다릴 때 호출합니다.


라즈베리파이에서 pause에 대한 매뉴얼을 확인합시다.
root@raspberrypi:/home/pi# info pause
PAUSE(2)                  Linux Programmer's Manual                 PAUSE(2)
NAME         top
       pause - wait for signal

매뉴얼에서 출력하는 결과와 같이 시그널을 기다리는 역할을 수행합니다.


pause() 함수를 유저 공간에서 실행했을 때 커널에서 어떤 함수가 실행되는지 다른 각도로 살펴봅시다.
 

위 그림과 같이, 유저 공간에서 pause() 함수를 호출하면 해당 시스템 콜 핸들러인 sys_pause() 함수가 실행합니다.

pause() 함수에 대한 시스템 콜 핸들러는 sys_pause() 함수이니, sys_pause() 함수 구현부를 확인할 필요가 있습니다. 먼저 sys_pause() 함수 선언부를 확인하면 void형으로 인자를 받지 않는다는 사실을 알 수 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/syscalls.h]
asmlinkage long sys_pause(void);

다음 sys_pause() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 SYSCALL_DEFINE0(pause)
2 {
3 while (!signal_pending(current)) {
4 __set_current_state(TASK_INTERRUPTIBLE);
5 schedule();
6 }
7 return -ERESTARTNOHAND;
8 }

3번째 줄 코드에서 while 문 조건을 먼저 점검합시다. 
프로세스에 전달된 팬딩 시그널이 없으면 프로세스를 TASK_INTERRUPTIBLE 상태로 바꾸고 schedule() 함수를 호출해서 휴면에 들어갑니다.

팬딩 시그널이란 프로세스에게 시그널이 전달되어 처리해야 할 시그널이 있는 상태를 의미합니다.

프로세스가 시그널을 받아서 다시 깨어나면 어떤 코드를 실행할까요? 3번 코드를 실행합니다. 펜딩된 시그널이 있으면 signal_pending() 함수는 1을 반환하니 while 문 실행을 끝내고 7번 코드를 실행해서 –ERESTARTNOHAND를 반환합니다. 이후 sys_pause() 함수를 빠져나와 ret_fast_syscall 레이블에서 시그널을 받아 처리를 합니다.

이번 절에서 유저 공간에서 시그널을 설정하면 리눅스 커널에서 어떤 흐름으로 시그널을 설정하는지 점검했습니다. 코드 분석으로 시그널을 설정할 때는 sigaction() 함수를 호출하면 프로세스의 태스트 디스크립터 struct task_struct sighand->action[] 배열에 접근한 후 시그널을 설정한다는 사실을 알 수 있습니다.

유저 공간에서 시그널 함수를 호출하면 리눅스 커널은 배경 작업으로 이와 같은 동작을 한다는 사실을 확인할 수 있습니다.

핑백

덧글

댓글 입력 영역