Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

231224
1178
109353


IRQ Stack(ARM64) - Overview [Linux][Kernel] IRQ(Interrupt)

IRQ Stack란 뭘까요. IRQ가 쓰는 스택이라고 말할 수 있나요? 이번 시간에 IRQ Stack에 대한 내용을 조금 더 짚어 보겠습니다. 


IRQ Stack이란 IRQ가 떳을 때 사용하는 스택 공간입니다. 달리 설명하면 인터럽트 컨택스트로 코드가 실행될 때 스택에 지역 변수를 할당 받고 함수를 호출할 때 프레임 포인터 레지스터를 스택에 푸쉬합니다. 


프로세스가 생성될 때 스택 공간을 할당 받고 이 공간에서 코드가 실행됩니다. 스택이 어떻게 사용되는지 간단히 설명하면 아래와 같습니다. ARM32 비트 아키덱처에서 프로세스에게 0x2000 바이트 만큼 스택을 할당합니다.

아래 그림에서는 0xD000_2000 스택 Top 주소에서 함수가 호출되다가 인터럽트가 발생하면 같은 스택 공간에 인터럽트 벡터 함수로  점프합니다. 이후 인러텁트 서비스 루틴이 호출됩니다.

[스택주소]
0xD000_0000 // <<-- 스택 Bottom 주소
 // .. 생략 ...
0xD000_0FF0
0xD000_0FF8   D
0xD000_0FFC
0xD000_1000  irq_el1  //<<-- IRQ Stack Bottom 주소

// .. 생략 ...
0xD000_1FE8
0xD000_1FEC
0xD000_1FF0     C
0xD000_1FF4
0xD000_1FF8     B
0xD000_1FFC
0xD000_2000    A  //<-- 스택 Top 주소



이렇게 인터럽트가 발생하면 프로세스에 할당된 스택에서 인터럽트 서브 루틴을 실행하는데 그럼 왜 IRQ 용 스택을 따로 쓸까요? 그 이유는 ARM64 비트 아키텍처에서 커널 공간에서 쓰이는 스택 사이즈 0x4000 바이트가 충분하지 않다는 게 가장 큰 이유죠. 아래는 ARM32 비트 아키텍처에서 network packet을 처리하는 함수 흐름입니다. IPI call을 하는 동작인데, 콜 스택이 매우 깁니다.
-000|do_undefinstr()
-001|__und_svc_fault(asm)
 -->|exception
-002|__list_add(new = 0xEC4B3C80, prev = 0xEC4B3C80, next = 0xEC4B3BC8)  // <<-- kernel panic
-003|____napi_schedule(inline)
-003|rps_trigger_softirq()
-004|csd_unlock(inline)
-004|flush_smp_call_function_queue()
-005|handle_IPI() // ipinr = 4 //<<--IPI_TIMER
-006|gic_handle_irq()
-007|__irq_svc(asm)
 -->|exception
-008|__preempt_count_sub(inline)
-008|preempt_count_sub()
-009|should_resched(inline)
-009|__local_bh_enable_ip()
-010|qtaguid_mt()
-011|ipt_do_table()
-012|nf_iterate()
-013|nf_hook_slow()
-014|NF_HOOK_THRESH(inline)
-014|NF_HOOK(inline)
-014|ip_local_deliver()
-015|ip_rcv_finish()
-016|__netif_receive_skb_core()
-017|rcu_read_unlock(inline)
-017|process_backlog()
-018|static_key_count(inline)
-018|static_key_false(inline)
-018|trace_napi_poll(inline)
-018|net_rx_action()
-019|static_key_count(inline)
-019|static_key_false(inline)
-019|trace_softirq_exit(inline)
-019|__do_softirq()
-020|do_softirq_own_stack(inline)
-020|invoke_softirq(inline)
-020|irq_exit()
-021|handle_IPI() // ipinr = 4 //<<--IPI_TIMER
-022|gic_handle_irq()
-023|__irq_svc(asm) 
 -->|exception
-024|arch_local_irq_restore(inline)
-024|__slab_alloc.constprop.8()
-025|slab_alloc_node(inline)
-025|slab_alloc(inline)
-025|kmem_cache_alloc()
-026|new_handle(inline)
-026|jbd2__journal_start()
-027|__ext4_journal_start_sb()
-028|ext4_da_write_begin()
-029|generic_perform_write()
-030|__generic_file_write_iter()
-031|ext4_file_write_iter()
-032|new_sync_write()
-033|vfs_write()
-034|sdcardfs_write()
-035|vfs_write()
-036|SYSC_write(inline)
-036|sys_write()
-037|ret_fast_syscall(asm)

위 코드가 ARM64 비트 아키텍처에서 수행되면 스택 오버플로우가 발생합니다. 그래서 각 CPU별로 IRQ Stack 공간을 부팅 도중 따로 할당해 놓고 인터럽트가 발생할 때 마다 IRQ 스택에서 함수를 호출합니다.

IRQ Stack 공간은 per-cpu 타입 irq_stack 변수로 확인할 수 있습니다.

아래 코드를 보면 IRQ Stack irq_stack이란per-cpu 타입 변수로 처리된다는 사실을 있구요. IRQ Stack 사이즈는0x4000이군요.

staticinline bool on_irq_stack(unsigned long sp, int cpu)

{

/* variable names the same as kernel/stacktrace.c */

unsigned long low = (unsigned long)per_cpu(irq_stack, cpu);

unsigned long high = low + IRQ_STACK_START_SP; //IRQ_STACK_START_SP = 0x3FFC

 

return (low <= sp && sp <= high);

}

 
이제 좀 더 깊게 프로세스가 인터럽트가 발생하면 IRQ Stack으로 이동하며 코드를 실행하는지 분석 하겠습니다.
IRQ가 Trigger되면 el1_irq란 벡터 주소로 점프합니다.
el1_irq:
kernel_entry 1
enable_dbg
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_off
#endif

get_thread_info tsk
irq_handler  //<<--
 
위 코드에서 irq_handler는 사실 매크로로 정의됐습니다. 네번 째 줄에 irq_stack_entry 코드를 볼 수 있습니다.
.macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry
blr x1
irq_stack_exit
.endm

.text

아래 irq_stack_entry  코드가 인터럽트가 발생하면 IRQ Stack으로 스택 실행 공간을 이동하는 핵심 코드입니다.

[1]: 해당 CPU에 해당 하는 irq_stack 주소를 가져와 x25에 저장합니다.
[2]: x26에 IRQ Stack 사이즈를 대입하네요.
[3]: x26에 irq_stack 주소(x25)에 스택 사이즈(x26)를 더해요.
[4]: 스택 주소를 IRQ Stack 주소로 바꾸네요.
[5]: 원래 스택 주소가 담겨져 있는 x19와 x29를 IRQ Stack bottom address에서 -0x10 만큼 떨어진 위치에 Push하네요.
.macro irq_stack_entry
mov x19, sp // preserve the original sp

/*
* Compare sp with the current thread_info, if the top
* ~(THREAD_SIZE - 1) bits match, we are on a task stack, and
* should switch to the irq stack.
*/
and x25, x19, #~(THREAD_SIZE - 1)
cmp x25, tsk
b.ne 9998f

this_cpu_ptr irq_stack, x25, x26  //<<--[1]
mov x26, #IRQ_STACK_START_SP //<<--[2]
add x26, x25, x26 //<<--[3]

/* switch to the irq stack */
mov sp, x26 //<<--[3]

/*
* Add a dummy stack frame, this non-standard format is fixed up
* by unwind_frame()
*/
stp     x29, x19, [sp, #-16]!  //<<--[4]
mov x29, sp

정리하면 인터럽트가 발생하면 irq_stack_entry 매크로 코드가 실행되어 인터럽트 서비스 루틴을 처리하기 위한 전용 인터럽트 스택 주소로 점프합니다. 이렇게 인터럽트는 아키텍처와 연관된 동작이 많은 것 같습니다.

다음에는 실제 코어 덤프에서 IRQ Stack이 어떻게 Push되는지 점검해볼께요


Reference(프로세스 관리)
4.9 프로세스 컨택스트 정보는 어떻게 저장할까?
 4.9.1 컨택스트 소개
 4.9.2 인터럽트 컨택스트 정보 확인하기
 4.9.3 Soft IRQ 컨택스트 정보 확인하기
 4.9.4 선점 스케줄링 여부 정보 저장
4.10 프로세스 디스크립터 접근 매크로 함수
 4.10.1 current_thread_info()
 4.10.2 current 매크로란
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적 

덧글

댓글 입력 영역