Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

29137
1186
128674


ARM32 - 프로세스(Process) preempt_disable(), preemption 스케줄(Schedule) 조건 분석 4. 프로세스(Process) 관리

저번 시간에 preempt_disable() 함수 호출로 커널 패닉이 일어나는 문제가 있었는데요.
이 매크로 함수와 preemption의 관계에 대해서 좀 더 알아보고자 해요.

preemption은 리눅스 커널 핵심 개념 중의 핵심이죠.

preempt_disable() 함수를 호출하면 스택 주소를 꺼내와서, current_thread_info에 있는 preempt_count에 1을 더하는 짓만 하거든요.

그럼 current_thread_info의 preempt_count를 어떻게 접근하냐구요?
어떤 프로그램이 돌던 스택 주소를 접근하면 해당 current_thread_info를 가져올 수 있어요.
static inline struct thread_info *current_thread_info(void)
{
register unsigned long sp asm ("sp");
return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));  // THREAD_SIZE = 0x2000
}

실제 덤프에서 스택 주소를 아무거나 입력하면 해당 current_thread_info를 가져올 수 있어요.
v.v %all (struct thread_info*)([아무런 스택 주소] & ~0x1fff) 

v.v %all (struct thread_info*)(0xD20C7F80 & ~0x1fff)
  (struct thread_info *) (struct thread_info*)(0xD20C7F80 & ~0x1fff) = 0xD20C6000 -> (
    (long unsigned int) flags = 0x0,
    (int) preempt_count = 0x1, //<<--
    (mm_segment_t) addr_limit = 0xBF000000,
    (struct task_struct *) task = 0xD4AA9600,
    (struct exec_domain *) exec_domain = 0xC1815CF8,
    (__u32) cpu = 0x0,

v.v % (struct thread_info*)(0xDB1EC348 & ~0x1fff)    
  (struct thread_info *) (struct thread_info*)(0xDB1EC348 & ~0x1fff) = 0xDB1EC000 
  (struct thread_info*) = 0xDB1EC000
    flags = 0x2,
    preempt_count = 0x00010302, <<--// HARDIRQ_OFFSET | SOFTIRQ_OFFSET X 3|  PREEMPT_OFFSET
    addr_limit = 0xBF000000,
    task = 0xDD72E540,
    exec_domain = 0xC181B35C,
    cpu = 0x3,  

그럼 어떤 프로세가 돌고 있는데 커널이 언제 제어권을 획득할 수 있을까요?
크게 보면 IRQ와 시스템 콜이 실행될 때라고 말할 수 있는데요.

음, 갑자기 어떤 선배가 자신을 한탄하면서 했던 말이 떠오르네요.
나이 40 줄 먹은 개발자는 언제 짤릴지 모른다.(그러니 실력을 키우자.)

커널 프로그램은 언제 도중에 실행이 멈출 지 모르죠.  IRQ가 Trigger되면요.

IRQ가 뜨면 실행 중인 프로그램은 바로 동작을 멈추고 IRQ vector tabel로 점프를 해요.
모든 프로그램은 언제 실행이 멈출 지 모르는 상태에서 돌고 있는 거죠. 맞나요? 항상 그런 건 아니죠.

그럼 irq vector 함수로 가봅시다.

현재 스택 주소가 0xD00C7034 라고 가정해볼께요.

아래 [1], [2], [3] 번 코드가 현재 실행 중인 코드의 스택 주소를 통해 preempt_count를 가져오는 코드거든요.

[1]:  0xD00C7034 >> 0xD(13) = 0x00068063
      R9가 0x00068063으로 업데이트 되는군요. 참고로 lsr 명령어는 해당 값 만큼 Right Shift연산을 하고 나머지 값을 0을 채우죠.
      
[2]:  0x00068063 <<  0xD(13) = 0xD00C6000
      R9가 스택 Top 주소로 업데이트 되었어요.
      
      v.v (struct thread_info*)(0xD00C7034 & ~0x1fff) 이 T32 명령어와 똑같다고 할 수 있죠.   

[3]: struct thread_info.preempt_count 멤버는 0x8 만큼 오프셋이 떨어져 있거든요.
     preempt_count값이 r8로 업데이트 됩니다.

[4]: preempt_count값이 0x0이면 svc_preempt를 호출해서 preemption을 실행하구요.
아니면 바로 빠져나오는 거죠.
     
0xc0fde6c0 <__irq_svc>: sub     sp, sp, #76     ; 0x4c
0xc0fde6c4 <__irq_svc+0x4>:     tst     sp, #4
// .. 생략..
0xc0fde710 <__irq_svc+0x50>:    ldr     pc, [r1]
0xc0fde714 <__irq_svc+0x54>:    lsr     r9, sp, #13  //<<--[1]
0xc0fde718 <__irq_svc+0x58>:    lsl     r9, r9, #13  //<<--[2]
0xc0fde71c <__irq_svc+0x5c>:    ldr     r8, [r9, #4]  //<<--[3]
0xc0fde720 <__irq_svc+0x60>:    ldr     r0, [r9]
0xc0fde724 <__irq_svc+0x64>:    teq     r8, #0
0xc0fde728 <__irq_svc+0x68>:    movne   r0, #0
0xc0fde72c <__irq_svc+0x6c>:    tst     r0, #2
0xc0fde730 <__irq_svc+0x70>:    blne    0xc0fde750 <svc_preempt>  //<<--[4]
0xc0fde734 <__irq_svc+0x74>:    ldr     r0, [sp, #72]   ; 0x48
0xc0fde738 <__irq_svc+0x78>:    mcr     15, 0, r0, cr3, cr0, {0}
0xc0fde73c <__irq_svc+0x7c>:    msr     SPSR_fsxc, r5
0xc0fde740 <__irq_svc+0x80>:    sub     r0, sp, #4
0xc0fde744 <__irq_svc+0x84>:    strex   r1, r2, [r0]

아주 어렵게 분석을 했는데요. __irq_svc 함수 전체의 기능을 요약하면 아래와 같아요.
1> irq_handler를 호출해서 IRQ 처리
[1] 번 코드에서 실행되요. irq_handler 매크로 정의대로 gic_handle_irq함수 호출이 되죠. (handle_arch_irq)

2> current_thread_info에 있는 preempt_count값을 보고 값이 0x0이면 preemption을 하는거죠.
[2]~[5]: 위에서 계속 분석한 코드죠.

0xc0fde6c0 <__irq_svc>: sub     sp, sp, #76     ; 0x4c
0xc0fde6c4 <__irq_svc+0x4>:     tst     sp, #4
0xc0fde6c8 <__irq_svc+0x8>:     subeq   sp, sp, #4
0xc0fde6cc <__irq_svc+0xc>:     stm     sp, {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}
0xc0fde6d0 <__irq_svc+0x10>:    ldm     r0, {r3, r4, r5}
0xc0fde6d4 <__irq_svc+0x14>:    add     r7, sp, #48     ; 0x30
0xc0fde6d8 <__irq_svc+0x18>:    mvn     r6, #0
0xc0fde6dc <__irq_svc+0x1c>:    add     r2, sp, #76     ; 0x4c
0xc0fde6e0 <__irq_svc+0x20>:    addeq   r2, r2, #4
0xc0fde6e4 <__irq_svc+0x24>:    push    {r3}            ; (str r3, [sp, #-4]!)
0xc0fde6e8 <__irq_svc+0x28>:    mov     r3, lr
0xc0fde6ec <__irq_svc+0x2c>:    stm     r7, {r2, r3, r4, r5, r6}
0xc0fde6f0 <__irq_svc+0x30>:    mrc     15, 0, r0, cr3, cr0, {0}
0xc0fde6f4 <__irq_svc+0x34>:    str     r0, [sp, #72]   ; 0x48
0xc0fde6f8 <__irq_svc+0x38>:    mov     r0, #81 ; 0x51
0xc0fde6fc <__irq_svc+0x3c>:    mcr     15, 0, r0, cr3, cr0, {0}
0xc0fde700 <__irq_svc+0x40>:    isb     sy
0xc0fde704 <__irq_svc+0x44>:    ldr     r1, [pc, #64]   ; 0xc0fde74c  //<<--[1]
0xc0fde708 <__irq_svc+0x48>:    mov     r0, sp
0xc0fde70c <__irq_svc+0x4c>:    add     lr, pc, #0
0xc0fde710 <__irq_svc+0x50>:    ldr     pc, [r1]
0xc0fde714 <__irq_svc+0x54>:    lsr     r9, sp, #13  //<<--[2]
0xc0fde718 <__irq_svc+0x58>:    lsl     r9, r9, #13  //<<--[3]
0xc0fde71c <__irq_svc+0x5c>:    ldr     r8, [r9, #4] //<<--[4]
0xc0fde720 <__irq_svc+0x60>:    ldr     r0, [r9]
0xc0fde724 <__irq_svc+0x64>:    teq     r8, #0
0xc0fde728 <__irq_svc+0x68>:    movne   r0, #0
0xc0fde72c <__irq_svc+0x6c>:    tst     r0, #2
0xc0fde730 <__irq_svc+0x70>:    blne    0xc0fde750 <svc_preempt> //<<--[5]

어셈블 코드만 보니 지겹다구요? 이제 인라인 어셈블코드 좀 볼께요. 이제 눈에 좀 잘들어오죠?
[kernel/arch/arm/kernel/entry-armv.S]
__irq_svc:
svc_entry
irq_handler

#ifdef CONFIG_PREEMPT
get_thread_info tsk
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
ldr r0, [tsk, #TI_FLAGS] @ get flags
teq r8, #0 @ if preempt count != 0
movne r0, #0 @ force flags to 0
tst r0, #_TIF_NEED_RESCHED
blne svc_preempt
#endif

정리 좀 할께요.
1> 어떤 코드가 실행되던 IRQ는 Trigger될 수 있다. (조건: local_irq_disable만 호출하지 않은 상태라면)
2> IRQ 핸들러가 실행된 다음에, current_thread_info에 있는 preempt_count값을 보고 값이 0x0이면 preemption한다.
2.1> 만약 preempt_disable()이 호출되어 preempt_count값이 0보다 크면 preemption을 안한다.

그럼, local_irq_disable() 함수를 실행하면 어떻게 될까요? 이 함수를 실행하면 "cpsid i" 인스터럽션이 실행되어 하드웨어적으로 IRQ가 안 뜬다고 할 수 있는데요. 
IRQ가 안 뜨니 __irq_svc vector로 점프를 하지도 않을 터이고, preemption이 일어나지 않겠죠.
static inline __attribute__((always_inline)) __attribute__((no_instrument_function)) void arch_local_irq_disable(void)
{
 asm volatile(
  " cpsid i @ arch_local_irq_disable"
  :
  :
  : "memory", "cc");
}

local_irq_disable() 함수는 정말 조심해서 써야 해요. 

IRQ가 안 뜨면 시스템이 느려져 이상 동작(유식한 용어로 latency라고 하죠)을 할꺼에요.
한번 해보세요.

+local_irq_disable()
  schedule();
+local_irq_enable()  

덧글

댓글 입력 영역