Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

186162
807
85254


[리눅스커널][인터럽트] 인터럽트 벡터 분석하기 5장. 인터럽트 핸들링

5.3 인터럽트 핸들러는 언제 호출하나?

"인터럽트 핸들러는 언제 호출할까요?"라고 누군가 물어보면 인터럽트가 발생할 때 호출한다고 대답할 수 있습니다. "인터럽트가 발생하면 가장 먼저 어떤 코드가 실행될까?"라고 질문하면 어떻게 대답할 수 있을까요? 그동안 "인터럽트가 발생하면 실행하는 코드를 멈추고 실행 정보를 저장한다"라고 배웠습니다. 여기서 실행 중인 정보가 뭘까요? 이 정보를 어떻게 저장할까요?  

이런 궁금증을 해소하기 위해 이번 절에서는 인터럽트가 발생한 후 인터럽트 벡터에서 인터럽트 핸들러까지 실행 흐름을 살펴봅니다. 처리 과정을 요약하면 다음과 같습니다.
1. 인터럽트 벡터 실행
  : 실행 중인 프로세스 레지스터 세트를 프로세스 스택에 저장
2. 커널 인터럽트 공통 함수를 호출한 후 __handle_irq_event_percpu() 함수 실행
  : 인터럽트에 해당하는 인터럽트 디스크립터를 읽어 속성 정보 업데이트
3. 인터럽트 핸들러 함수 호출

5.3.1 인터럽트 벡터 분석하기

인터럽트가 발생하면 이를 처리하는 방식은 CPU 아키텍처에 의존적입니다. 라즈베리파이는 ARMv7 CPU를 탑재했으니 이 기준으로 인터럽트 처리 방식을 살펴봅시다.

인터럽트가 발생하면 다음과 같이 커널 모드와 유저 모드 별로 지정한 주소로 브랜치합니다.
 - __irq_svc: 커널 모드
 - __irq_usr: 유저 모드

이렇게 인터럽트가 발생하면 각 모드 별로 실행하는 __irq_svc와 __irq_usr와 같은 레이블을 인터럽트 벡터라고 부릅니다. 소프트웨어 관점으로 인터럽트가 발생하면 가장 먼저 봐야할 코드 분석의 출발점이 인터럽트 벡터입니다.

_irq_svc 인터럽트 벡터 동작 흐름 알아보기

커널 모드에서 인터럽트가 발생하면 실행하는 __irq_svc 레이블 중심으로 분석을 진행하겠습니다.

인터럽트가 발생하면 실행하는 __irq_svc 레이블 코드는 크게 다음 동작을 합니다.
 - 스택 공간에 실행 중인 프로세스 레지스터 세트를 푸시
 - handle_arch_irq 전역 변수에 지정한 함수인 bcm2836_arm_irqchip_handle_irq() 함수 호출 
       ; 라즈비안에서 handle_arch_irq 변수는 bcm2836_arm_irqchip_handle_irq()
         함수 주소를 지정  

_irq_svc 인터럽트 벡터 코드 분석하기

__irq_svc 레이블 어셈블리 코드는 다음과 같습니다.
https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/kernel/entry-armv.S]
01 807a2a20 <__irq_svc>:
02 807a2a20: e24dd04c sub sp, sp, #76 ; 0x4c 
03 807a2a24: e31d0004 tst sp, #4
04 807a2a28: 024dd004 subeq sp, sp, #4
05 807a2a2c: e88d1ffe stm sp,{r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip}
06 807a2a30: e8900038 ldm r0, {r3, r4, r5}
...
07 807a2a5c: e3a0147f mov r1, #2130706432 ; 0x7f000000
08 807a2a60: e5891008 str r1, [r9, #8]
09 807a2a64: e58d004c str r0, [sp, #76] ; 0x4c  
10 807a2a68: ebe93147 bl 801eef8c <trace_hardirqs_off>
11 807a2a6c: e59f1024 ldr r1, [pc, #36] ; 807a2a98 <__irq_svc+0x78>  
12 807a2a70: e1a0000d mov r0, sp
13 807a2a74: e28fe000 add lr, pc, #0
14 807a2a78: e591f000 ldr pc, [r1]    
15 807a2a7c: ebe930e3 bl 801eee10 <trace_hardirqs_on>
16 807a2a80: e59d104c ldr r1, [sp, #76] ; 0x4c  
17 807a2a84: e5891008 str r1, [r9, #8]
...
18 807a2a98: 80c089ac .word 0x80c089ac

__irq_svc 레이블에서 가장 먼저 실행하는 어셈블리 코드를 보겠습니다.
02 807a2a20: e24dd04c sub sp, sp, #76 ; 0x4c

여기서 sp는 현재 실행 중인 스택 주소를 뜻합니다. sub은 뺄셈 연산을 하는 어셈블리 명령어입니다. 현재 스택 주소에서 0x4C 만큼 빼는 연산은 스택 공간을 0x4C 바이트만큼 확보하는 동작입니다.

스택 주소를 0x4C만큼 빼는 연산이 왜 스택 공간을 0x4C 바이트만큼 확보하는 동작일까요? 이전 장에서 스택은 높은 주소에서 낮은 주소로 방향으로 자란다고 했습니다. 그래서 0x4C만큼 스택 주소를 빼는 겁니다. 만약 반대로 스택이 낮은 주소에서 높은 주소로 자라면 당연히 스택 주소에서 0x4C만큼 더해야 합니다. 왜 스택 공간을 확보할까요? 이는 스택 공간에 현재 실행 중인 프로세스의 레지스터를 저장하기 위해서 입니다.

다음 5 번째와 16 번째 줄 코드를 보겠습니다.
05 807a2a2c: e88d1ffe stm sp,{r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip}
..
16 807a2a80: e59d104c ldr r1, [sp, #76] ; 0x4c  

우선 stm이란 명령어가 보입니다. 좀 낯선 ARM 어셈블리 코드 같지만 자주 쓰는 명령어입니다. stm #A {#B, #C} 란 명령어는 #A가 위치한 메모리 공간에 #B와 #C를 저장하는 동작입니다. 05 번째 줄 코드를 실행하면 스택이 위치한 메모리 공간에 r1부터 ip 레지스터까지 저장합니다. 이후 16 번째 줄 코드까지 실행하면 다음 그림처럼 레지스터가 저장됩니다. 
 
  [그림 5.11] 레지스터 세트를 프로세스 스택에 푸시되는 과정

그동안 “인터럽트가 발생하면 현재 실행 중인 프로세스 정보를 저장한다”란 내용이 이 코드에 담겨 있습니다. 즉 인터럽트가 발생하기 전에 수행됐던 레지스터 세트를 스택 공간에 저장하는 것입니다.

다음 11 번째 줄 코드를 보겠습니다.
11 807a2a6c: e59f1024 ldr r1, [pc, #36] ; 807a2a98 <_irq_svc+0x78>  
...
18 807a2a98: 80c089ac .word 0x80c089ac

ldr이란 명령어는 다음 형식으로 씁니다. 
”ldr r1, [#A]”

#A 메모리 공간에 있는 값을 r1에 저장하는 동작입니다. 이 내용을 참고해 16 번째 줄 코드를 분석하면 807a2a98메모리 공간에 있는 0x80c089ac 값을 r1으로 로딩하는 동작입니다. 그런데 0x80c089ac 주소에는 handle_arch_irq 전역 변수가 있습니다. 

다음 14 번째 줄 코드를 보겠습니다.
14 807a2a78: e591f000 ldr pc, [r1]    

이 코드에서 handle_arch_irq란 전역 변수 위치가 가르키는 함수를 프로그램 카운터 즉 실행 주소로 저장합니다. 여기서 bcm2836_arm_irqchip_handle_irq() 함수를 호출합니다. 그 이유는 부팅할 때 handle_arch_irq란 변수에 bcm2836_arm_irqchip_handle_irq() 함수 주소를 저장하기 때문입니다. 

handle_arch_irq 전역 변수에 bcm2836_arm_irqchip_handle_irq() 함수 주소는 언제 설정할까요? 
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/irqchip/irq-bcm2836.c]
01 static int __init bcm2836_arm_irqchip_l1_intc_of_init(struct device_node *node,
02       struct device_node *parent)
03 {
...
04 bcm2836_arm_irqchip_smp_init();
05
06 set_handle_irq(bcm2836_arm_irqchip_handle_irq);

bcm2836(브로드컴 SoC 이름)의 인터럽트 콘트롤러를 초기화하는 bcm2836_arm_irqchip_l1_intc_of_init() 함수 06 번째 줄 코드를 보면 set_handle_irq() 함수를 bcm2836_arm_irqchip_handle_irq 인자로 호출합니다.

이어서 set_handle_irq() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/irq/handle.c]
01 int __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
02 {
03 if (handle_arch_irq)
04 return -EBUSY;
05
06 handle_arch_irq = handle_irq;
07 return 0;
08 }

06 번째 줄 코드와 같이 set_handle_irq() 함수 인자인 handle_irq를 handle_arch_irq 전역 변수에 저장합니다.

handle_arch_irq 전역 변수는 시스템 아키텍처 별로 인터럽트 콘트롤러 함수를 저장하는 인터페이스입니다. 대부분 ARM을 탑재한 칩셋은 gic_handle_irq() 함수를 handle_arch_irq로 등록해서 인터럽트 벡터 이후 인터럽트를 처리합니다. 하지만 라즈베리파이에 탑재된 bcm2836 (SoC: System On Chip)은 인터럽트 처리를 칩셋 구조에 맞게 설계한 것으로 보입니다. 이렇게 인터럽트를 처리하는 방식은 칩셋을 설계한 개발자에 따라 다릅니다.

인터럽트가 발생하면 인터럽트 벡터로 실행 흐름을 브랜치합니다. 이후 다음 동작을 합니다.
 - 인터럽트 벡터는 실행 중인 프로세스 레지스터 세트를 자신의 스택 공간에 푸시
 - bcm2836_arm_irqchip_handle_irq() 함수를 호출해 커널 인터럽트 내부 함수 호출 

위 동작 중 프로세스 스택 공간에 레지스터 세트를 저장하는 과정을 다음 소절에서 확인해보겠습니다.

# Reference (인터럽트 처리)




    핑백

    덧글

    댓글 입력 영역