Reproducing AArch64 Kernel Exceptions

Oct 9, 2026

연휴를 맞아 몇년 동안 책장에 모셔 놨던 《임베디드 레시피》를 꺼냈다. 이직 후 구매했다가 바쁘다는 핑계로 펼쳐 보지도 못했는데 막상 읽어 보니 ARM AArch32 아키텍처 부분을 제외하고 유용한 실무 내용이 많았다. 그 중에서 마지막 장〈How to debug〉은 AArch32 아키텍처 기준 Exception 상황 몇 가지를 소개하는데 AArch64 와 다른 내용이 꽤 있었다.

  1. Interrupt Lock에 의한 Watch dog Reset
  2. Task Lock에 의한 Watch dog Reset
  3. Data abort (invalid write)
  4. Stack Corruption과 Prefetch Abort
  5. Abort in ISR
  6. Branch/go to address 0
  7. Watch dog Reset in ISR

이들을 Yocto Linux 를 이용해 AArch64 QEMUARM64 에뮬레이션 환경에서 테스트할 수 있도록 하나씩 커널 모듈 코드로 작성해보자.

Interrupt Lock에 의한 Watch dog Reset

아래 코드는 local_irq_disable() 로 현재 CPU의 IRQ를 비활성화한 뒤 대기한다. 결국 타이머 인터럽트 처리 지연으로 watchdog timeout이 발생해 리셋된다.

static noinline void kfault_irq_lock(void)
{
	pr_emerg("kfault-lab: irq-lock: disabling local IRQs and spinning\n");
	local_irq_disable();
	for (;;)
		cpu_relax();
}

선점 비활성화 및 Watch dog Reset

IRQ 비활성화 뿐 아니라 선점 비활성화도 Watch dog 리셋 원인이 될 수 있다. 앞선 IRQ 비활성화와 달리 preempt_disable() 은 인터럽트는 발생하지만 무한 루프를 도는 태스크로 인해 CPU의 다른 태스크 처리가 지연되면서 리셋이 발생한다.

static noinline void kfault_task_lock(void)
{
	pr_emerg("kfault-lab: task-lock: preemption disabled, IRQs enabled\n");
	preempt_disable();
	for (;;)
		cpu_relax();
}

마찬가지로 GDB를 이용해 살펴보면 아래와 같이 루프 지점을 찾아낼 수 있다.

(remote) gef➤  x/20i $pc-12
   0xffff80007987044c:  ldr     w0, [x1, #8]
   0xffff800079870450:  add     w0, w0, #0x1
   0xffff800079870454:  str     w0, [x1, #8]
=> 0xffff800079870458:  yield
   0xffff80007987045c:  b       0xffff800079870458
   0xffff800079870460:  .inst   0x80fceee8 ; undefined
   0xffff800079870464:  .inst   0xffff8000 ; undefined
   0xffff800079870468:  mov     x9, x30

Data abort (invalid write)

Data Abort는 잘못된 주소 변환, 접근 권한으로 발생한다. 아래의 코드는 유효하지 않은 주소 공간에 강제로 데이터를 write 하여 발생하는 경우다.

  static noinline void kfault_data_abort(void)
  {
  	volatile u32 *bad = (volatile u32 *)0xdeadbeefUL;

  	pr_emerg("kfault-lab: triggering data abort at %px\n", bad);
  	*bad = 0x4b464c54;
  }

Stack corruption

Stack corruption은 잘못된 스택 영역 할당 및 call depth 로 발생한다. 아래 코드는 재귀호출을 이용해 stack corruption 상황을 유발한다. 참고로 barrier_data(frame) 은 Tail Call Optimization(새로운 스택 프레임을 생성하지 않고 현재 스택 프레임을 사용하도록 최적화 하는 기법)을 막기 위해 사용된다. 또한 CONFIG_VMAP_STACK 을 사용하는 arm64 커널에서는 스택 경계의 매핑되지 않은 페이지(guard page)에 접근하면 Data abort가 발생한다.

  static noinline void kfault_stack_overflow(unsigned int depth)
  {
  	volatile u8 frame[1024];

  	frame[0] = depth;
  	if ((depth & 0x7) == 0) {
  		pr_emerg("kfault-lab: stack-overflow depth=%u\n", depth);
  	}

  	kfault_stack_overflow(depth + 1);

  	/* Prevent tail-call optimization and keep each frame live. */
  	barrier_data(frame);
  }

Instruction abort (prefetch abort in AArch32)

Data abort와 마찬가지로 잘못된 주소 변환, 접근 권한으로 발생한다. Data abort가 데이터 읽기·쓰기 중 발생하는 것과 달리 instructino abort는 실행하고자 하는 명령에 접근하다가 발생한다.

static noinline void kfault_prefetch_abort(void)
{
	unsigned long pc = 0;
	void (*bad)(void);

	/* Keep the zero target in a register: branch, rather than a compile-time trap. */
	asm volatile("" : "+r"(pc));
	bad = (void (*)(void))pc;
	pr_emerg("kfault-lab: branching to 0x0 (prefetch/instruction abort)\n");
	bad();
}