ARM Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

0239
1625
172602


[리눅스커널] 시그널: handle_signal() 함수와 시그널 핸들러 호출 코드 분석하기 12. 시그널

우리는 유저 공간에서 시그널 핸들러를 설정하면 다음과 같이 동작한다고 알고 있습니다.

    해당 시그널이 발생하면 지정한 시그널 핸들러가 실행된다.

이렇게 시그널이 발생했을 때 해당 시그널 핸들러를 호출하는 동작을 수행하는 실체는 handle_signal() 함수입니다.

handle_signal() 함수 분석에 앞서 어떤 조건으로 handle_signal() 함수를 호출하는지 알아볼까요?  get_signal() 함수를 호출하는 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 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() 함수를 시작으로 유저 프로세스가 등록한 시그널 핸들러를 실행하는 함수 흐름은 다음과 같습니다.

 
[그림 12.12] 시그널 핸들러 실행 전체 흐름도

위 전체 흐름도의 전제 조건은 RPi_signal 프로세스가 pause() 함수에서 시그널을 기다리며 휴면 상태에 있었다는 것입니다.

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

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

setup_return() 함수는 다음 기능을 수행합니다.
시그널 정보 구조체에서 시그널 핸들러 함수 주소를 읽음
유저 공간에서 복귀하면 바로 시그널 핸들러를 실행하도록 struct pt_regs 구조체 ARM_pc 필드에 시그널 핸들러 함수 주소를 써줌. 여기서 struct pt_regs 구조체는 유저 공간에서 레지스터 세트 정보를 의미함

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

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

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

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

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

우리는 유저 어플리케이션에서 시그널 핸들러를 등록하면 당연히 시그널 핸들러가 제대로 실행된다고 가정하고 프로그램을 작성합니다. 하지만 커널이 유저 어플리케이션에 지정한 시그널 핸들러를 호출하는 과정은 상당히 복잡합니다. 또한 ARM 아키텍처 관련 동작이 상당히 많습니다.
1. 커널 스택 공간에 저장된 프레임 정보를 유저 공간에 백업한 후 다시 커널 공간으로 이동 후 복사
2. 프로그램 카운터에 시그널 핸들러 주소 복사 
3. 시그널 핸들러 호출 후 시스템 콜을 발생해 다시 커널 공간으로 복귀

이렇게 실행 흐름이 복잡하니 코드를 자세히 분석하기 전에 전체 흐름을 먼저 파악할 필요가 있습니다.  

handle_signal() 함수 분석하기

다음은 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 구조체 크기만큼 유저모드 스택에 추가 공간을 할당합니다.  

    이런 처리를 하는 이유는 무엇일까요?

이 질문에 대답을 하기 앞서 먼저 시스템 콜이 발생하면 유저 프로세스 레지스터 세트를 저장하는 과정에 대해 살펴보겠습니다. 시스템 콜로 유저 공간에서 커널 공간으로 이동하면 커널 공간 프로세스 스택 최하단 주소에 유저 프로세스 실행 정보가 담긴 레지스터 세트를 저장합니다. 시스템 콜 핸들러 처리를 마무리 한 후 커널 공간에서 다시 유저 공간으로 복귀할 때 커널 프로세스 스택 최하단 주소에 저장된 백업한 레지스터 세트를 로딩합니다.

그런데 이 레지스터 세트를 유저 공간 스택 공간을 잡아서 다시 백업하겠다는 것입니다.

이런 처리를 하는 이유는 다음과 같습니다. 

    시그널 핸들러를 호출한 다음에 이전에 실행했던 유저 프로세스 레지스터 세트 정보를 
    다시 커널 공간 프로세스 스택 최하단 주소 공간으로 백업한다.

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

프로그램 카운터에 시그널 핸들러 함수 주소 저장

다음 setup_return() 함수를 분석합시다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm64/kernel/signal.c]
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 }

여기서 struct pt_regs 구조체 *regs는 유저 공간에서 실행할 프로세스 레지스터 세트를 의미합니다.

먼저 5번째 줄 코드를 보겠습니다.
5 regs->ARM_r0 = ksig->sig;

ARM_r0 필드에 시그널 번호를 저장합니다. 이 코드가 실행돼 유저 공간 시그널 핸들러 첫 번째 인자로 시그널 번호가 전달되는 것입니다.

다음 7~8번째 줄 코드를 봅시다.
7 regs->ARM_lr = retcode;
8 regs->ARM_pc = handler;

ARM_lr 필드에는 시그널 핸들러를 실행하고 다시 커널 공간으로 복귀할 코드 주소를 저장하고 ARM_pc 필드에 시그널 핸들러 주소를 저장합니다.

유저 프로세스가 실행 도중 시그널 핸들러를 호출되는 핵심 코드입니다. 

시그널 핸들러 호출 후 복귀하는 sys_sigreturn() 함수 처리 흐름

시그널 핸들러 실행 후 유저 모드로 복귀할 코드는 sigreturn_codes 배열로 관리합니다. 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 레지스터에 119번 시스템 콜 번호를 저장하고 "svc 0x0" 명령어를 실행해 다시 커널 모드로 진입합니다.

119번에 해당하는 POSIX 시스템 콜은 sigreturn이며 커널 공간에서 sys_sigreturn() 함수를 실행합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/kernel/signal.c]
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() 함수를 실행해 다음과 같은 처리를 합니다. 

    이전에 유저 모드 스택에 저장해놓은 원래 프로세스 스택 최하단 주소에 저장했던 유저 
   모드 레지스터 세트를 다시 프로세스 스택 최하단 주소에 다시 복구한다.

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

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




핑백

덧글

댓글 입력 영역