Exception -9- (ARM64 Fault Handler)

<kernel v5.4>

Exception -9- (ARM64 Fault Handler)

 

메모리 fault 처리는 아키텍처 관련 코드를 수행한 후 아키텍쳐 공통 메모리 fault 코드를 수행한다.

  • 먼저 do_mem_abort() 함수를 호출하며, 페이지 fault 처리에 대해  do_page_fault() -> __do_page_fault() 함수 순서대로 수행한다.
  • 그 후 아키텍처 공통(generic) 메모리 fault 처리 함수인 handle_mm_fault() 함수로 진입한다.

 

다음 그림은 유저 및 커널 모드 수행 중 sync exception 발생 시 처리되는 함수 흐름이다.

 

do_mem_abort()

arch/arm64/mm/fault.c

asmlinkage void __exception do_mem_abort(unsigned long addr, unsigned int esr,
                                         struct pt_regs *regs)
{
        const struct fault_info *inf = esr_to_fault_info(esr);

        if (!inf->fn(addr, esr, regs))
                return;

        if (!user_mode(regs)) {
                pr_alert("Unhandled fault at 0x%016lx\n", addr);
                mem_abort_decode(esr);
                show_pte(addr);
        }

        arm64_notify_die(inf->name, regs,
                         inf->sig, inf->code, (void __user *)addr, esr);
}

가상 주소 @addr에 해당하는 공간에 접근하다 fault가 발생하였고, @esr(Exception Syndrom Register) 값과 스택에 저장한 context(pt_regs)를 인자로 가지고 진입하였다. 해당 가상 메모리 영역에 대한 fault 처리를 수행한다.

  • 코드 라인 4에서 @esr 값으로 static하게 정의된 fault_info[] 배열에서 구조체 fault_info를 알아온다.
  • 코드 라인 6~7에서 fault_info[] 배열에 미리 static으로 지정해둔 해당 함수를 실행한다.
    • 예) “level 1 permission fault”가 발생한 경우 do_page_fault() 함수를 호출한다.
  • 코드 라인 9~13에서 유저 영역이 아닌 가상 주소에서 fault가 발생한 경우 다음과 같은 디버그용 정보를 출력한다.
    • “Unhandled fault at 0x################” 에러 메시지
    • “Mem abort info: ESR=0x########” 등의 메모리 abort 정보
    • 데이터 abort 인 경우 “Data abort info:” 정보도 추가
    • swapper 또는 user 페이지 테이블 정보
  • 코드 라인 15~16에서 유저 영역에서의 fault는 해당 프로세스를 kill하고, 커널 영역에서의 fault인 경우 die 처리한다.

 


페이지 fault 처리

do_page_fault()

arch/arm64/mm/fault.c -1/3-

static int __kprobes do_page_fault(unsigned long addr, unsigned int esr,
                                   struct pt_regs *regs)
{
        const struct fault_info *inf;
        struct mm_struct *mm = current->mm;
        vm_fault_t fault, major = 0;
        unsigned long vm_flags = VM_READ | VM_WRITE;
        unsigned int mm_flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;

        if (kprobe_page_fault(regs, esr))
                return 0;

        /*
         * If we're in an interrupt or have no user context, we must not take
         * the fault.
         */
        if (faulthandler_disabled() || !mm)
                goto no_context;

        if (user_mode(regs))
                mm_flags |= FAULT_FLAG_USER;

        if (is_el0_instruction_abort(esr)) {
                vm_flags = VM_EXEC;
                mm_flags |= FAULT_FLAG_INSTRUCTION;
        } else if (is_write_abort(esr)) {
                vm_flags = VM_WRITE;
                mm_flags |= FAULT_FLAG_WRITE;
        }

        if (is_ttbr0_addr(addr) && is_el1_permission_fault(addr, esr, regs)) {
                /* regs->orig_addr_limit may be 0 if we entered from EL0 */
                if (regs->orig_addr_limit == KERNEL_DS)
                        die_kernel_fault("access to user memory with fs=KERNEL_DS",
                                         addr, esr, regs);

                if (is_el1_instruction_abort(esr))
                        die_kernel_fault("execution of user memory",
                                         addr, esr, regs);

                if (!search_exception_tables(regs->pc))
                        die_kernel_fault("access to user memory outside uaccess routines",
                                         addr, esr, regs);
        }

        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);
  • 코드 라인 5에서 현재 태크스가 사용중인 메모리 관리에 해당하는 mm을 알아온다.
  • 코드 라인 7에서 vm 플래그 초기 값으로 VM_READ와 VM_WRITE플래그를 지정한다.
  • 코드 라인 8에서 mm 플래그 초기 값으로 FAULT_FLAG_ALLOW_RETRY와 FAULT_FLAG_KILLABLE 플래그를 지정한다.
  • 코드 라인 10~11에서 mm fault 발생 하였으므로 kprobe 디버그를 사용중이면 kprobe 상태에 따라 해당 kprobe fault 함수를 호출한다.
  • 코드 라인 17~18에서 irq context에서 fault가 발생하였거나, 해당 태스크의 fault hander를 disable한 경우 유저 fault 처리를 생략하고 곧장 커널에 대한 fault 처리 루틴을 수행하러 no_context: 레이블로 이동한다.
  • 코드 라인 20~21에서 유저 모드에서 fault가 발생한 경우 mm 플래그에 FAULT_FLAG_USER 플래그를 추가한다.
  • 코드 라인 23~25에서 유저 모드에서 명령어를 수행하다 fault가 발생한 경우 vm 플래그에 VM_EXEC를 대입하고, mm 플래그에 FAULT_FLAG_INSTRUCTION을 추가한다.
  • 코드 라인 26~29에서 쓰기 시도 중에 fault가 발생한 경우 vm 플래그에 VM_WRITE를 대입하고, mm 플래그에 FAULT_FLAG_WRITE를 추가한다.
  • 코드 라인 31~44에서 커널 모드에서 유저 영역에 접근 시 permission 문제로 fault가 발생한 경우 다음 3 가지 경우에 한하여 die 처리한다.
    • 메모리 제한이 없었던 경우
    • 커널 모드의 명령어를 처리중 fault가 발생한 경우
    • exception 테이블에 등록되지 않은 예외인 경우
  • 코드 라인 46에서 perf 디버그를 목적으로 page fault에 대한 카운팅을 알 수 있게 하기 위해 PERF_COUNT_SW_PAGE_FAULTS 카운터를 1 증가시킨다.

 

arch/arm64/mm/fault.c -2/3-

        /*
         * As per x86, we may deadlock here. However, since the kernel only
         * validly references user space from well defined areas of the code,
         * we can bug out early if this is from code which shouldn't.
         */
        if (!down_read_trylock(&mm->mmap_sem)) {
                if (!user_mode(regs) && !search_exception_tables(regs->pc))
                        goto no_context;
retry:
                down_read(&mm->mmap_sem);
        } else {
                /*
                 * The above down_read_trylock() might have succeeded in which
                 * case, we'll have missed the might_sleep() from down_read().
                 */
                might_sleep();
#ifdef CONFIG_DEBUG_VM
                if (!user_mode(regs) && !search_exception_tables(regs->pc)) {
                        up_read(&mm->mmap_sem);
                        goto no_context;
                }
#endif
        }

       fault = __do_page_fault(mm, addr, mm_flags, vm_flags);
        major |= fault & VM_FAULT_MAJOR;

        if (fault & VM_FAULT_RETRY) {
                /*
                 * If we need to retry but a fatal signal is pending,
                 * handle the signal first. We do not need to release
                 * the mmap_sem because it would already be released
                 * in __lock_page_or_retry in mm/filemap.c.
                 */
                if (fatal_signal_pending(current)) {
                        if (!user_mode(regs))
                                goto no_context;
                        return 0;
                }

                /*
                 * Clear FAULT_FLAG_ALLOW_RETRY to avoid any risk of
                 * starvation.
                 */
                if (mm_flags & FAULT_FLAG_ALLOW_RETRY) {
                        mm_flags &= ~FAULT_FLAG_ALLOW_RETRY;
                        mm_flags |= FAULT_FLAG_TRIED;
                        goto retry;
                }
        }
        up_read(&mm->mmap_sem);

mm->mmap_sem 읽기 락을 획득한 채로 __do_page_fault() 함수를 수행한다.

  • 코드 라인 6에서 현재 태스크의 mm에서 read 락을 획득 시도한다.
  • 코드 라인 7~8에서 만일 락을 획득하지 못했고, fault 발생한 곳이 유저 모드가 아니면서 exception 테이블에 등록된 예외도 없으면 곧장 커널에 대한 fault 처리 루틴을 수행하러 no_context: 레이블로 이동한다.
  • 코드 라인 9~10에서 retry: 레이블이다. 여기서 read 락을 획득한다.
  • 코드 라인 11~23에서 만일 읽기 락을 획득 시도하다 정상적으로 획득한 경우, 즉 race 컨디션 없이 락을 획득한 경우에 먼저 premption point를 수행해준다.
    • preemption 옵션 중 하나인 voluntry 커널 설정인 경우 더 높은 우선 순위의 다른 태스크를 먼저 처리하기 위해 sleep 할 수 있다.
  • 코드 라인 25에서 __do_page_fault() 함수를 호출하여 페이지 fault에 대한 처리를 수행한 후 fault 결과를 알아온다.
  • 코드 라인 26에서 fault 결과 중 VM_FAULT_MAJOR 플래그가 있는 경우 major에도 추가한다.
  • 코드 라인 28~50에서 fault 결과에 retry 요청이 있는 경우이다.
    • fatal 시그널이 존재하는 경우 유저 모드에서 fault가 발생한 경우이면 0을 반환하고, 커널 모드에서 fault가 발생한 경우 no_context: 레이블로 이동한다.
    • mm 플래그에 retry를 허용하는 경우에 한하여 mm 플래그에서 FAULT_FLAG_ALLOW_RETRY 플래그를 제거하고, retry를 시도했다는 뜻의 FAULT_FLAG_TRIED를 추가한 후 retry 레이블로 이동한다.
  • 코드 라인 51에서 fault 처리를 위해 획득했었던 read 락을 풀어준다.

 

arch/arm64/mm/fault.c -3/3-

        /*
         * Handle the "normal" (no error) case first.
         */
        if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP |
                              VM_FAULT_BADACCESS)))) {
                /*
                 * Major/minor page fault accounting is only done
                 * once. If we go through a retry, it is extremely
                 * likely that the page will be found in page cache at
                 * that point.
                 */
                if (major) {
                        current->maj_flt++;
                        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1, regs,
                                      addr);
                } else {
                        current->min_flt++;
                        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1, regs,
                                      addr);
                }

                return 0;
        }

        /*
         * If we are in kernel mode at this point, we have no context to
         * handle this fault with.
         */
        if (!user_mode(regs))
                goto no_context;

        if (fault & VM_FAULT_OOM) {
                /*
                 * We ran out of memory, call the OOM killer, and return to
                 * userspace (which will retry the fault, or kill us if we got
                 * oom-killed).
                 */
                pagefault_out_of_memory();
                return 0;
        }

        inf = esr_to_fault_info(esr);
        set_thread_esr(addr, esr);
        if (fault & VM_FAULT_SIGBUS) {
                /*
                 * We had some memory, but were unable to successfully fix up
                 * this page fault.
                 */
                arm64_force_sig_fault(SIGBUS, BUS_ADRERR, (void __user *)addr,
                                      inf->name);
        } else if (fault & (VM_FAULT_HWPOISON_LARGE | VM_FAULT_HWPOISON)) {
                unsigned int lsb;

                lsb = PAGE_SHIFT;
                if (fault & VM_FAULT_HWPOISON_LARGE)
                        lsb = hstate_index_to_shift(VM_FAULT_GET_HINDEX(fault));

                arm64_force_sig_mceerr(BUS_MCEERR_AR, (void __user *)addr, lsb,
                                       inf->name);
        } else {
                /*
                 * Something tried to access memory that isn't in our memory
                 * map.
                 */
                arm64_force_sig_fault(SIGSEGV,
                                      fault == VM_FAULT_BADACCESS ? SEGV_ACCERR : SEGV_MAPERR,
                                      (void __user *)addr,
                                      inf->name);
        }

        return 0;

no_context:
        __do_kernel_fault(addr, esr, regs);
        return 0;
}
  • 코드 라인 4~23에서 fault 처리 결과 에러도 없고, 매핑도 정상이고 접근에 문제가 없었던 경우 major 변수 값에 따른 perf 카운터를 증가시키고, 정상 결과(0)를 반환한다.
  • 코드 라인 29~30에서 fault가 커널에서 발생한 경우는 커널을 계속 진행할 수 없어 no_context로 이동한다.
  • 코드 라인 32~40에서 OOM(메모리 부족) 상황인 경우 OOM 킬을 수행하고 정상 결과(0)를 반환한다.
    • OOM score가 높은 유저 태스크를 죽여 메모리를 확보한다.
  • 코드 라인 42에서 esr 값으로 fault_info를 알아온다.
  • 코드 라인 43에서 현재 태스크에 esr 값(내부 함수에서 조건에 따라 esr 값이 변경된다)을 기록한다.
  • 코드 라인 44~69에서 fault 주소에 접근하려 했던 프로세스에 다음과 같은 시그널을 전송한다. 참고로 해당 프로세스는 시그널을 받아 처리할 수 있는 핸들러를 추가할 수 있다. SIGBUS 및 SIGSEGV 시그널 등에 대한 처리 핸들러가 설치되지 않은 프로세스는 디폴트로 kill 된다.
    • SIGBUS fault인 경우 해당 프로세스에 SIGBUS 시그널을 전송한다.
    • HWPOISON fault 검출인 경우 메모리 에러로 프로세스에 SIGBUS 시그널을 전송한다.
    • 그 외 fault인 경우 해당 프로세스에 SIGSEGV 시그널을 전송한다.
  • 코드 라인 71에서 정상 결과(0)를 반환한다.
  • 코드 라인 73~75에서 no_context: 레이블이다. 커널이 더 이상 처리를 수행할 수 없어 fault에 대한 메시지를 출력하고 커널을 die 처리한다단 exception이 설치된 명령은 die 처리하지 않는다.

 

유저 페이지 fault 처리

__do_page_fault()

arch/arm64/mm/fault.c

static vm_fault_t __do_page_fault(struct mm_struct *mm, unsigned long addr,
                           unsigned int mm_flags, unsigned long vm_flags)
{
        struct vm_area_struct *vma = find_vma(mm, addr);

        if (unlikely(!vma))
                return VM_FAULT_BADMAP;

        /*
         * Ok, we have a good vm_area for this memory access, so we can handle
         * it.
         */
        if (unlikely(vma->vm_start > addr)) {
                if (!(vma->vm_flags & VM_GROWSDOWN))
                        return VM_FAULT_BADMAP;
                if (expand_stack(vma, addr))
                        return VM_FAULT_BADMAP;
        }

        /*
         * Check that the permissions on the VMA allow for the fault which
         * occurred.
         */
        if (!(vma->vm_flags & vm_flags))
                return VM_FAULT_BADACCESS;
        return handle_mm_fault(vma, addr & PAGE_MASK, mm_flags);
}

유저 주소에 대한 페이지 fault 처리이다.

  • 코드 라인 4~7에서 현재 태스크의 mm내에서 fault 주소로 vma 영역을 찾는다.
  • 코드 라인 13~18에서 vma 영역이 스택인 경우 스택을 확장 시도한다.
  • 코드 라인 24~25에서 vma 영역이 요청한 속성을 허용하지 않는 경우 VM_FAULT_BADACCESS 결과를 반환한다.
    • vma 공간이 vm 플래그(VM_READ, VM_WRITE, VM_EXEC 등)를 지원하지 않은 경우이다.
  • 코드 라인 26에서 아키텍처 mm fault 처리는 완료되었다. 이 다음부터는 아키텍처와 무관한 코드로 작성된 generic mm fault 루틴을 수행한다.

 

커널 페이지 fault 처리

__do_kernel_fault()

arch/arm64/mm/fault.c

static void __do_kernel_fault(unsigned long addr, unsigned int esr,
                              struct pt_regs *regs)
{
        const char *msg;

        /*
         * Are we prepared to handle this kernel fault?
         * We are almost certainly not prepared to handle instruction faults.
         */
        if (!is_el1_instruction_abort(esr) && fixup_exception(regs))
                return;

        if (WARN_RATELIMIT(is_spurious_el1_translation_fault(addr, esr, regs),
            "Ignoring spurious kernel translation fault at virtual address %016lx\n", addr))
                return;

        if (is_el1_permission_fault(addr, esr, regs)) {
                if (esr & ESR_ELx_WNR)
                        msg = "write to read-only memory";
                else
                        msg = "read from unreadable memory";
        } else if (addr < PAGE_SIZE) {
                msg = "NULL pointer dereference";
        } else {
                msg = "paging request";
        }

        die_kernel_fault(msg, addr, esr, regs);
}

커널 페이지에 대한 fault 처리를 수행한다.

  • 코드 라인 10~11에서 커널 명령(instruction)에서 fault가 발생하지 않았거나, 커널 명령에서 fault가 발생했어도 이에 대응하는 exception 테이블이 등록되어 있는 경우 커널 페이지에 대한 fault가 아니므로 그냥 함수를 빠져나간다.
  • 코드 라인 13~15에서 거짓(spurious) 커널 페이지 변환 fault인 경우 경고 메시지를 출력한다.
    • ratelimit 방식을 사용하여 디폴트로 최대 5초 이내에 10번만 메시지를 출력하도록 제한한다.
  • 코드 라인 17~21에서 커널 permission fault인 경우의 메시지를 선택한다.
  • 코드 라인 22~23에서 첫 페이지에 대한 접근의 경우의 메시지를 선택한다.
  • 코드 라인 24~26에서 그 외의 경우 메시지를 선택한다.
  • 코드 라인 28에서 커널 context를 덤프하고 die 처리를 한다.

 

die_kernel_fault()

arch/arm64/mm/fault.c

static void die_kernel_fault(const char *msg, unsigned long addr,
                             unsigned int esr, struct pt_regs *regs)
{
        bust_spinlocks(1);

        pr_alert("Unable to handle kernel %s at virtual address %016lx\n", msg,
                 addr);

        mem_abort_decode(esr);

        show_pte(addr);
        die("Oops", regs, esr);
        bust_spinlocks(0);
        do_exit(SIGKILL);
}

커널 context를 덤프하고 die 처리를 한다. 다음과 같은 내용들을 출력한다.

  • 메모리 abort 정보
  • 데이터 abort 정보
  • 페이지 테이블 정보
  • 레지스터 덤프
  • 커널 스택 덤프

 

다음은 0x00003fc8에 접근하다 발생한 커널 fault 예제이다.

[  110.965771] Unable to handle kernel paging request at virtual address 00003fc8
[  110.968799] Mem abort info:
[  110.969351]   Exception class = DABT (current EL), IL = 32 bits
[  110.969828]   SET = 0, FnV = 0
[  110.969988]   EA = 0, S1PTW = 0
[  110.970142] Data abort info:
[  110.970560]   ISV = 0, ISS = 0x00000006
[  110.970758]   CM = 0, WnR = 0
[  110.971357] user pgtable: 4k pages, 39-bit VAs, pgd = ffffffc06527e000
[  110.972077] [0000000000003fc8] *pgd=00000000a6761003, *pud=00000000a6761003, *pmd=0000000000000000
[  110.972846] Internal error: Oops: 96000006 [#1] PREEMPT SMP
[  110.973711] Modules linked in:
[  110.974527] CPU: 3 PID: 1034 Comm: sleep Not tainted 4.14.67-v8-qemu+ #74
[  110.974840] Hardware name: linux,dummy-virt (DT)
[  110.975749] task: ffffffc0650bd700 task.stack: ffffff800b0d8000
[  110.976622] PC is at cpuacct_charge+0x34/0xa8
[  110.977911] LR is at update_curr+0x98/0x228
[  110.978260] pc : [<ffffff80080ec54c>] lr : [<ffffff80080d5030>] pstate: a00001c5
[  110.978803] sp : ffffff800b0dba60
[  110.978985] x29: ffffff800b0dba60 x28: ffffffc06ffb0480 
[  110.979768] x27: ffffff80092aa7c0 x26: ffffffc06d57d780 
[  110.980104] x25: ffffff80092aa000 x24: 0000000000000008 
[  110.980295] x23: ffffffc06ffb0480 x22: 0000000001c8e1f0 
[  110.980460] x21: 0000000000000001 x20: 0000000001c8e1f0 
[  110.980858] x19: ffffffc0650b9d00 x18: 0000000000000000 
[  110.981157] x17: 0000000000000000 x16: ffffff8008111500 
[  110.981402] x15: 0000000000000000 x14: 00000000004bb6d4 
[  110.981611] x13: 00000000ff9cea5c x12: 0000000000000000 
[  110.981902] x11: 00000000ff9cea94 x10: 00000000004ce000 
[  110.982104] x9 : 000000000000075a x8 : 0000000000000400 
[  110.982268] x7 : 000000000000075a x6 : 0000000002739d7c 
[  110.982424] x5 : 00ffffffffffffff x4 : 0000000000000015 
[  110.983490] x3 : 0000000000000000 x2 : ffffffffff76abc0 
[  110.983734] x1 : 0000000000003ec0 x0 : 0000000000003ec0 
[  110.983940] Process sleep (pid: 1034, stack limit = 0xffffff800b0d8000)
[  110.984213] Call trace:
[  110.984388] Exception stack(0xffffff800b0db920 to 0xffffff800b0dba60)
[  110.984723] b920: 0000000000003ec0 0000000000003ec0 ffffffffff76abc0 0000000000000000
[  110.984970] b940: 0000000000000015 00ffffffffffffff 0000000002739d7c 000000000000075a
[  110.985185] b960: 0000000000000400 000000000000075a 00000000004ce000 00000000ff9cea94
[  110.985396] b980: 0000000000000000 00000000ff9cea5c 00000000004bb6d4 0000000000000000
[  110.985624] b9a0: ffffff8008111500 0000000000000000 0000000000000000 ffffffc0650b9d00
[  110.985878] b9c0: 0000000001c8e1f0 0000000000000001 0000000001c8e1f0 ffffffc06ffb0480
[  110.986092] b9e0: 0000000000000008 ffffff80092aa000 ffffffc06d57d780 ffffff80092aa7c0
[  110.986308] ba00: ffffffc06ffb0480 ffffff800b0dba60 ffffff80080d5030 ffffff800b0dba60
[  110.986528] ba20: ffffff80080ec54c 00000000a00001c5 0000000000000003 ffffffc06d553e00
[  110.986825] ba40: 0000007fffffffff ffffff80092a9b20 ffffff800b0dba60 ffffff80080ec54c
[  110.987497] [<ffffff80080ec54c>] cpuacct_charge+0x34/0xa8
[  110.987842] [<ffffff80080d5030>] update_curr+0x98/0x228
[  110.988085] [<ffffff80080d6108>] dequeue_task_fair+0x68/0x528
[  110.988251] [<ffffff80080ce440>] deactivate_task+0xa8/0xf0
[  110.988406] [<ffffff80080da9f4>] load_balance+0x454/0x960
[  110.988693] [<ffffff80080db2a4>] pick_next_task_fair+0x3a4/0x6c8
[  110.988987] [<ffffff8008b00bac>] __schedule+0x104/0x898
[  110.989226] [<ffffff8008b01374>] schedule+0x34/0x98
[  110.989459] [<ffffff8008b05084>] do_nanosleep+0x7c/0x168
[  110.989739] [<ffffff80081113f4>] hrtimer_nanosleep+0xa4/0x128
[  110.990058] [<ffffff8008111570>] compat_SyS_nanosleep+0x70/0x90
[  110.990300] Exception stack(0xffffff800b0dbec0 to 0xffffff800b0dc000)
[  110.990825] bec0: 00000000ff9cea6c 0000000000000000 00000000f7b82590 00000000ff9cea6c
[  110.991237] bee0: 0000000005f5e100 0000000000000000 00000000004b8e80 00000000000000a2
[  110.991768] bf00: 0000000000000000 0000000000000000 00000000004ce000 00000000ff9cea94
[  110.992084] bf20: 0000000000000000 00000000ff9cea5c 00000000004bb6d4 0000000000000000
[  110.992521] bf40: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
[  110.993118] bf60: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
[  110.993655] bf80: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
[  110.994227] bfa0: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
[  110.995517] bfc0: 00000000f7ab4a10 0000000060040010 00000000ff9cea6c 00000000000000a2
[  110.995998] bfe0: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
[  110.996235] [<ffffff8008083b18>] __sys_trace_return+0x0/0x4
[  110.996618] Code: 52800035 f9401260 8b010000 b4000080 (f9408400) 
[  110.997850] ---[ end trace cdf6df9c4e5b5c95 ]---
[  110.998316] note: sleep[1034] exited with preempt_count 2

 


테이블 변환 fault 처리

do_translation_fault()

arch/arm64/mm/fault.c

static int __kprobes do_translation_fault(unsigned long addr,
                                          unsigned int esr,
                                          struct pt_regs *regs)
{
        if (is_ttbr0_addr(addr))
                return do_page_fault(addr, esr, regs);

        do_bad_area(addr, esr, regs);
        return 0;
}

가상 주소를 물리 주소로 변환하는 페이지 테이블을 사용한 변환에 fault가 발생하였을 수행을 한다.

  • 코드 라인 5~6에서 fault 발생 주소가 유저 영역에 접근하는 경우 페이지 fault 처리로 이동한다.
  • 코드 라인 8에서 영역 침범과 관련한 fault 처리를 수행하도록 이동한다.

 

영역 침범 fault 처리

do_bad_area()

arch/arm64/mm/fault.c

static void do_bad_area(unsigned long addr, unsigned int esr, struct pt_regs *regs)
{
        /*
         * If we are in kernel mode at this point, we have no context to
         * handle this fault with.
         */
        if (user_mode(regs)) {
                const struct fault_info *inf = esr_to_fault_info(esr);

                set_thread_esr(addr, esr);
                arm64_force_sig_fault(inf->sig, inf->code, (void __user *)addr,
                                      inf->name);
        } else {
                __do_kernel_fault(addr, esr, regs);
        }
}

영역 침범에 대한 fault 처리를 수행한다.

  • 코드 라인 7~12에서 유저 모드에서 동작 중에 fault 발생한 경우 해당 프로세스에 시그널을 전송한다.
    • 해당 프로세스가 die 한다. 만일 프로세스에 시그널 핸들러가 설치된 경우 해당 핸들러 코드가 동작하다.
  • 코드 라인 13~15에서 커널 fault 처리를 수행하고 die 한다.

 


정렬 fault 처리

do_alignment_fault()

arch/arm64/mm/fault.c

static int do_alignment_fault(unsigned long addr, unsigned int esr,
                              struct pt_regs *regs)
{
        do_bad_area(addr, esr, regs);
        return 0;
}

정렬 fault가 발생한 겨우 영역 침범과 동일한 코드를 호출하여 처리한다.

 

참고

 

Exception -8- (ARM64 Handler)

<kernel v5.4>

 

Context 백업

pt_regs 구조체

arch/arm64/include/asm/ptrace.h

/*
 * This struct defines the way the registers are stored on the stack during an
 * exception. Note that sizeof(struct pt_regs) has to be a multiple of 16 (for
 * stack alignment). struct user_pt_regs must form a prefix of struct pt_regs.
 */
struct pt_regs {
        union {
                struct user_pt_regs user_regs;
                struct {
                        u64 regs[31];
                        u64 sp;
                        u64 pc;
                        u64 pstate;
                };
        };
        u64 orig_x0;
#ifdef __AARCH64EB__
        u32 unused2;
        s32 syscallno;
#else
        s32 syscallno;
        u32 unused2;
#endif

        u64 orig_addr_limit;
        /* Only valid when ARM64_HAS_IRQ_PRIO_MASKING is enabled. */
        u64 pmr_save;
        u64 stackframe[2];
};

exception 발생 시 스택에 저장될 레지스터들이다.

  •  regs[31]
    • x0~x30(lr) 범용 레지스터가 저장된다.
  • sp
    • 중단될 때의 스택 레지스터가 저장된다.
  • pc
    • 중단될 때 복귀할 주소가 저장된다.
  • pstate
    • 중단될 때의 PSTATE 값이 저장된다.
  • orig_x0
  • syscallno
    • syscall 호출 번호
  • orig_addr_limit
    • 유저 태스크의 주소 제한 값이 저장된다.
  • pmr_save
    • irq priority mask 값이 저장된다.
  • stackframe[]

 

user_pt_regs 구조체

arch/arm64/include/uapi/asm/ptrace.h

struct user_pt_regs {
        __u64           regs[31];
        __u64           sp;
        __u64           pc;
        __u64           pstate;
};

pr_regs의 앞부분이 동일하다.

 

kernel_entry 매크로

arch/arm64/kernel/entry.S -1/2-

.       .macro  kernel_entry, el, regsize = 64
        .if     \regsize == 32
        mov     w0, w0                          // zero upper 32 bits of x0
        .endif
        stp     x0, x1, [sp, #16 * 0]
        stp     x2, x3, [sp, #16 * 1]
        stp     x4, x5, [sp, #16 * 2]
        stp     x6, x7, [sp, #16 * 3]
        stp     x8, x9, [sp, #16 * 4]
        stp     x10, x11, [sp, #16 * 5]
        stp     x12, x13, [sp, #16 * 6]
        stp     x14, x15, [sp, #16 * 7]
        stp     x16, x17, [sp, #16 * 8]
        stp     x18, x19, [sp, #16 * 9]
        stp     x20, x21, [sp, #16 * 10]
        stp     x22, x23, [sp, #16 * 11]
        stp     x24, x25, [sp, #16 * 12]
        stp     x26, x27, [sp, #16 * 13]
        stp     x28, x29, [sp, #16 * 14]

        .if     \el == 0
        clear_gp_regs
        mrs     x21, sp_el0
        ldr_this_cpu    tsk, __entry_task, x20  // Ensure MDSCR_EL1.SS is clear,
        ldr     x19, [tsk, #TSK_TI_FLAGS]       // since we can unmask debug
        disable_step_tsk x19, x20               // exceptions when scheduling.

        apply_ssbd 1, x22, x23

        .else
        add     x21, sp, #S_FRAME_SIZE
        get_current_task tsk
        /* Save the task's original addr_limit and set USER_DS */
        ldr     x20, [tsk, #TSK_TI_ADDR_LIMIT]
        str     x20, [sp, #S_ORIG_ADDR_LIMIT]
        mov     x20, #USER_DS
        str     x20, [tsk, #TSK_TI_ADDR_LIMIT]
        /* No need to reset PSTATE.UAO, hardware's already set it to 0 for us */
        .endif /* \el == 0 */
        mrs     x22, elr_el1
        mrs     x23, spsr_el1
        stp     lr, x21, [sp, #S_LR]

        /*
         * In order to be able to dump the contents of struct pt_regs at the
         * time the exception was taken (in case we attempt to walk the call
         * stack later), chain it together with the stack frames.
         */
        .if \el == 0
        stp     xzr, xzr, [sp, #S_STACKFRAME]
        .else
        stp     x29, x22, [sp, #S_STACKFRAME]
        .endif
        add     x29, sp, #S_STACKFRAME

context 전환을 목적으로 레지스터들을 백업한다. el0 또는 AArch32 여부에 따라 약간 차이가 있다.

  • 코드 라인 2~4에서 EL0 AArch32에서 exception 진입한 경우 32비트 레지스터를 사용하기 위해 x0 레지스터의 상위 32비트를 0으로 클리어한다.
  • 코드 라인 5~19에서 context 저장을 목적으로 x0~x29 레지스터를 스택에 백업한다.
  • 코드 라인 21~22에서 EL0에서 exception 진입한 경우 x0~x29 레지스터를 0으로 클리어한다.
  • 코드 라인 23에서 중단된 기존 스택 주소(sp_el0)를 잠시 x21에 백업해둔다.
  • 코드 라인 24~26에서 __entry_task 태스크의 thread_info.flag 값을 x19 레지스터로 알아와서 싱글 스텝 디버깅 비트가 켜져있으면 싱글 스텝 디버거를 끈다.
    • __entry_task는 per-cpu 변수로 선언되었다.
  • 코드 라인 28에서 SSBD(Speculative Store Bypass Disable) 기능을 사용하는 경우 Speculative Store 공격으로 부터 커널을 보호하기 위해 유저와 커널의 전환 시 EL1이 아닌 상위(EL2 or EL3)에서 수행하는 workaround를 적용한다.  사용하지 않는 경우 그냥 skip 한다.
  • 코드 라인 30~31에서 EL0가 아닌 곳에서 exception 진입한 경우 pt_regs 만큼 스택을 키우기 전의 스택 주소를 잠시 x21에 저장한다.
  • 코드 라인 32~37에서 현재 thread_info.addr_limit 값을 스택의 orig_addr_limit 위치에 백업한 후 최대 유저 주소 한계 값을 지정한다.
    • pt_regs.orig_addr_limit <- thread_info.addr_limit
    • thread_info.addr_limit <- USER_DS
      • 예) CONFIG_ARM64_VA_BITS_48 커널 옵션이 사용되는 경우 USER_DS=0x0000_ffff_ffff_ffff
      • 예) CONFIG_ARM64_VA_BITS_52 커널 옵션이 사용되는 경우 USER_DS=0x000f_ffff_ffff_ffff
  • 코드 라인 40~41에서 x22 <- elr_el1 및 x23 <- spsr_el1을 수행한다.
  • 코드 라인 42에서 lr과 pt_regs 만큼 스택 키우기 전의 스택 주소가 담긴 x21 레지스터 값을 pt_regs.x30(lr)과 pt_regs.sp 위치에 저장한다.
  • 코드 라인 49~50에서 EL0에서 진입한 경우 스택의 x0, x1 위치에 0을 저장한다.
  • 코드 라인 51~52에서 EL1에서 진입한 경우 x29와 중단되어 복귀할 주소(pc -> elr_el1)가 담긴 x22 레지스터 값을 pt_regs.stackframe[] 위치에 저장한다.
  • 코드 라인 54에서 pt_regs.stackframe[] 주소를 x29 레지스터에 대입한다.

 

arch/arm64/kernel/entry.S -2/2-

#ifdef CONFIG_ARM64_SW_TTBR0_PAN
        /*
         * Set the TTBR0 PAN bit in SPSR. When the exception is taken from
         * EL0, there is no need to check the state of TTBR0_EL1 since
         * accesses are always enabled.
         * Note that the meaning of this bit differs from the ARMv8.1 PAN
         * feature as all TTBR0_EL1 accesses are disabled, not just those to
         * user mappings.
         */
alternative_if ARM64_HAS_PAN
        b       1f                              // skip TTBR0 PAN
alternative_else_nop_endif

        .if     \el != 0
        mrs     x21, ttbr0_el1
        tst     x21, #TTBR_ASID_MASK            // Check for the reserved ASID
        orr     x23, x23, #PSR_PAN_BIT          // Set the emulated PAN in the saved SPSR
        b.eq    1f                              // TTBR0 access already disabled
        and     x23, x23, #~PSR_PAN_BIT         // Clear the emulated PAN in the saved SPSR
        .endif

        __uaccess_ttbr0_disable x21
1:
#endif

        stp     x22, x23, [sp, #S_PC]

        /* Not in a syscall by default (el0_svc overwrites for real syscall) */
        .if     \el == 0
        mov     w21, #NO_SYSCALL
        str     w21, [sp, #S_SYSCALLNO]
        .endif

        /*
         * Set sp_el0 to current thread_info.
         */
        .if     \el == 0
        msr     sp_el0, tsk
        .endif

        /* Save pmr */
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
        mrs_s   x20, SYS_ICC_PMR_EL1
        str     x20, [sp, #S_PMR_SAVE]
alternative_else_nop_endif

        /*
         * Registers that may be useful after this macro is invoked:
         *
         * x20 - ICC_PMR_EL1
         * x21 - aborted SP
         * x22 - aborted PC
         * x23 - aborted PSTATE
        */
        .endm
  • 코드 라인 1에서 CONFIG_ARM64_SW_TTBR0_PAN 커널 옵션은 보안을 향상 시키기 위해 커널이 유저 영역의 접근을 방지하기 위해 사용된다.
  • 코드 라인 10~12에서 ARM64_HAS_PAN 기능을 갖춘 시스템에서는 skip 한다.
  • 코드 라인 14~20에서 EL0에서 진입한 경우가 아니면 ttbr0_el1 레지스터의 ASID 필드 값이 0인 경우 이미 ttbr0 액세스가 disable 되어 있으므로 x23 레지스터에 PSR_PAN_BIT를 설정하고 skip 한다. ASID 필드 값이 존재하는 경우 x23 레지스터에 PSR_PAN_BIT를 클리어하고 계속 진행한다.
  • 코드 라인 22에서 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 금지하도록 ttbr을 설정한다.
    • x21 레지스터는 매크로에서 임시로 사용되며 크래시된다.
    • 참고: copy_from_user() | 문c
  • 코드 라인 26에서 중단되어 복귀할 주소(pc -> elr_el1)가 담긴 x22 레지스터와 중단될 때의 상태(spsr) 값이 담긴 x23 레지스터를 pt_regs.pc와 pt_regs->psr 위치에 저장한다.
  • 코드 라인 29~32에서 EL0에서 진입한 경우이면 초기 값으로 NO_SYSCALL(-1) 값을 pt_regs.syscallno 위치에 백업한다.
    • -1은 단지 초기 값일뿐 syscall 번호가 아니다.
  • 코드 라인 37~39에서 EL0에서 진입한 경우이면 현재 스레드 주소를 스택(sp_el0)으로 지정한다.
  • 코드 라인 42~45에서 irq priority masking이 가능한 시스템이면 SYS_ICC_PMR_EL1 레지스터를 읽어 pt_regs.pmr_save위치에 백업해둔다. irq priority masking이 가능한 시스템이 아닌 경우 nop으로 채운다.

 

clear_gp_regs 매크로

arch/arm64/kernel/entry.S

.       .macro  clear_gp_regs
        .irp    n,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29
        mov     x\n, xzr
        .endr
        .endm

컴파일러로 하여금 x0~x29 레지스터를 0으로 채우게 한다.

  • mov x0, xzr
  • mov z1, xzr
  • mov z29, xzr

 

ldr_this_cpu 매크로

arch/arm64/include/asm/assembler.h

        /*
         * @dst: Result of READ_ONCE(per_cpu(sym, smp_processor_id()))
         * @sym: The name of the per-cpu variable
         * @tmp: scratch register
         */
        .macro ldr_this_cpu dst, sym, tmp
        adr_l   \dst, \sym
alternative_if_not ARM64_HAS_VIRT_HOST_EXTN
        mrs     \tmp, tpidr_el1
alternative_else
        mrs     \tmp, tpidr_el2
alternative_endif
        ldr     \dst, [\dst, \tmp]
        .endm

로컬 cpu에서 per-cpu 변수 @sym의 주소를 @dst에 알아온다. 임시 @tmp 레지스터는 스크래치 된다.

  • tpidr_el1 (또는 tpidr_el2)는 per-cpu offset을 담고 있다.

 

disable_step_tsk 매크로

arch/arm64/include/asm/assembler.h

        .macro  disable_step_tsk, flgs, tmp
        tbz     \flgs, #TIF_SINGLESTEP, 9990f
        mrs     \tmp, mdscr_el1
        bic     \tmp, \tmp, #DBG_MDSCR_SS
        msr     mdscr_el1, \tmp
        isb     // Synchronise with enable_dbg
9990:
        .endm

싱글 스텝 디버깅을 사용하는 중이면 디버거를 끈다.

  • 어떤 태스크의 SINGLESTEP 플래그(@flgs) 비트가 사용중인 경우 MDSCR_EL1(Monitor Debug System Control Register EL1) 레지스터의 SS(Software Step control) 비트를 disable 한다.

 

get_current_task 매크로

arch/arm64/include/asm/assembler.h

/*
 * Return the current task_struct.
 */
.       .macro  get_current_task, rd
        mrs     \rd, sp_el0
        .endm

현재 태스크를 @rd 레지스터로 가져온다.

  • 스택의 가장 아래에 struct task_struct가 존재한다.

 

 


Context 복구

kernel_exit 매크로

arch/arm64/kernel/entry.S -1/2-

        .macro  kernel_exit, el
        .if     \el != 0
        disable_daif

        /* Restore the task's original addr_limit. */
        ldr     x20, [sp, #S_ORIG_ADDR_LIMIT]
        str     x20, [tsk, #TSK_TI_ADDR_LIMIT]

        /* No need to restore UAO, it will be restored from SPSR_EL1 */
        .endif

        /* Restore pmr */
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
        ldr     x20, [sp, #S_PMR_SAVE]
        msr_s   SYS_ICC_PMR_EL1, x20
        /* Ensure priority change is seen by redistributor */
        dsb     sy
alternative_else_nop_endif

        ldp     x21, x22, [sp, #S_PC]           // load ELR, SPSR
        .if     \el == 0
        ct_user_enter
        .endif

#ifdef CONFIG_ARM64_SW_TTBR0_PAN
        /*
         * Restore access to TTBR0_EL1. If returning to EL0, no need for SPSR
         * PAN bit checking.
         */
alternative_if ARM64_HAS_PAN
        b       2f                              // skip TTBR0 PAN
alternative_else_nop_endif

        .if     \el != 0
        tbnz    x22, #22, 1f                    // Skip re-enabling TTBR0 access if the PSR_PAN_BIT is set
        .endif

        __uaccess_ttbr0_enable x0, x1

        .if     \el == 0
        /*
         * Enable errata workarounds only if returning to user. The only
         * workaround currently required for TTBR0_EL1 changes are for the
         * Cavium erratum 27456 (broadcast TLBI instructions may cause I-cache
         * corruption).
         */
        bl      post_ttbr_update_workaround
        .endif
1:
        .if     \el != 0
        and     x22, x22, #~PSR_PAN_BIT         // ARMv8.0 CPUs do not understand this bit
        .endif
2:
#endif
  • 코드 라인 2~7에서 el0 exception에서 진입하였던 경우가 아니면 PSTATE의 DAIF 플래그들을 마스크하여 인터럽트 및 Abort Exception들을 허용하지 않게 한다. 그런 후 스택에 백업해둔 tsk->addr_limit을 복구한다.
  • 코드 라인 13~18에서 Pesudo-NMI 기능을 사용할 수 있는 시스템의 경우 스택에 백업해둔 pt_regs->pmr_save를 SYS_ICC_PMR_EL1 레지스터로 복구한다. 그런 후 redistributor가 변경된 값을 액세스 할 수 있도록 dsb 명령을 통해 operation이 완료될 때까지 대기한다.
  • 코드 라인 20에서 중단 점으로 복귀하기 위해 pt_regs->pc와 spsr 값을 x21, x22 레지스터로 읽어온다.
  • 코드 라인 21~23에서 el0 exception에서 진입하였던 경우 context 트래킹 디버그를 지원한다.
SW TTBR0 PAN 동작 – 커널의 유저 영역 분리
  • 코드 라인 30~32에서 ARMv8.1 아키텍처 이상에서 커널에서 유저 공간의 접근을 막는 PAN(Privilege Access Never) 기능을 지원하는 경우 SW  TTBR0 PAN을 skip 한다.
  • 코드 라인 34~36에서 el0 exception에서 진입하였던 경우가 아니면 중단 점에서의 PSTATE 레지스터의 PAN 비트가 설정되어 동작 중인 경우 재설정을 피하기 위해 1f 레이블로 이동한다.
  • 코드 라인 38에서 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 허용하게 한다.
  • 코드 라인 40~48에서 유저 복귀 전에 Cavium 칩의 erratum을 지원한다.
    • 내부적으로 TTBRx_el1을 갱신하는 경우 ic iallu; dsb nsh; isb 등의 베리어등을 수행해야 한다.
  • 코드 라인 49~52에서 1: 레이블에서는 el0 exception에서 진입하였던 경우가 아니면 ARMv8.0 아키텍처는 PSTATE의 PAN 비트를 모르므로 클리어한다.

 

arch/arm64/kernel/entry.S -2/2-

        .if     \el == 0
        ldr     x23, [sp, #S_SP]                // load return stack pointer
        msr     sp_el0, x23
        tst     x22, #PSR_MODE32_BIT            // native task?
        b.eq    3f

#ifdef CONFIG_ARM64_ERRATUM_845719
alternative_if ARM64_WORKAROUND_845719
#ifdef CONFIG_PID_IN_CONTEXTIDR
        mrs     x29, contextidr_el1
        msr     contextidr_el1, x29
#else
        msr contextidr_el1, xzr
#endif
alternative_else_nop_endif
#endif
3:
#ifdef CONFIG_ARM64_ERRATUM_1418040
alternative_if_not ARM64_WORKAROUND_1418040
        b       4f
alternative_else_nop_endif
        /*
         * if (x22.mode32 == cntkctl_el1.el0vcten)
         *     cntkctl_el1.el0vcten = ~cntkctl_el1.el0vcten
         */
        mrs     x1, cntkctl_el1
        eon     x0, x1, x22, lsr #3
        tbz     x0, #1, 4f
        eor     x1, x1, #2      // ARCH_TIMER_USR_VCT_ACCESS_EN
        msr     cntkctl_el1, x1
4:
#endif
        apply_ssbd 0, x0, x1
        .endif

        msr     elr_el1, x21                    // set up the return data
        msr     spsr_el1, x22
        ldp     x0, x1, [sp, #16 * 0]
        ldp     x2, x3, [sp, #16 * 1]
        ldp     x4, x5, [sp, #16 * 2]
        ldp     x6, x7, [sp, #16 * 3]
        ldp     x8, x9, [sp, #16 * 4]
        ldp     x10, x11, [sp, #16 * 5]
        ldp     x12, x13, [sp, #16 * 6]
        ldp     x14, x15, [sp, #16 * 7]
        ldp     x16, x17, [sp, #16 * 8]
        ldp     x18, x19, [sp, #16 * 9]
        ldp     x20, x21, [sp, #16 * 10]
        ldp     x22, x23, [sp, #16 * 11]
        ldp     x24, x25, [sp, #16 * 12]
        ldp     x26, x27, [sp, #16 * 13]
        ldp     x28, x29, [sp, #16 * 14]
        ldr     lr, [sp, #S_LR]
        add     sp, sp, #S_FRAME_SIZE           // restore sp

        .if     \el == 0
alternative_insn eret, nop, ARM64_UNMAP_KERNEL_AT_EL0
#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
        bne     5f
        msr     far_el1, x30
        tramp_alias     x30, tramp_exit_native
        br      x30
5:
        tramp_alias     x30, tramp_exit_compat
        br      x30
#endif
        .else
        eret
        .endif
        sb
        .endm
  • 코드 라인 1~3에서 el0에서 exception 진입한 경우 sp_el0를 복원한다.
  • 코드 라인 4~5에서 중단점에서의 PSTATE 값에 MODE32 비트가 설정된 경우 3f 레이블로 이동한다.
  • 코드 라인 7~16에서 Cortext-A53 아키텍처에서 잘못된 load가 발생할 수 있는 에러를 방지하기 위해 erratum 84715 코드를 지원하며, 설명은 생략한다.
  • 코드 라인 18~32에서 Cortex-A76 아키텍처의 AArch32에서 Generic 타이머의 잘못된 읽기가 발생할 수 있는 에러를 방지하기 위해 erratum 1165522 코드를 지원하며, 설명은 생략한다.
  • 코드 라인 33에서 SSBD(Speculative Store Bypass Disable) 기능을 사용하는 경우 Speculative Store 공격으로 부터 커널을 보호하기 위해 유저와 커널의 전환 시 EL1이 아닌 상위(EL2 or EL3)에서 수행하는 workaround를 적용한다.  사용하지 않는 경우 그냥 skip 한다.
레지스터들 복원
  • 코드 라인 36~37에서 중단 점 pc를 elr_el1 레지스터에 기록하고, 중단 점에서의 PSTATE 값을 spsr_el1에 다시 기록하여 복원한다.
  • 코드 라인 38~52에서 스택에 백업해둔 x0~x29까지의 레지스터 정보를 다시 복원한다.
  • 코드 라인 53~54에서 스택에 백업해둔 lr(x30) 레지스터를 복원하고, 스택을 pt_regs 사이즈만큼 pop 한다.
trampoline 사용 – 유저에서 커널 영역 분리
  • 코드 라인 56~66에서 el0에서 exception 진입한 경우이다. 유저 영역에서 커널 영역을 완전히 분리하여 액세스를 방지하기 위해 trampoline 벡터를 이용하여 tramp_vectors를 vbar_el1에 지정하고, 커널 영역은 분리한 후 exception 발생하였던 중단점으로 복귀한다.
  • 코드 라인 70에서 Speculation barrier 명령을 수행한다. (dsb, isb)

 

ct_user_enter 매크로

arch/arm64/kernel/entry.S

        .macro ct_user_enter
#ifdef CONFIG_CONTEXT_TRACKING
        bl      context_tracking_user_enter
#endif
        .endm

디버그를 위해 context 트래킹 커널 옵션을 사용하는 경우 유저 복귀 전 trace 출력을 지원한다.

 

sb 매크로

arch/arm64/include/asm/assembler.h

/*
 * Speculation barrier
 */
        .macro  sb
alternative_if_not ARM64_HAS_SB
        dsb     nsh
        isb
alternative_else
        SB_BARRIER_INSN
        nop
alternative_endif
        .endm

Speculation barrier로 시스템이 지원하지 않으면 dsb, isb를 사용하고, 지원하는 경우 sb 전용 명령을 사용한다.

 


SSBD(Speculative Store Bypass Disable)

Speculative Store 공격으로 부터 커널을 보호하기 위해 유저와 커널의 전환 시 EL1이 아닌 상위(EL2 or EL3)에서 수행하는 workaround를 적용한다.

 

apply_ssbd 매크로

arch/arm64/kernel/entry.S

.       // This macro corrupts x0-x3. It is the caller's duty
        // to save/restore them if required.
        .macro  apply_ssbd, state, tmp1, tmp2
#ifdef CONFIG_ARM64_SSBD
alternative_cb  arm64_enable_wa2_handling
        b       .L__asm_ssbd_skip\@
alternative_cb_end
        ldr_this_cpu    \tmp2, arm64_ssbd_callback_required, \tmp1
        cbz     \tmp2,  .L__asm_ssbd_skip\@
        ldr     \tmp2, [tsk, #TSK_TI_FLAGS]
        tbnz    \tmp2, #TIF_SSBD, .L__asm_ssbd_skip\@
        mov     w0, #ARM_SMCCC_ARCH_WORKAROUND_2
        mov     w1, #\state
alternative_cb  arm64_update_smccc_conduit
        nop                                     // Patched to SMC/HVC #0
alternative_cb_end
.L__asm_ssbd_skip\@:
#endif
        .endm

SSBD 기능을 적용할 수 있는 시스템에서 유저와 커널의 전환 시 상위(EL2 or EL3)에서 수행하게 한다. 적용되지 않는 시스템은 그냥 skip 한다.

  • 코드 라인 2~5에서 시스템이 SSBD 기능을 지원하는 경우 nop을 수행하고, 지원하지 않는 경우 branch 명령대로 .L__asm_ssbd_skip 레이블로 이동한다.
    • alternative_cb을 통해 부트업 타임에 arm64_enable_wa2_handling() 함수를 수행하여 SSBD 기능이 동작하는 경우 다음 branch 명령을 nop으로 대체한다.
  • 코드 라인 6~7에서 per-cpu 변수인 arm64_ssbd_callback_required 값이 0인 경우 .L__asm_ssbd_skip 레이블로 이동한다.
  • 코드 라인 8~9에서 현재 태스크에 TIF_SSBD 플래그가 설정된 경우가 아니면 .L__asm_ssbd_skip 레이블로 이동한다.
  • 코드 라인 10~14에서 w0 레지스터에 ARM_SMCCC_ARCH_WORKAROUND_2를 준비하고, w1 레지스터에 @state를 지정한 뒤 EL2의 hvc 또는 EL3의 smc를 호출한다.

 

arm64_update_smccc_conduit()

arch/arm64/kernel/cpu_errata.c

void __init arm64_update_smccc_conduit(struct alt_instr *alt,
                                       __le32 *origptr, __le32 *updptr,
                                       int nr_inst)
{
        u32 insn;

        BUG_ON(nr_inst != 1);

        switch (psci_ops.conduit) {
        case PSCI_CONDUIT_HVC:
                insn = aarch64_insn_get_hvc_value();
                break;
        case PSCI_CONDUIT_SMC:
                insn = aarch64_insn_get_smc_value();
                break;
        default:
                return;
        }

        *updptr = cpu_to_le32(insn);
}

SSBD 기능을 적용할 수 있는 시스템에서 hvc 또는 smc 명령으로 대체한다.

 

arm64_enable_wa2_handling()

arch/arm64/kernel/cpu_errata.c

void __init arm64_enable_wa2_handling(struct alt_instr *alt,
                                      __le32 *origptr, __le32 *updptr,
                                      int nr_inst)
{
        BUG_ON(nr_inst != 1);
        /*
         * Only allow mitigation on EL1 entry/exit and guest
         * ARCH_WORKAROUND_2 handling if the SSBD state allows it to
         * be flipped.
         */
        if (arm64_get_ssbd_state() == ARM64_SSBD_KERNEL)
                *updptr = cpu_to_le32(aarch64_insn_gen_nop());
}

SSBD 기능을 적용할 수 있는 시스템에서 nop으로 대체한다.

 


EL1 Exception

커널 동작 중 sync exception

el1_sync

arch/arm64/kernel/entry.S

        .align  6
el1_sync:
        kernel_entry 1
        mrs     x1, esr_el1                     // read the syndrome register
        lsr     x24, x1, #ESR_ELx_EC_SHIFT      // exception class
        cmp     x24, #ESR_ELx_EC_DABT_CUR       // data abort in EL1
        b.eq    el1_da
        cmp     x24, #ESR_ELx_EC_IABT_CUR       // instruction abort in EL1
        b.eq    el1_ia
        cmp     x24, #ESR_ELx_EC_SYS64          // configurable trap
        b.eq    el1_undef
        cmp     x24, #ESR_ELx_EC_PC_ALIGN       // pc alignment exception
        b.eq    el1_pc
        cmp     x24, #ESR_ELx_EC_UNKNOWN        // unknown exception in EL1
        b.eq    el1_undef
        cmp     x24, #ESR_ELx_EC_BREAKPT_CUR    // debug exception in EL1
        b.ge    el1_dbg
        b       el1_inv

el1_ia:
        /*
         * Fall through to the Data abort case
         */
el1_da:
        /*
         * Data abort handling
         */
        mrs     x3, far_el1
        inherit_daif    pstate=x23, tmp=x2
        untagged_addr   x0, x3
        mov     x2, sp                          // struct pt_regs
        bl      do_mem_abort

        kernel_exit 1
el1_pc:
        /*
         * PC alignment exception handling. We don't handle SP alignment faults,
         * since we will have hit a recursive exception when trying to push the
         * initial pt_regs.
         */
        mrs     x0, far_el1
        inherit_daif    pstate=x23, tmp=x2
        mov     x2, sp
        bl      do_sp_pc_abort
        ASM_BUG()
el1_undef:
        /*
         * Undefined instruction
         */
        inherit_daif    pstate=x23, tmp=x2
        mov     x0, sp
        bl      do_undefinstr
        kernel_exit 1
el1_dbg:
        /*
         * Debug exception handling
         */
        cmp     x24, #ESR_ELx_EC_BRK64          // if BRK64
        cinc    x24, x24, eq                    // set bit '0'
        tbz     x24, #0, el1_inv                // EL1 only
        gic_prio_kentry_setup tmp=x3
        mrs     x0, far_el1
        mov     x2, sp                          // struct pt_regs
        bl      do_debug_exception
        kernel_exit 1
el1_inv:
        // TODO: add support for undefined instructions in kernel mode
        inherit_daif    pstate=x23, tmp=x2
        mov     x0, sp
        mov     x2, x1
        mov     x1, #BAD_SYNC
        bl      bad_mode
        ASM_BUG()
ENDPROC(el1_sync)

el1에서 동기 exception이 발생하여 진입한 경우이다.

  • 코드 라인 3에서 context 전환을 위해 레지스터들을 스택에 pt_regs 구조체 만큼 백업한다.
  • 코드 라인 4~18에서 esr_el1(Exception Syndrom Register EL1) 레지스터를 읽어와서(x1) EC(Exception Class) 필드 값에 따라 다음과 같이 처리한다.
    • ESR_ELx_EC_DABT_CUR (0x25)
      • Data Abort인 경우 el1_da 레이블로 이동한다.
    • ESR_ELx_EC_IABT_CUR (0x21)
      • Instruction Abort인 경우 el1_ia 레이블로 이동한다.
    • ESR_ELx_EC_SYS64 (0x18)
      • Configurable Trap인 경우 el1_undef 레이블로 이동한다.
    • ESR_ELx_EC_PC_ALIGN (0x22)
      • PC Alignment Exception인 경우 el1_pc 레이블로 이동한다.
    • ESR_ELx_EC_UNKNOWN (0x00)
      • Unknown Exception인 경우 el1_undef 레이블로 이동한다.
    • ESR_ELx_EC_BREAKPT_CUR (0x31)
    • ESR_ELx_EC_SOFTSTP_CUR (0x33)
    • ESR_ELx_EC_WATCHPT_CUR (0x35)
      • 싱글 스텝 디버거를 사용하는 몇 종류의 Exception(0x31~)들인 경우 el1_dbg 레이블로 이동한다.
    • 그 외는 모두 처리 불가능한 abort exception이며, el1_inv 레이블로 이동한다.
el1_ia & el1_da 레이블 – 명령 및 데이터 abort exception
  • 코드 라인 28에서 far_el1(Fault Address Register EL1) 레지스터에서 fault 발생한 가상 주소를 읽어온다.
  • 코드 라인 29에서 중단된 곳의 PSTATE 값(x23) 중 DAIF 비트를 현재 PSTATE에 반영(복구)한다.
  • 코드 라인 30~32에서 do_mem_abort() 함수를 호출하고, 세 개의 인자는 다음과 같다.
    • 첫 번째 인자(x0) 읽어온 fault 가상 주소에 address 태그가 있으면 제거한 가상 주소
    • 두 번째 인자(x1) ESR 값
    • 세 번째 인자(x2) 스택의 pt_regs
  • 코드 라인 34에서 exception을 빠져나가서 중단점으로 복귀하기 위해 context를 복구한다.
    • 스택에 저장해둔 레지스터들을 다시 읽어들인다.
el1_pc 레이블 – PC alignment exception
  • 코드 라인 41~44에서 dp_sp_pc_abort() 함수를 호출하여 “SP/PC alignment exception” 출력하고 스스로 다운(die)된다.
    • 참고로 초기 pt_reg를 푸시하려고 할 때 재귀 예외가 발생하는 문제로 스택 alignment exceptin은 처리하지 않고, pc alignment excption만 처리한다.
  • 코드 라인 45에서 스스로 break 디버그 exception을 발생시킨다. exception이 발생하면 ESR_ELx 레지스터에는 다음과 같은 정보가 담긴다.
    • ESR_ELx.EC = 0x3c
    • ESR_ELx.ISS = BUG_BRK_IMM(0x800)
el1_undef 레이블 – 아키텍처 미정의 명령 exception
  • 코드 라인 50~52에서 do_undefinstr() 함수를 호출하여 아키텍처가 디코드하여 수행할 수 없는 명령(instruction)에 대해 다음과 같이 구분하여 처리한다.
    • 해당 명령에 대해 등록된 후크가 있는 경우 해당 후크 함수를 호출한다.
      • 후크 함수의 등록은 register_undef_hook() 함수를 사용한다.
      • 예) ssbd 기능 호출 시 아키텍처에 hvc 또는 smc 명령이 없으면 이를 emulation 한다.
    • 유저 application에서 아키텍처에 없는 명령을 사용한 경우 SIGILL 시그널을 발생시켜 application을 kill 시킨다.
    • 커널 코드에서 아키텍처에 없는 명령을 사용한 경우 시스템을 die() 처리한다. (커널 옵션에 따라 리셋)
  • 코드 라인 53에서 exception을 빠져나가서 중단점으로 복귀하기 위해 context를 복구한다.
el1_dbg 레이블 – 디버그 exception
  • 코드 라인 58~60에서 Exception 신드롬 레지스터의 exception class 코드 값이 짝수인 경우 싱글 스텝 디버그 처리를 하지 않고, el1_inv 레이블로 이동한다. 그런데 ESR_ELx_EC_BRK64(0x3c)의 경우는 짝수지만 강제로 홀수로 만들어 el1_inv 레이블로 보내지 않는다.
  • 코드 라인 61에서 Pesudo-NMI를 지원하는 시스템인 경우 irq 우선순위를 통과시키도록 허용한다.
  • 코드 라인 62~64에서 다음 exception class 들에 대해 싱글 스텝 디버깅 처리를 위한 do_debug_exception() 함수를 호출한다.
    • ESR_ELx_EC_BREAKPT_CUR(0x31)
    • ESR_ELx_EC_SOFTSTP_CUR(0x33)
    • ESR_ELx_EC_WATCHPT_CUR(0x35)
    • ESR_ELx_EC_BRK64(0x3c)
  • 코드 라인 65에서 exception을 빠져나가서 중단점으로 복귀하기 위해 context를 복구한다.
el1_inv 레이블 –  그 외 처리 불가능 abort exception
  • 코드 라인 68~72에서 bad_mode() 함수를 호출한 후 panic() 처리한다. (커널 옵션에 따라 리셋)

 

inherit_daif 매크로

arch/arm64/include/asm/assembler.h

.       /* Only on aarch64 pstate, PSR_D_BIT is different for aarch32 */
        .macro  inherit_daif, pstate:req, tmp:req
        and     \tmp, \pstate, #(PSR_D_BIT | PSR_A_BIT | PSR_I_BIT | PSR_F_BIT)
        msr     daif, \tmp
        .endm

PSTATE 값이 담긴 @pstate 레지스터의 DAIF 플래그만을 PSTATE에 반영한다. 임시로 사용된 @tmp 레지스터는 크래시된다.

 

untagged_addr 매크로

arch/arm64/include/asm/asm-uaccess.h

/*
 * Remove the address tag from a virtual address, if present.
 */
        .macro  untagged_addr, dst, addr
        sbfx    \dst, \addr, #0, #56
        and     \dst, \dst, \addr
        .endm

FAR(Fault Address Register)에서 읽어온 가상 주소의 상위 8비트 address 태그가 존재하면 제거한다.

  • 코드 라인 2에서 @addr 값의 하위 56비트를 @dst로 복사한다.
    • @addr 값은 FAR(Fault Address Register)에서 읽은 가상 주소가 담겨 있고, 상위 8비트는 unknown 상태일 수도 있다.
  • 코드 라인 3에서 위의 값을 다시 @addr 값과 and 하여 unknown 비트들을 제거한다.

 

gic_prio_kentry_setup 매크로

arch/arm64/kernel/entry.S

        .macro  gic_prio_kentry_setup, tmp:req
#ifdef CONFIG_ARM64_PSEUDO_NMI
        alternative_if ARM64_HAS_IRQ_PRIO_MASKING
        mov     \tmp, #(GIC_PRIO_PSR_I_SET | GIC_PRIO_IRQON)
        msr_s   SYS_ICC_PMR_EL1, \tmp
        alternative_else_nop_endif
#endif
        .endm

Pesudo-NMI를 지원하는 시스템인 경우 nmi 뿐만 아니라 일반 irq도 허용하도록 pmr에 기록한다. @tmp 레지스터에는 플래그 값을 반환한다.

 


커널 동작 중 irq exception

el1_irq

arch/arm64/kernel/entry.S

        .align  6
el1_irq:
        kernel_entry 1
        gic_prio_irq_setup pmr=x20, tmp=x1
        enable_da_f

#ifdef CONFIG_ARM64_PSEUDO_NMI
        test_irqs_unmasked      res=x0, pmr=x20
        cbz     x0, 1f
        bl      asm_nmi_enter
1:
#endif

#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_off
#endif

        irq_handler

#ifdef CONFIG_PREEMPT
        ldr     x24, [tsk, #TSK_TI_PREEMPT]     // get preempt count
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
        /*
         * DA_F were cleared at start of handling. If anything is set in DAIF,
         * we come back from an NMI, so skip preemption
         */
        mrs     x0, daif
        orr     x24, x24, x0
alternative_else_nop_endif
        cbnz    x24, 1f                         // preempt count != 0 || NMI return path
        bl      arm64_preempt_schedule_irq      // irq en/disable is done inside
1:
#endif

#ifdef CONFIG_ARM64_PSEUDO_NMI
        /*
         * When using IRQ priority masking, we can get spurious interrupts while
         * PMR is set to GIC_PRIO_IRQOFF. An NMI might also have occurred in a
         * section with interrupts disabled. Skip tracing in those cases.
         */
        test_irqs_unmasked      res=x0, pmr=x20
        cbz     x0, 1f
        bl      asm_nmi_exit
1:
#endif

#ifdef CONFIG_TRACE_IRQFLAGS
#ifdef CONFIG_ARM64_PSEUDO_NMI
        test_irqs_unmasked      res=x0, pmr=x20
        cbnz    x0, 1f
#endif
        bl      trace_hardirqs_on
1:
#endif

        kernel_exit 1
ENDPROC(el1_irq)

커널 동작 중 발생한 인터럽트(irq, nmi)를 처리한다.

  • 코드 라인 3에서 커널에서 exception이 발생하여 진입하였다. context를 스택에 백업한다.
  • 코드 라인 4에서 Pesudo-NMI를 지원하는 시스템에서 priority mask 레지스터에 @pmr 값을 기록한다.
  • 코드 라인 5에서 DAIF 중 I(irq)를 제외하고 enable(unmask) 한다.
  • 코드 라인 7~12에서 Pesudo-NMI 기능을 사용하는 시스템에서 nmi 진입 코드를 수행한다. Pesudo-NMI 기능을 사용하지 않는 시스템이거나 irq들이 unmask 상태인 경우 nmi 진입과 관련된 코드를 수행하지 않고 skip 한다.
  • 코드 라인 14~16에서 트레이스 출력이 허용된 경우에만 수행한다.
  • 코드 라인 18에서 irq 전용 스택으로 전환하고 인터럽트 컨트롤러의 irq 핸들러를 호출한다. 완료 후 태스크 스택으로 복원한다.
  • 코드 라인 20~33에서 커널 preemption이 가능한 커널 옵션을 사용하는 시스템에서 thread_info->preempt_count 값을 읽어 0인 경우 preemption이 요청되었다. 이러한 경우 우선 순위가 더 높은 태스크를 처리하기 위해 현재 태스크를 슬립하고 리스케줄한다. 만일 nmi 처리를 끝내고 돌아온 경우라면 DAIF 플래그 중 하나라도 설정되어 있다. 이 경우도 preemption 처리를 skip 한다.
  • 코드 라인 35~45에서 Pesudo-NMI 기능을 사용하는 시스템에서 nmi 처리가 끝난 경우 nmi 완료 코드를 수행한다. Pesudo-NMI 기능을 사용하지 않는 시스템이거나 irq들이 unmask 상태인 경우 nmi 처리 완료와 관련된 코드를 수행하지 않고 skip 한다.
  • 코드 라인 47~54에서 hard irq on에 대한 트레이스 출력을 수행한다.
  • 코드 라인 56에서 스택으로부터 context를 복원하고 exception이 발생하였던 중단점으로 돌아간다.

 

gic_prio_irq_setup 매크로

arch/arm64/kernel/entry.S

        .macro  gic_prio_irq_setup, pmr:req, tmp:req
#ifdef CONFIG_ARM64_PSEUDO_NMI
        alternative_if ARM64_HAS_IRQ_PRIO_MASKING
        orr     \tmp, \pmr, #GIC_PRIO_PSR_I_SET
        msr_s   SYS_ICC_PMR_EL1, \tmp
        alternative_else_nop_endif
#endif
        .endm

Pesudo-NMI를 지원하는 시스템에서 priority mask 레지스터에 @pmr 값을 기록한다.

  • 실제 기록 시 @pmr 값에 GIC_PRIO_PSR_I_SET(0x10) 비트를 더해 SYS_ICC_PMR_EL1에 기록한다.
  • GIC_PRIO_PSR_I_SET 값은 PSTATE의 I 비트 복원이 잘안되는 현상이 있어 정확한 복원을 위해 비트 확인이 가능하도록 추가하였다.

 

enable_da_f  매크로

arch/arm64/include/asm/assembler.h

        /* IRQ is the lowest priority flag, unconditionally unmask the rest. */
.       .macro enable_da_f
        msr     daifclr, #(8 | 4 | 1)
        .endm

PSTATE의 DA_F 플래그들을 클리어한다.

  • I(irq) 플래그는 처리하지 않는다.
  • 참고로 IRQ exception된 경우 아키텍처는 자동으로 I를 마스크하며 exception 진입된다.

 

test_irqs_unmasked 매크로

arch/arm64/kernel/entry.S

.       /*
         * Set res to 0 if irqs were unmasked in interrupted context.
         * Otherwise set res to non-0 value.
         */
        .macro  test_irqs_unmasked res:req, pmr:req
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
        sub     \res, \pmr, #GIC_PRIO_IRQON
alternative_else
        mov     \res, xzr
alternative_endif
        .endm
#endif

Priority masking이 가능한 시스템인 경우 @pmr 값을 확인하여 irq가 unmask 상태인지 확인한다.

  • Priority masking이 가능한 시스템이 아닌 경우 항상 0을 반환한다.

 

irq_handler 매크로

arch/arm64/kernel/entry.S

/*
 * Interrupt handling.
 */
.       .macro  irq_handler
        ldr_l   x1, handle_arch_irq
        mov     x0, sp
        irq_stack_entry
        blr     x1
        irq_stack_exit
        .endm

인터럽트를 처리하기 위해 인터럽트 컨트롤러의 핸들러 함수를 호출한다.

  • 인터럽트 컨트롤러가 초기화될 때 전역 (*handle_arch_irq) 후크 함수에 해당 인터럽트 컨트롤러의 핸들러 함수가 대입된다.
    • 예) gic-v3 -> gic_of_init()

 


IRQ 전용 스택 사용

irq_stack_ptr

arch/arm64/kernel/irq.c

DEFINE_PER_CPU(unsigned long *, irq_stack_ptr);

컴파일 타임에 static하게 irq 스택을 가리키는 포인터가 cpu 마다 사용된다.

  • 초기화 함수는 init_IRQ() -> init_irq_stacks() 함수이다.
  • CONFIG_VMAP_STACK을 사용하는 경우 vmalloc 영역에 irq 스택을 할당하여 사용한다.

 

irq_stack

arch/arm64/kernel/irq.c

/* irq stack only needs to be 16 byte aligned - not IRQ_STACK_SIZE aligned. */
DEFINE_PER_CPU_ALIGNED(unsigned long [IRQ_STACK_SIZE/sizeof(long)], irq_stack);
  • CONFIG_VMAP_STACK을 사용하지 않는 경우 cpu 마다 컴파일 타임에 준비한 irq_stack을 사용한다.

 

irq_stack_entry 매크로

arch/arm64/kernel/entry.S

        .macro  irq_stack_entry
        mov     x19, sp                 // preserve the original sp

        /*
         * Compare sp with the base of the task stack.
         * If the top ~(THREAD_SIZE - 1) bits match, we are on a task stack,
         * and should switch to the irq stack.
         */
        ldr     x25, [tsk, TSK_STACK]
        eor     x25, x25, x19
        and     x25, x25, #~(THREAD_SIZE - 1)
        cbnz    x25, 9998f

        ldr_this_cpu x25, irq_stack_ptr, x26
        mov     x26, #IRQ_STACK_SIZE
        add     x26, x25, x26

        /* switch to the irq stack */
        mov     sp, x26
9998:
        .endm

인터럽트 컨트롤러의 핸들러 함수를 호출하기 전에 irq 스택으로 전환한다.

  • 코드 라인 2에서 스택을 x19에 백업해둔다.
  • 코드 라인 9~12에서 현재 스택이 thread_info->stack에서 지정한 태스크용 스택 범위 밖에 있는 경우 이미 irq 스택을 사용 중이므로 9998: 레이블로 이동한다.
  • 코드 라인 14~19에서 irq 스택으로 전환한다.

 

irq_stack_exit 매크로

arch/arm64/kernel/entry.S

        /*
         * x19 should be preserved between irq_stack_entry and
         * irq_stack_exit.
         */
.       .macro  irq_stack_exit
        mov     sp, x19
        .endm

인터럽트 컨트롤러의 핸들러 함수의 호출 이후 태스크 스택으로 복원한다.

  • 이 매크로와 한쌍인 irq_stack_entry 매크로에서 x19 레지스터를 사용하여 기존 스택을 백업해두었었다.

 


EL0 Exception

유저 동작 중 sync exception

el0_sync

arch/arm64/kernel/entry.S -1/3-

/*
 * EL0 mode handlers.
 */
        .align  6
el0_sync:
        kernel_entry 0
        mrs     x25, esr_el1                    // read the syndrome register
        lsr     x24, x25, #ESR_ELx_EC_SHIFT     // exception class
        cmp     x24, #ESR_ELx_EC_SVC64          // SVC in 64-bit state
        b.eq    el0_svc
        cmp     x24, #ESR_ELx_EC_DABT_LOW       // data abort in EL0
        b.eq    el0_da
        cmp     x24, #ESR_ELx_EC_IABT_LOW       // instruction abort in EL0
        b.eq    el0_ia
        cmp     x24, #ESR_ELx_EC_FP_ASIMD       // FP/ASIMD access
        b.eq    el0_fpsimd_acc
        cmp     x24, #ESR_ELx_EC_SVE            // SVE access
        b.eq    el0_sve_acc
        cmp     x24, #ESR_ELx_EC_FP_EXC64       // FP/ASIMD exception
        b.eq    el0_fpsimd_exc
        cmp     x24, #ESR_ELx_EC_SYS64          // configurable trap
        ccmp    x24, #ESR_ELx_EC_WFx, #4, ne
        b.eq    el0_sys
        cmp     x24, #ESR_ELx_EC_SP_ALIGN       // stack alignment exception
        b.eq    el0_sp
        cmp     x24, #ESR_ELx_EC_PC_ALIGN       // pc alignment exception
        b.eq    el0_pc
        cmp     x24, #ESR_ELx_EC_UNKNOWN        // unknown exception in EL0
        b.eq    el0_undef
        cmp     x24, #ESR_ELx_EC_BREAKPT_LOW    // debug exception in EL0
        b.ge    el0_dbg
        b       el0_inv

#ifdef CONFIG_COMPAT
        .align  6
el0_sync_compat:
        kernel_entry 0, 32
        mrs     x25, esr_el1                    // read the syndrome register
        lsr     x24, x25, #ESR_ELx_EC_SHIFT     // exception class
        cmp     x24, #ESR_ELx_EC_SVC32          // SVC in 32-bit state
        b.eq    el0_svc_compat
        cmp     x24, #ESR_ELx_EC_DABT_LOW       // data abort in EL0
        b.eq    el0_da
        cmp     x24, #ESR_ELx_EC_IABT_LOW       // instruction abort in EL0
        b.eq    el0_ia
        cmp     x24, #ESR_ELx_EC_FP_ASIMD       // FP/ASIMD access
        b.eq    el0_fpsimd_acc
        cmp     x24, #ESR_ELx_EC_FP_EXC32       // FP/ASIMD exception
        b.eq    el0_fpsimd_exc
        cmp     x24, #ESR_ELx_EC_PC_ALIGN       // pc alignment exception
        b.eq    el0_pc
        cmp     x24, #ESR_ELx_EC_UNKNOWN        // unknown exception in EL0
        b.eq    el0_undef
        cmp     x24, #ESR_ELx_EC_CP15_32        // CP15 MRC/MCR trap
        b.eq    el0_cp15
        cmp     x24, #ESR_ELx_EC_CP15_64        // CP15 MRRC/MCRR trap
        b.eq    el0_cp15
        cmp     x24, #ESR_ELx_EC_CP14_MR        // CP14 MRC/MCR trap
        b.eq    el0_undef
        cmp     x24, #ESR_ELx_EC_CP14_LS        // CP14 LDC/STC trap
        b.eq    el0_undef
        cmp     x24, #ESR_ELx_EC_CP14_64        // CP14 MRRC/MCRR trap
        b.eq    el0_undef
        cmp     x24, #ESR_ELx_EC_BREAKPT_LOW    // debug exception in EL0
        b.ge    el0_dbg
        b       el0_inv

el0에서 동기 exception이 발생하여 진입한 경우이다.

  • 코드 라인 3에서 context 전환을 위해 레지스터들을 스택에 pt_regs 구조체 만큼 백업한다.
  • 코드 라인 4~29에서 esr_el1(Exception Syndrom Register EL1) 레지스터를 읽어와서 EC(Exception Class) 필드 값에 따라 다음과 같이 처리한다.
    • ESR_ELx_EC_SVC64 (0x15)
      • EL0 AArch64 application에서 syscall 호출한 경우 el0_svc 레이블로 이동한다.
    • ESR_ELx_EC_DABT_LOW (0x24)
      • EL0에서 Data Abort인 경우 el0_da 레이블로 이동한다.
    • ESR_ELx_EC_IABT_LOW (0x20)
      • EL0에서 Instruction Abort인 경우 el0_ia 레이블로 이동한다.
    • ESR_ELx_EC_FP_ASIMD (0x07)
      • Advanced SIMD 또는 부동 소숫점 명령 사용 시 트랩된 경우 el0_fpsimd_acc 레이블로 이동한다.
    • ESR_ELx_EC_SVE (0x19)
      • SVE 기능으로 트랩된 경우 el0_sve_acc 레이블로 이동한다.
    • ESR_ELx_EC_FP_EXC64 (0x2c)
      • 부동 소숫점 명령으로 exception 발생 시 el0_fpsimd_exc 레이블로 이동한다.
    • ESR_ELx_EC_SYS64 (0x18)
      • MSR 및 MRS 명령에 의해 트랩되었고
    • ESR_ELx_EC_WFx (0x1)
      • WFI 또는 WFE 명령으로 트랩된 경우가 아니면 el0_sys 레이블로 이동한다.
    • ESR_ELx_EC_SP_ALIGN (0x26)
      • SP Alignment Exception인 경우 el0_sp 레이블로 이동한다.
    • ESR_ELx_EC_PC_ALIGN (0x22)
      • PC Alignment Exception인 경우 el0_pc 레이블로 이동한다.
    • ESR_ELx_EC_UNKNOWN (0x00)
      • Unknown Exception인 경우 el0_undef 레이블로 이동한다.
    • ESR_ELx_EC_BREAKPT_LOW (0x30)
    • ESR_ELx_EC_SOFTSTP_LOW (0x32)
    • ESR_ELx_EC_WATCHPT_LOW (0x34)
    • ESR_ELx_EC_BRK64 (0x3c)
      • EL0에서 싱글 스텝 디버거를 사용한 트랩인 경우 el0_dbg 레이블로 이동한다.
    • 그 외는 모두 처리 불가능한 abort exception이며, el0_inv 레이블로 이동한다.
  • 코드 라인 34에서 context 전환을 위해 레지스터들을 스택에 pt_regs 구조체 만큼 백업한다.
  • 코드 라인 35~63에서 esr_el1(Exception Syndrom Register EL1) 레지스터를 읽어와서 EC(Exception Class) 필드 값에 따라 다음과 같이 처리한다.
    • ESR_ELx_EC_SVC32 (0x11)
      • AArch32 application에서 syscall 호출한 경우 el0_svc_compat 레이블로 이동한다.
    • ESR_ELx_EC_DABT_LOW (0x24)
      • EL0에서 Data Abort인 경우 el0_da 레이블로 이동한다.
    • ESR_ELx_EC_IABT_LOW (0x20)
      • EL0에서 Instruction Abort인 경우 el0_ia 레이블로 이동한다.
    • ESR_ELx_EC_FP_ASIMD (0x07)
      • Advanced SIMD 또는 부동 소숫점 명령 사용 시 트랩된 경우 el0_fpsimd_acc 레이블로 이동한다.
    • ESR_ELx_EC_FP_EXC32 (0x28)
      • 부동 소숫점 명령으로 exception 발생 시 el0_fpsimd_exc 레이블로 이동한다.
    • ESR_ELx_EC_PC_ALIGN (0x22)
      • PC Alignment Exception인 경우 el0_pc 레이블로 이동한다.
    • ESR_ELx_EC_UNKNOWN (0x00)
      • Unknown Exception인 경우 el0_undef 레이블로 이동한다.
    • ESR_ELx_EC_CP15_32 (0x03)
    • ESR_ELx_EC_CP15_64 (0x04)
      • CP15 명령으로 트랩된 경우 el0_cp15 레이블로 이동한다.
    • ESR_ELx_EC_CP14_MR (0x05)
    • ESR_ELx_EC_CP14_LS (0x06)
    • ESR_ELx_EC_CP14_64 (0x0c)
      • CP14 명령으로 트랩된 경우 el0_undef 레이블로 이동한다.
    • ESR_ELx_EC_BREAKPT_LOW (0x30)
    • ESR_ELx_EC_SOFTSTP_LOW (0x32)
    • ESR_ELx_EC_WATCHPT_LOW (0x34)
    • ESR_ELx_EC_BRK64 (0x3c)
      • EL0에서 싱글 스텝 디버거를 사용한 트랩인 경우 el0_dbg 레이블로 이동한다.
    • 그 외는 모두 처리 불가능한 abort exception이며, el0_inv 레이블로 이동한다.

 

arch/arm64/kernel/entry.S -2/3-

el0_svc_compat:
        gic_prio_kentry_setup tmp=x1
        mov     x0, sp
        bl      el0_svc_compat_handler
        b       ret_to_user

        .align  6
el0_cp15:
        /*
         * Trapped CP15 (MRC, MCR, MRRC, MCRR) instructions
         */
        ct_user_exit_irqoff
        enable_daif
        mov     x0, x25
        mov     x1, sp
        bl      do_cp15instr
        b       ret_to_user
#endif

el0_da:
        /*
         * Data abort handling
         */
        mrs     x26, far_el1
        ct_user_exit_irqoff
        enable_daif
        untagged_addr   x0, x26
        mov     x1, x25
        mov     x2, sp
        bl      do_mem_abort
        b       ret_to_user
el0_ia:
        /*
         * Instruction abort handling
         */
        mrs     x26, far_el1
        gic_prio_kentry_setup tmp=x0
        ct_user_exit_irqoff
        enable_da_f
#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_off
#endif
        mov     x0, x26
        mov     x1, x25
        mov     x2, sp
        bl      do_el0_ia_bp_hardening
        b       ret_to_user
el0_fpsimd_acc:
        /*
         * Floating Point or Advanced SIMD access
         */
        ct_user_exit_irqoff
        enable_daif
        mov     x0, x25
        mov     x1, sp
        bl      do_fpsimd_acc
        b       ret_to_user
el0_sve_acc:
        /*
         * Scalable Vector Extension access
         */
        ct_user_exit_irqoff
        enable_daif
        mov     x0, x25
        mov     x1, sp
        bl      do_sve_acc
        b       ret_to_user
el0_fpsimd_exc:
        /*
         * Floating Point, Advanced SIMD or SVE exception
         */
        ct_user_exit_irqoff
        enable_daif
        mov     x0, x25
        mov     x1, sp
        bl      do_fpsimd_exc
        b       ret_to_user
el0_svc_compat 레이블 – AArch32에서 SVC 호출
  • 코드 라인 2에서 Pesudo-NMI를 지원하는 시스템인 경우 irq 우선순위를 통과시키도록 허용한다.
  • 코드 라인 3~4에서 el0_svc_compat_handler() 함수를 호출하여 syscall 서비스를 수행한다.
  • 코드 라인 5에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_cp15 레이블 – CP15 (MRC, MCR, MRRC, MCRR) 트랩
  • 코드 라인 12에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 13에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 14~16에서 do_cp15instr() 함수를 호출한다.
    • 유저 application에서 타이머 카운터(CNTVCT) 레지스터 값을 읽기 위해 트랩을 사용하였다.
  • 코드 라인 17에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_da 레이블 – Data Abort exception
  • 코드 라인 24에서 far_el1(Fault Address Register EL1) 레지스터에서 fault 발생한 가상 주소를 읽어온다.
  • 코드 라인 25에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 26에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 27~30에서 do_mem_abort() 함수를 호출하고, 세 개의 인자는 다음과 같다.
    • 첫 번째 인자(x0) 읽어온 fault 가상 주소에 address 태그가 있으면 제거한 가상 주소
    • 두 번째 인자(x1) ESR(Exception Syndrom Register) 값
    • 세 번째 인자(x2) 스택의 pt_regs
  • 코드 라인 31에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_ia 레이블 – Instruction Abort exception
  • 코드 라인 36에서 far_el1(Fault Address Register EL1) 레지스터에서 fault 발생한 가상 주소를 읽어온다.
  • 코드 라인 37에서 Pesudo-NMI를 지원하는 시스템인 경우 irq 우선순위를 통과시키도록 허용한다.
  • 코드 라인 38에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 39에서 PSTATE의 DA_F를 클리어하여 인터럽트만 제외하고 exception을 허용하게 한다.
  • 코드 라인 40~42에서 트레이스 출력이 허용된 경우에만 수행한다.
  • 코드 라인 43~46에서 do_el0_ia_bp_hardening() 함수를 호출하고, 세 개의 인자는 다음과 같다.
    • 첫 번째 인자(x0) 읽어온 fault 가상 주소에 address 태그가 있으면 제거한 가상 주소
    • 두 번째 인자(x1) ESR(Exception Syndrom Register) 값
    • 세 번째 인자(x2) 스택의 pt_regs
  • 코드 라인 47에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_fpsimd_acc 레이블 – 부동 소숫점 또는 Advanced SIMD 트랩
  • 코드 라인 52에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 54에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 54~56에서 do_fpsimd_acc() 함수를 호출한다. 함수 내부는 경고만 출력한다.
  • 코드 라인 57에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_sve_acc 레이블 – SVE 호출
  • 코드 라인 62에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 63에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 64~66에서 do_sve_acc() 함수를 호출하여 태스크의 첫 호출인 경우 SVE를 지원하기 위해 준비 작업을 수행한다.
    • SVE(Scalable Vector Extension)는 ARMv8.2 아키텍처 이상에서 Advanced SIMD의 벡터를 더 wide한 벡터를 지원한다.
  • 코드 라인 67에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_fpsimd_exc 레이블 – 부동 소숫점 또는 Advanced SIMD exception
  • 코드 라인 72에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 73에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 74~76에서 do_fpsimd_exc() 함수를 호출하여 현재 태스크에 SIGFPE 시그널을 발생시킨다.
  • 코드 라인 77에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.

 

arch/arm64/kernel/entry.S -3/3-

el0_sp:
        ldr     x26, [sp, #S_SP]
        b       el0_sp_pc
el0_pc:
        mrs     x26, far_el1
el0_sp_pc:
        /*
         * Stack or PC alignment exception handling
         */
        gic_prio_kentry_setup tmp=x0
        ct_user_exit_irqoff
        enable_da_f
#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_off
#endif
        mov     x0, x26
        mov     x1, x25
        mov     x2, sp
        bl      do_sp_pc_abort
        b       ret_to_user
el0_undef:
        /*
         * Undefined instruction
         */
        ct_user_exit_irqoff
        enable_daif
        mov     x0, sp
        bl      do_undefinstr
        b       ret_to_user
el0_sys:
        /*
         * System instructions, for trapped cache maintenance instructions
         */
        ct_user_exit_irqoff
        enable_daif
        mov     x0, x25
        mov     x1, sp
        bl      do_sysinstr
        b       ret_to_user
el0_dbg:
        /*
         * Debug exception handling
         */
        tbnz    x24, #0, el0_inv                // EL0 only
        mrs     x24, far_el1
        gic_prio_kentry_setup tmp=x3
        ct_user_exit_irqoff
        mov     x0, x24
        mov     x1, x25
        mov     x2, sp
        bl      do_debug_exception
        enable_da_f
        b       ret_to_user
el0_inv:
        ct_user_exit_irqoff
        enable_daif
        mov     x0, sp
        mov     x1, #BAD_SYNC
        mov     x2, x25
        bl      bad_el0_sync
        b       ret_to_user
ENDPROC(el0_sync)
el0_sp 레이블 – SP alignment exection
  • 코드 라인 2~3에서 스택에 저장한 중단점에서의 스택 값을 읽은 후 아래 el0_sp_pc: 레이블로 이동한다.
el0_pc 레이블 – PC alignment exection
  • 코드 라인 5에서 far_el1(Fault Address Register EL1) 레지스터에서 fault 발생한 가상 주소를 읽어온다.
  • 코드 라인 6에서 el0_sp와 el0_pc가 공동으로 사용하는 el0_sp_pc 레이블이다.
  • 코드 라인 10에서 Pesudo-NMI를 지원하는 시스템인 경우 irq 우선순위를 통과시키도록 허용한다.
  • 코드 라인 11에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 12에서 PSTATE의 DA_F를 클리어하여 인터럽트만 제외하고 exception을 허용하게 한다.
  • 코드 라인 13~15에서 트레이스 출력이 허용된 경우에만 수행한다.
  • 코드 라인 16~19에서 do_sp_pc_abort() 함수를 호출하여 현재 태스크에 SIGKILL을 발생시킨다. 그리고, 세 개의 인자는 다음과 같다.
    • 첫 번째 인자(x0) 읽어온 fault 가상 주소에 address 태그가 있으면 제거한 가상 주소
    • 두 번째 인자(x1) ESR(Exception Syndrom Register) 값
    • 세 번째 인자(x2) 스택의 pt_regs
  • 코드 라인 20에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_undef 레이블 – Undefined instruction exception
  • 코드 라인 25에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 26에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 27~28에서 do_undefinstr() 함수를 호출하여 특정 명령에 대해 등록된 후크 함수를 호출한다. 만일 그러한 후크 함수가 없으면 현재 태스크에 SIGILL을 발생시킨다.
  • 코드 라인 29에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_sys 레이블 – 시스템 명령 트랩
  • 코드 라인 34에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 35에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 36~38에서 do_sysinstr() 함수를 호출하여 특정 시스템 명령에 대해 등록된 후크 함수를 호출한다. 만일 그러한 후크 함수가 없으면 현재 태스크에 SIGILL을 발생시킨다.
  • 코드 라인 39에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_dbg 레이블 – 디버그 트랩
  • 코드 라인 44에서 el0인 경우 el0_inv 레이블로 이동한다.
  • 코드 라인 45에서 far_el1(Fault Address Register EL1) 레지스터에서 fault 발생한 가상 주소를 읽어온다.
  • 코드 라인 46에서 Pesudo-NMI를 지원하는 시스템인 경우 irq 우선순위를 통과시키도록 허용한다.
  • 코드 라인 47에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 48~51에서 do_debug_exception() 함수를 호출한다.
  • 코드 라인 52에서 PSTATE의 DA_F를 클리어하여 인터럽트만 제외하고 exception을 허용하게 한다.
  • 코드 라인 53에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_inv 레이블 – 그 외 처리 불가능 abort exception
  • 코드 라인 55에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 56에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 57~60에서 bad_el0_sync 함수를 호출하여 현재 태스크에 SIGILL을 발생시킨다.
  • 코드 라인 61에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.

 


유저 동작 중 irq exception

el0_irq

arch/arm64/kernel/entry.S

        .align  6
el0_irq:
        kernel_entry 0
el0_irq_naked:
        gic_prio_irq_setup pmr=x20, tmp=x0
        ct_user_exit_irqoff
        enable_da_f

#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_off
#endif

#ifdef CONFIG_HARDEN_BRANCH_PREDICTOR
        tbz     x22, #55, 1f
        bl      do_el0_irq_bp_hardening
1:
#endif
        irq_handler

#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_on
#endif
        b       ret_to_user
ENDPROC(el0_irq)

64비트 유저 동작 중 발생한 인터럽트(irq, nmi)를 처리한다.

  • 코드 라인 3에서 64비트 유저에서 exception이 발생하여 진입하였다. context를 스택에 백업한다.
  • 코드 라인 4에서 32비트 유저 irq exception도 이곳에서 공통으로 처리하기 위해 진입을 위한 el0_irq_naked 레이블이다.
  • 코드 라인 5에서 Pesudo-NMI를 지원하는 시스템에서 priority mask 레지스터에 @pmr 값을 기록한다.
  • 코드 라인 6에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 7에서 PSTATE의 DAIF 중 I(irq)를 제외하고 enable(unmask) 한다.
  • 코드 라인 9~11에서 hardirq off에 대해 트레이스 출력이 허용된 경우에만 수행한다.
  • 코드 라인 13~17에서 BP hardning을 위해 do_el0_irq_bp_hardening() 함수를 호출한다.
  • 코드 라인 18에서 irq 전용 스택으로 전환하고 인터럽트 컨트롤러의 irq 핸들러를 호출한다. 완료 후 태스크 스택으로 복원한다.
  • 코드 라인 20~22에서 hardirq on에 대해 트레이스 출력이 허용된 경우에만 수행한다.
  • 코드 라인 23에서 스택으로부터 context를 복원하고 exception이 발생하였던 중단점으로 돌아간다.

 

AArch32 EL0 irq exception

el0_irq_compat

arch/arm64/kernel/entry.S

        .align  6
el0_irq_compat:
        kernel_entry 0, 32
        b       el0_irq_naked

32bit 유저 동작 중 발생한 인터럽트(irq, nmi)를 처리한다.

  • 코드 라인 3에서 32비트 유저에서 exception이 발생하여 진입하였다. context를 스택에 백업한다.
  • 코드 라인 4에서 el0_irq_naked 레이블로 이동한다.

 


BP(Branch Predictor) Hardening

일부 고성능 ARM64 시스템의 경우 BP(Branch Predictor)를 사용한 Speculation 부채널 공격(side channel Attck)을 통해 시스템의 보안이 뚫리는 경우가 발생하여 이를 보완하기 위해 BP를 숨기는 기능을 수행한다.

 

do_el0_ia_bp_hardening()

arch/arm64/mm/fault.c

asmlinkage void __exception do_el0_ia_bp_hardening(unsigned long addr,
                                                   unsigned int esr,
                                                   struct pt_regs *regs)
{
        /*
         * We've taken an instruction abort from userspace and not yet
         * re-enabled IRQs. If the address is a kernel address, apply
         * BP hardening prior to enabling IRQs and pre-emption.
         */
        if (!is_ttbr0_addr(addr))
                arm64_apply_bp_hardening();

        local_daif_restore(DAIF_PROCCTX);
        do_mem_abort(addr, esr, regs);
}

유저 모드에서 instruction abort exception이 발생한 경우 bp hardning 기능을 수행한 후 메모리 fault 처리를 수행한다.

  • 코드 라인 10~11에서 fault가 발생한 주소가 유저 주소인 경우 bp hardning을 수행한다.
  • 코드 라인 13에서 PSTATE의 DAIF를 모두 클리어해 인터럽트 등을 허용한다.
  • 코드 라인 14에서 fault 처리를 수행한다.

 

is_ttbr0_addr()

arch/arm64/mm/fault.c

static inline bool is_ttbr0_addr(unsigned long addr)
{
        /* entry assembly clears tags for TTBR0 addrs */
        return addr < TASK_SIZE;
}

유저 가상 주소인지 여부를 반환한다.

 

arm64_apply_bp_hardening()

arch/arm64/include/asm/mmu.h

static inline void arm64_apply_bp_hardening(void)
{
        struct bp_hardening_data *d;

        if (!cpus_have_const_cap(ARM64_HARDEN_BRANCH_PREDICTOR))
                return;

        d = arm64_get_bp_hardening_data();
        if (d->fn)
                d->fn();
}

ARM64_HARDEN_BRANCH_PREDICTOR 기능(capability)을 가진 시스템에서 workround가 필요한 경우 호출된다.

  • Qualcom FALKOR 아키텍처의 경우다음 함수가 호출된다.
    • qcom_link_stack_sanitization()
  • 그 외 smccc 호출을 통해 해당 아키텍처가 workaround가 필요한 경우 다음 함수중 하나가 호출된다.
    • call_hvc_arch_workaround_1()
    • call_smc_arch_workaround_1()

 

arm64_get_bp_hardening_data()

arch/arm64/include/asm/mmu.h

static inline struct bp_hardening_data *arm64_get_bp_hardening_data(void)
{
        return this_cpu_ptr(&bp_hardening_data);
}

 

bp_hardening_data

arch/arm64/kernel/cpu_errata.c

DEFINE_PER_CPU_READ_MOSTLY(struct bp_hardening_data, bp_hardening_data);

 

bp_hardening_data 구조체

arch/arm64/include/asm/mmu.h

struct bp_hardening_data {
        int                     hyp_vectors_slot;
        bp_hardening_cb_t       fn;
};
  • hyp_vectors_slot
    • KVM을 사용하는 경우 사용될 4개의 하이퍼 바이저 벡터용 슬롯
  •  fn
    • Branch Predict 숨김 기능을 위해 workaround 기능을 수행할 콜백 함수가 담긴다.

 


유저 복귀

ct_user_exit_irqoff 매크로

arch/arm64/kernel/entry.S

/*
 * Context tracking subsystem.  Used to instrument transitions
 * between user and kernel mode.
 */
        .macro ct_user_exit_irqoff
#ifdef CONFIG_CONTEXT_TRACKING
        bl      enter_from_user_mode
#endif
        .endm

유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.

  • 유저 사용 시간 산출 및 트레이스 출력을 수행한다.

 

ret_to_user & finish_ret_to_user 레이블

arch/arm64/kernel/entry.S

/*
 * "slow" syscall return path.
 */
ret_to_user:
        disable_daif
        gic_prio_kentry_setup tmp=x3
        ldr     x1, [tsk, #TSK_TI_FLAGS]
        and     x2, x1, #_TIF_WORK_MASK
        cbnz    x2, work_pending
finish_ret_to_user:
        enable_step_tsk x1, x2
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
        bl      stackleak_erase
#endif
        kernel_exit 0
ENDPROC(ret_to_user)

exception을 빠져나가서 유저 중단점으로 복귀하기 전에 pending 작업을 처리한 후 context를 복구한다. pending 작업들에는 리스케줄, 시그널 처리 등이 있다.

  • 코드 라인 2에서 PSTATE의 DAIF를 마스크하여 인터럽트 등을 허용하지 않는다.
  • 코드 라인 3에서 Pesudo-NMI를 지원하는 시스템인 경우 irq 우선순위를 통과시키도록 허용한다.
  • 코드 라인 4~6에서 thread_info->flag에 _TIF_WORK_MASK에 해당하는 플래그들이 있는 경우 이에 대한 pending 작업을 수행한다.
  • 코드 라인 8에서 현재 태스크에 싱글 스텝 기능이 꺼져 있으면 활성화시킨다.
  • 코드 라인 9~11에서 보안을 위해 위해 syscall 호출후 복귀전에 커널 스택의 빈 공간을 STACKLEAK_POISON(-0xBEEF) 값으로 클리어한다.
  • 코드 라인 12에서 스택으로부터 context를 복원하고 exception이 발생하였던 중단점으로 돌아간다.

 

_TIF_WORK_MASK

arch/arm64/include/asm/thread_info.h

#define _TIF_WORK_MASK          (_TIF_NEED_RESCHED | _TIF_SIGPENDING | \
                                 _TIF_NOTIFY_RESUME | _TIF_FOREIGN_FPSTATE | \
                                 _TIF_UPROBE | _TIF_FSCHECK)

커널에서 유저로 Context 복귀하기 전에 수행할 작업에 대한 플래그들이다.

 

work_pending 레이블

arch/arm64/kernel/entry.S

/*
 * Ok, we need to do extra processing, enter the slow path.
 */
work_pending:
        mov     x0, sp                          // 'regs'
        bl      do_notify_resume
#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_on               // enabled while in userspace
#endif
        ldr     x1, [tsk, #TSK_TI_FLAGS]        // re-check for single-step
        b       finish_ret_to_user

pending된 작업을 처리한다. (Slowpath 작업이므로 EL0 복귀 시에만 수행한다.)

  • 코드 라인 2~3에서 pending된 작업을 처리한다. 인자로 pt_regs와 thread_info->flag를 사용한다.
  • 코드 라인 4~6에서 hard irq on에 대한 트레이스 출력을 수행한다.
  • 코드 라인 7~8에서 thread_info->flags 값을 x1 레지스터로 다시 읽어들인 후 finish_ret_to_user 레이블로 이동한다.

 

enable_step_tsk 매크로

arch/arm64/include/asm/assembler.h

        /* call with daif masked */
        .macro  enable_step_tsk, flgs, tmp
        tbz     \flgs, #TIF_SINGLESTEP, 9990f
        mrs     \tmp, mdscr_el1
        orr     \tmp, \tmp, #DBG_MDSCR_SS
        msr     mdscr_el1, \tmp
9990:
        .endm

현재 태스크에 싱글 스텝 기능이 꺼져 있으면 활성화시킨다.

  • 코드 라인 3에서 @flgs(thread_info->flag) 값에 TIF_SINGLESTEP 값이 없으면 9990 레이블로 이동한다.
  • 코드 라인 4~6에서 MDSCR_EL1(Monitor Debug System Control Register)의 SS 비트를 설정하여 software step 기능을 활성화한다. @tmp 레지스터는 스크래치 레지스터로 이용된다.

 

참고

 

Exception -7- (ARM64 Vector)

<kernel v5.4>

ARM64 Exception

ARM64 Exception 종류

ARM64 Exception 벡터에 포함된 exception 종류는 다음과 같다.  Synchronous를 제외하고 나머지 3개는 비동기이다.

  • Synchronous
    • Instruction Abort
      • MMU를 통해 명령 fetch 에러
        • 예) Execute Never로 마킹된 매핑 주소로 접근하는 경우 발생한다.
    • Data Abort
      • MMU를 통해 데이터 fetch 에러
        • 예) 권한 실패 및 SP 또는 PC alignment 체크에 의해 발생한다.
    • Exception Generating 명령들
      • SVC(Supervisor Call)
        • 유저 모드에서 OS 서비스 요청 (syscall, 동기 exception)
      • HVC
        • Guest OS에서 Hypervisor 요청 (동기 exception)
      • SMC
        • normal(non-secure) 월드에서 secure 월드 서비스 요청 (동기 exception)
  • IRQ
    • 디바이스에서 인터럽트 서비스를 요청하는 경우 발생한다.
    • irq 처리 중 fiq 요청에 대한 preemption은 가능하지만 irq 재진입(irq preemption)은 허용하지 않는다.
    • ARM32 아키텍처는 irq preemption을 허용하지만 ARM 리눅스 커널이 이를 허용하지 않는 설계로 구현되어 있다.
    • Pesudo-NMI를 지원하는 ARM64 아키텍처와 ARM64 커널이 사용될 때 nmi 관련 디바이스에 한해 irq preemption을 허용한다.
  • FIQ
    • IRQ보다 더 빠른 인터럽트 서비스를 요청하는 경우 발생한다.
    • 리눅스 커널의 fiq 핸들러 함수인 handle_fiq_as_nmi()에 처리 코드를 추가하여 사용하여야 한다.
      • 디폴트 처리로 아무것도 하지 않는다. (nop)
      • 예) rpi2의 경우 usb 호스트 드라이버(dwc_otg)에서 사용한다.
    • irq 보다 높은 우선 순위를 갖고, irq 처리 중에도 preemption될 수 있다.
    • 일반적으로 secure 펌웨어에서 fiq를 처리하고, 리눅스 커널에서는 fiq를 처리하지 않는다.
  • SError (System Error)
    • 비동기 Data Abort
      • 예) 캐시 라인을 시스템 메모리에 writeback하느 과정에서 중단되는 경우 발생한다.

 

Reset

최고 레벨의 특별한 exception 이다. 리셋되는 주소는 IMPLEMENTATION DEFINED이고, RVBAR_ELn 레지스터로 지정할 수 있다.

 

Exception 레벨별 벡터 테이블

다음 그림은 ARMv8 아키텍처에서 운영되는 각 Exception 레벨에서 운영중인 벡터들을 보여준다.

  • 파란색 글씨는 ARM64 커널이 디폴트로 지원하는 핸들러들이다. (KVM 포함)

 

리눅스 커널의 EL2 부팅

예) rpi4가 부팅하면 Hyper 모드로 호스트 OS가 동작하도록 EL2로 부팅 후 EL2용 벡터와 핸들러들을 설치한다. 그 후 EL1용 벡터와 핸들러들을 설치한다. , Exception 발생 시 일부 코드(현재 HVC 서비스 3개)만 EL2에서 동작시키고, 나머지 커널 코드의 실행은 EL1에서 수행한다. 즉 처음 부팅된 리눅스 커널이 하이퍼바이저 역할을 일부 수행하는 것이다. 그 후 Qemu/KVM을 사용하여 동작시키는 Guest OS들은 EL1에 벡터와 핸들러들을 설치하고, 커널 코드 운영도 EL1에서 수행한다.

 

Exception 벡터 테이블 주소

벡터 테이블의 주소는 AArch64 및 AArch32에 대해 다음과 같은 레지스터를 사용하여 지정한다.  리눅스 커널의 경우 boot cpu는 __primary_switched: 레이블에서 EL1용 벡터 위치를 VBAR_EL1에 지정하고, 나머지 cpu들은 __secondary_switched: 레이블에서 지정하였다. 하이퍼모드로 운영되는 경우 install_el2_stub: 레이블에서 EL2용 벡터 위치를 VBAR_EL2에 지정한다.

  • for AArch64
    • VBAR_EL3,  VBAR_EL2,  VBAR_EL1
  • for AArch32
    •  HVBAR

 

Vector 선언

EL1 벡터

vectors

arch/arm64/kernel/entry.S

/*
 * Exception vectors.
 */
        .pushsection ".entry.text", "ax"

        .align  11
ENTRY(vectors)
        kernel_ventry   1, sync_invalid                 // Synchronous EL1t
        kernel_ventry   1, irq_invalid                  // IRQ EL1t
        kernel_ventry   1, fiq_invalid                  // FIQ EL1t
        kernel_ventry   1, error_invalid                // Error EL1t

        kernel_ventry   1, sync                         // Synchronous EL1h
        kernel_ventry   1, irq                          // IRQ EL1h
        kernel_ventry   1, fiq_invalid                  // FIQ EL1h
        kernel_ventry   1, error                        // Error EL1h

        kernel_ventry   0, sync                         // Synchronous 64-bit EL0
        kernel_ventry   0, irq                          // IRQ 64-bit EL0
        kernel_ventry   0, fiq_invalid                  // FIQ 64-bit EL0
        kernel_ventry   0, error                        // Error 64-bit EL0

#ifdef CONFIG_COMPAT
        kernel_ventry   0, sync_compat, 32              // Synchronous 32-bit EL0
        kernel_ventry   0, irq_compat, 32               // IRQ 32-bit EL0
        kernel_ventry   0, fiq_invalid_compat, 32       // FIQ 32-bit EL0
        kernel_ventry   0, error_compat, 32             // Error 32-bit EL0
#else
        kernel_ventry   0, sync_invalid, 32             // Synchronous 32-bit EL0
        kernel_ventry   0, irq_invalid, 32              // IRQ 32-bit EL0
        kernel_ventry   0, fiq_invalid, 32              // FIQ 32-bit EL0
        kernel_ventry   0, error_invalid, 32            // Error 32-bit EL0
#endif
END(vectors)
  • 총 16개의 벡터 엔트리들로 구성되며, 각각의 엔트리에 0x80 바이트를 제공한다.
    • ARMv7이 엔트리당 4바이트의 공간만을 제공한 것에 비해 ARMv8은 충분한 크기를 제공하는 것을 알 수 있다.
  • 16개의 벡터 엔트리들을 4개씩 전체 4 세트로 묶는 경우 각 세트들은 다음과  같은 특징을 가지고있다.
    • 첫 번째, 현재 코드가 수행 중인 exception 레벨과 동일한 exception 레벨에서 excetion이 발생하였고, SP0를 사용하고 있었다.
      • 리눅스 커널은 이에 해당하는 핸들러들을 처리하지 않도록 invalid 핸들러들로 연결되어 있다.
    • 두 번째, 현재 코드가 수행 중인 exception 레벨과 동일한 exception 레벨에서 excetion이 발생하였고, SPn(n>0)을 사용하고 있었다.
      • 커널(EL1)에서 exception이 발생하였고, 이 때 SP1을 사용 중인 경우이다.
      • 예) sync exception이 발생하는 경우 el1_sync: 레이블로 이동하여 sync 관련 루틴을 수행한다.
    • 세 번째, 현재 코드가 수행 중인 exception 레벨보다 하위 exception 레벨이며 AArch64 수행 중이다.
      • AArch64 유저(EL0)에서 exception이 발생한 경우이다.
    • 네 번째, 현재 코드가 수행 중인 exception 레벨보다 하위 exception 레벨이며 AArch32 수행 중이다.
      • AArch32 유저(EL0)에서 exception이 발생한 경우이다.

 

EL2 벡터

__kvm_hyp_vector

arch/arm64/kernel/entry.S

ENTRY(__kvm_hyp_vector)
        invalid_vect    el2t_sync_invalid       // Synchronous EL2t
        invalid_vect    el2t_irq_invalid        // IRQ EL2t
        invalid_vect    el2t_fiq_invalid        // FIQ EL2t
        invalid_vect    el2t_error_invalid      // Error EL2t

        valid_vect      el2_sync                // Synchronous EL2h
        invalid_vect    el2h_irq_invalid        // IRQ EL2h
        invalid_vect    el2h_fiq_invalid        // FIQ EL2h
        valid_vect      el2_error               // Error EL2h

        valid_vect      el1_sync                // Synchronous 64-bit EL1
        valid_vect      el1_irq                 // IRQ 64-bit EL1
        invalid_vect    el1_fiq_invalid         // FIQ 64-bit EL1
        valid_vect      el1_error               // Error 64-bit EL1

        valid_vect      el1_sync                // Synchronous 32-bit EL1
        valid_vect      el1_irq                 // IRQ 32-bit EL1
        invalid_vect    el1_fiq_invalid         // FIQ 32-bit EL1
        valid_vect      el1_error               // Error 32-bit EL1
ENDPROC(__kvm_hyp_vector)

Guest OS에서 exception이 발생한 경우 el1_sync, el1_irq, el1_error 레이블로 이동한다.

 

다음 그림은 exception이 발생하였을 때 호출되는 exception 벡터 엔트리 레이블들을 가리킨다.

 

Vector가 저장되는 섹션 위치

ENTRY_TEXT

include/asm-generic/vmlinux.lds.h

#define ENTRY_TEXT                                                      \
                ALIGN_FUNCTION();                                       \
                __entry_text_start = .;                                 \
                *(.entry.text)                                          \
                __entry_text_end = .;

.extry.text 섹션에 벡터코드가 포함되며 ENTRY_TEXT로 정의되어 있다.

 

arch/arm64/kernel/vmlinux.lds.S

.       .head.text : {
                _text = .;
                HEAD_TEXT
        }
        .text : {                       /* Real text segment            */
                _stext = .;             /* Text and read-only data      */
                        __exception_text_start = .;
                        *(.exception.text)
                        __exception_text_end = .;
                        IRQENTRY_TEXT
                        SOFTIRQENTRY_TEXT
                        ENTRY_TEXT
                        TEXT_TEXT
                        SCHED_TEXT
                        CPUIDLE_TEXT
                        LOCK_TEXT
                        KPROBES_TEXT
                        HYPERVISOR_TEXT
                        IDMAP_TEXT
                        HIBERNATE_TEXT
                        TRAMP_TEXT
                        *(.fixup)
                        *(.gnu.warning)
                . = ALIGN(16);
                *(.got)                 /* Global offset table          */

벡터코드가 포함된 ENTRY_TEXT는 .text 영역에 포함되어 있다.

 

kernel_ventry 매크로

arch/arm64/kernel/entry.S

        .macro kernel_ventry, el, label, regsize = 64
        .align 7
#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
alternative_if ARM64_UNMAP_KERNEL_AT_EL0
        .if     \el == 0
        .if     \regsize == 64
        mrs     x30, tpidrro_el0
        msr     tpidrro_el0, xzr
        .else
        mov     x30, xzr
        .endif
        .endif
alternative_else_nop_endif
#endif

        sub     sp, sp, #S_FRAME_SIZE
#ifdef CONFIG_VMAP_STACK
        /*
         * Test whether the SP has overflowed, without corrupting a GPR.
         * Task and IRQ stacks are aligned to (1 << THREAD_SHIFT).
         */
        add     sp, sp, x0                      // sp' = sp + x0
        sub     x0, sp, x0                      // x0' = sp' - x0 = (sp + x0) - x0 = sp
        tbnz    x0, #THREAD_SHIFT, 0f
        sub     x0, sp, x0                      // x0'' = sp' - x0' = (sp + x0) - sp = x0
        sub     sp, sp, x0                      // sp'' = sp' - x0 = (sp + x0) - x0 = sp
        b       el\()\el\()_\label

0:
        /*
         * Either we've just detected an overflow, or we've taken an exception
         * while on the overflow stack. Either way, we won't return to
         * userspace, and can clobber EL0 registers to free up GPRs.
         */

        /* Stash the original SP (minus S_FRAME_SIZE) in tpidr_el0. */
        msr     tpidr_el0, x0

        /* Recover the original x0 value and stash it in tpidrro_el0 */
        sub     x0, sp, x0
        msr     tpidrro_el0, x0

        /* Switch to the overflow stack */
        adr_this_cpu sp, overflow_stack + OVERFLOW_STACK_SIZE, x0

        /*
         * Check whether we were already on the overflow stack. This may happen
         * after panic() re-enables interrupts.
         */
        mrs     x0, tpidr_el0                   // sp of interrupted context
        sub     x0, sp, x0                      // delta with top of overflow stack
        tst     x0, #~(OVERFLOW_STACK_SIZE - 1) // within range?
        b.ne    __bad_stack                     // no? -> bad stack pointer

        /* We were already on the overflow stack. Restore sp/x0 and carry on. */
        sub     sp, sp, x0
        mrs     x0, tpidrro_el0
#endif
        b       el\()\el\()_\label
        .endm

Exception이 발생되면 16개의 exception vector 엔트리 중 해당하는 exception 위치로 jump 되어 수행한다.

  • 코드 라인 1에서 인자 @el 은 exception 레벨을 의미하고, @label은 jump할 라벨, 그리고 @regsize는 시스템 레지스터의 사이즈를 의미한다.
  • 코드 라인 2에서 .align 7을 지정하여 벡터간 128바이트 단위로 정렬하도록 한다.
  • 코드 라인 3에서 고성능 아키텍처에서 문제가 되었던 추측 공격(Speculation 공격, side 채널 공격)을 통해 유저 영역에서 MMU 권한 체크를 우회하여 커널 액세스가 가능해지는 버그가 있다. 이러한 커널 액세스를 방지하기 위해 유저 영역에서 커널 영역을 완전히 분리하여 액세스를 방지하는 CONFIG_UNMAP_KERNEL_AT_EL0 커널 옵션이며 이를 설정하여 사용하는 경우 성능을 약간 희생한다.
    • 참고로 exception 페이지는 trampoline 페이지를 통해 매핑을 허용한다.
  • 코드 라인 4에서 위 보안 기능을 사용하기 위해 fake cpu 기능(capability) 하나를 추가하였다. 이 ARM64_UNMAP_KERNEL_AT_EL0 capability를 지원하는 시스템에서만 코드가 동작하게 제한한다.
  • 코드 라인 5~12에서 EL0 AArch64(64bit user application)에서 exception이 발생한 경우 TPIDRRO_EL0 레지스터 값을 x30 레지스터에 백업하고, 0으로 기록한다. 만일 EL0 AArch32(32bit user application)에서 exception이 발생한 경우 x30을 0으로 기록한다.
  • 코드 라인 16에서 context 전환 목적으로 현재 레지스터들을 백업하기 위해 pt_regs 구조체 사이즈(S_FRAME_SIZE)만큼  스택을 증가시킨다. (growth down)
  • 코드 라인 17에서 CONFIG_VMAP_STACK 커널 옵션을 사용하면 스택을 생성할 때 vmalloc 공간을 이용하여 생성한다. 스택은 물리적으로 연속된 공간이 필요 없고, 가상 주소 공간만 연속되면 문제 없으므로 vmalloc 공간에 리니어 매핑하여 사용하는 것이 메모리의 파편화 방지에 도움이 된다.
  • 코드 라인 22~23에서 sp 레지스터와 x0 레지스터 값을 교환할 목적으로먼저 x0 <- sp를 수행한다.이때 sp에는 sp + x0 값이 잠시 담겨있다.
    • 두 레지스터 간의 교환이 필요할 때 DRAM 메모리를 사용할 수 있다. 그러나 exception 루틴에서 DRAM에 접근하면 수백 사이클의 대기 시간이 소요되어 매우 느려지므로 수학적으로 가감 연산만을 사용하여 두 레지스터를 교환할 수 있다.
  • 코드 라인 24에서 sp 레지스터를 범용 레지스터인 x0 레지스터로 옮겨야 tbnz 명령을 사용하여 특정 비트가 설정되었는지 여부를 확인할 수 있다. 여기서 스택 사이즈 또는 태스크 사이즈로 정렬되었는지 여부를 테스트하여 정렬되지 않은 경우 0: 레이블로 이동한다.
    • 스택 레지스터의 THREAD_SHIFT 위치의 비트가 설정된 경우 스택 정렬되지 않은 것으로 판단한다.
  • 코드 라인 25~26에서 x0 레지스터와 sp 레지스터 값을 다시 원래대로 복귀시킨다.
  • 코드 라인 27에서 이 매크로에 전달된 @el 값과 @label을 연결하여 만든 레이블로 점프한다.
    • 문자열과 매크로 인자 값을 연결하는 \() 문자는 접착재 같이 사용되므로 제거한다.
    • 예) \el=1, \label=irq
      • b el\()\el\()_\label -> “b el1_irq
  • 코드 라인 29에서 스택이 정렬되지 않은채로 호출되어 이동해온 0 레이블이다. 이 레이블에서는 깨진 스택 대신 static하게 만들어진 per-cpu 오버플로우용 스택을 사용하는 코드로 진행된다.
  • 코드 라인 37에서 x0 레지스터에는 sp 레지스터 값과 바뀐채로 이동해왔다. 이 값을 잠시 tpidr_el0에 저장해둔다.
    • tpidr_el0 <- sp
  • 코드 라인 40~41에서 원래 x0 레지스터 값으로 복원하여 잠시 tpidrro_el0에 저장해둔다.
    • tpidrro_el0 <- x0
  • 코드 라인 44에서 현재 cpu에 해당하는 오버 플로우용 스택을 지정한다.
    • 스택은 growth down으로 동작하므로 스택의 높은 주소를 지정한다.
    • 정적 per-cpu 선언된 overflow_stack은 4K 크기로 사용된다.
  • 코드 라인 50~53에서 백업해둔 스택 값을 x0 레지스터로 읽어와서 현재 스택 범위 이내에 있는지 비교하여 범위 밖인 경우 __bad_stack 레이블로 이동한다.
  • 코드 라인 56~59에서 x0 레지스터와 스택을 다시 복구하고 이 매크로에 전달된 @el 값과 @label을 연결하여 만든 레이블로 점프한다.

 

adr_this_cpu 매크로

arch/arm64/include/asm/assembler.h

.       /*
         * @dst: Result of per_cpu(sym, smp_processor_id()) (can be SP)
         * @sym: The name of the per-cpu variable
         * @tmp: scratch register
         */
        .macro adr_this_cpu, dst, sym, tmp
        adrp    \tmp, \sym
        add     \dst, \tmp, #:lo12:\sym
alternative_if_not ARM64_HAS_VIRT_HOST_EXTN
        mrs     \tmp, tpidr_el1
alternative_else
        mrs     \tmp, tpidr_el2
alternative_endif
        add     \dst, \dst, \tmp
        .endm

현재 cpu가 접근할 per-cpu 변수 @sym의 주소를 @dst에 담아 반환한다. (예: @dst <- 현재 cpu로 접근할 per-cpu @sym) 이 매크로는 스크래치 레지스터 @tmp가 하나 소모된다.

  • 코드 라인 2에서 인자로 전달받은 per-cpu 변수 @sym에 대한 페이지 단위로 절삭된 주소를 @tmp에 담는다.
    • 현재 cpu의 pc 주소로 부터 4G 이내의 상대 주소(relative addressing)를 사용하여 알아온다.
    • 예) @sym(overflow_stack 주소 0x1234_5678)이 현재 cpu의 주소로부터 +-4G 이내인 조건에서 페이지 단위로 절삭된 0x1234_5000 값을 @tmp에 대입한다.
  • 코드 라인 3에서 @sym의 하위 12비트를 @tmp에 더해 @dst에 산출한다.
    • 예) @dst(0x1234_5678) = @tmp(0x1234_5000) + 하위 12비트 @sym(0x678)
  • 코드 라인 4~8에서 현재 cpu에 대한 per-cpu offset 값이 담겨있는 tpidr_el1(하이퍼 바이저 지원시 tpidr_el2) 값을 @tmp에 읽어온다.
  • 코드 라인 9에서 per-cpu offset 값이 적용된 per-cpu 변수 overflow_stack 주소를 @dst에 담아 반환한다.

 

adr vs adrp

  • adr 명령은 심볼의 주소를 상대 주소(relative addressing)를 사용하여 알아온다. 20비트(추가 1비트는 부호) 상대 주소 기법을 사용하므로 현재 cpu pc 주소로부터 심볼 위치까지 지원하는 범위가 +-1M 주소로 제한된다.
  • adrp 명령은 adr과 유사하지만 더 넓은 범위를 지원하기 위해 심볼 주소의 하위 12비트를 사용하지 않는 20비트(추가 1비트는 부호, 12비트 좌쉬프트) 상대 주소 기법을 사용하므로 더욱 넓은 범위인 +-4G 주소 범위에 한하여 페이지 단위로 절삭된 주소를 알아온다.

 

TPID 레지스터 관련

TPIDR_EL0 (Thread ID Register EL0)
  • 유저(EL0) 영역에서 TLS(Thread Location Storage)를 사용하기 위해 스레드 데이터가 위치한 베이스 주소를 저장한다.
  • Exception이 발생할 때 vmap 스택에서 overflow가 발생하여 overflow용 스택을 사용하는 경우 이 레지스터를 임시 저장 영역으로 사용된다.
  • TPIDR_EL0의 경우 커널 영역(EL1) 및 유저 영역(EL0)에서 읽고 쓰기가 가능하다.

 

TPIDRRO_EL0 (Thread ID Register EL0 with User Read Only)
  • TPIDR_EL0와 같은 목적으로 유저(EL0) 영역에서 TLS(Thread Location Storage)를 사용하기 위해 스레드 데이터가 위치한 베이스 주소를 저장한다.
  • Exception이 발생할 때 vmap 스택에서 overflow가 발생하여 overflow용 스택을 사용하는 경우 이 레지스터도 임시 저장 영역으로 사용된다.
  • 커널 영역(EL1)에서 읽고 쓰기가 가능하며, 유저 영역(EL0)에서는 읽기만 가능하다.

 

TPIDR_EL1 (Thread ID Register EL1)
  • per-cpu 베이스 주소를 기억하기 위해 사용한다.
  • 커널(EL1) 영역에서 읽고 쓸 수 있다.

 

참고

 

 

Interrupts -12- (irq desc)

<kernel v5.4>

IRQ 디스크립터

IRQ 디스크립터를 관리하는 두 가지 방법

CONFIG_SPARSE_IRQ 커널 옵션을 사용유무에 따라 IRQ 번호를 관리하는 방법이 두 가지로 나뉜다.

  • Sparse IRQ
    • 커널 옵션을 사용하는 경우 필요한 IRQ 번호에 대한 irq_desc 구조체를 동적으로 할당하고 Radix Tree를 사용하여 관리한다.
    • arm64 시스템은 기본적으로 CONFIG_SPARSE_IRQ 커널 옵션을 구성하여 사용한다.
    • 관련 함수
      • irq_alloc_desc*()
      • irq_free_desc*()
  • Flat IRQ
    • 커널 옵션을 사용하지 않는 경우 max IRQ 번호만큼 irq_dest 구조체 배열을 컴파일 시 정적으로 할당하여 사용한다.

 

NR_IRQS vs nr_irqs

NR_IRQS는 컴파일 타임에 지정되는 irq 수이고, nr_irqs는 런타임에 결정되어 사용되는 irq 수이다.

  • NR_CPUS vs nr_cpus와 동일하게 사용된다.

 


early irq 초기화

1) Sparse IRQ

early_irq_init() – Sparse

kernel/irq/irqdesc.c

#ifdef CONFIG_SPARSE_IRQ
int __init early_irq_init(void)
{
        int i, initcnt, node = first_online_node;
        struct irq_desc *desc;

        init_irq_default_affinity();

        /* Let arch update nr_irqs and return the nr of preallocated irqs */
        initcnt = arch_probe_nr_irqs();
        printk(KERN_INFO "NR_IRQS: %d, nr_irqs: %d, preallocated irqs: %d\n",
               NR_IRQS, nr_irqs, initcnt);

        if (WARN_ON(nr_irqs > IRQ_BITMAP_BITS))
                nr_irqs = IRQ_BITMAP_BITS;

        if (WARN_ON(initcnt > IRQ_BITMAP_BITS))
                initcnt = IRQ_BITMAP_BITS;

        if (initcnt > nr_irqs)
                nr_irqs = initcnt;

        for (i = 0; i < initcnt; i++) {
                desc = alloc_desc(i, node, 0, NULL, NULL);
                set_bit(i, allocated_irqs);
                irq_insert_desc(i, desc);
        }
        return arch_early_irq_init();
}
#endif

irq 디스크립터를 sparse하게 관리할 수 있는 radix 트리를 구성하고 초기화한다.

  • 코드 라인 7에서 모든 cpu를 대상으로 사용하도록 전역 irq_default_affinity 변수에 cpu 비트맵 매스크를 할당하고 모두 1로 설정한다.
  • 코드 라인 10에서 사용할 최대 irq 갯수를 알아온다.
  • 코드 라인 11~12에서 컴파일 때 지정한 최대 NR_IRQS 값과 재조정된 nr_irqs 값, 그리고 아키텍처에 따라 미리 할당해야 할 irq 수 등을 알아와 로그 정보로 출력한다.
    • 예) “NR_IRQS: 64, nr_irqs: 64, preallocated irqs: 0”
  • 코드 라인 14~15에서 전역 nr_irqs가 IRQ_BITMAP_BITS를 초과하지 않도록 제한한다.
    • IRQ_BITMAP_BITS
      • sparse 옵션에서 NR_IRQS+8192개
      • flat 옵션에서 NR_IRQS
  • 코드 라인 17~18에서 initcnt가 IRQ_BITMAP_BITS를 초과하지 않도록 제한한다.
  • 코드 라인 20~21에서 nr_irqs를 아키텍처에 따라 미리 할당해야 할 irq 수로 제한한다.
  • 코드 라인 23~27에서 아키텍처에 따라 미리 할당해야 할 수(initcnt) 수 만큼 순회하며 irq 디스크립터를 할당한다. 전역 allocated_irqs 비트마스크의 irq 번호(0번부터 시작)에 해당하는 비트를 설정하고, 전역 irq_desc_tree에 추가한다.
  • 코드 라인 28에서 아키텍처별로 early irq 초기화 루틴을 수행한다.
    • 현재 x86, ia64 아키텍처에서 제공된다.

 

다음 그림은 sparse하게 irq 디스크립터가 관리되는 모습을 보여준다.

early_irq_init-1

 

2) Flat IRQ

early_irq_init() – Flat

kernel/irq/irqdesc.c

#else /* !CONFIG_SPARSE_IRQ */
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
 [0 ... NR_IRQS-1] = {
 .handle_irq = handle_bad_irq,
 .depth = 1,
 .lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
 }
};

int __init early_irq_init(void)
{
        int count, i, node = first_online_node;
        struct irq_desc *desc;

        init_irq_default_affinity();

        printk(KERN_INFO "NR_IRQS:%d\n", NR_IRQS);

        desc = irq_desc;
        count = ARRAY_SIZE(irq_desc);

        for (i = 0; i < count; i++) {
                desc[i].kstat_irqs = alloc_percpu(unsigned int);
                alloc_masks(&desc[i], node);
                raw_spin_lock_init(&desc[i].lock);
                lockdep_set_class(&desc[i].lock, &irq_desc_lock_class);
                mutex_init(&desc[i].request_mutex);
                desc_set_defaults(i, &desc[i], node, NULL);
        }
        return arch_early_irq_init();
}
#endif

irq 디스크립터를 flat하게 관리하도록 전역 irq_desc[] 배열을 초기화한다.

  • 코드 라인 15에서 모든 cpu를 대상으로 사용하도록 전역 irq_default_affinity 변수에 cpu 비트맵 매스크를 할당하고 모두 1로 설정한다.
  • 코드 라인 17에서 컴파일 때 지정한 최대 NR_IRQS 값을 로그 정보로 출력한다.
    • 예) “NR_IRQS: 64”
  • 코드 라인 19~29에서 NR_IRQS 수만큼 순회하며 irq 디스크립터를 초기화한다.
  • 코드 라인 30에서 아키텍처별로 early irq 초기화 루틴을 수행한다.
    • 현재 x86, ia64 아키텍처에서 제공된다.

 

다음 그림은 flat하게 irq 디스크립터가 관리되는 모습을 보여준다.

early_irq_init-2

 


irq affinity

“irqaffinity=” 커널 파라미터 설정

irq_affinity_setup()

kernel/irq/irqdesc.c

static int __init irq_affinity_setup(char *str)
{
        alloc_bootmem_cpumask_var(&irq_default_affinity);
        cpulist_parse(str, irq_default_affinity);
        /*
         * Set at least the boot cpu. We don't want to end up with
         * bugreports caused by random comandline masks
         */
        cpumask_set_cpu(smp_processor_id(), irq_default_affinity);
        return 1;
}
__setup("irqaffinity=", irq_affinity_setup);

“irqaffinity=<cpu ranges>” 커널 파라미터를 설정하여 irq를 처리할 수 있는 cpu를 지정하며, 이외에 기본적으로 boot cpu도 포함시킨다.

  • 예) “irqaffinity=0-1,6-7”

 

디폴트 irq affinity 초기화

init_irq_default_affinity()

kernel/irq/irqdesc.c

static void __init init_irq_default_affinity(void)
{
        if (!cpumask_available(irq_default_affinity))
                zalloc_cpumask_var(&irq_default_affinity, GFP_NOWAIT);
        if (cpumask_empty(irq_default_affinity))
                cpumask_setall(irq_default_affinity);
}

“irqaffinity=” 커널 파라미터가 설정되지 않은 경우 디폴트로 모든 cpu에서 동작하도록 제한하지 않는다.

  • 코드 라인 3~4에서 할당 방식의 cpu 비트맵 마스크인 경우 비트맵 마스크의 메모리를 할당한다.
  • 코드 라인 5~6에서 디폴트 affinity가 설정되지 않은 경우에 한하여 모든 cpu에서 동작하도록 모두 1로 설정한다.

 


이 루틴은 해당 시스템의 인터럽트 컨트롤러를 초기화하고 각각의 인터럽트 번호에 따른 핸들러들을 준비하는 과정이다. 인터럽트 컨트롤러는 시스템마다 다른 각각의 하드웨어를 사용하므로 분석을하고자 하는 시스템의 하드웨어에 대한 지식이 필요한다. 따라서 각 시스템에서 사용하는 인터럽트 컨틀롤러의 데이터 시트를 참고하기 바란다.

 

리눅스 커널에서 구현 방법은 크게 다음과 같이 두 가지로 나뉜다.

  • 아키텍처 전용 머신 코드를 수행하여 인터럽트 컨트롤러를 초기화하고 핸들러를 구성하는 방법
    • arch/arm/mach-로 시작하는 디렉토리
    • 대부분의 임베디드 시스템에서 처음 사용된 방식으로 곧장 각 머신의 인터럽트 컨트롤러 설정 등이 구현되어 있다.
        • rpi 및 rp2 예)
          • 내부 인터럽트 초기화 함수는 Device Tree를 사용한다.
          • 커널 v4.4 부터는 지원되지 않는다.
  • Device Tree에 설정된 내용을 분석하여 해당 드라이버를 구동하고 인터럽트 컨트롤러 하드웨어를 초기화하고 핸들러를 구성하는 방법
    • kernel/chip.c (헬퍼) -> drivers/irqchip 디렉토리의 각 인터럽트 컨트롤러 드라이버
      • irq_domain 등을 사용하여 리눅스 irq와 hw irq의 매핑을 지원하는 구조로 복잡해졌지만 점차 시스템들이 이 방향으로 구성하고 있다.
      • arm64도 Device Tree 구성을 읽어 인터럽트 초기화 함수들을 호출한다.
      • rpi 및 rpi2 예)
        • 커널 v4.4 부터 지원하기 시작하였다.

 

VMAP Stack 지원

64비트 시스템의 경우 vmalloc 공간이 충분하기 때문에 cpu마다 stack 공간으로 vmalloc 공간을 사용할 수 있도록 지원한다.

 


IRQ 초기화

 

init_IRQ() – ARM64

arch/arm64/kernel/irq.c

void __init init_IRQ(void)
{
        init_irq_stacks();
        irqchip_init();
        if (!handle_arch_irq)
                panic("No interrupt controller found.");

        if (system_uses_irq_prio_masking()) {
                /*
                 * Now that we have a stack for our IRQ handler, set
                 * the PMR/PSR pair to a consistent state.
                 */
                WARN_ON(read_sysreg(daif) & PSR_A_BIT);
                local_daif_restore(DAIF_PROCCTX_NOIRQ);
        }
}

인터럽트 처리 핸들러를 위해 인터럽트 컨트롤러의 준비 및 필요한 설정 수행한다.

  • 코드 라인 3에서 per-cpu irq용 스택을 할당하여 준비한다.
  • 코드 라인 4~6에서 인터럽트 컨트롤러를 초기화하기 위한 irqchip 초기화를 수행한다.
  • 코드 라인 8~15에서 시스템이 irq priority 마스킹을 사용할 수 있는 경우 irq를 블럭한다. 단 Pesudo-NMI는 허용한다.

 

init_irq_stacks()

arch/arm64/kernel/irq.c

#ifdef CONFIG_VMAP_STACK
static void init_irq_stacks(void)
{
        int cpu;
        unsigned long *p;

        for_each_possible_cpu(cpu) {
                p = arch_alloc_vmap_stack(IRQ_STACK_SIZE, cpu_to_node(cpu));
                per_cpu(irq_stack_ptr, cpu) = p;
        }
}
#else
/* irq stack only needs to be 16 byte aligned - not IRQ_STACK_SIZE aligned. */
DEFINE_PER_CPU_ALIGNED(unsigned long [IRQ_STACK_SIZE/sizeof(long)], irq_stack);

static void init_irq_stacks(void)
{
        int cpu;

        for_each_possible_cpu(cpu)
                per_cpu(irq_stack_ptr, cpu) = per_cpu(irq_stack, cpu);
}
#endif

per-cpu irq 스택을 생성한다.

  • 디폴트로 사용되는 CONFIG_VMAP_STACK 커널 옵션은 스택을 vmalloc 공간에 매핑하여 사용할 수 있게 한다.

 

init_IRQ() – ARM32

arch/arm/kernel/irq.c

void __init init_IRQ(void)
{
        int ret;

        if (IS_ENABLED(CONFIG_OF) && !machine_desc->init_irq)
                irqchip_init();
        else
                machine_desc->init_irq();

        if (IS_ENABLED(CONFIG_OF) && IS_ENABLED(CONFIG_CACHE_L2X0) &&
            (machine_desc->l2c_aux_mask || machine_desc->l2c_aux_val)) {
                if (!outer_cache.write_sec)
                        outer_cache.write_sec = machine_desc->l2c_write_sec;
                ret = l2x0_of_init(machine_desc->l2c_aux_val,
                                   machine_desc->l2c_aux_mask);
                if (ret)
                        pr_err("L2C: failed to init: %d\n", ret);
        }

        uniphier_cache_init();
}

인터럽트 처리 핸들러를 위해 인터럽트 컨트롤러의 준비 및 필요한 설정 수행한다. 시스템 구성에 따라 머신 디스크립터 또는 DTB 둘 중 하나의 방법을 사용하여 초기화 한다.

  • 코드 라인 5~8에서 시스템이 Device Tree를 지원하면서 머신 디스크립터에서 init_irq 후크가 설정되어 있지 않으면 Device Tree를 위해 irqchip_init() 함수를 호출하고, 그렇지 않은 경우 머신 디스크립터에 준비한 init_irq 후크에 설정된 콜백 함수를 호출한다.
    • rpi2 예)
      • 머신의 init_irq에 등록한 bcm2709_init_irq() 함수를 호출한다.
    • 참고: A new generic IRQ layer | LWN.net
  • 코드 라인 10~18에서 outer 캐시를 가진 시스템에 대해 초기화를 수행한다.
    • DTB를 사용할 수 있는 커널에서 L2X0 캐시 컨트롤러를 사용하는 시스템이고 머신에 l2c_aux_mask 또는 l2_aux_val이 설정된 경우 outer 캐시에 대한 콜백함수를 준비하고 초기화 함수를 호출한다.
  • 코드 라인 20에서 UniPhier outer 캐시 컨트롤러를 사용하는 시스템을 초기화한다.

 

arch/arm64/kernel/irq.c – arm64 참고

void __init init_IRQ(void)
{
        irqchip_init();
        if (!handle_arch_irq)
                panic("No interrupt controller found.");
}

arm64에서는 DTB 만을 지원하므로 irqchip_init() 함수를 곧바로 호출한다.

 

인터럽트 컨트롤러 초기화

시스템에 따라 다양한 종류의 인터럽트 컨트롤러를 사용하는데 rpi2에서 사용하는 bcm2709용 인터럽트 컨트롤러 초기화 함수를 호출하기로 한다.

  • Device Tree 미 사용시 (CONFIG_OF=n)
    • rpi
      • arch/arm/mach-bcm2708/bcm2708.c – bcm2708_init_irq()
        • 내부 초기화 함수에서는 device tree를 사용한다.
    • rp2
      • arch/arm/mach-bcm2709/bcm2709.c – bcm2709_init_irq()
        • 내부 초기화 함수에서는 device tree를 사용한다.
  • Device Tree 사용 시
    • of_irq_init() 함수를 통해 Device Tree에 있는 인터럽트 컨트롤러 정보를 읽어오고 해당 드라이버의 초기화 함수들을 호출한다.
    • GIC
      • drivers/irqchip/irq-gic.c – gic_of_init()
    • Exynos Combiner
      • drivers/irqchip/exynos-combiner.c – combiner_of_init()
    • rpi
      • drivers/irqchip/irq-bcm2835.c – armctrl_of_init()
    • rpi2
      • 두 개의 드라이버를 사용한다. (커널 v4.4 부터 적용)
        • drivers/irqchip/irq-bcm2835.c – bcm2836_armctrl_of_init()
        • drivers/irqchip/irq-bcm2836.c – bcm2836_arm_irqchip_l1_intc_of_init()

 


IRQ 디스크립터 할당

irq_alloc_descs()

include/linux/irq.h

#define irq_alloc_descs(irq, from, cnt, node)   \
        __irq_alloc_descs(irq, from, cnt, node, THIS_MODULE, NULL)

요청 @irq 번호부터 @cnt 수 만큼 irq 디스크립터를 @node에 할당한다. 요청한 @irq 번호가 지정되지 않은 값(-1)이면 @from 번호부터 검색하여 배정한다. 그리고 현재 모듈을 irq 디스크립터의 owner로 설정한다.

 

irq_alloc_descs_from()

include/linux/irq.h

#define irq_alloc_descs_from(from, cnt, node)   \
        irq_alloc_descs(-1, from, cnt, node)

새로운 irq 번호로, @from에서 검색하여 @cnt 수 만큼 irq 디스크립터를 @node에 할당한다. 그리고 현재 모듈을 irq 디스크립터의 owner로 설정한다.

 

__irq_alloc_descs()

kernel/irq/irqdesc.c

/**
 * irq_alloc_descs - allocate and initialize a range of irq descriptors
 * @irq:        Allocate for specific irq number if irq >= 0
 * @from:       Start the search from this irq number
 * @cnt:        Number of consecutive irqs to allocate.
 * @node:       Preferred node on which the irq descriptor should be allocated
 * @owner:      Owning module (can be NULL)
 * @affinity:   Optional pointer to an affinity mask array of size @cnt which
 *              hints where the irq descriptors should be allocated and which
 *              default affinities to use
 *
 * Returns the first irq number or error code
 */
int __ref
__irq_alloc_descs(int irq, unsigned int from, unsigned int cnt, int node,
                  struct module *owner, const struct irq_affinity_desc *affinity)
{
        int start, ret;

        if (!cnt)
                return -EINVAL;

        if (irq >= 0) {
                if (from > irq)
                        return -EINVAL;
                from = irq;
        } else {
                /*
                 * For interrupts which are freely allocated the
                 * architecture can force a lower bound to the @from
                 * argument. x86 uses this to exclude the GSI space.
                 */
                from = arch_dynirq_lower_bound(from);
        }

        mutex_lock(&sparse_irq_lock);

        start = bitmap_find_next_zero_area(allocated_irqs, IRQ_BITMAP_BITS,
                                           from, cnt, 0);
        ret = -EEXIST;
        if (irq >=0 && start != irq)
                goto unlock;

        if (start + cnt > nr_irqs) {
                ret = irq_expand_nr_irqs(start + cnt);
                if (ret)
                        goto unlock;
        }
        ret = alloc_descs(start, cnt, node, affinity, owner);
unlock:
        mutex_unlock(&sparse_irq_lock);
        return ret;
}
EXPORT_SYMBOL_GPL(__irq_alloc_descs);

요청 @irq 번호부터 @cnt 수 만큼 irq 디스크립터를 @node에 할당한다. 요청한 @irq 번호가 지정되지 않은 값(-1)이면 @from 번호부터 검색하여 배정한다. 그리고 현재 모듈을 irq 디스크립터의 owner로 설정한다.

  • 코드 라인 7~8에서 @cnt가 지정되지 않은 경우 -EINVAL 에러로 반환한다.
  • 코드 라인 10~13에서 @irq 번호(0번 이상)를 지정한 경우 검색 시작할 번호 @from에 @irq 번호를 대입한다. 단 @from이 고정 irq 번호보다 큰 경우 -EINVAL 에러로 반환한다.
  • 코드 라인 14~21에서 @irq 번호(0번 이상)를 지정하지 않은 경우 새 irq 번호를 발부받기 위해 @from 부터 시작하도록 한다. 단 아키텍처에 따라 from 값이 바뀔 수 있다.
    • x86 아키텍처에서는 GSI 공간을 피해야 한다.
  • 코드 라인 25~26에서 인터럽트 할당 비트맵에서 @from 비트부터 연속된 @cnt 수 만큼의 할당되지 않은 비트를 찾는다.
    • 참고: Bitmap Operations | 문c
    • 예) 0b0000_0000_1111_0000_1111_0000
      • from=5, cnt=2 -> start=8
      • from=5, cnt=5 -> start=16
  • 코드 라인 28~29에서 @irq 번호를 지정하였고, 찾은 시작 irq 번호와 다른 경우 -EEXIST 에러를 반환한다.
  • 코드 라인 31~35에서 할당할 공간이 부족한 경우 irq 공간을 필요한 수 만큼 확장한다. 확장이 불가능한 경우 -EEXIST 에러를 반환한다.
  • 코드 라인 36~39에서start irq 디스크립터 부터 @cnt 수 만큼 @node에 할당한다. 그리고 모듈 owner를 설정하고 start irq 번호를 반환한다.

 

alloc_descs() – FLAT IRQ 용

kernel/irq/irqdesc.c

static inline int alloc_descs(unsigned int start, unsigned int cnt, int node,
                              const struct irq_affinity_desc *affinity,
                              struct module *owner)
{
        u32 i;

        for (i = 0; i < cnt; i++) {
                struct irq_desc *desc = irq_to_desc(start + i);

                desc->owner = owner;
        }
        bitmap_set(allocated_irqs, start, cnt);
        return start;
}

flat irq의 경우 irq 디스크립터가 이미 준비되어 있으므로 별도로 생성할 필요 없고, 사용할 @start irq 번호부터 @cnt 수 만큼 할당 비트들만을 설정한다. 반환하는 값은 @start 값과 동일하다.

 

alloc_descs() – SPARSE IRQ 용

kernel/irq/irqdesc.c

static int alloc_descs(unsigned int start, unsigned int cnt, int node,
                       const struct irq_affinity_desc *affinity,
                       struct module *owner)
{
        struct irq_desc *desc;
        int i;

        /* Validate affinity mask(s) */
        if (affinity) {
                for (i = 0; i < cnt; i++) {
                        if (cpumask_empty(&affinity[i].mask))
                                return -EINVAL;
                }
        }

        for (i = 0; i < cnt; i++) {
                const struct cpumask *mask = NULL;
                unsigned int flags = 0;

                if (affinity) {
                        if (affinity->is_managed) {
                                flags = IRQD_AFFINITY_MANAGED |
                                        IRQD_MANAGED_SHUTDOWN;
                        }
                        mask = &affinity->mask;
                        node = cpu_to_node(cpumask_first(mask));
                        affinity++;
                }

                desc = alloc_desc(start + i, node, flags, mask, owner);
                if (!desc)
                        goto err;
                irq_insert_desc(start + i, desc);
                irq_sysfs_add(start + i, desc);
                irq_add_debugfs_entry(start + i, desc);
        }
        bitmap_set(allocated_irqs, start, cnt);
        return start;

err:
        for (i--; i >= 0; i--)
                free_desc(start + i);
        return -ENOMEM;
}

@start irq 디스크립터 부터 @cnt 수 만큼 irq 디스크립터를 @node에 할당한다. 그리고 모듈 owner를 설정한 후 radix tree에 추가한다. 반환 값은 @start irq 번호이다.

  • 코드 라인 9~14에서 @affinity가 지정된 경우 @cnt 수만큼 @affinity[i].mask가 모두 설정되어 있는지 확인한다. 하나라도 설정되지 않은 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 16에서 에서 인터럽트 수인 @cnt 수만큼 반복한다.
  • 코드 라인 20~28에서 @affinity 정보가 있는 경우 인자로 받은 @node 대신 affinity[i].mask에 지정된 첫 cpu에 대한 노드를 사용한다.
    • affinity++하여 다음 구조체를 선택한다.
  • 코드 라인 30~32에서 @start+i 번호의 irq 디스크립터를 @node에 할당한다. 그리고 모듈 owner를 설정한다.
  • 코드 라인 33에서 할당한 irq 디스크립터를 radix tree에 추가한다.
  • 코드 라인 34에서 할당한 irq 디스크립터를 sysfs에 추가한다.
  • 코드 라인 35에서 할당한 riq 디스크립터를 debugfs에 추가한다.
  • 코드 라인 37에서 전역 allocated_irqs 비트맵에 할당된 인터럽트 번호에 해당하는 비트들을 설정한다.
  • 코드 라인 38에서 성공 하였으므로 @start irq 번호를 반환한다.

 

다음 그림은 하나의 irq descriptor가 할당되고 초기화되는 모습을 보여준다.

alloc_desc-1

 

alloc_masks()

kernel/irq/irqdesc.c

static int alloc_masks(struct irq_desc *desc, int node)
{
        if (!zalloc_cpumask_var_node(&desc->irq_common_data.affinity,
                                     GFP_KERNEL, node))
                return -ENOMEM;

#ifdef CONFIG_GENERIC_IRQ_EFFECTIVE_AFF_MASK
        if (!zalloc_cpumask_var_node(&desc->irq_common_data.effective_affinity,
                                     GFP_KERNEL, node)) {
                free_cpumask_var(desc->irq_common_data.affinity);
                return -ENOMEM;
        }
#endif

#ifdef CONFIG_GENERIC_PENDING_IRQ
        if (!zalloc_cpumask_var_node(&desc->pending_mask, GFP_KERNEL, node)) {
#ifdef CONFIG_GENERIC_IRQ_EFFECTIVE_AFF_MASK
                free_cpumask_var(desc->irq_common_data.effective_affinity);
#endif
                free_cpumask_var(desc->irq_common_data.affinity);
                return -ENOMEM;
        }
#endif
        return 0;
}

irq 디스크립터에서 사용하는 여러 cpu 비트맵 마스크를 할당받는다.

 

desc_set_defaults()

kernel/irq/irqdesc.c

static void desc_set_defaults(unsigned int irq, struct irq_desc *desc, int node,
                              const struct cpumask *affinity, struct module *owner)
{
        int cpu;

        desc->irq_common_data.handler_data = NULL;
        desc->irq_common_data.msi_desc = NULL;

        desc->irq_data.common = &desc->irq_common_data;
        desc->irq_data.irq = irq;
        desc->irq_data.chip = &no_irq_chip;
        desc->irq_data.chip_data = NULL;
        irq_settings_clr_and_set(desc, ~0, _IRQ_DEFAULT_INIT_FLAGS);
        irqd_set(&desc->irq_data, IRQD_IRQ_DISABLED);
        irqd_set(&desc->irq_data, IRQD_IRQ_MASKED);
        desc->handle_irq = handle_bad_irq;
        desc->depth = 1;
        desc->irq_count = 0;
        desc->irqs_unhandled = 0;
        desc->tot_count = 0;
        desc->name = NULL;
        desc->owner = owner;
        for_each_possible_cpu(cpu)
                *per_cpu_ptr(desc->kstat_irqs, cpu) = 0;
        desc_smp_init(desc, node, affinity);
}

요청한  @irq 번호에 해당하는 irq 디스크립터 @desc를 디폴트 값으로 초기화하고, @affinity와 @owner를 지정한다.

  • 코드 라인 6에서 인터럽트 핸들러 데이터를 null로 초기화한다.
  • 코드 라인 7에서 msi 디스크립터가 없는 상태로 초기화한다.
  • 코드 라인 9에서 irq 디스크립터의 하이라키를 표현하는 irq_data의 가장 상위에서 irq 디스크립터에 있는 irq 공통 데이터 구조체를 가리킨다.
  • 코드 라인 10에서 irq 디스크립터에 인자로 받은 @irq 번호를 지정한다.
  • 코드 라인 11~12에서 irq를 처리할 irqchip과 chip_data는 지정되지 않은 상태로 초기화한다.
    • 지정되지 않은 경우 no_irq_chip() 함수를 가리킨다.
  • 코드 라인 13에서 irq 디스크립터의 플래그를 모드 제거하고, 디폴트 초기화 플래그를 설정한다.
    • arm32: IRQ_NOPROBE | IRQ_NOREQUEST
    • arm64: 없음
  • 코드 라인 14~15에서 irq 디스크립터에 disabled, masked 플래그들을 설정한다.
  • 코드 라인 16에서 인터럽트 핸들러 함수를 지정하지 않은 상태로 초기화한다.
    • 지정되지 않은 경우 handle_bad_irq() 함수를 가리킨다.
  • 코드 라인 17~24에서 irq 디스크립터의 각종 값들을 초기화한다.
  • 코드 라인 25에서 irq 디스크립터에 @affinity를 적용한다. 지정되지 않은 경우 전역 irq_default_affinity 값을 적용한다.

 

irq_settings_clr_and_set()

kernel/irq/settings.h

static inline void
irq_settings_clr_and_set(struct irq_desc *desc, u32 clr, u32 set)
{       
        desc->status_use_accessors &= ~(clr & _IRQF_MODIFY_MASK);
        desc->status_use_accessors |= (set & _IRQF_MODIFY_MASK);
}

irq 디스크립터의 status_use_accessors에서 인수 @clr로 요청한 비트들을 clear하고 인수 @set으로 요청한 비트들을 설정한다.

 

irqd_set()

kernel/irq/internals.h

static inline void irqd_set(struct irq_data *d, unsigned int mask)
{
        d->state_use_accessors |= mask;
}

irq 디스크립터의 status_use_accessors에서 인수 @mask로 요청한 비트들을 설정한다.

 

desc_smp_init()

kernel/irq/irqdesc.c

static void desc_smp_init(struct irq_desc *desc, int node,
                          const struct cpumask *affinity)
{
        if (!affinity)
                affinity = irq_default_affinity;
        cpumask_copy(desc->irq_common_data.affinity, affinity);

#ifdef CONFIG_GENERIC_PENDING_IRQ
        cpumask_clear(desc->pending_mask);
#endif
#ifdef CONFIG_NUMA
        desc->irq_common_data.node = node;
#endif
}

요청한 irq 디스크립터 @desc에서 사용 가능한 cpu들을 지정하기 위해 @affinity 비트마스크를 지정한다. @affinity가 null인 경우 디폴트 affinity가 사용된다.

 

irq_insert_desc()

kernel/irq/irqdesc.c

static void irq_insert_desc(unsigned int irq, struct irq_desc *desc)
{
        radix_tree_insert(&irq_desc_tree, irq, desc);
}

전역 irq_desc_tree에 요청 @irq 번호를 키로 irq 디스크립터 @desc를 추가한다.

 


Local IRQ 제어

  • local
    • 현재 cpu
  • raw
    • 커널 옵션에 따른 논리적인 구현의 차이
  • arch
    • 아키텍처 의존적인 구현의 차이

 

Local IRQ Disable

 local_irq_disable()

include/linux/irqflags.h

#define local_irq_disable()     do { raw_local_irq_disable(); } while (0)

현재 CPU의 인터럽트를 disable

 

raw_local_irq_disable()

include/linux/irqflags.h

#define raw_local_irq_disable()         arch_local_irq_disable()

 

arch_local_irq_disable() – ARM32

arch/arm/include/asm/irqflags.h

static inline void arch_local_irq_disable(void)
{
        asm volatile(
                "       cpsid i                 @ arch_local_irq_disable"
                :
                :
                : "memory", "cc");
}

 

arch_local_irq_disable() – ARM64

arch/arm64/include/asm/irqflags.h

static inline void arch_local_irq_disable(void)
{
        if (system_has_prio_mask_debugging()) {
                u32 pmr = read_sysreg_s(SYS_ICC_PMR_EL1);

                WARN_ON_ONCE(pmr != GIC_PRIO_IRQON && pmr != GIC_PRIO_IRQOFF);
        }

        asm volatile(ALTERNATIVE(
                "msr    daifset, #2             // arch_local_irq_disable",
                __msr_s(SYS_ICC_PMR_EL1, "%0"),
                ARM64_HAS_IRQ_PRIO_MASKING)
                :
                : "r" ((unsigned long) GIC_PRIO_IRQOFF)
                : "memory");
}
  • Pesudo-NMI를 지원하는 경우 Priority Mask 레지스터를 사용하여 디폴트 irq만 disable하고, nmi에 사용하는 irq는 통과시키는 기법을 사용한다.
  • Pesudo-NMI를 지원하지 않는 경우 PSTATE.I 플래그를 설정하여 irq를 disable 한다.

 

Local IRQ Enable

local_irq_enable()

include/linux/irqflags.h”

#define local_irq_enable()      do { raw_local_irq_enable(); } while (0)

현재 CPU의 인터럽트를 enable

 

raw_local_irq_enaable()

include/linux/irqflags.h

#define raw_local_irq_enable()          arch_local_irq_enable()

 

arch_local_irq_enable() – ARM32

arch/arm/include/asm/irqflags.h

static inline void arch_local_irq_enable(void)
{
        asm volatile(
                "       cpsie i                 @ arch_local_irq_enable"
                :
                :
                : "memory", "cc");
}

 

arch_local_irq_enable() – ARM64

arch/arm64/include/asm/irqflags.h

static inline void arch_local_irq_enable(void)
{
        if (system_has_prio_mask_debugging()) {
                u32 pmr = read_sysreg_s(SYS_ICC_PMR_EL1);

                WARN_ON_ONCE(pmr != GIC_PRIO_IRQON && pmr != GIC_PRIO_IRQOFF);
        }

        asm volatile(ALTERNATIVE(
                "msr    daifclr, #2             // arch_local_irq_enable\n"
                "nop",
                __msr_s(SYS_ICC_PMR_EL1, "%0")
                "dsb    sy",
                ARM64_HAS_IRQ_PRIO_MASKING)
                :
                : "r" ((unsigned long) GIC_PRIO_IRQON)
                : "memory");
}
  • Pesudo-NMI를 지원하는 경우 Priority Mask 레지스터를 사용하여 디폴트 irq와 nmi에 사용하는 irq 모두 통과시킨다.
  • Pesudo-NMI를 지원하지 않는 경우 PSTATE.I 플래그를 클리어하여 irq를 enable 한다.

 

Local IRQ 백업

local_irq_save()

include/linux/irqflags.h

#define local_irq_save(flags)                                   \
        do {                                                    \
                raw_local_irq_save(flags);                      \
        } while (0)

현재 CPU의 irq 상태를 백업하고 disable 한다.

 

raw_local_irq_save()

include/linux/irqflags.h

#define raw_local_irq_save(flags)                       \
        do {                                            \
                typecheck(unsigned long, flags);        \
                flags = arch_local_irq_save();          \
        } while (0)

현재 CPU의 cpsr값을 변수에 저장한다.

 

arch_local_irq_save() – ARM32

arch/arm/include/asm/irqflags.h

static inline unsigned long arch_local_irq_save(void)
{
        unsigned long flags;

        asm volatile(
                "       mrs     %0, " IRQMASK_REG_NAME_R "      @ arch_local_irq_save\n"
                "       cpsid   i"
                : "=r" (flags) : : "memory", "cc");
        return flags;
}

 

arch_local_irq_save() – ARM64

arch/arm64/include/asm/irqflags.h

static inline unsigned long arch_local_irq_save(void)
{
        unsigned long flags;

        flags = arch_local_save_flags();

        /*
         * There are too many states with IRQs disabled, just keep the current
         * state if interrupts are already disabled/masked.
         */
        if (!arch_irqs_disabled_flags(flags))
                arch_local_irq_disable();

        return flags;
}

 

arch_local_irq_save_flags() – ARM64

arch/arm64/include/asm/irqflags.h

static inline unsigned long arch_local_save_flags(void)
{
        unsigned long flags;

        asm volatile(ALTERNATIVE(
                "mrs    %0, daif",
                __mrs_s("%0", SYS_ICC_PMR_EL1),
                ARM64_HAS_IRQ_PRIO_MASKING)
                : "=&r" (flags)
                :
                : "memory");

        return flags;
}

 

Local IRQ 복구

local_irq_restore()

include/linux/irqflags.h

#define local_irq_restore(flags) do { raw_local_irq_restore(flags); } while (0)

현재 cpu에서 백업해둔 irq 상태를 복구한다.

 

raw_local_irq_restore()
#define raw_local_irq_restore(flags)                    \
        do {                                            \
                typecheck(unsigned long, flags);        \
                arch_local_irq_restore(flags);          \
        } while (0)

변수에 저장된 cpsr값을 읽어 현재 CPU의 cpsr_c 부분을 변경(현재 CPU의 인터럽트 상태를 저장되었던 상태로 되돌림)

 

arch_local_irq_restore() – ARM32

arch/arm/include/asm/irqflags.h

static inline void arch_local_irq_restore(unsigned long flags)
{
        asm volatile(
                "       msr     " IRQMASK_REG_NAME_W ", %0      
                :
                : "r" (flags)
                : "memory", "cc");
}

#define IRQMASK_REG_NAME_W "cpsr_c"

 

arch_local_irq_restore() – ARM64

arch/arm64/include/asm/irqflags.h

static inline void arch_local_irq_restore(unsigned long flags)
{
        asm volatile(ALTERNATIVE(
                        "msr    daif, %0\n"
                        "nop",
                        __msr_s(SYS_ICC_PMR_EL1, "%0")
                        "dsb    sy",
                        ARM64_HAS_IRQ_PRIO_MASKING)
                :
                : "r" (flags)
                : "memory");
}

 


APIs

irq 디스크립터 할당 & 해제

  • irq_alloc_descs()
  • irq_alloc_descs_from()
  • __irq_alloc_descs()
  • alloc_descs()
  • irq_free_descs()
  • irq_alloc_hwirqs()
  • irq_free_hwirqs()

 

irq 핸들러

  • generic_handle_irq()
  • generic_handle_irq_desc()
  • __handle_domain_irq()
  • __irq_set_preflow_handler()

 

기타

  • irq_data_to_desc()
  • irq_desc_get_irq()
  • irq_desc_get_irq_data()
  • irq_desc_get_chip()
  • irq_desc_get_chip_data()
  • irq_desc_get_handler_data()
  • irq_desc_has_action()
  • irq_has_action()
  • irq_set_handler_locked()
  • irq_set_chip_handler_name_locked()
  • irq_balancing_disabled()
  • irq_is_percpu()
  • irq_is_percpu_devid()
  • irq_get_percpu_devid_partition()

 


구조체 및 데이터

IRQ 디스크립터

include/linux/irqdesc.h

/**
 * struct irq_desc - interrupt descriptor
 * @irq_common_data:    per irq and chip data passed down to chip functions
 * @kstat_irqs:         irq stats per cpu
 * @handle_irq:         highlevel irq-events handler
 * @preflow_handler:    handler called before the flow handler (currently used by sparc)
 * @action:             the irq action chain
 * @status:             status information
 * @core_internal_state__do_not_mess_with_it: core internal status information
 * @depth:              disable-depth, for nested irq_disable() calls
 * @wake_depth:         enable depth, for multiple irq_set_irq_wake() callers
 * @tot_count:          stats field for non-percpu irqs
 * @irq_count:          stats field to detect stalled irqs
 * @last_unhandled:     aging timer for unhandled count
 * @irqs_unhandled:     stats field for spurious unhandled interrupts
 * @threads_handled:    stats field for deferred spurious detection of threaded handlers
 * @threads_handled_last: comparator field for deferred spurious detection of theraded handlers
 * @lock:               locking for SMP
 * @affinity_hint:      hint to user space for preferred irq affinity
 * @affinity_notify:    context for notification of affinity changes
 * @pending_mask:       pending rebalanced interrupts
 * @threads_oneshot:    bitfield to handle shared oneshot threads
 * @threads_active:     number of irqaction threads currently running
 * @wait_for_threads:   wait queue for sync_irq to wait for threaded handlers
 * @nr_actions:         number of installed actions on this descriptor
 * @no_suspend_depth:   number of irqactions on a irq descriptor with
 *                      IRQF_NO_SUSPEND set
 * @force_resume_depth: number of irqactions on a irq descriptor with
 *                      IRQF_FORCE_RESUME set
 * @rcu:                rcu head for delayed free
 * @kobj:               kobject used to represent this struct in sysfs
 * @request_mutex:      mutex to protect request/free before locking desc->lock
 * @dir:                /proc/irq/ procfs entry
 * @debugfs_file:       dentry for the debugfs file
 * @name:               flow handler name for /proc/interrupts output
 */
struct irq_desc {
        struct irq_common_data  irq_common_data;
        struct irq_data         irq_data;
        unsigned int __percpu   *kstat_irqs;
        irq_flow_handler_t      handle_irq;
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
        irq_preflow_handler_t   preflow_handler;
#endif
        struct irqaction        *action;        /* IRQ action list */
        unsigned int            status_use_accessors;
        unsigned int            core_internal_state__do_not_mess_with_it;
        unsigned int            depth;          /* nested irq disables */
        unsigned int            wake_depth;     /* nested wake enables */
        unsigned int            tot_count;
        unsigned int            irq_count;      /* For detecting broken IRQs */
        unsigned long           last_unhandled; /* Aging timer for unhandled count */
        unsigned int            irqs_unhandled;
        atomic_t                threads_handled;
        int                     threads_handled_last;
        raw_spinlock_t          lock;
        struct cpumask          *percpu_enabled;
        const struct cpumask    *percpu_affinity;
#ifdef CONFIG_SMP
        const struct cpumask    *affinity_hint;
        struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
        cpumask_var_t           pending_mask;
#endif
#endif
        unsigned long           threads_oneshot;
        atomic_t                threads_active;
        wait_queue_head_t       wait_for_threads;
#ifdef CONFIG_PM_SLEEP
        unsigned int            nr_actions;
        unsigned int            no_suspend_depth;
        unsigned int            cond_suspend_depth;
        unsigned int            force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
        struct proc_dir_entry   *dir;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
        struct dentry           *debugfs_file;
        const char              *dev_name;
#endif
#ifdef CONFIG_SPARSE_IRQ
        struct rcu_head         rcu;
        struct kobject          kobj;
#endif
        struct mutex            request_mutex;
        int                     parent_irq;
        struct module           *owner;
        const char              *name;
} ____cacheline_internodealigned_in_smp;

한 개의 irq 디스크립터이다.

  •  irq_common_data
    • irq 공통 정보
    • 아래 irq_data 같이 부모 관계로 연결되는 곳에서도 공통으로 사용되는 정보이다.
  •  irq_data
    • chip 기능에 전달되는 irq와 chip 데이터
    • 이 구조체들은 하이라키 도메인 구성된 경우 부모 관계로 연결된다.
  • *kstat_irqs
    • cpu 별 irq 통계 카운터
  • handle_irq
    • 하이 레벨 irq 이벤트 핸들러
  • preflow_handler
    • preflow 핸들러
  • *action
    • irq action 체인으로 유저 디바이스가 요청한 인터럽트 핸들러가 추가되는 리스트이다.
  • status_use_accessors
    • irq 상태 정보
  • core_internal_state__do_not_mess_with_it
    • 코어 내부 상태 정보
    • irqd->state으로 표현( state가 매크로)
  • depth
    • 네스트된 irq_disable() 호출을 위한 disable depth
  • wake_depth
    • 다중 irq_set_irq_wake() 호출을 위한 enable depth
  • irq_count
    • stalled irq들을 감지하기 위한 상태 필드
  • last_unhandled
    • 언핸들드 카운트를 위한 aging 타이머
  • irqs_unhandled
    • 가짜 언핸들된 인터럽트들을 위한 상태 필드
  • threads_handled
    • 스레드 핸들러의 연기된 가짜 감지를 위한 상태 필드
  • threads_handled_last
    • 스레드 핸들러의 연기된 가짜 감지를 위한 비교 필드
  • lock
    • SMP lock
  • *percpu_enabled
    • cpu 비트마스크
  • *percpu_affinity
    • cpu 제한된 경우 cpu affinity를 표현한 비트마스크
  • *affinity_hint
  • *affinity_notify
    • affnity가 변경 시 통지를 위한 context
  • pending_mask
    • 지연된 리밸런스된 인터럽트들
  • threads_oneshot
  • threads_active
    • 현재 동작중인 irqaction 스레드의 수
  • wait_for_threads
    • 스레드된 핸들러를 위해 기다릴 sync_irq를 위한 대기큐
  • nr_actions
    • 이 irq 디스크립터에  설치된 액션 수
  • no_suspend_depth
    • IRQF_NO_SUSPEND 셋을 가진 irq 디스크립터의 irqactions 수
  • cond_suspend_depth
  • force_resume_depth
    • IRQF_FORCE_RESUME 셋을 가진 irq 디스크립터의 irqactions 수
  • dir
    • /proc/irq/에 표시될 procfs 엔트리
    • default로 irq 숫자 (예: /proc/irq/83)
  • *debugfs_file
    • 디버그용 (/sys/fs/debug)
  • *dev_name
    • 디버그용
  • rcu
  • kobj
  • request_mutex
  • parent_irq
    • cascade 연결 시 부모 irq 번호
  • *owner
    • 모듈을 가리킨다.
  • name
    • /proc/interrupts에 출력될 flow 핸들러 이름
    • 예) uart-pl011

 

irq_data 구조체

include/linux/irq.h

/**
 * struct irq_data - per irq chip data passed down to chip functions
 * @mask:               precomputed bitmask for accessing the chip registers
 * @irq:                interrupt number
 * @hwirq:              hardware interrupt number, local to the interrupt domain
 * @common:             point to data shared by all irqchips
 * @chip:               low level interrupt hardware access
 * @domain:             Interrupt translation domain; responsible for mapping
 *                      between hwirq number and linux irq number.
 * @parent_data:        pointer to parent struct irq_data to support hierarchy
 *                      irq_domain
 * @chip_data:          platform-specific per-chip private data for the chip
 *                      methods, to allow shared chip implementations
 */
struct irq_data {
        u32                     mask;
        unsigned int            irq;
        unsigned long           hwirq;
        struct irq_common_data  *common;
        struct irq_chip         *chip;
        struct irq_domain       *domain;
#ifdef  CONFIG_IRQ_DOMAIN_HIERARCHY
        struct irq_data         *parent_data;
#endif
        void                    *chip_data;
};

이 구조체는 irqchip들에 제공될 정보들이다. irq 디스크립터에 기본 내장되어 있고, 하이라키 도메인을 구성하는 경우 추가 생성되어 부모 관계로 연결된다.

  •  mask
    • chip 레지스터들에 접근하기 위한 미리 계산된 비트마스크
  • irq
    • 리눅스가 관리하는 인터럽트 번호 (virq)
  • hwirq
    • 하드웨어 인터럽트 번호
  • *common
    • irq 디스크립터에 있는 공통 정보 구조체를 가리킨다.
  • *chip
    • 로우 레벨 인터럽트 컨트롤러 하드웨어 제어를 담당하는 irq_chip 구조체를 가리킨다.
  • *domain
    • irq 도메인을 가리킨다. (hwirq -> irq 변환)
  • *parent_data
    • 하이라키 irq 도메인을 지원하기 위한 부모 irq_data를 가리킨다.
  • chip_data
    • chip 메소드와 공유 chip 구현을 허락하기 위한 플랫폼 specific per-chip private 데이터

 

irq_common_data 구조체

include/linux/irq.h

/**
 * struct irq_common_data - per irq data shared by all irqchips
 * @state_use_accessors: status information for irq chip functions.
 *                      Use accessor functions to deal with it
 * @node:               node index useful for balancing
 * @handler_data:       per-IRQ data for the irq_chip methods
 * @affinity:           IRQ affinity on SMP. If this is an IPI
 *                      related irq, then this is the mask of the
 *                      CPUs to which an IPI can be sent.
 * @effective_affinity: The effective IRQ affinity on SMP as some irq
 *                      chips do not allow multi CPU destinations.
 *                      A subset of @affinity.
 * @msi_desc:           MSI descriptor
 * @ipi_offset:         Offset of first IPI target cpu in @affinity. Optional.
 */
struct irq_common_data {
        unsigned int            __private state_use_accessors;
#ifdef CONFIG_NUMA
        unsigned int            node;
#endif
        void                    *handler_data;
        struct msi_desc         *msi_desc;
        cpumask_var_t           affinity;
#ifdef CONFIG_GENERIC_IRQ_EFFECTIVE_AFF_MASK
        cpumask_var_t           effective_affinity;
#endif
#ifdef CONFIG_GENERIC_IRQ_IPI
        unsigned int            ipi_offset;
#endif
};

하이라키 irq 도메인을 구성하는 irq data들이 사용하는 irq 공통 정보이다.

  • state_use_accessors
    • irq chip 기능들에 제공할 irq 디스크립터의 accessor 정보
  • node
    • 노드 인덱스
  • *handler_data
    • irq_chip 메소드를 위한 per-IRQ 데이터
  • *msi_desc
    • MSI 디스크립터를 가리킨다.
  • affinity
    • SMP에서의 IRQ affinity
  • effective_affinity
  • ipi_offset

 

IRQ Data 상태

include/linux/irq.h

/*
 * Bit masks for irq_common_data.state_use_accessors
 *
 * IRQD_TRIGGER_MASK            - Mask for the trigger type bits
 * IRQD_SETAFFINITY_PENDING     - Affinity setting is pending
 * IRQD_ACTIVATED               - Interrupt has already been activated
 * IRQD_NO_BALANCING            - Balancing disabled for this IRQ
 * IRQD_PER_CPU                 - Interrupt is per cpu
 * IRQD_AFFINITY_SET            - Interrupt affinity was set
 * IRQD_LEVEL                   - Interrupt is level triggered
 * IRQD_WAKEUP_STATE            - Interrupt is configured for wakeup
 *                                from suspend
 * IRQD_MOVE_PCNTXT             - Interrupt can be moved in process
 *                                context
 * IRQD_IRQ_DISABLED            - Disabled state of the interrupt
 * IRQD_IRQ_MASKED              - Masked state of the interrupt
 * IRQD_IRQ_INPROGRESS          - In progress state of the interrupt
 * IRQD_WAKEUP_ARMED            - Wakeup mode armed
 * IRQD_FORWARDED_TO_VCPU       - The interrupt is forwarded to a VCPU
 * IRQD_AFFINITY_MANAGED        - Affinity is auto-managed by the kernel
 * IRQD_IRQ_STARTED             - Startup state of the interrupt
 * IRQD_MANAGED_SHUTDOWN        - Interrupt was shutdown due to empty affinity
 *                                mask. Applies only to affinity managed irqs.
 * IRQD_SINGLE_TARGET           - IRQ allows only a single affinity target
 * IRQD_DEFAULT_TRIGGER_SET     - Expected trigger already been set
 * IRQD_CAN_RESERVE             - Can use reservation mode
 */
enum {
        IRQD_TRIGGER_MASK               = 0xf,
        IRQD_SETAFFINITY_PENDING        = (1 <<  8),
        IRQD_ACTIVATED                  = (1 <<  9),
        IRQD_NO_BALANCING               = (1 << 10),
        IRQD_PER_CPU                    = (1 << 11),
        IRQD_AFFINITY_SET               = (1 << 12),
        IRQD_LEVEL                      = (1 << 13),
        IRQD_WAKEUP_STATE               = (1 << 14),
        IRQD_MOVE_PCNTXT                = (1 << 15),
        IRQD_IRQ_DISABLED               = (1 << 16),
        IRQD_IRQ_MASKED                 = (1 << 17),
        IRQD_IRQ_INPROGRESS             = (1 << 18),
        IRQD_WAKEUP_ARMED               = (1 << 19),
        IRQD_FORWARDED_TO_VCPU          = (1 << 20),
        IRQD_AFFINITY_MANAGED           = (1 << 21),
        IRQD_IRQ_STARTED                = (1 << 22),
        IRQD_MANAGED_SHUTDOWN           = (1 << 23),
        IRQD_SINGLE_TARGET              = (1 << 24),
        IRQD_DEFAULT_TRIGGER_SET        = (1 << 25),
        IRQD_CAN_RESERVE                = (1 << 26),
};

irq_common_data.state_use_accessors에서 사용되는 플래그들이다.

  •  IRQD_TRIGGER_MASK
    • 트리거 타입 마스크 (4비트)
  • IRQD_SETAFFINITY_PENDING
    • affinity 설정이 지연되는 중
  • IRQD_NO_BALANCING
    • no 밸런싱
  • IRQD_PER_CPU
    • 코어별 전용 인터럽트 배정되는 경우 per-cpu 인터럽트로 구성
  • IRQD_AFFINITY_SET
    • affnity 설정된 경우
  • IRQD_LEVEL
    • 레벨 트리거 설정된 경우 (그렇지 않으면 edge 트리거)
  • IRQD_WAKERUP_STATE
    • suspend된 작업을 깨워 동작시키는 인터럽트 형태로 설정
  • IRQD_MOVE_PCNTXT
    • 인터럽트가 process context로 이동된 경우
  • IRQD_IRQ_DISABLED
    • 인터럽트를 disable한 상태
  • IRQD_IRQ_MASKED
    • 인터럽트의 masked 상태
  • IRQD_IRQ_INPROGRESS
    • 인터럽트가 진행중인 상태
  • IRQD_WAKERUP_ARMED
    • 꺠우는 모드 armed
  • IRQD_FORWARDED_TO_VCPU
    • 가상화를 담당하는 vCPU 인터페이스로 전달 가능
  • IRQD_IRQ_STARTED
    • 인터럽트가 서비스 시작된 상태
  • IRQD_MANAGED_SHUTDOWN
    • affinity가 지정되지 않아 인터럽트가 shutdown된 상태
  • IRQD_SINGLE_TARGET
    • 하나의 cpu(affinity target)만 지정 가능한 상태
  • IRQD_DEFAULT_TRIGGER_SET
    • 트리거가 설정된 상태
  • IRQD_CAN_RESERVE
    • 예약 모드 사용 가능한 상태

 

참고

 

Interrupts -11- (IC Driver for rpi2)

<kernel v4.0>

Interrupts -11- (IC Driver for rpi2)

Device Tree로 hierarchy irq chip 구성

설명에 사용된 샘플 보드는 다음과 같다.

  • 64비트 rock960
    • GIC v3 인터럽트 컨트롤러
    • rock 사의 rk3399칩셋은 GIC v3를 사용한다.
    • 커널은 v5.4를 기준으로 설명하였다.
  • 32비트 rpi2
    • BCM2836 전용 인터럽트 컨트롤러
    • 커널은 v4.4를 기준으로 설명하였다.

 

다음 그림은 rpi2에서 설정되는 5개의 irq_chip을 보여준다.

3 개의 인터럽트 컨트롤러

  • gpio pinctrl 디바이스
    • gpio를 사용하는 pinctrl 디바이스 드라이버에 구현되어 있다.
      • 인터럽트 컨트롤러 디바이스 드라이버가 아니므로 IRQCHIP_DECLARE()를 사용하지 않았으므로 __chip_id_of_table 섹션에 인터럽트 컨트롤러 엔트리가 등록되지 않는다.
        • 인터럽트 초기화 루틴 irqchip_init()에서 초기화 함수를 호출하지 않는다.
    • DT 스크립트에 “gpio-controller;” 및 “interrupt-controller;” 두 가지 기능의 컨트롤러가 동작한다.
      • 메인 기능이 gpio를 사용하는 pinctrl 디바이스이다.
      • 추가 기능으로 gpio에서 인터럽트 라인 컨트롤 기능을 수행할 수 있다.
        • gpio pin 들을 인터럽트 라인으로 사용할 수 있어서 이들에 대한 set/clear 등의 마스킹 동작을 위해 irq_chip이 구현되어 있다.
  • GPU 인터럽트 컨트롤러
    • 0 ~ 2번 뱅크를 입력으로 받아들이며 각각 최대 32개의 인터럽트 라인을 가지고 있다.
    •  rpi에서도 사용하는 인터럽트 컨트롤러
  • LOCAL 인터럽트 컨트롤러
    • rpi2에서 SMP core 간 IPI 등의 소프트 인터럽트 처리 등을 위해 별도로 추가되었다.
    • 3 개의 irq_chip으로 구성
      • gpu 인터럽트를 처리하기 위한 irq_chip
      • 4개의 timer 인터럽트를 처리하기 위한 irq_chip
      • 1개의 pmu 인터럽트를 처리하기 위한 irq_chip

 


DT용 인터럽트 컨트롤러 초기화 – rpi2

 

부모 컨트롤러 Part

bcm2836_arm_irqchip_l1_intc_of_init()

drivers/irqchip/irq-bcm2836.c

static int __init bcm2836_arm_irqchip_l1_intc_of_init(struct device_node *node,
                                                      struct device_node *parent)
{
        intc.base = of_iomap(node, 0);
        if (!intc.base) {
                panic("%s: unable to map local interrupt registers\n",
                        node->full_name);
        }

        bcm2835_init_local_timer_frequency();

        intc.domain = irq_domain_add_linear(node, LAST_IRQ + 1,
                                            &bcm2836_arm_irqchip_intc_ops,
                                            NULL);
        if (!intc.domain)
                panic("%s: unable to create IRQ domain\n", node->full_name);

        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_CNTPSIRQ,
                                         &bcm2836_arm_irqchip_timer);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_CNTPNSIRQ,
                                         &bcm2836_arm_irqchip_timer);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_CNTHPIRQ,
                                         &bcm2836_arm_irqchip_timer);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_CNTVIRQ,
                                         &bcm2836_arm_irqchip_timer);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_GPU_FAST,
                                         &bcm2836_arm_irqchip_gpu);
        bcm2836_arm_irqchip_register_irq(LOCAL_IRQ_PMU_FAST,
                                         &bcm2836_arm_irqchip_pmu);

        bcm2836_arm_irqchip_smp_init();

        set_handle_irq(bcm2836_arm_irqchip_handle_irq);
        return 0;
}

bcm2836 로컬 인터럽트에 해당하는 1개의  irq domain 과 3 개의 irq chip을 구성하고 사용하는 인터럽트들을 매핑한다.

  • 코드 라인 4~8에서 인수로 전달받은 인터럽트 컨트롤러 노드의 base 레지스터를 IO 매핑하여 가상주소를 알아온다. 만일 매핑이 실패하면 panic 처리를 한다.
    • rpi2: 0xf400_0000
  • 코드 라인 10~14에서 10개의 irq로 리니어 domain을 추가한다. domain의 변환 함수가 ops에 등록되어 운용된다. 만일 irq domain이 추가되지 않는 경우 panic 처리를 한다.
    • .xlate = irq_domain_xlate_onecell
  • 코드 라인 18~25에서 4개의 타이머 인터럽트를 timer용 irq chip에 구성하고 매핑한다.
  • 코드 라인 26~27에서 1개의 gpu 인터럽트를 gpu용 irq chip에 구성하고 매핑한다.
  • 코드 라인 28~29에서 1개의 pmu 인터럽트를 pmu용 irq chip에 구성하고 매핑한다.
  • 코드 라인 31에서 cpu on/off 등 상태 변경에 따른 notify를 받기 위해 연동하고, ipi 호출을 위해 연동함수를 등록한다.
  • 코드 라인 33에서 irq exception 처리 루틴에서 호출하도록 irq 핸들러 함수를 전역 handle_arch_irq에 등록한다.

 

자식 컨트롤러 Part

bcm2836_armctrl_of_init()

drivers/irqchip/irq-bcm2835.c

static int __init bcm2836_armctrl_of_init(struct device_node *node,
                                          struct device_node *parent)
{
        return armctrl_of_init(node, parent, true);
}

처리할 인터럽트 컨트롤러에 대한 디바이스 노드와 상위 인터럽트 컨트롤러에 대한 디바이스 노드 정보를 갖고 armctrl_of_init() 함수를 호출한다.

 

armctrl_of_init()

drivers/irqchip/irq-bcm2835.c

static int __init armctrl_of_init(struct device_node *node,
                                  struct device_node *parent,
                                  bool is_2836)
{
        void __iomem *base;
        int irq, b, i;

        base = of_iomap(node, 0);
        if (!base)
                panic("%s: unable to map IC registers\n",
                        node->full_name);

        intc.base = base;
        intc.domain = irq_domain_add_linear(node, NUMBER_IRQS * 2,
                                            &armctrl_ops, NULL);
        if (!intc.domain)
                panic("%s: unable to create IRQ domain\n", node->full_name);

        for (b = 0; b < NR_BANKS; b++) { 
                intc.pending[b] = base + reg_pending[b];
                intc.enable[b] = base + reg_enable[b];
                intc.disable[b] = base + reg_disable[b];

                for (i = 0; i < bank_irqs[b]; i++) {
                        irq = irq_create_mapping(intc.domain, MAKE_HWIRQ(b, i));
                        BUG_ON(irq <= 0);
                        irq_set_chip_and_handler(irq, &armctrl_chip,
                                handle_level_irq);
                        irq_set_probe(irq);
                }
        }

        if (is_2836) {
                int parent_irq = irq_of_parse_and_map(node, 0);

                if (!parent_irq) {
                        panic("%s: unable to get parent interrupt.\n",
                              node->full_name);
                }
                irq_set_chained_handler(parent_irq, bcm2836_chained_handle_irq);
        } else {
                set_handle_irq(bcm2835_handle_irq);
        }

        if (is_2836) {
                intc.local_regmap =
                        syscon_regmap_lookup_by_compatible("brcm,bcm2836-arm-local");
                if (IS_ERR(intc.local_regmap)) {
                        pr_err("Failed to get local register map. FIQ is disabled for cpus > 1\n");
                        intc.local_regmap = NULL;
                }
        }

        /* Make a duplicate irq range which is used to enable FIQ */
        for (b = 0; b < NR_BANKS; b++) {
                for (i = 0; i < bank_irqs[b]; i++) {
                        irq = irq_create_mapping(intc.domain,
                                        MAKE_HWIRQ(b, i) + NUMBER_IRQS);
                        BUG_ON(irq <= 0); 
                        irq_set_chip(irq, &armctrl_chip);
                        irq_set_probe(irq);
                }
        }
        init_FIQ(FIQ_START);

        return 0;
}

 

bcm2836 gpu 인터럽트에 해당하는 irq domain 과 irq chip을 구성하고 사용하는 인터럽트들을 매핑한다.

  • 코드 라인 8~13에서 인수로 전달받은 인터럽트 컨트롤러 노드의 base 레지스터를 IO 매핑하여 가상주소를 알아온다. 만일 매핑이 실패하면 panic 처리를 한다.
    • rpi2: 0xf300_b200
  • 코드 라인 14~17에서 3개 뱅크(각 32개 irq)가 연결되는 96개 irq + 96개 fiq에 대응하도록 irq domain을 구성하고 매핑한다.
    • irq domain의 ops
      • .xlate =armctrl_xlate
  • 코드 라인 19~22에서 각 뱅크의 pending 레지스터와 enable 및 disable을 레지스터의 가상 주소를 대입한다.
  • 코드 라인 24~25에서 각 뱅크에서 처리할 hw 인터럽트 수 만큼을 irq domain과 irq 디스크립터에 매핑한다.
    • 3 개의 각 뱅크에서 매핑할 hwirq 들
      • bank#0
        • hwirq=0~7까지 8개
        • irq=22~29
      • bank#1
        • hwirq=32~63까지 32개
        • irq=32~63
      • bank#2
        • hwirq=64~95까지 32개
        • irq=62~93
  • 코드 라인 27~30에서 “ARMCTRL-level” 이라는 이름의 irq chip을 설정하고 핸들러로 handle_level_irq()를 사용한다.
  • 코드 라인 33~40에서 이 드라이버 코드는 bcm2835(rpi)와 혼용하여 사용하므로 bcm2836(rpi2)인 경우 부모 인터럽트 컨트롤의 hw irq를 알아와서 이 인터럽트 핸들러로 bcm2836_chained_handle_irq() 함수를 대입한다. 만일 Device Tree 스크립트에서 parent irq 번호를 알아오지 못한 경우 panic 처리한다.
    • 모든 gpu 인터럽트들 중 하나라도 인터럽트가 발생하면 부모 인터럽트 컨트롤러의 8번 hwirq도 인터럽트가 발생한다.
  • 코드 라인 41~43에서 bcm2835(rpi)에서 호출한 경우 핸들러로 bcm2835_handle_irq() 함수를 대입한다.
  • 코드 라인 45~52에서 Device Tree에서 System Control Register 정보를 읽어서 base 주소를 대입한다. 못 읽어오는 경우 메시지를 출력한다. 이 때 2 번째 cpu 부터 fiq 인터럽트를 수신할 수 없게된다.
    • rpi2: 물리 주소=0x4000_0000, size=0x100
  • 코드 라인 55~58에서 각 뱅크에서 처리할 hw 인터럽트 수 만큼을 irq domain과 irq 디스크립터에 매핑한다. 이 인터럽트들은 fiq에 해당한다.
    • 3 개의 각 뱅크에서 매핑할 hwirq 들
      • bank#0
        • hwirq=96~103까지 8개
        • irq=94~101
      • bank#1
        • hwirq=128~159까지 32개
        • irq=102~133
      • bank#2
        • hwirq=160~191까지 32개
        • irq=134~165
  • 코드 라인 60~62에서 “ARMCTRL-level” 이라는 이름의 irq chip을 설정하고 probe를 설정한다.
  • 코드 라인 64에서 default FIQ 인터럽트 처리기를 백업하기 위해 fiq 벡터 값과 fiq 모드에서 사용하는 레지스터들 일부(r8 ~ r12, sp, lr)를 백업해둔다.
    • fiq 사용 시 멀티플 레지스터 로드 및 저장 시 깨질 수 있기에 복원을 위해 초기 핸들러를 백업해 둔다.

 

 

GPIO 컨트롤러 Part

인터럽트 컨트롤러에서 초기화하지 않고 GPIO의 pinctrl 드라이버에서 초기화된다.  초기화 과정의 소스는 여기서 설명하지 않고 아래 그림만 참고한다.

 


IRQ exception 처리 후 컨트롤러가 설정한 핸들러 함수 호출

부모 컨트롤러  Part

bcm2836_arm_irqchip_handle_irq()

static void
__exception_irq_entry bcm2836_arm_irqchip_handle_irq(struct pt_regs *regs)
{
        int cpu = smp_processor_id();
        u32 stat;

        stat = readl_relaxed(intc.base + LOCAL_IRQ_PENDING0 + 4 * cpu);
        if (stat & BIT(LOCAL_IRQ_MAILBOX0)) {
#ifdef CONFIG_SMP
                void __iomem *mailbox0 = (intc.base +
                                          LOCAL_MAILBOX0_CLR0 + 16 * cpu);
                u32 mbox_val = readl(mailbox0);
                u32 ipi = ffs(mbox_val) - 1;

                writel(1 << ipi, mailbox0);
                handle_IPI(ipi, regs);
#endif
        } else if (stat) {
                u32 hwirq = ffs(stat) - 1;

                handle_IRQ(irq_linear_revmap(intc.domain, hwirq), regs);
        }
}

mailbox0 레지스터를 읽어 pending irq가 있는 경우 IPI 핸들러를 호출하고 그렇지 않은 경우 IRQ 핸들러를 호출한다.

  • rpi2: 부모 인터럽트 컨틀롤러에서 발생한 irq들은 4개의 timer와 pmu 인터럽트이다.

 

  • 코드 라인 7~8에서 현재 cpu에 대한 local pending 레지스터를 읽어서 bit4를 통해 mailbox가 수신되었는지 확인한다. 만일 수신된 경우
  • 코드 라인 9~17에서 수신된 mailbox에서 가장 먼저(msb) 처리할 IPI 번호를 읽어 그 비트를 클리어하고 handle_IPI() 함수를 호출하여 IPI 처리를 수행하게 한다.
  • 코드 라인 18~22에서 hwirq  -> irq 번호로 reversemap을 사용하여 transalation하여 얻은 번호로 handle_irq()를 호출한다.

 

자식 컨트롤러  Part

bcm2836_chained_handle_irq()

drivers/irqchip/irq-bcm2835.c

 

static void bcm2836_chained_handle_irq(struct irq_desc *desc)
{
        u32 hwirq;

        while ((hwirq = get_next_armctrl_hwirq()) != ~0)
                generic_handle_irq(irq_linear_revmap(intc.domain, hwirq));
}

gpu pending 레지스터 값을 읽어서 hwirq 값을 irq로 변환한 후 generic_handle_irq() 함수를 호출한다.

  • 변환한 irq에 등록한 핸들러 함수를 호출한다.
    • rpi2: handle_level_irq()

 

get_next_armctrl_hwirq()

drivers/irqchip/irq-bcm2835.c

static u32 get_next_armctrl_hwirq(void)
{
        u32 stat = readl_relaxed(intc.pending[0]) & BANK0_VALID_MASK;

        if (stat == 0)
                return ~0;
        else if (stat & BANK0_HWIRQ_MASK)
                return MAKE_HWIRQ(0, ffs(stat & BANK0_HWIRQ_MASK) - 1);
        else if (stat & SHORTCUT1_MASK)
                return armctrl_translate_shortcut(1, stat & SHORTCUT1_MASK);
        else if (stat & SHORTCUT2_MASK)
                return armctrl_translate_shortcut(2, stat & SHORTCUT2_MASK);
        else if (stat & BANK1_HWIRQ)
                return armctrl_translate_bank(1);
        else if (stat & BANK2_HWIRQ)
                return armctrl_translate_bank(2);
        else
                BUG();
}

gpu pending 레지스터 값을 읽어서 hwirq로 변환한다.

  • 코드 라인 3에서 로컬 pending  레지스터 값을 읽어서 관련 비트들만 마스크한다.
    • timer 4개, mailbox 4개, gpu 매핑 1개, pmu 매핑 1개, shortcut 11개 비트들
  • 코드 라인 5~6에서 pending 인터럽트가 없는 경우 ~0(호출 함수에서 루프의 끝을 의미)을 반환한다.
  • 코드 라인 7~8에서 local 인터럽트 8개(bit7:0) 중 하나에 속한 경우 0번 뱅크에 해당하는 hwirq로 변환하여 반환한다.
  • 코드 라인 9~10에서 shortcut1에 해당하는 인터럽트가 발생한 경우 1번 뱅크에 해당하는 hwirq로 변환하여 반환한다.
    • bit10 ~ bit14 -> <7, 9, 10, 18, 19> + 32(bank1)
  • 코드 라인 11~12에서 shortcut2에 해당하는 인터럽트가 발생한 경우 2번 뱅크에 해당하는 hwirq로 변환하여 반환한다.
    • bit15 ~ bit20 -> <21, 22, 23, 24, 25, 30> + 64(bank2)
  • 코드 라인 13~14에서 bank1에 해당하는 인터럽트인 경우 gpu#1 pending 레지스터를 읽어 가장 우측 비트(lsb)가 1로 설정된 인터럽트에 해당하는 hwirq로 변환하여 반환한다.
    • bit0 ~ bit31 -> 30 ~ 61
  • 코드 라인 15~16에서 bank2에 해당하는 인터럽트인 경우 gpu#2 pending 레지스터를 읽어 가장 우측 비트(lsb)가 1로 설정된 인터럽트에 해당하는 hwirq로 변환하여 반환한다.
    • bit0 ~ bit31 -> 62 ~ 93

 

armctrl_translate_shortcut()

drivers/irqchip/irq-bcm2835.c

static u32 armctrl_translate_shortcut(int bank, u32 stat)
{
        return MAKE_HWIRQ(bank, shortcuts[ffs(stat >> SHORTCUT_SHIFT) - 1]);
}

shortcut에 해당하는 인터럽트가 발생한 경우 해당 뱅크에 해당하는 hwirq로 변환하여 반환한다.

  • bit10 ~ bit14 -> <7, 9, 10, 18, 19> + 32(bank1)
  • bit15 ~ bit20 -> <21, 22, 23, 24, 25, 30> + 64(bank2)

 

armctrl_translate_bank()

drivers/irqchip/irq-bcm2835.c

/*
 * Handle each interrupt across the entire interrupt controller.  This reads the
 * status register before handling each interrupt, which is necessary given that
 * handle_IRQ may briefly re-enable interrupts for soft IRQ handling.
 */

static u32 armctrl_translate_bank(int bank)
{
        u32 stat = readl_relaxed(intc.pending[bank]);

        return MAKE_HWIRQ(bank, ffs(stat) - 1);
}

gpu#1 또는 gpu#2 pending 레지스터에서 읽은 stat 값에서 가장 우측 비트에 해당하는 hwirq를 반환한다.

  • bit0 ~ bit31 -> 30 ~ 61
  • bit0 ~ bit31 -> 62 ~ 93

 

drivers/irqchip/irq-bcm2835.c

/* Put the bank and irq (32 bits) into the hwirq */
#define MAKE_HWIRQ(b, n)        ((b << 5) | (n))
#define HWIRQ_BANK(i)           (i >> 5)
#define HWIRQ_BIT(i)            BIT(i & 0x1f)

 


irq chip의 콜백 함수 – rpi2

ARMCTRL-level용

armctrl_chip

drivers/irqchip/irq-bcm2835.c

static struct irq_chip armctrl_chip = {
        .name = "ARMCTRL-level",
        .irq_mask = armctrl_mask_irq,
        .irq_unmask = armctrl_unmask_irq
};

bcm2835 인터럽트용 irq_chip 콜백 함수들

 

armctrl_mask_irq()

drivers/irqchip/irq-bcm2835.c

static void armctrl_mask_irq(struct irq_data *d)
{
        if (d->hwirq >= NUMBER_IRQS)
                writel_relaxed(REG_FIQ_DISABLE, intc.base + REG_FIQ_CONTROL);
        else
                writel_relaxed(HWIRQ_BIT(d->hwirq),
                               intc.disable[HWIRQ_BANK(d->hwirq)]);
}

요청한 인터럽트(hwirq) 라인을 마스크하여 인터럽트 진입을 허용하지 않게 한다.

  • 코드 라인 3~4에서 hwirq가 NUMBER_IRQS(96) 이상인 경우 FIQ 전체에 대한 인터럽트 마스크를 수행한다.
  • 코드 라인 5~7에서 요청 IRQ에 대한 마스크를 수행한다.
    • 1개 뱅크당 32개의 인터럽트 라인을 제어할 수 있다.

 

armctrl_unmask_irq()

drivers/irqchip/irq-bcm2835.c

static void armctrl_unmask_irq(struct irq_data *d)
{
        if (d->hwirq >= NUMBER_IRQS) {
                if (num_online_cpus() > 1) {
                        unsigned int data;
                        int ret;

                        if (!intc.local_regmap) {
                                pr_err("FIQ is disabled due to missing regmap\n");
                                return;
                        }

                        ret = regmap_read(intc.local_regmap,
                                          ARM_LOCAL_GPU_INT_ROUTING, &data);
                        if (ret) {
                                pr_err("Failed to read int routing %d\n", ret);
                                return;
                        }

                        data &= ~0xc;
                        data |= (1 << 2);
                        regmap_write(intc.local_regmap,
                                     ARM_LOCAL_GPU_INT_ROUTING, data);
                }

                writel_relaxed(REG_FIQ_ENABLE | hwirq_to_fiq(d->hwirq),
                               intc.base + REG_FIQ_CONTROL);
        } else {
                writel_relaxed(HWIRQ_BIT(d->hwirq),
                               intc.enable[HWIRQ_BANK(d->hwirq)]);
        }
}

요청한 인터럽트(hwirq) 라인을 언마스크하여 인터럽트 진입을 허용하게 한다.

  • 코드 라인 3에서 hwirq가 NUMBER_IRQS(96) 이상인 경우
  • 코드 라인 4~24에서 online cpu가 1개를 초과하는 경우 2개 이상인 경우 bcm2836에 있는 local routing 레지스터를 설정할 수 있고, 여기에서 irq cpu 라우팅을 fiq cpu 라우팅으로 변경한다.
    • local routing  레지스터 값은 하위 3비트만 유효하다.
      • 최하위 비트 2개는 cpu core 번호
      • bit2는 0=irq, 1=fiq
    • 이 조작으로 영향을 받는 인터럽트는 core 전용이 아닌 공통 인터럽트들로 아래와 같다.
      • GPU IRQ
      • Local Timer
      • AXI error
      • 미 사용 15개의 local peripheral 인터럽트들
    • 이 조작으로 영향을 받지 않는 인터럽트들은 core 인터럽트로 아래와 같다.
      • 4개의 코어 타이머 인터럽트들 x 4 cpus
      • 1개의 pm 인터럽트 x 4 cpus
      • 4개의 mailbox 인터럽트들 x 4 cpus
  • 코드 라인 26~27에서 FIQ Control 레지스터의 8번 비트를 켜고 bit0~bit7의 irq(fiq) source를 지정한다.
    • FIQ Control 레지스터: 버스(VC) 주소=0x7e00_b20c, ARM 물리 주소=0x3f00_b20c, ARM 가상 주소=0xf300_b20c
    • 1개의 irq 소스를 fiq로 라우팅할 수 있다.
      • rpi2: 고석 처리가 요구되는 usb를 fiq로 라우팅하여 사용한다.
  • 코드 라인 28~31에서 요청 IRQ를 enable을 시킨다.
    • 3개의 뱅크에서 각 뱅크당 최대 32개의 인터럽트 라인을 제어할 수 있다.

 


Argument 파싱 for DT

armctrl_xlate() – RPI2

arch/arm/mach-bcm2709/armctrl.c

/* from drivers/irqchip/irq-bcm2835.c */
static int armctrl_xlate(struct irq_domain *d, struct device_node *ctrlr,
        const u32 *intspec, unsigned int intsize,
        unsigned long *out_hwirq, unsigned int *out_type)
{
        if (WARN_ON(intsize != 2))
                return -EINVAL;

        if (WARN_ON(intspec[0] >= NR_BANKS))
                return -EINVAL;

        if (WARN_ON(intspec[1] >= IRQS_PER_BANK))
                return -EINVAL;

        if (WARN_ON(intspec[0] == 0 && intspec[1] >= NR_IRQS_BANK0))
                return -EINVAL;

        if (WARN_ON(intspec[0] == 3 && intspec[1] > 3 && intspec[1] != 5 && intspec[1] != 9))
                return -EINVAL;

        if (intspec[0] == 0) 
                *out_hwirq = ARM_IRQ0_BASE + intspec[1];
        else if (intspec[0] == 1)
                *out_hwirq = ARM_IRQ1_BASE + intspec[1];
        else if (intspec[0] == 2)
                *out_hwirq = ARM_IRQ2_BASE + intspec[1];
        else
                *out_hwirq = ARM_IRQ_LOCAL_BASE + intspec[1];

        /* reverse remap_irqs[] */
        switch (*out_hwirq) {
        case INTERRUPT_VC_JPEG:
                *out_hwirq = INTERRUPT_JPEG;
                break;
        case INTERRUPT_VC_USB:
                *out_hwirq = INTERRUPT_USB;
                break;
        case INTERRUPT_VC_3D:
                *out_hwirq = INTERRUPT_3D;
                break;
        case INTERRUPT_VC_DMA2:
                *out_hwirq = INTERRUPT_DMA2;
                break;
        case INTERRUPT_VC_DMA3:
                *out_hwirq = INTERRUPT_DMA3;
                break;
        case INTERRUPT_VC_I2C:
                *out_hwirq = INTERRUPT_I2C;
                break;
        case INTERRUPT_VC_SPI:
                *out_hwirq = INTERRUPT_SPI;
                break;
        case INTERRUPT_VC_I2SPCM:
                *out_hwirq = INTERRUPT_I2SPCM;
                break;
        case INTERRUPT_VC_SDIO:
                *out_hwirq = INTERRUPT_SDIO;
                break;
        case INTERRUPT_VC_UART:
                *out_hwirq = INTERRUPT_UART;
                break;
        case INTERRUPT_VC_ARASANSDIO:
                *out_hwirq = INTERRUPT_ARASANSDIO;
                break;
        }

        *out_type = IRQ_TYPE_NONE;
        return 0;
}

DT 스크립트에 설정한 argumnet 값으로 hwirq 번호를 알아온다.

  • 인터럽트를 사용하는 디바이스는 “interrupts = { bank, irq }” 속성 값을 전달한다.
    • bcm2708(rpi) 및 bcm2709(rpi2)에서는 2 개의 인수를 받는데 처음 것은 bank 이고, 두 번째 것은 각 뱅크에 해당하는 개별 인터럽트 번호(0~31번 )이다.
    • rpi2 예) out_hwirq = 0~85, 96~99, 101, 105

 

  • 코드 라인 6~7에서 DT 스크립트에서 설정한 “interrupts = { bank, irq }” 형태로 인수가 2개가 아닌 경우 -EINVAL 에러를 반환한다.
  • 코드 라인 9~10에서 bank 값이 4 이상인 경우 -EINVAL 에러를 반환한다.
    • rpi: 0~2까지 가능
    • rpi2: 0~3까지 가능
  • 코드 라인 12~13에서 irq 값이 bank당 최대 값 수(32) 이상인 경우 -EINVAL 에러를 반환한다.
    • rpi & rpi2: bank당 0~31까지 가능
  • 코드 라인 15~16에서 0번 bank를 사용하는 irq 값이 bank #0 최대 인터럽트 수 이상인 경우 -EIVAL 에러를 반환한다.
    • rpi & rpi2: bank당 0~20까지 가능
  • 코드 라인 18~19에서 3번 bank의 경우 irq 번호가 0~3, 5 및 9번이 아닌 경우 -EINVAL 에러를 반환한다.
    • 0~3: core timer, 5=mail box #1, 9=pmu
  • 코드 라인 21~22에서 bank 0번의 경우 64번 부터 hw 인터럽트가 시작된다.
  • 코드 라인 23~24에서 bank 1번의 경우 0번 부터 hw 인터럽트가 시작된다.
  • 코드 라인 25~26에서 bank 2번의 경우 32번 부터 hw 인터럽트가 시작된다.
  • 코드 라인 27~28에서 bank 3번의 경우 96번 부터 hw 인터럽트가 시작된다.
  • 코드 라인 41~65에서 Video Core에서 사용하는 인터럽트들(#0 ~ #64) 중 11개가 공유되어 Local ARM에 라우팅(#74~#84) 된다.
    • 예) 7(vc_usb)  -> 74(arm_usb)
  • 코드 라인 67~68에서 에러가 없는 경우 out_type에 IRQ_TYPE_NONE을 대입한후 0을 반환한다.

 


GPIO pinctrl-bcm2835용

rpi 및 rpi2에는 2개의 뱅크에 나뉘어 총 54개의 gpio 라인이 제공되고 이를 28개, 18개, 8개로 나누어 81~83번의 hw 인터럽트를 발생시킨다.

 

bcm2835_gpio_irq_chip

drivers/pinctrl/bcm/pinctrl-bcm2835.c

static struct irq_chip bcm2835_gpio_irq_chip = {
        .name = MODULE_NAME,
        .irq_enable = bcm2835_gpio_irq_enable,
        .irq_disable = bcm2835_gpio_irq_disable,
        .irq_set_type = bcm2835_gpio_irq_set_type,
};

bcm2835의 gpio 인터럽트용 irq_chip 콜백 함수들

 

bcm2835_gpio_irq_enable()

drivers/pinctrl/bcm/pinctrl-bcm2835.c

static void bcm2835_gpio_irq_enable(struct irq_data *data)
{
        struct bcm2835_pinctrl *pc = irq_data_get_irq_chip_data(data);
        unsigned gpio = irqd_to_hwirq(data);
        unsigned offset = GPIO_REG_SHIFT(gpio);
        unsigned bank = GPIO_REG_OFFSET(gpio);
        unsigned long flags;

        spin_lock_irqsave(&pc->irq_lock[bank], flags);
        set_bit(offset, &pc->enabled_irq_map[bank]);
        bcm2835_gpio_irq_config(pc, gpio, true);
        spin_unlock_irqrestore(&pc->irq_lock[bank], flags);
}

gpio irq를 enable 한다. (hwirq=0~53)

  • 코드 라인 3에서 irq_chip->chip_data에 저장해둔 pc(pin control)를 알아온다.
  • 코드 라인 4에서 gpio hwirq 번호를 알아온다.
  • 코드 라인 5~6에서 hwirq에 대한 bank와 offset을 구한다.
    • 예) hwirq=53인 경우 bank=1, offset=21
  • 코드 라인 10에서 hwirq에 해당하는 enable 비트를 설정한다.
  • 코드 라인 11에서 hwirq의 트리거 타입에 따른 gpio 레지스터를 조작하여 enable 설정을 한다.

 

bcm2835_gpio_irq_disable()

drivers/pinctrl/bcm/pinctrl-bcm2835.c

static void bcm2835_gpio_irq_disable(struct irq_data *data)
{
        struct bcm2835_pinctrl *pc = irq_data_get_irq_chip_data(data);
        unsigned gpio = irqd_to_hwirq(data);
        unsigned offset = GPIO_REG_SHIFT(gpio);
        unsigned bank = GPIO_REG_OFFSET(gpio);
        unsigned long flags;

        spin_lock_irqsave(&pc->irq_lock[bank], flags);
        bcm2835_gpio_irq_config(pc, gpio, false);
        /* Clear events that were latched prior to clearing event sources */
        bcm2835_gpio_set_bit(pc, GPEDS0, gpio);
        clear_bit(offset, &pc->enabled_irq_map[bank]);
        spin_unlock_irqrestore(&pc->irq_lock[bank], flags);
}

gpio irq를 disable 한다. (hwirq=0~53)

  • 코드 라인 3에서 irq_chip->chip_data에 저장해둔 pc(pin control)를 알아온다.
  • 코드 라인 4에서 hwirq 번호를 알아온다.
    • rpi2: 81 ~ 83
  • 코드 라인 5~6에서 hwirq에 대한 bank와 offset을 구한다.
    • 예) hwirq=81인 경우 bank=2, offset=17
  • 코드 라인 10에서 hwirq의 트리거 타입에 따른 gpio 레지스터를 조작하여 disable 설정을 한다.
  • 코드 라인 12에서 2 개의 GPEDS(GPIO Event Detect Status) Register 중 요청 hwirq에 해당하는 비트를 1로 기록하여 이벤트 발생 비트를 클리어한다.
  • 코드 라인 13에서 hwirq에 해당하는 enable 비트를 클리어한다.

 

bcm2835_gpio_irq_set_type()

drivers/pinctrl/bcm/pinctrl-bcm2835.c

static int bcm2835_gpio_irq_set_type(struct irq_data *data, unsigned int type)
{
        struct bcm2835_pinctrl *pc = irq_data_get_irq_chip_data(data);
        unsigned gpio = irqd_to_hwirq(data);
        unsigned offset = GPIO_REG_SHIFT(gpio);
        unsigned bank = GPIO_REG_OFFSET(gpio);
        unsigned long flags;
        int ret;

        spin_lock_irqsave(&pc->irq_lock[bank], flags);

        if (test_bit(offset, &pc->enabled_irq_map[bank]))
                ret = __bcm2835_gpio_irq_set_type_enabled(pc, gpio, type);
        else 
                ret = __bcm2835_gpio_irq_set_type_disabled(pc, gpio, type);

        spin_unlock_irqrestore(&pc->irq_lock[bank], flags);

        return ret;
}

gpio irq에 해당하는 트리거 타입을 설정한다. (hwirq=0~53)

  • 타입 설정 전에 요청 irq에 대해 disable 한 후 트리거 타입을 바꾼다. 그런 후 enable 비트가 설정된 경우 해당 irq에 대해 enable 시킨다.

 


bcm2836-gpu용

bcm2836_arm_irqchip_gpu

drivers/irqchip/irq-bcm2836.c

static struct irq_chip bcm2836_arm_irqchip_gpu = {
        .name           = "bcm2836-gpu",
        .irq_mask       = bcm2836_arm_irqchip_mask_gpu_irq,
        .irq_unmask     = bcm2836_arm_irqchip_unmask_gpu_irq,
};

bcm2836의 GPU 인터럽트용 irq_chip 콜백 함수들

 

bcm2836_arm_irqchip_mask_gpu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_mask_gpu_irq(struct irq_data *d)
{
}

1개 gpu 인터럽트의 mask 처리 요청에 대해 아무것도 수행하지 않는다.

 

bcm2836_arm_irqchip_unmask_gpu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_unmask_gpu_irq(struct irq_data *d)
{
}

1개 gpu 인터럽트의 unmask 처리 요청에 대해 아무것도 수행하지 않는다.

 

bcm2836-pmu용

bcm2836_arm_irqchip_pmu

drivers/irqchip/irq-bcm2836.c

static struct irq_chip bcm2836_arm_irqchip_pmu = {
        .name           = "bcm2836-pmu",
        .irq_mask       = bcm2836_arm_irqchip_mask_pmu_irq,
        .irq_unmask     = bcm2836_arm_irqchip_unmask_pmu_irq,
};

bcm2836의 pmu 인터럽트용 irq_chip 콜백 함수들

 

bcm2836_arm_irqchip_mask_pmu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_mask_pmu_irq(struct irq_data *d)
{
        writel(1 << smp_processor_id(), intc.base + LOCAL_PM_ROUTING_CLR);
}

1개 pmu 인터럽트의 mask 처리 요청을 수행한다.

 

bcm2836_arm_irqchip_unmask_pmu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_unmask_pmu_irq(struct irq_data *d)
{
        writel(1 << smp_processor_id(), intc.base + LOCAL_PM_ROUTING_SET);
}

1개 pmu 인터럽트의 unmask 처리 요청을 수행한다.

 

bcm2836-timer용

bcm2836_arm_irqchip_timer

drivers/irqchip/irq-bcm2836.c

static struct irq_chip bcm2836_arm_irqchip_timer = {
        .name           = "bcm2836-timer",
        .irq_mask       = bcm2836_arm_irqchip_mask_timer_irq,
        .irq_unmask     = bcm2836_arm_irqchip_unmask_timer_irq,
};

bcm2836의 각 코어에 구성된 4개 local 타이머용 irq_chip 콜백 함수들

 

bcm2836_arm_irqchip_mask_timer_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_mask_timer_irq(struct irq_data *d)
{
        bcm2836_arm_irqchip_mask_per_cpu_irq(LOCAL_TIMER_INT_CONTROL0,
                                             d->hwirq - LOCAL_IRQ_CNTPSIRQ,
                                             smp_processor_id());
}

4개의 local timer 인터럽트는 cpu별로 동작하는 per-cpu 타입인데 이 중 현재 cpu에 요청한 타이머 인터럽트(irq_data->hwirq(0~3))를 마스크 처리한다.

  • cpu별 LOCAL_TIMER_INT_CONTROL 레지스터
    • 0x4000_0040 (#0번 core) ~ 0x4000_004c(#3번 core)
  • 4 개 timer 인터럽트 중 요청한 인터럽트(0 ~ 3번 중 하나)를 마스크한다.

 

bcm2836_arm_irqchip_mask_per_cpu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_mask_per_cpu_irq(unsigned int reg_offset,
                                                 unsigned int bit,
                                                 int cpu)
{
        void __iomem *reg = intc.base + reg_offset + 4 * cpu;

        writel(readl(reg) & ~BIT(bit), reg);
}

bcm2836 local 인터럽트 컨트롤러에서 per-cpu 인터럽트에 대한 마스크를 수행한다.

  • rpi2:
    • intc.base는 0x4000_0000
    • reg_offset는 0x40

 

bcm2836_arm_irqchip_unmask_timer_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_unmask_timer_irq(struct irq_data *d)
{
        bcm2836_arm_irqchip_unmask_per_cpu_irq(LOCAL_TIMER_INT_CONTROL0,
                                               d->hwirq - LOCAL_IRQ_CNTPSIRQ,
                                               smp_processor_id());
}

4개의 local timer 인터럽트는 cpu별로 동작하는 per-cpu 타입인데 이 중 현재 cpu에 요청한 타이머 인터럽트(irq_data->hwirq(0~3))를 언마스크 처리한다.

  • cpu별 LOCAL_TIMER_INT_CONTROL 레지스터
    • 0x4000_0040 (#0번 core) ~ 0x4000_004c(#3번 core)
  • 4 개 timer 인터럽트 중 요청한 인터럽트(0 ~ 3번 중 하나)를 클리어한다.

 

bcm2836_arm_irqchip_unmask_per_cpu_irq()

drivers/irqchip/irq-bcm2836.c

static void bcm2836_arm_irqchip_unmask_per_cpu_irq(unsigned int reg_offset,
                                                 unsigned int bit,
                                                 int cpu)
{
        void __iomem *reg = intc.base + reg_offset + 4 * cpu;

        writel(readl(reg) | BIT(bit), reg);
}

bcm2836 local 인터럽트 컨트롤러에서 per-cpu 인터럽트에 대한 언마스크를 수행한다.

 


머신 specific 인터럽트 컨트롤러 초기화

디바이스 트리를 사용하지 않는 경우 다음 코드들이 적용된다.

  • 참고로 ARM32 시스템에서 최근 v4.4 이상 커널들과 모든 ARM64 시스템은 모두 디바이스 트리를 사용하는 방식을 사용한다.

 

bcm2708_init_irq() & bcm2709_init_irq()

arch/arm/mach-bcm2708/bcm2708.c

void __init bcm2708_init_irq(void)
{
        armctrl_init(__io_address(ARMCTRL_IC_BASE), 0, 0, 0);
}

인터럽트 컨트롤러의 base 가상 주소를 인수로 인터럽트 컨트롤러를 초기화한다.

 

arch/arm/mach-bcm2709/bcm2709.c

void __init bcm2709_init_irq(void)
{
        armctrl_init(__io_address(ARMCTRL_IC_BASE), 0, 0, 0);
}

인터럽트 컨트롤러의 base 가상 주소를 인수로 인터럽트 컨트롤러를 초기화한다.

 

rpi vs rpi2 ARM 인터럽트 컨트롤러 주소

BCM2708을 사용하는 rpi와 BCM2709를 사용하는 rpi2는 내부에서 사용하는 디바이스들의 구성은 거의 유사하나 peripheral base 물리 주소가 다르고 이에 따른 가상 주소도 다르다.

  • peripheral base 물리 주소 -> 가상 주소
    • rpi: 0x2000_0000 -> 0xf200_0000
    • rpi2: 0x3f00_0000 -> 0xf300_0000

 

rpi 물리 주소

#define BCM2708_PERI_BASE        0x20000000
#define ARM_BASE                 (BCM2708_PERI_BASE + 0xB000)    /* BCM2708 ARM control block */
#define ARMCTRL_IC_BASE          (ARM_BASE + 0x200)           /* ARM interrupt controller */
  • rpi의 인터럽트 컨트롤러 물리 주소 = 0x2000_b200
  • rpi의 인터럽트 컨트롤러 가상 주소 = 0xf200_b200

 

rpi2 물리 주소

#define BCM2708_PERI_BASE        0x3F000000
#define ARM_BASE                 (BCM2708_PERI_BASE + 0xB000)    /* BCM2708 ARM control block */
#define ARMCTRL_IC_BASE          (ARM_BASE + 0x200)           /* ARM interrupt controller */
  • rpi2의 인터럽트 컨트롤러 물리 주소 = 0x3f00_b200
  • rpi2의 인터럽트 컨트롤러 가상 주소 = 0xf300_b200

 

armctrl_init() – bcm2709 용

arch/arm/mach-bcm2709/armctrl.c

/**
 * armctrl_init - initialise a vectored interrupt controller
 * @base: iomem base address
 * @irq_start: starting interrupt number, must be muliple of 32
 * @armctrl_sources: bitmask of interrupt sources to allow
 * @resume_sources: bitmask of interrupt sources to allow for resume
 */
int __init armctrl_init(void __iomem * base, unsigned int irq_start,
                        u32 armctrl_sources, u32 resume_sources)
{
        unsigned int irq;

        for (irq = 0; irq < BCM2708_ALLOC_IRQS; irq++) {
                unsigned int data = irq;
                if (irq >= INTERRUPT_JPEG && irq <= INTERRUPT_ARASANSDIO)
                        data = remap_irqs[irq - INTERRUPT_JPEG];
                if (irq >= IRQ_ARM_LOCAL_CNTPSIRQ && irq <= IRQ_ARM_LOCAL_TIMER) {
                        irq_set_percpu_devid(irq);
                        irq_set_chip_and_handler(irq, &armctrl_chip, handle_percpu_devid_irq);
                        set_irq_flags(irq, IRQF_VALID | IRQF_NOAUTOEN);
                } else {
                        irq_set_chip_and_handler(irq, &armctrl_chip, handle_level_irq);
                        set_irq_flags(irq, IRQF_VALID | IRQF_PROBE | IRQF_DISABLED);
                }
                irq_set_chip_data(irq, (void *)data);
        }

        armctrl_pm_register(base, irq_start, resume_sources);
        init_FIQ(FIQ_START);
        armctrl_dt_init();
        return 0;
}

bcm2709용 인터럽트 컨트롤러를 초기화하고 각 인터럽트에 대한 핸들러들을 준비한다.

  • 코드 라인 6에서 인터럽트 컨트롤러에 할당된 irq 수 만큼 루프를 돈다.
    • rpi: SPARE_ALLOC_IRQS=394
    • rpi2: SPARE_ALLOC_IRQS=480
  • 코드 라인 8~9에서 GPU용 리눅스 irq를 hwirq로 변환한다.
    • irq 번호가 INTERRUPT_JPEG(74) ~ INTERRUPT_ARASANSDIO(84) 번까지의 irq는 GPU(VC)가 ARM용으로 routing 한 리눅스 irq 번호이므로 기존 GPU(VC)용 hwirq 번호를 알아낸 후 data에 대입한다.
    • GPU(VC) hardware irq 중 11개의 irq는 ARM에서 같이 공유하여 사용하기 위해 ARM pending 레지스터로 라우팅(변환)하여 사용한다.
    • remap_irqs[]
      • INTERRUPT_VC_JPEG(7)
      • INTERRUPT_VC_USB(9)
      • (..생략..)
      • INTERRUPT_VC_ARASANSDIO(62) 까지 총 11개가 등록되어 있다.
  • 코드 라인 10~13에서 irq 번호가 IRQ_ARM_LOCAL_CNTPSIRQ(96) ~ IRQ_ARM_LOCAL_TIMER(107)번 사이의 local 인터럽트인 경우 core별로 전용 사용되므로 per-cpu 디바이스를 취급하는 핸들러를 설정한다.
  • 코드 라인 14~17에서 그 외의 irq 번호인 경우 일반 핸들러를 설정한다.
  • 코드 라인 18에서 hwirq 번호로 chip_data를 구성한다.
  • 코드 라인 21에서 PM(Power Management)를 위해 armctrl_info 구조체에 인터럽트 정보를 대입해둔다.
  • 코드 라인 22에서 FIQ 인터럽트 핸들러를 준비한다.
  • 코드 라인 23에서 Device Tree 스크립트를 통해 해당 인터럽트 컨트롤러 정보를 읽어 irq_domain을 구성한다.
  • 코드 라인 24에서 성공 값 0을 반환한다.

 

다음 그림은 armctrl_init() 함수의 처리 과정을 보여준다.

 

 


Machine용 IRQ Handler (Kernel v4.0 for rpi2)

 

arch_irq_handler_default 매크로(for RPI2)

arch/arm/mach-bcm2709/include/mach/entry-macro.S

/*
 * Interrupt handling.  Preserves r7, r8, r9
 */
        .macro  arch_irq_handler_default
1:	get_irqnr_and_base r0, r2, r6, lr
	.endm

 

IPI(Inter Process Interrupt) 처리

arch/arm/mach-bcm2709/include/mach/entry-macro.S

        .macro  get_irqnr_and_base, irqnr, irqstat, base, tmp

        /* get core number */
        mrc     p15, 0, \base, c0, c0, 5
        ubfx    \base, \base, #0, #2

        /* get core's local interrupt controller */
        ldr     \irqstat, = __io_address(ARM_LOCAL_IRQ_PENDING0)        @ local interrupt source
        add     \irqstat, \irqstat, \base, lsl #2
        ldr     \tmp, [\irqstat]
#ifdef CONFIG_SMP
        /* test for mailbox0 (IPI) interrupt */
        tst     \tmp, #0x10
        beq     1030f

        /* get core's mailbox interrupt control */
        ldr     \irqstat, = __io_address(ARM_LOCAL_MAILBOX0_CLR0)       @ mbox_clr
        add     \irqstat, \irqstat, \base, lsl #4
        ldr     \tmp, [\irqstat]
        clz     \tmp, \tmp
        rsb     \irqnr, \tmp, #31
        mov     \tmp, #1
        lsl     \tmp, \irqnr
        str     \tmp, [\irqstat]  @ clear interrupt source
        dsb
        mov     r1, sp
        adr     lr, BSYM(1b)
        b       do_IPI
#endif

 

pending 레지스터를 읽어 IPI 처리 요청이 있는 경우 mailbox0를 읽어 인터럽트 소스를 알아와서 모든 설정된 IPI 처리를 한다. 처리 순서는 높은 IPI 번호부터 처리한다. (irqnr=r0, irqstat=r2, base=r6, tmp=lr)

  • 코드 라인 4~10에서 cpu 번호를 구한 후 그 cpu에 해당하는 pending 레지스터 값을 읽어와서 \tmp에 대입한다.
    • 예) cpu #3에 대한 레지스터 가상 주소: ARM_LOCAL_IRQ_PENDING0(0xf400_0060) + cpu(3) * 4 = 0xf400_006c
  • 코드 라인 13~14에서 IPI(mailbox0) 인터럽트 요청이 아닌 경우 1030 레이블로 이동한다.
    • pending 레지스터의 0x10 비트: mailbox0
  • 코드 라인 17~19에서 cpu에 해당하는 mailbox0 값을 읽어와서 \tmp에 대입한다.
  • 코드 라인 20~21에서 가장 좌측에 설정된 인터럽트 비트를 \irqnr에 대입한다. (IPI는 역순으로 우선 처리된다.)
  • 코드 라인 22~24에서 클리어하고자 하는 IPI 인터럽트 비트만 설정하여 읽어왔던 mailbox0에 저장한다.
    • clz: 좌측부터 0으로 시작하는 비트 수
    • mailbox는 str 명령을 통하여 값이 저장되는 것이 아니라 값에 해당하는 인터럽트 소스들을 클리어 한다.
  • 코드 라인 26~28에서 두 번째 인수 r1에 스택에 있는 pt_regs를 대입하고, 복귀 주소로 arch_irq_handler_default 매크로 안에 있는 get_irqnr_and_base 호출 위치를 설정한 다음 IPI 처리를 위해 do_IPI() 함수를 호출한다.
    • IPI 처리가 다 완료될 때 까지 루프를 돈다.
    • 참고: do_IPI() | 문c

 

 

 

ARM(GPU) 및 ARM 인터럽트 처리
1030:
        /* check gpu interrupt */
        tst     \tmp, #0x100
        beq     1040f

        ldr     \base, =IO_ADDRESS(ARMCTRL_IC_BASE)
        /* get masked status */
        ldr     \irqstat, [\base, #(ARM_IRQ_PEND0 - ARMCTRL_IC_BASE)]
        mov     \irqnr, #(ARM_IRQ0_BASE + 31)
        and     \tmp, \irqstat, #0x300           @ save bits 8 and 9
        /* clear bits 8 and 9, and test */
        bics    \irqstat, \irqstat, #0x300
        bne     1010f

        tst     \tmp, #0x100
        ldrne   \irqstat, [\base, #(ARM_IRQ_PEND1 - ARMCTRL_IC_BASE)]
        movne   \irqnr, #(ARM_IRQ1_BASE + 31)
        @ Mask out the interrupts also present in PEND0 - see SW-5809
        bicne   \irqstat, #((1<<7) | (1<<9) | (1<<10))
        bicne   \irqstat, #((1<<18) | (1<<19))
        bne     1010f

        tst     \tmp, #0x200
        ldrne   \irqstat, [\base, #(ARM_IRQ_PEND2 - ARMCTRL_IC_BASE)]
        movne   \irqnr, #(ARM_IRQ2_BASE + 31)
        @ Mask out the interrupts also present in PEND0 - see SW-5809
        bicne   \irqstat, #((1<<21) | (1<<22) | (1<<23) | (1<<24) | (1<<25))
        bicne   \irqstat, #((1<<30))
        beq     1020f
1010:
        @ For non-zero x, LSB(x) = 31 - CLZ(x^(x-1))
        sub     \tmp, \irqstat, #1
        eor     \irqstat, \irqstat, \tmp
        clz     \tmp, \irqstat
        sub     \irqnr, \tmp
        b       1050f

 

pending #0 레지스터를 읽어 ARM(GPU) #1, ARM(GPU)#2, ARM #0 인터럽트 순서대로 발생한 인터럽트 소스의 ISR을 수행한다.

  • 예) ARM(GPU) #1에 배치된 arm dma #0 인터럽트가 요청된 경우
    • pending 0 레지스터 값=0x100, pending 1 레지스터 값=0x1_0000 (bit16), 최종 인터럽트 번호 \irqnr=16
  • 예) ARM(GPU) #2에 배치한 arm gpio 인터럽트가 요청된 경우
    • pending 0 레지스터 값=0x200, pending 2 레지스터 값=0x10_0000 (bit20), 최종 인터럽트 번호 \irqnr=52
  • 예) ARM #0에 배치한 arm uart 인터럽트가 요청된 경우
    • pending 0 레지스터 값=0x8_0000 (bit19), 최종 인터럽트 번호 \irqnr=83

 

  • 코드 라인 3~4에서 ARM(GPU) 인터럽트 처리 요청이 없으면 1040 레이블로 이동한다.
  • 코드 라인 6~8에서 pending #0 레지스터 값을 읽어 /irqstat에 저장한다.
    • ARM 인터럽트 컨트롤러 가상 주소를 구해 \base에 저장한다.
      • =IO_ADDRESS(0x3f00_0000(기본 주소) + 0xb000(peri offset) + 0x200(인터럽트 컨트롤러 offset) = 0xf300_b200
  • 코드 라인 9에서 ARM #0이 담당하는 IRQ 끝 번호 값(95)을 \irqnr에 대입한다.
  • 코드 라인 10~13에서 pending #0 레지스터에서 읽었던 /irqstat에서 ARM #1(GPU #1) 또는 ARM #2(GPU #2)에 해당하는 비트를 클리어한다. 클리어 전에도 설정된 적 없으면 ARM #0 인터럽트를 처리하기 위해 1010 레이블로 이동한다.
  • 코드 라인 15~21에서 ARM #1(GPU #1)에 해당하는 경우 pending #1 레지스터 값을 읽어 /irqstat에 저장하되 VideoCore에서 사용하는 인터럽트에 해당하는 7, 9, 10, 18, 19번 비트들을 제거한다. IRQ #1(GPU #1)의 인터럽트 끝 번호(31)를 \irqnr에 대입한 후 해당 인터럽트 수행을 위해 1010 레이블로 이동한다.
  • 코드 라인 23~29에서 ARM #2(GPU #2)에 해당하는 경우 pending #2 레지스터 값을 읽어 /irqstat에 저장하되 VideoCore에서 사용하는 인터럽트에 해당하는21~25, 30번 비트들을 제거한다. IRQ #2(GPU #2)의 인터럽트 끝 번호(63)를 \irqnr에 대입하고 해당 인터럽트 수행을 위해 계속 아래 1010 레이블로 진행한다.
  • 코드 라인 32~36에서 처리할 인터럽트 끝 번호(irq0=95, irq1=31, ir12=63, local irq=127)에서 \irqstat의 첫 인터럽트 비트에 해당하는 번호를 뺀 후 해당 인터럽트 수행을 위해 1050 레이블로 이동한다.

 

Local Interrupt 처리
1040:
        cmp     \tmp, #0
        beq     1020f

        /* handle local (e.g. timer) interrupts */
        @ For non-zero x, LSB(x) = 31 - CLZ(x^(x-1))
        mov     \irqnr, #(ARM_IRQ_LOCAL_BASE + 31)
        sub     \irqstat, \tmp, #1
        eor     \irqstat, \irqstat, \tmp
        clz     \tmp, \irqstat
        sub     \irqnr, \tmp
1050:
        mov     r1, sp
        @
        @ routine called with r0 = irq number, r1 = struct pt_regs *
        @
        adr     lr, BSYM(1b)
        b       asm_do_IRQ

1020:   @ EQ will be set if no irqs pending
        .endm

local ARM 인터럽트 처리요청이 있는 경우 발생한 인터럽트 소스의 ISR을 수행한다.

  • 예) Local ARM 인터럽트에 배치한 arm timer 인터럽트가 요청된 경우
    • local interrupt controller 레지스터 값=0x8, 최종 인터럽트 번호 \irqnr=99

 

  • 코드 라인 2~3에서 pending 레지스터 값이 0인 경우 더이상 처리할 인터럽트가 없으므로 종료한다.
  • 코드 라인 7~11에서 Local ARM이 처리하는 끝 번호(127) 값을 \irqnr에 대입한다.  끝 번호(127)에서 /tmp 인터럽트 비트들 중 가장 마지막 비트에 해당하는 번호를  뺀다
  • 코드 라인 13~18에서 r1 레지스터에 pt_regs 주소를 대입하고, lr에 이 매크로(get_irqnr_and_base)를 호출하는 곳을 담고 ISR을 호출한다.

 


FIQ 초기화

init_FIQ()

arch/arm/kernel/fiq.c

void __init init_FIQ(int start)
{
        unsigned offset = FIQ_OFFSET;
        dfl_fiq_insn = *(unsigned long *)(0xffff0000 + offset);
        get_fiq_regs(&dfl_fiq_regs);
        fiq_start = start;
}

default FIQ 인터럽트 처리기를 백업하기 위해 fiq 벡터 값과 fiq 모드에서 사용하는 레지스터들 일부(r8 ~ r12, sp, lr)를 백업해둔다.

  • fiq가 필요한 owner task에서 핸들러를 준비하여 요청/해제등을 수행 시 파괴된 레지스터와 fiq 벡터들을 복원시 사용하기 위해 백업해둔다.
    • STM 및 LDM 수행은 보통 atomic 하게 수행되지만 cpu가 low interrupt latency 모드를 사용하는 경우 STM과 LDM 명령이 atomic하게 이루어지지 않아 STM 도중에 fiq exception 되는 경우 완전하게 스택에 기록하지 않는 문제가 발생하고 fiq 인터럽트 서비스 루틴 수행 후 다시 원래 모드 루틴으로 복귀할 때 LDM으로 읽은 값(최종 값이 보통 pc)에 가베지가 로드되어 문제가 발생할 수 있다. 이런 경우를 위해 fiq 모드에서 사용하였던 default 인터럽트 핸들러용 레지스터들을 백업해두고 이를 복원하는 용도로 사용한다.
    • fiq는 irq에 비해 약 10 ~ 20여배 latency가 작다. (10 ~ 20 ns vs 수 백 ns)
    • 참고: [ARM] 3256/1: Make the function-returning ldm’s use sp as the base register
  • rpi2 예)
    • “usb_fiq” 라는 이름의 usb 드라이버에서 fiq를 사용한다.
    • hcd_init_fiq() – drivers/usb/host/dwc_otg/dwc_otg_hcd_linux.c

 

  • 코드 라인 3~4에서 fiq에 해당하는 exception 벡터에 기록된 명령을 읽어와서 전역 변수 dfl_fiq_insn에 저장한다.
  • 코드 라인 5에서 fiq 모드로 진입한 후 레지스터 백업 장소 dfl_fiq_regs->r8 위치에 레지스터 r8 ~ r12, sp, lr을 저장하고 다시 svc 모드로 돌아온다.
  • 코드 라인 6에서 fiq_start에 hw irq 시작 번호를 대입한다.
    • rpi: 85
    • rpi2: 128

 

ENTRY(__get_fiq_regs)
        mov     r2, #PSR_I_BIT | PSR_F_BIT | FIQ_MODE
        mrs     r1, cpsr
        msr     cpsr_c, r2      @ select FIQ mode
        mov     r0, r0          @ avoid hazard prior to ARMv4
        stmia   r0!, {r8 - r12}
        str     sp, [r0], #4
        str     lr, [r0]
        msr     cpsr_c, r1      @ return to SVC mode 
        mov     r0, r0          @ avoid hazard prior to ARMv4
        ret     lr
ENDPROC(__get_fiq_regs)

FIQ 모드로 진입한 후 인수 r0 레지스터가 가리키는 주소에 현재 레지스터 r8 ~ r12, sp, lr을 저장하고 다시 SVC 모드로 돌아온다.

 

Device Tree에서 인터럽트 컨트롤러 노드 정보를 읽어 irq domain 생성

armctrl_dt_init()

arch/arm/mach-bcm2709/armctrl.c

void __init armctrl_dt_init(void)
{
        struct device_node *np;
        struct irq_domain *domain;

        np = of_find_compatible_node(NULL, NULL, "brcm,bcm2708-armctrl-ic");
        if (!np)
                return;

        domain = irq_domain_add_legacy(np, BCM2708_ALLOC_IRQS,
                                        IRQ_ARMCTRL_START, 0,
                                        &armctrl_ops, NULL);
        WARN_ON(!domain);
}

디바이스 트리 스크립트에 설정된 인터럽트 컨트롤러 명이 “brcm,bcm2708-armctrl-ic”인 경우 irq_domain을 추가하고 legacy 방법으로 domain을 최대 할당 인터럽트 수 만큼 지정한다.

  • 최대 할당 인터럽트 수
    • rpi: 394
    • rpi2: 480

 

참고