Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

94258
1323
114596


[리눅스커널][시그널] 시그널 생성: 유저 프로세스 kill() 함수 실행 12. Signal

시그널 처리 동작은 크게 시그널 생성과 시그널 처리 과정으로 분류할 수 있습니다. 이 중 시그널 생성 동작에 대해 알아봅시다.

리눅스에서는 어떻게 하면 시그널을 생성할 수 있을까요? 가장 쉽게 시그널을 생성하는 방법을 소개합니다.
1. 라즈베리파이의 터미널인 X-Terminal로 kill 명령어를 입력
2. 유저 어플리케이션에서 kill() 함수나 tgkill()과 같은 시그널을 생성하는 함수 호출
3. 커널이 스스로 시그널을 전달해야 한다고 판단하면 자체로 시그널을 생성 

시그널 생성 과정은 크게 2가지 단계 동작으로 분류할 수 있습니다.
1. 시그널을 받을 프로세스 태스크 디스크립터에 접근 후 생성된 시그널 정보를 써줍니다.
2. 시그널을 받을 프로세스를 깨웁니다.

먼저 유저 공간에서 어떤 흐름으로 시그널이 생성되는지 kill(), tgkill() 함수 실행 흐름 중심으로 알아봅시다.

유저 프로세스 kill() 함수 실행

유저 공간에서 kill 명령어를 입력하거나 유저 어플리케이션에서 kill() 함수를 실행하면 시스템 콜 핸들러인 sys_kill() 함수를 호출합니다. sys_kill() 함수를 실행한 후 어떤 과정으로 시그널을 생성하는지 분석하겠습니다.
 

먼저 sys_kill() 함수 선언부를 봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/syscalls.h]
asmlinkage long sys_kill(pid_t pid, int sig);

asmlinkage 지정자와 함께 함수를 선언했으니 어셈블리 코드에서 이 함수를 호출함을 알 수 있습니다.

sys_kill() 함수에 전달하는 인자는 다음과 같습니다.
인자 속성
pid_t pid 시그널을 전달받을 프로세스 PID
int sig 정수형 시그널 번호

만약 종료시키려는 프로세스의 PID가 895이면 “kill –SIGKILL 895” 명령어를 입력하면 됩니다. 이 명령어를 실행하면 sys_kill() 함수가 실행하며 두 번째 sig 인자로 정수형 시그널 번호 9가 전달됩니다.


“kill –SIGKILL 895” 명령어에서 –SIGKILL 대신 -9로 바꾼 다음 “kill –9 895” 명령어를 입력해도 같은 동작을 수행합니다. 이 명령어를 입력했을 때 ftrace를 확인하면 다음과 같습니다. kill() 함수 시스템 콜 번호는 37입니다. 
1 strace-894   [001] ....   328.965123: sys_enter: NR 37 (37f, 9, 0, 0, 881c0, 37f)          
2 strace-894   [001] ....   328.965141: sys_exit: NR 37 = 0

위 ftrace 로그에서 “NR 37” 메시지는 어떤 의미일까요? 37번은 시스템 콜 번호입니다. 37번 시스템 콜 번호는 어떻게 확인할 수 있을까요?

라즈베리파이에서 다음 해더 파일에서 확인할 수 있습니다.
root@raspberrypi:/usr/include/arm-linux-gnueabihf/asm/unistd.h
#define __NR_kill (__NR_SYSCALL_BASE+ 37)

kill() 함수 시스템 콜 번호는 37임을 알 수 있습니다.
 
1번째 줄 로그를 보겠습니다.
1 strace-894   [001] ....   328.965123: sys_enter: NR 37 (37f, 9, 0, 0, 881c0, 37f)          

ftrace로 시스템 콜 로그를 볼 때 다음과 같이 시스템 콜 핸들러로 전달된 아규먼트를 먼저 봐야 합니다.
(37f, 9, 0, 0, 881c0, 37f)

첫 번째 인자인 37f는 16진수 정수(십진수: 895)인데 종료하려는 프로세스 PID를 의미합니다. 두 번째 인자는 9입니다. 이는 시그널 전달 인자를 의미하며 프로세스를 강제 종료하는 정수값입니다.

다음 리눅스 커널 코드에서 9란 정수가 어떤 의미인지 알 수 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/uapi/asm/signal.h]
#define SIGKILL 9

프로세스를 강제 종료할 때 쓰는 매크로입니다.

2번째 줄 로그로 sys_kill() 함수는 0을 반환함을 알 수 있습니다.
2 strace-894   [001] ....   328.965141: sys_exit: NR 37 = 0


함수 인자와 반환값을 점검했으니 sys_kill() 함수 코드를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
2 {
3 struct siginfo info;
4
5 info.si_signo = sig;
6 info.si_errno = 0;
7 info.si_code = SI_USER;
8 info.si_pid = task_tgid_vnr(current);
9 info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
10
11 return kill_something_info(sig, &info, pid);
12 }

5~9번째 줄 코드는 시그널 속성을 struct siginfo 구조체에 저장합니다.
각각 속성 의미는 아래 주석문을 참고하시기 바랍니다.
5 info.si_signo = sig;  // 시그널 번호
6 info.si_errno = 0;
7 info.si_code = SI_USER;  // 시그널 유저 코드
8 info.si_pid = task_tgid_vnr(current); // 프로세스 PID
9 info.si_uid = from_kuid_munged(current_user_ns(), current_uid()); // 프로세스 UID

11번째 줄 코드를 보겠습니다.
11 return kill_something_info(sig, &info, pid);

kill_something_info() 함수를 호출합니다. 시그널 정보를 저장한 info 지역 변수를 두 번째 인자로 전달합니다.

다음 kill_something_info() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 static int kill_something_info(int sig, struct siginfo *info, pid_t pid)
2 {
3 int ret;
4
5 if (pid > 0) {
6 rcu_read_lock();
7 ret = kill_pid_info(sig, info, find_vpid(pid));
8 rcu_read_unlock();
9 return ret;
10 }
11
12 /* -INT_MIN is undefined.  Exclude this case to avoid a UBSAN warning */
13 if (pid == INT_MIN)
14 return -ESRCH;
15
16 read_lock(&tasklist_lock);
17 if (pid != -1) {
18 ret = __kill_pgrp_info(sig, info,
19 pid ? find_vpid(-pid) : task_pgrp(current));
20 } else {
21 int retval = 0, count = 0;
22 struct task_struct * p;
23
24 for_each_process(p) {
25 if (task_pid_vnr(p) > 1 &&
26 !same_thread_group(p, current)) {
27 int err = group_send_sig_info(sig, info, p);
28 ++count;
29 if (err != -EPERM)
30 retval = err;
31 }
32 }
33 ret = count ? retval : -ESRCH;
34 }
35 read_unlock(&tasklist_lock);
36
37 return ret;
38 }

함수 세 번째 인자로 전달된 pid가 0보다 크면 6~9번째 줄 코드를 실행합니다. 대부분 kill() 함수 호출을 수행하면 처리하는 실행 흐름이며 kill_pid_info() 함수를 호출합니다.

다음 13~14번째 줄 코드를 보겠습니다.
13 if (pid == INT_MIN)
14 return -ESRCH;

프로세스 PID가 INT_MIN이면 14번 줄 코드를 실행해서 –ESRCH를 반환해서 즉시 함수 실행을 종료하는 동작입니다.

다음 17~34번째 줄 코드를 보겠습니다. 
17 if (pid != -1) {
18 ret = __kill_pgrp_info(sig, info,
19 pid ? find_vpid(-pid) : task_pgrp(current));
20 } else {
21 int retval = 0, count = 0;
22 struct task_struct * p;
23
24 for_each_process(p) {
25 if (task_pid_vnr(p) > 1 &&
26 !same_thread_group(p, current)) {
27 int err = group_send_sig_info(sig, info, p);
28 ++count;
29 if (err != -EPERM)
30 retval = err;
31 }
32 }
33 ret = count ? retval : -ESRCH;
34 }

kill_something_info() 함수 5번째 줄 코드에서 이미 pid가 0보다 큰지 점검했습니다.
5 if (pid > 0) {
6 rcu_read_lock();
7 ret = kill_pid_info(sig, info, find_vpid(pid));
8 rcu_read_unlock();
9 return ret;
10 }

만약 pid가 0보다 크면 5~10번째 줄 코드를 실행하고 함수를 빠져 나갔을 것입니다. 따라서 17~34번째 줄 코드는 pid가 음수일 때 실행하는 코드임을 유추할 수 있습니다. 17~19번째 줄 코드는 pid가 음수, 나머지 20~34번째 줄 코드는 pid가 -1일 때 실행합니다.

17~19번재 줄 코드와 같이 pid가 -1보다 적으면 실행 중인 프로세스 그룹에 속한 스레드에게 시그널을 생성합니다. pid를 -1로 지정하면 현재 실행 중인 프로세스를 제외한 모든 프로세스에 시그널을 보냅니다. 

이렇게 kill 명령어로 pid 값에 따른 동작을 정리하면 다음과 같습니다.
명령어 설명
kill -9 [pid] pid 프로세스 종료
kill -9 -2 해당 프로세스가 속한 스레드 그룹 내 스레드를 종료
kill -9 -1 해당 프로세스 이외 모든 프로세스 종료 요청

다음  kill_pid_info() 함수를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 int kill_pid_info(int sig, struct siginfo *info, struct pid *pid)
2 {
3 int error = -ESRCH;
4 struct task_struct *p;
5
6 for (;;) {
7 rcu_read_lock();
8 p = pid_task(pid, PIDTYPE_PID);
9 if (p)
10 error = group_send_sig_info(sig, info, p);
11 rcu_read_unlock();
12 if (likely(!p || error != -ESRCH))
13 return error;
14
15 }
16 }

8번째 줄 코드를 보면 pid_task() 함수를 호출해서 pid에 대응하는 태스크 디스크립터 주소를 p란 지역 변수로 읽습니다.

pid에 대응하는 프로세스의 태스크 디스크립터 주소를 읽은 코드를 조금 더 짚어 보겠습니다. 우리는 “kill –SIGKILL [pid: 정수값]” 명령어로 프로세스를 종료시킬 수 있다고 알고 있습니다. 리눅스 커널에서 프로세스에 해당하는 정수 형 PID 값을 읽어서 태스크 디스크립터 주소로 변환하는 과정으로 pid에 해당하는 프로세스에게 시그널을 전달할 수 있는 것입니다.

pid를 처리하는 함수 코드 조각을 요약하면 다음과 같습니다.
1 static int kill_something_info(int sig, struct siginfo *info, pid_t pid)
2    ret = kill_pid_info(sig, info, find_vpid(pid));
3
4 int kill_pid_info(int sig, struct siginfo *info, struct pid *pid)
5    struct task_struct *p;
6    p = pid_task(pid, PIDTYPE_PID);

2번째 줄 코드를 보면 kill_pid_info() 함수 두 번째 인자로 find_vpid(pid) 함수를 호출해서 정수형 pid를 struct pid 구조체로 변환합니다. struct pid 구조체는 프로세스 PID를 관리하는 PID 객체입니다.

위 코드 조각 6번째 줄 코드를 보면 kill_pid_info() 함수로 전달된 3번째 인자인 pid를 pid_task() 함수에 전달해서 태스크 디스크립터 주소를 읽습니다. 여기서 pid는 정수형 PID가 아니라 kill_pid_info() 함수 두 번째 인자로 호출한 find_vpid(pid) 함수가 반환한 struct pid 구조체입니다.

이 방식으로 정수형 PID 값을 태스트 디스크립터 주소로 변환할 수 있습니다.

위 코드를 모아서 함수 하나를 작성해 보겠습니다. 다음과 같은 코드로 정수형 PID 값으로 프로세스 이름을 출력하고 태스크 디스크립터 주소를 읽을 수 있습니다.
1 struct task_struct * get_task_struct_with_pid(pid_t pid)
2      struct task_struct *p;
3      p = pid_task(find_vpid(pid), PIDTYPE_PID);
4        
5       if(p) {
6              printk("[+]task->comm: %s \n", p->comm);
7       }      
8       return p;
9 }

태스크 디스크립터 주소를 읽었으면 10번째 줄 코드와 같이 group_send_sig_info() 함수를 호출합니다.

12~13번째 줄 코드는 group_send_sig_info() 함수를 호출한 다음 실행합니다. kill_pid_info() 함수가 종료하는 조건을 점검하는데 group_send_sig_info() 함수가 -ESRCH를 반환하지 않으면 error 를 반환하면서 함수 실행을 종료합니다.

이어서 group_send_sig_info() 함수 코드를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 int group_send_sig_info(int sig, struct siginfo *info, struct task_struct *p)
2 {
3 int ret;
4
5 rcu_read_lock();
6 ret = check_kill_permission(sig, info, p);
7 rcu_read_unlock();
8
9 if (!ret && sig)
10 ret = do_send_sig_info(sig, info, p, true);
11
12 return ret;
}

6번째 줄 코드에서 check_kill_permission() 함수를 호출해서 시그널을 전달할 수 있는 권한을 점검합니다.


ps 명령어로 출력하는 프로세스 목록 중 "kill -9 [pid]" 명령어로 워커 스레드를 종료시키면 제대로 종료가 될까요?

커널 스레드인 워커 스레드는 종료되지 않습니다. kill 명령어를 실행하는 주인공은 bash란 유저 레벨 프로세스인데 커널 스레드 프로세스를 종료시키려고 했기 때문입니다. 이 check_kill_permission() 함수에서 권한을 점검하는 것입니다.

이렇게 권한을 점검한 후 시그널을 생성할 수 있는 조건이면 do_send_sig_info() 함수를 호출합니다. 다음 do_send_sig_info() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 int do_send_sig_info(int sig, struct siginfo *info, struct task_struct *p,
2 bool group)
3 {
4 unsigned long flags;
5 int ret = -ESRCH;
6
7 if (lock_task_sighand(p, &flags)) {
8 ret = send_signal(sig, info, p, group);
9 unlock_task_sighand(p, &flags);
10 }
11
12 return ret;
13}

9번째 줄 코드에서 send_signal() 함수를 호출합니다.

이어서 send_signal() 함수 코드를 보겠습니다. 
1 static int send_signal(int sig, struct siginfo *info, struct task_struct *t,
2 int group)
3 {
4 int from_ancestor_ns = 0;
5
6 #ifdef CONFIG_PID_NS
7 from_ancestor_ns = si_fromuser(info) &&
8    !task_pid_nr_ns(current, task_active_pid_ns(t));
9 #endif
10
11 return __send_signal(sig, info, t, group, from_ancestor_ns);
12 }

특별한 동작을 하는 함수는 아닙니다. 단지 __send_signal() 함수를 호출할 뿐입니다.

이번 소절에서는 유저 공간에서 kill() 함수를 호출하면 커널 공간에서 어떤 함수 흐름으로 시그널 인터페이스 함수를 호출하는지 알아봤습니다. 함수 목록을 다음과 같습니다.
SyS_kill
kill_pid_info
do_send_sig_info
__send_signal

이번 소절에서 함수 분석으로 시그널을 생성하기 전에 어떤 동작을 수행하는지 다음과 같이 알아 봤습니다.
1. 정수형 시그널 번호가 유효한지 점검
2. pid가 -1이면 시그널을 받을 수 있는 모든 프로세스에게 시그널 생성
3. 정수형 pid 값을 태스크 디스크립터 주소로 변환
4. 시그널을 생성하려는 프로세스가 시그널 생성 권한 점검

다음 소절에서는 tgkill() 함수를 유저 공간에서 실행하면 어떤 흐름으로 시그널 인터페이스 함수를 실행하는지 알아보겠습니다.

#Referene 시그널
시그널이란
시그널 설정은 어떻게 할까
시그널 생성 과정 함수 분석
프로세스는 언제 시그널을 받을까
시그널 전달과 처리는 어떻게 할까?
시그널 제어 suspend() 함수 분석 
시그널 ftrace 디버깅

"혹시 궁금점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답글 올려드리겠습니다!" 




핑백

덧글

댓글 입력 영역