Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

85235
1036
103655


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

유저 공간에서 tgkill 명령어를 실행하면 시스템 콜 핸들러인 sys_tgkill() 함수를 호출합니다. 이후 어떤 함수 흐름으로 동작하는지 sys_kill() 함수를 점검합시다.
 

먼저 sys_tgkill() 함수 선언부와 함수 인자를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/syscalls.h]
asmlinkage long sys_tgkill(pid_t tgid, pid_t pid, int sig);

인자 속성
pid_t tgid 시그널을 전달받을 스레드 그룹 프로세스의 PID
pid_t pid 시그널을 전달받을 프로세스 PID
int sig 정수형 시그널 번호

다음으로 sys_tgkill() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 SYSCALL_DEFINE3(tgkill, pid_t, tgid, pid_t, pid, int, sig)
2 {
3 /* This is only valid for single tasks */
4 if (pid <= 0 || tgid <= 0)
5 return -EINVAL;
6
7 return do_tkill(tgid, pid, sig);
8 }

먼저 예외 처리를 수행하는 4~5번째 줄 코드를 보겠습니다.
4 if (pid <= 0 || tgid <= 0)
5 return -EINVAL;

pid나 tgid가 0보다 같거나 작으면 5번째 코드를 실행합니다. -EINVAL이란 정수형 매크로를 반환하면서 함수 실행을 종료합니다.

4~5번째 줄 코드와 같이 pid와 tgid 인자 예외 조건을 점검한 다음 do_tkill() 함수를 호출합니다.

다음으로 do_tkill() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/signal.c]
01 static int do_tkill(pid_t tgid, pid_t pid, int sig)
02 {
03 struct siginfo info = {};
04
05 clear_siginfo(&info);
06 info.si_signo = sig;
07 info.si_errno = 0;
08 info.si_code = SI_TKILL;
09 info.si_pid = task_tgid_vnr(current);
10 info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
11
12 return do_send_specific(tgid, pid, sig, &info);
13 }

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

info란 지역변수에 시그널 정보를 저장한 다음 11번째 줄 코드에서 do_send_specific() 함수를 호출합니다.

이번에 do_send_specific() 함수 코드를 볼 차례입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 static int
2 do_send_specific(pid_t tgid, pid_t pid, int sig, struct siginfo *info)
3 {
4 struct task_struct *p;
5 int error = -ESRCH;
6
7 rcu_read_lock();
8 p = find_task_by_vpid(pid);
9 if (p && (tgid <= 0 || task_tgid_vnr(p) == tgid)) {
10 error = check_kill_permission(sig, info, p);
11
12 if (!error && sig) {
13 error = do_send_sig_info(sig, info, p, false);
14
15 if (unlikely(error == -ESRCH))
16 error = 0;
17 }
18 }
19 rcu_read_unlock();
20
21 return error;
22 }

먼저 8번째 줄 코드를 보겠습니다.
8 p = find_task_by_vpid(pid);

find_task_by_vpid() 함수를 호출해서 정수형 pid인자를 태스크 디스크립터 주소를 *p란 지역 변수에 저장합니다.


find_task_by_vpid() 함수는 정수형 PID를 태스크 디스크립터로 변환하는 유용한 함수이니 잘 알아 둡시다.


다음 9~10번째 줄 코드를 보겠습니다.
9 if (p && (tgid <= 0 || task_tgid_vnr(p) == tgid)) {
10 error = check_kill_permission(sig, info, p);

9번째 줄 코드는 현재 실행 중인 프로세스의 태스크 디스크립터 정보로 스레드 그룹 리더인지 점검합니다. 이 조건을 만족하면 10번째 줄 코드를 실행합니다

다음 10번째 줄 코드에서 check_kill_permission() 함수 호출로 시그널을 전달할 권한 유무를 점검합니다. 이렇게 시그널 생성 요청 전 check_kill_permission() 함수를 호출해서 시그널을 생성할 권한을 체크합니다.

다음 12~13번째 줄 코드입니다.
12 if (!error && sig) {
13 error = do_send_sig_info(sig, info, p, false);

시그널을 생성할 권한이 있고 시그널이 유효한 지 점검한 후 do_send_sig_info() 함수를 호출합니다.

다음 do_send_sig_info() 함수 코드를 보겠습니다.
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 }

8번째 줄 코드와 같이 send_signal() 함수를 호출해서 시그널을 생성을 요청합니다.

Trace32로 위와 같은 코드가 어떻게 실행했는지 확인하면 다음과 같은 콜 스택을 확인할 수 있습니다.
1 -000|__send_signal (sig = 1865235760, info = 0xAF2D3E40)
2 -001|do_send_sig_info(sig = 6, info = 0xAF2D3E40, p = 0x8F2C8000, ?)
3 -002|do_send_specific(tgid = 748, ?, sig = 6, info = 0xAF2D3E40)
4 -003|do_tkill(tgid = 748, pid = 748, sig = 6)
5 -004|sys_tgkill(?, ?, ?)
6 -005|ret_fast_syscall(asm)
7  -->|exception
8 -006|tgkill(asm)
9 -007|pthread_kill(?, sig = 6)
10 -008|raise(sig = 748)
11 -009|abort()

위 콜 스택은 __send_signal() 함수에 브래이크 포인트를 걸고 잡은 것입니다.

콜 스택 흐름을 살펴보면, 유저 공간에서 abort() 함수가 실행한 다음 tgkill() 함수가 호출됩니다. 이후 커널 공간으로 스위칭해서 sys_tgkill() 함수를 호출합니다. Pid는 748이고 전달하려는 시그널 번호는 6이므로 SIGIOT 시그널을 전달함을 알 수 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/uapi/asm/signal.h]
#define SIGIOT 6

특정 프로세스를 종료시키는 시그널을 전달할 때 tkill 명령어를 실행합니다.

이 함수 선언부를 보면서 인자값을 확인합시다.
SYSCALL_DEFINE2(tkill, pid_t, pid, int, sig)

pid_t pid: 종료시키려는 프로세스 PID
int sig: 시그널 번호

다음 함수 코드를 보겠습니다.
1 SYSCALL_DEFINE2(tkill, pid_t, pid, int, sig)
2 {
3 /* This is only valid for single tasks */
4 if (pid <= 0)
5 return -EINVAL;
6
7 return do_tkill(0, pid, sig);
8 }

4~5번째 줄 코드는 pid가 0보다 작을 때 -EINVAL 에러 값을 반환하면서 함수를 종료합니다.

이후 7번째 줄 코드와 같이 do_tkill() 함수를 호출합니다.


이번 소절에서는 유저 공간에서 kill()/tgkill() 함수를 호출할 때 어떤 흐름으로 시그널 인터페이스 함수에 접근하는지 알아 봤습니다. 시그널 인터페이스 함수에서는 다음과 같이 공통으로 시그널 생성을 위한 전처리 동작을 수행합니다.
1. 시그널을 생성할 권한 점검
2. pid나 tgid가 유효한 값인지 점검

여기까지 유저 공간에서 시그널을 생성하는 함수 흐름을 알아봤습니다. 

여기서 한 가지 의문이 생깁니다. 시그널은 유저 공간에서만 생성할 수 있을까요? 그렇지 않습니다. 커널 스스로 시그널을 생성할 상황이면 스스로 시그널을 생성합니다. 이 동작은 다음 소절에서 알아 보겠습니다.

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

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




핑백

덧글

댓글 입력 영역