Exception -3- (ARM32 Handler 2)

<kernel v5.4>

Data Abort 핸들러

__dabt_usr

arch/arm/kernel/entry-armv.S

        .align  5
__dabt_usr:
        usr_entry uaccess=0
        kuser_cmpxchg_check
        mov     r2, sp
        dabt_helper
        b       ret_from_exception
 UNWIND(.fnend          )
ENDPROC(__dabt_usr)

user 모드에서 data abort exception을 만나 진입하게 되면 data abort 핸들러를 수행한 후 다시 user 모드로 복귀한다.

  • 코드 라인 3에서 전체 레지스터를 스택에 pt_regs 구조체 순서로 백업한다.
  • 코드 라인 4에서 atomic 연산을 지원하지 못하는 아키텍처에서 atomic 하게 처리해야 하는 구간에서 인터럽트를 맞이하고 복귀할 때 그 atomic operation 구간의 시작부분으로 다시 돌아가도록 pt_regs의 pc를 조작한다.
  • 코드 라인 5~6에서 r2 레지스터에 스택 위치를 담고 data abort 핸들러를 호출한다.
  • 코드 라인 7에서 스택에 백업해둔 레지스터들을 다시 불러 읽은 후 user 모드로 복귀한다.

 

dabt_helper 매크로

arch/arm/kernel/entry-armv.S

        .macro  dabt_helper

        @
        @ Call the processor-specific abort handler:
        @
        @  r2 - pt_regs
        @  r4 - aborted context pc
        @  r5 - aborted context psr
        @
        @ The abort handler must return the aborted address in r0, and
        @ the fault status register in r1.  r9 must be preserved.
        @
#ifdef MULTI_DABORT
        ldr     ip, .LCprocfns
        mov     lr, pc
        ldr     pc, [ip, #PROCESSOR_DABT_FUNC]
#else
        bl      CPU_DABORT_HANDLER
#endif
        .endm

data abort 핸들러 함수를 호출한다.

  • 코드 라인 13~16에서 data abort 핸들러가 2 개 이상 있어야 하는 경우 MULTI_DABORT가 설정된다.
  • 코드 라인 17~19에서 빌드 타임에 정해진 아키텍처의 data abort 핸들러를 호출한다.

 

.LCprocfns

arch/arm/kernel/entry-armv.S

#ifdef MULTI_DABORT
.LCprocfns:
        .word   processor
#endif

 

CPU_DABORT_HANDLER

arch/arm/include/asm/glue-df.h

#ifdef CONFIG_CPU_ABRT_EV7
# ifdef CPU_DABORT_HANDLER
#  define MULTI_DABORT 1
# else
#  define CPU_DABORT_HANDLER v7_early_abort
# endif
#endif

빌드타임에 아키텍처가 ARMv7으로 정해진 경우 data abort 핸들러 함수는 v7_early_abort() 이다.

 

v7_early_abort()

arch/arm/mm/abort-ev7.S

/*
 * Function: v7_early_abort
 *
 * Params  : r2 = pt_regs
 *         : r4 = aborted context pc
 *         : r5 = aborted context psr
 *
 * Returns : r4 - r11, r13 preserved
 *
 * Purpose : obtain information about current aborted instruction.
 */
        .align  5
ENTRY(v7_early_abort)
        mrc     p15, 0, r1, c5, c0, 0           @ get FSR
        mrc     p15, 0, r0, c6, c0, 0           @ get FAR
        uaccess_disable ip                      @ disable userspace access

        /*
         * V6 code adjusts the returned DFSR.
         * New designs should not need to patch up faults.
         */

#if defined(CONFIG_VERIFY_PERMISSION_FAULT)
        /*
         * Detect erroneous permission failures and fix
         */
        ldr     r3, =0x40d                      @ On permission fault
        and     r3, r1, r3
        cmp     r3, #0x0d
        bne     do_DataAbort

        mcr     p15, 0, r0, c7, c8, 0           @ Retranslate FAR
        isb
        mrc     p15, 0, ip, c7, c4, 0           @ Read the PAR
        and     r3, ip, #0x7b                   @ On translation fault
        cmp     r3, #0x0b
        bne     do_DataAbort
        bic     r1, r1, #0xf                    @ Fix up FSR FS[5:0]
        and     ip, ip, #0x7e
        orr     r1, r1, ip, LSR #1
#endif

        b       do_DataAbort
ENDPROC(v7_early_abort)

data abort exception 상황이 발생 시 DFSR(Data Fault Status Register)에서 읽은 상태 코드를 갖고 doDataAbort() 함수를 호출하여 관련 fault 처리기를 호출한다.

  • 코드 라인 3에서 DFSR(Data Fault Status Register)를 통해 data abort exception에 대한 상태 코드를 읽어 r1 레지스터에 대입한다.
  • 코드 라인 4에서 DFAR(Data Fault Address Register)를 통해 data abort exception 당시의 물리 주소를 읽어 r0 레지스터에 대입한다.
  • 코드 라인 12~30에서 CONFIG_VERIFY_PERMISSION_FAULT 커널 옵션은 스냅드래곤의 qsd8x60 SoC의 오류를 교정하기 위해 사용된다.
  • 코드 라인 32에서 data fault 상태 코드에 따른 fault 처리기를 호출한다.

 

do_DataAbort()

arch/arm/mm/fault.c

/*
 * Dispatch a data abort to the relevant handler.
 */
asmlinkage void 
do_DataAbort(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
        const struct fsr_info *inf = fsr_info + fsr_fs(fsr);
        
        if (!inf->fn(addr, fsr & ~FSR_LNX_PF, regs))
                return; 

        pr_alert("8<--- cut here ---\n");
        pr_alert("Unhandled fault: %s (0x%03x) at 0x%08lx\n",
                inf->name, fsr, addr);
        show_pte(KERN_ALERT, current->mm, addr);

        arm_notify_die("", regs, inf->sig, inf->code, (void __user *)addr,
                       fsr, 0);
}

DFSR(Data Fault Status Register)로 부터 읽은 5비트로 구성된 fault 상태 코드에 따른 fault 핸들러 함수를 호출한다.

  • 코드 라인 4~7에서 fsr을 인덱스로 fsr_info[] 배열에서 fsr_info 구조체에 설정된 fault 핸들러 함수를 실행한다. 만일 결과가 성공(0)인 경우 함수를 빠져나간다.
  • 코드 라인 9~12에서 “Unhandled fault: ……” 크리티컬 메시지 와 pte 정보를 출력한다.
  • 코드 라인 14~15에서 관련 태스크의 종료 처리를 요청한다.
    • 유저 태스크인 경우 시그널을 보내 종료 처리를 하고, 커널 태스크인경우 시스템의 die() 처리를 진행한다.
      • 유저 태스크에 보낼 시그널은 fsr_info 구조체에 설정된 sig 값을 가져온다. (SIGSEGV, SIGBUS, SIGKILL 중 하나)

 

fsr_info[]

arch/arm/mm/fsr-2level.c

static struct fsr_info fsr_info[] = {
.       /*
         * The following are the standard ARMv3 and ARMv4 aborts.  ARMv5
         * defines these to be "precise" aborts.
         */
        { do_bad,               SIGSEGV, 0,             "vector exception"                 },
        { do_bad,               SIGBUS,  BUS_ADRALN,    "alignment exception"              },
        { do_bad,               SIGKILL, 0,             "terminal exception"               },
        { do_bad,               SIGBUS,  BUS_ADRALN,    "alignment exception"              },
        { do_bad,               SIGBUS,  0,             "external abort on linefetch"      },
        { do_translation_fault, SIGSEGV, SEGV_MAPERR,   "section translation fault"        },
        { do_bad,               SIGBUS,  0,             "external abort on linefetch"      },
        { do_page_fault,        SIGSEGV, SEGV_MAPERR,   "page translation fault"           },
        { do_bad,               SIGBUS,  0,             "external abort on non-linefetch"  },
        { do_bad,               SIGSEGV, SEGV_ACCERR,   "section domain fault"             },
        { do_bad,               SIGBUS,  0,             "external abort on non-linefetch"  },
        { do_bad,               SIGSEGV, SEGV_ACCERR,   "page domain fault"                },
        { do_bad,               SIGBUS,  0,             "external abort on translation"    },
        { do_sect_fault,        SIGSEGV, SEGV_ACCERR,   "section permission fault"         },
        { do_bad,               SIGBUS,  0,             "external abort on translation"    },
        { do_page_fault,        SIGSEGV, SEGV_ACCERR,   "page permission fault"            },
        /*
         * The following are "imprecise" aborts, which are signalled by bit
         * 10 of the FSR, and may not be recoverable.  These are only
         * supported if the CPU abort handler supports bit 10.
         */
        { do_bad,               SIGBUS,  0,             "unknown 16"                       },
        { do_bad,               SIGBUS,  0,             "unknown 17"                       },
        { do_bad,               SIGBUS,  0,             "unknown 18"                       },
        { do_bad,               SIGBUS,  0,             "unknown 19"                       },
        { do_bad,               SIGBUS,  0,             "lock abort"                       }, /* xscale */
        { do_bad,               SIGBUS,  0,             "unknown 21"                       },
        { do_bad,               SIGBUS,  BUS_OBJERR,    "imprecise external abort"         }, /* xscale */
        { do_bad,               SIGBUS,  0,             "unknown 23"                       },
        { do_bad,               SIGBUS,  0,             "dcache parity error"              }, /* xscale */
        { do_bad,               SIGBUS,  0,             "unknown 25"                       },
        { do_bad,               SIGBUS,  0,             "unknown 26"                       },
        { do_bad,               SIGBUS,  0,             "unknown 27"                       },
        { do_bad,               SIGBUS,  0,             "unknown 28"                       },
        { do_bad,               SIGBUS,  0,             "unknown 29"                       },
        { do_bad,               SIGBUS,  0,             "unknown 30"                       },
        { do_bad,               SIGBUS,  0,             "unknown 31"                       },
};

DFSR(Data Fault Status Register)로 부터 읽은 5비트로 구성된 fault 상태 코드에 따라 설정된 fault 핸들러 함수를 dispatch하기 위해 사용된다.

  • do_bad()
    • 기타 exception으로 아무것도 수행하지 않고 1을 반환한다.
  • do_translation_fault()
    • 섹션 변환 실패 시 호출한다.
  • do_page_fault()
    • 2 가지의 경우로 페이지 변환 실패 또는 페이지 권한 위반 시 호출한다.
  • do_sect_fault()
    • 섹션 페이지의 권한 위반 시 호출한다.

 

fsr_fs()

arch/arm/mm/fault.h

static inline int fsr_fs(unsigned int fsr) 
{
        return (fsr & FSR_FS3_0) | (fsr & FSR_FS4) >> 6;
}

DFSR(Data Fault Status Register)로부터 읽어온 fsr 값에서 bit[0~3]과 bit[10]으로 5개의 bit를 연달아 구성해서 반환한다.

  • 예) fsr=0x0000_040a -> 0x1a

 

__dabt_invalid

arch/arm/kernel/entry-armv.S

__dabt_invalid:
        inv_entry BAD_DATA
        b       common_invalid
ENDPROC(__dabt_invalid)

허용하지 않은 모드에서 data abort exception 핸들러에 진입하여 실패 처리를 위한 루틴이다

  • 코드 라인 2에서 레지스터를 스택에 백업한다.
    • 스택에 레지스터들을 백업하기 위한 공간(struct pt_regs)을 확보하고  r1~r14(lr)까지 백업해둔다. r1 레지스터에는 reason 값을 대입한다.
  • 코드 라인 3에서 common_invalid 레이블로 이동하야 스택의 pt_regs 위치에 레지스터들을 백업하고 “Oops” 출력 및 panic() 처리한다.

 

__dabt_svc

arch/arm/kernel/entry-armv.S

        .align  5
__dabt_svc:
        svc_entry uaccess=0
        mov     r2, sp
        dabt_helper
 THUMB( ldr     r5, [sp, #S_PSR]        )       @ potentially updated CPSR
        svc_exit r5                             @ return from exception
 UNWIND(.fnend          )
ENDPROC(__dabt_svc)

svc 모드에서 data abort exception을 만나 진입하게 되면 data abort 핸들러를 수행한 후 다시 svc 모드로 복귀한다.

  • 코드 라인 3에서 전체 레지스터를 스택에 pt_regs(svc_pt_regs) 구조체 순서로 백업한다.
  • 코드 라인 4~5에서 r2 레지스터에 스택 위치를 담고 data abort 핸들러를 호출한다.
  • 코드 라인 7에서 스택에 백업해둔 레지스터들을 다시 불러 읽은 후 svc 모드로 복귀한다.

 


Prefetch Abort 핸들러

__pabt_usr

arch/arm/kernel/entry-armv.S

        .align  5
__pabt_usr:
        usr_entry
        mov     r2, sp                          @ regs
        pabt_helper
 UNWIND(.fnend          )
        /* fall through */
/*
 * This is the return code to user mode for abort handlers
 */
ENTRY(ret_from_exception)
 UNWIND(.fnstart        )
 UNWIND(.cantunwind     )
        get_thread_info tsk
        mov     why, #0
        b       ret_to_user
 UNWIND(.fnend          )
ENDPROC(__pabt_usr)
ENDPROC(ret_from_exception)

user 모드에서 pre-fetch abort exception을 만나 진입하게 되면 pre-fetch abort 핸들러를 수행한 후 다시 user 모드로 복귀한다.

  • 처리가 유사한 __dabt_usr 소스 설명 참고

 

pabt_helper 매크로

arch/arm/kernel/entry-armv.S

        .macro  pabt_helper
        @ PABORT handler takes pt_regs in r2, fault address in r4 and psr in r5
#ifdef MULTI_PABORT
        ldr     ip, .LCprocfns
        mov     lr, pc
        ldr     pc, [ip, #PROCESSOR_PABT_FUNC]
#else
        bl      CPU_PABORT_HANDLER
#endif
        .endm

pre-fetch abort 핸들러 함수를 호출한다.

  • 처리가 유사한 dabt_helper 소스 설명 참고

 

CPU_PABORT_HANDLER

arch/arm/include/asm/glue-pf.h

#ifdef CONFIG_CPU_PABRT_V7
# ifdef CPU_PABORT_HANDLER
#  define MULTI_PABORT 1
# else 
#  define CPU_PABORT_HANDLER v7_pabort
# endif
#endif

빌드타임에 아키텍처가 ARMv7으로 정해진 경우 pre-fetch abort 핸들러 함수는 v7_pabort() 이다.

 

v7_pabort

arch/arm/mm/pabort-v7.S

/*
 * Function: v7_pabort
 *
 * Params  : r2 = pt_regs
 *         : r4 = address of aborted instruction
 *         : r5 = psr for parent context
 *
 * Returns : r4 - r11, r13 preserved
 *
 * Purpose : obtain information about current prefetch abort.
 */
        .align  5
ENTRY(v7_pabort)
        mrc     p15, 0, r0, c6, c0, 2           @ get IFAR
        mrc     p15, 0, r1, c5, c0, 1           @ get IFSR
        b       do_PrefetchAbort
ENDPROC(v7_pabort)

pre-fetch abort exception 상황이 발생 시 IFSR에서 읽은 상태 코드를 갖고 do_PrefetchAbort() 함수를 호출하여 관련 fault 처리기를 호출한다.

 

do_PrefetchAbort()

arch/arm/mm/fault.c

asmlinkage void
do_PrefetchAbort(unsigned long addr, unsigned int ifsr, struct pt_regs *regs)
{
        const struct fsr_info *inf = ifsr_info + fsr_fs(ifsr);

        if (!inf->fn(addr, ifsr | FSR_LNX_PF, regs))
                return;

        pr_alert("Unhandled prefetch abort: %s (0x%03x) at 0x%08lx\n",
                inf->name, ifsr, addr);

        arm_notify_die("", regs, inf->sig, inf->code, (void __user *)addr,
                       ifsr, 0);
}

pre-fetch abort exception 상황이 발생 시 PFSR(Pre-fetch Fault Status Register)에 기록된 5비트의 상태 코드를 인덱스로 ifsr_info[] 배열에 설정된 fault 처리기를 호출한다.

  • 처리가 유사한 do_DataAbort() 소스 설명 참고

 

ifsr_info[]

arch/arm/mm/fsr-2level.c

static struct fsr_info ifsr_info[] = {
        { do_bad,               SIGBUS,  0,             "unknown 0"                        },
        { do_bad,               SIGBUS,  0,             "unknown 1"                        },
        { do_bad,               SIGBUS,  0,             "debug event"                      },
        { do_bad,               SIGSEGV, SEGV_ACCERR,   "section access flag fault"        },
        { do_bad,               SIGBUS,  0,             "unknown 4"                        },
        { do_translation_fault, SIGSEGV, SEGV_MAPERR,   "section translation fault"        },
        { do_bad,               SIGSEGV, SEGV_ACCERR,   "page access flag fault"           },
        { do_page_fault,        SIGSEGV, SEGV_MAPERR,   "page translation fault"           },
        { do_bad,               SIGBUS,  0,             "external abort on non-linefetch"  },
        { do_bad,               SIGSEGV, SEGV_ACCERR,   "section domain fault"             },
        { do_bad,               SIGBUS,  0,             "unknown 10"                       },
        { do_bad,               SIGSEGV, SEGV_ACCERR,   "page domain fault"                },
        { do_bad,               SIGBUS,  0,             "external abort on translation"    },
        { do_sect_fault,        SIGSEGV, SEGV_ACCERR,   "section permission fault"         },
        { do_bad,               SIGBUS,  0,             "external abort on translation"    },
        { do_page_fault,        SIGSEGV, SEGV_ACCERR,   "page permission fault"            },
        { do_bad,               SIGBUS,  0,             "unknown 16"                       },
        { do_bad,               SIGBUS,  0,             "unknown 17"                       },
        { do_bad,               SIGBUS,  0,             "unknown 18"                       },
        { do_bad,               SIGBUS,  0,             "unknown 19"                       },
        { do_bad,               SIGBUS,  0,             "unknown 20"                       },
        { do_bad,               SIGBUS,  0,             "unknown 21"                       },
        { do_bad,               SIGBUS,  0,             "unknown 22"                       },
        { do_bad,               SIGBUS,  0,             "unknown 23"                       },
        { do_bad,               SIGBUS,  0,             "unknown 24"                       },
        { do_bad,               SIGBUS,  0,             "unknown 25"                       },
        { do_bad,               SIGBUS,  0,             "unknown 26"                       },
        { do_bad,               SIGBUS,  0,             "unknown 27"                       },
        { do_bad,               SIGBUS,  0,             "unknown 28"                       },
        { do_bad,               SIGBUS,  0,             "unknown 29"                       },
        { do_bad,               SIGBUS,  0,             "unknown 30"                       },
        { do_bad,               SIGBUS,  0,             "unknown 31"                       },
};

IFSR(Pre-fetch Fault Status Register)로 부터 읽은 5비트로 구성된 fault 상태 코드에 따라 설정된 fault 핸들러 함수를 dispatch하기 위해 사용된다.

  • do_bad()
    • 기타 exception으로 아무것도 수행하지 않고 1을 반환한다.
  • do_translation_fault()
    • 섹션 변환 실패 시 호출한다.
  • do_page_fault()
    • 2 가지의 경우로 페이지 변환 실패 또는 페이지 권한 위반 시 호출한다.
  • do_sect_fault()
    • 섹션 페이지의 권한 위반 시 호출한다.

 

__pabt_invalid

arch/arm/kernel/entry-armv.S

__pabt_invalid:
        inv_entry BAD_PREFETCH
        b       common_invalid
ENDPROC(__pabt_invalid)

허용하지 않은 모드에서 pre-fetch abort exception 핸들러에 진입하여 실패 처리를 위한 루틴이다

  • 처리가 유사한 __dabt_invalid 소스 설명 참고

 

__pabt_svc

arch/arm/kernel/entry-armv.S

__pabt_svc:
        svc_entry trace=0
        mov     r2, sp                          @ regs
        pabt_helper
        svc_exit r5                             @ return from exception
 UNWIND(.fnend          )
ENDPROC(__pabt_svc)

svc 모드에서 pre-fetch abort exception을 만나 진입하게 되면 pre-fetch abort 핸들러를 수행한 후 다시 svc 모드로 복귀한다.

  • 처리가 유사한 __dabt_svc 소스 설명 참고

 


Fault 처리 관련 -1-

do_bad()

arch/arm/mm/fault.c

/*
 * This abort handler always returns "fault".
 */
static int
do_bad(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
        return 1;
}

특별한 처리 없이 항상 실패로 반환한다.

 

Fault 처리 관련 -2-

do_translation_fault()

arch/arm/mm/fault.c

/*
 * First Level Translation Fault Handler
 *
 * We enter here because the first level page table doesn't contain
 * a valid entry for the address.
 *
 * If the address is in kernel space (>= TASK_SIZE), then we are
 * probably faulting in the vmalloc() area.
 *
 * If the init_task's first level page tables contains the relevant
 * entry, we copy the it to this task.  If not, we send the process
 * a signal, fixup the exception, or oops the kernel.
 *
 * NOTE! We MUST NOT take any locks for this case. We may be in an
 * interrupt or a critical region, and should only copy the information
 * from the master page table, nothing more.
 */
#ifdef CONFIG_MMU
static int __kprobes
do_translation_fault(unsigned long addr, unsigned int fsr,
                     struct pt_regs *regs)
{
        unsigned int index;
        pgd_t *pgd, *pgd_k;
        pud_t *pud, *pud_k;
        pmd_t *pmd, *pmd_k;

        if (addr < TASK_SIZE)
                return do_page_fault(addr, fsr, regs);

        if (user_mode(regs))
                goto bad_area;
        
        index = pgd_index(addr);

        pgd = cpu_get_pgd() + index;
        pgd_k = init_mm.pgd + index;
        
        if (pgd_none(*pgd_k))
                goto bad_area;
        if (!pgd_present(*pgd))
                set_pgd(pgd, *pgd_k);
        
        pud = pud_offset(pgd, addr);
        pud_k = pud_offset(pgd_k, addr);

        if (pud_none(*pud_k))
                goto bad_area;
        if (!pud_present(*pud))
                set_pud(pud, *pud_k);

        pmd = pmd_offset(pud, addr);
        pmd_k = pmd_offset(pud_k, addr);

#ifdef CONFIG_ARM_LPAE
        /*
         * Only one hardware entry per PMD with LPAE.
         */
        index = 0;
#else
        /*
         * On ARM one Linux PGD entry contains two hardware entries (see page
         * tables layout in pgtable.h). We normally guarantee that we always
         * fill both L1 entries. But create_mapping() doesn't follow the rule.
         * It can create inidividual L1 entries, so here we have to call
         * pmd_none() check for the entry really corresponded to address, not
         * for the first of pair.
         */
        index = (addr >> SECTION_SHIFT) & 1;
#endif
        if (pmd_none(pmd_k[index]))
                goto bad_area;

        copy_pmd(pmd, pmd_k);
        return 0;

bad_area:
        do_bad_area(addr, fsr, regs);
        return 0;
}

첫 번째 레벨에서 translation fault가 발생한 경우 유저 영역이거나 커널 영역이면서 테이블 엔트리가 비어있는 경우 do_page_fauilt() 함수에서 처리하게 한다. 커널 영역인 경우 해당 주소의 테이블 엔트리(pgd, pud, pmd)가 present 플래그만 빠져있는 경우라면 커널 엔트리에서 유저 엔트리로 복사한다.

  • 코드 라인 11~12에서 fault 주소 addr이 user 주소 공간인 경우 do_page_fault() 함수를 호출한다.
  • 코드 라인 14~15에서 kernel address space이지만 user mode에서 fault된 경우 bad_area 레이블로 이동하여 do_bad_area() 함수를 호출한다.
  • 코드 라인 17에서 fault 주소로 pgd 엔트리의 인덱스 번호를 알아온다.
  • 코드 라인 19~20에서 유저 테이블의 pgd 엔트리 및 커널 테이블의 pgd 엔트리 주소를 산출한다.
  • 코드 라인 22~23에서 커널용 pgd 엔트리 값이 0으로 비어 있는 경우 bad_area 레이블로 이동하여 do_bad_area() 함수를 호출한다.
  • 코드 라인 24~25에서 유저용 pgd 엔트리가 present 설정이 없는 경우 커널용 pgd 엔트리 값을 유저용 pgd 엔트리에 복사한다.
  • 코드 라인 27~28에서 유저용 pud 엔트리 및 커널용 pud 엔트리 주소를 산출한다.
  • 코드 라인 30~31에서 커널용 pud 엔트리 값이 0으로 비어 있는 경우 bad_area 레이블로 이동하여 do_bad_area() 함수를 호출한다.
  • 코드 라인 32~33에서 유저용 pud 엔트리가 present 설정이 없는 경우 커널용 pud 엔트리 값을 유저용 pud 엔트리에 복사한다.
  • 코드 라인 35~36에서 유저용 pmd 엔트리 및 커널용 pmd 엔트리 주소를 산출한다.
  • 코드 라인 52에서 pmd 엔트리가 홀 수 섹션인 경우 짝 수 섹션 단위로 절삭한다.
  • 코드 라인 54~55에서 커널용 pmd 엔트리 값이 0으로 비어 있는 경우 bad_area 레이블로 이동하여 do_bad_area() 함수를 호출한다.
  • 코드 라인 57~58에서 커널용 pmd 엔트리 값을 유저용 pmd 엔트리에 복사하고 성공(0)으로 복귀한다.
  • 코드 라인 60~62에서 bad_area: 레이블에서는 do_bad_area() 함수를 호출한 후 성공(0)으로 복귀한다.

 

cpu_get_pgd()

arch/arm/include/asm/proc-fns.h

#define cpu_get_pgd()   \
        ({                                              \
                unsigned long pg;                       \
                __asm__("mrc    p15, 0, %0, c2, c0, 0"  \
                         : "=r" (pg) : : "cc");         \
                pg &= ~0x3fff;                          \
                (pgd_t *)phys_to_virt(pg);              \
        })
#endif

유저 테이블의 가상 주소를 산출한다.

  • 코드 라인 4~7에서 물리 주소를 담고 있는 TTBR0 값을 읽어와서 lsb 14비트를 절삭한 물리 주소를 가상 주소로 반환한다.

 

Fault 처리 관련 -3-

do_sect_fault()

arch/arm/mm/fault.c

/*
 * Some section permission faults need to be handled gracefully.
 * They can happen due to a __{get,put}_user during an oops.
 */
static int
do_sect_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{       
        do_bad_area(addr, fsr, regs);
        return 0;               
}

섹션 접근 권한 fault가 발생된 경우 do_bad_area() 함수를 호출하여 다음과 같은 처리를 한다.

  • 유저 모드에서 exception된 경우 해당 유저 태스크는 SIGSEGV 시그널을 받아서 종료된다.
  • 유저 모드가 아닌 모드에서 exception된 경우 die() 처리를 한다. 만일 ex_table(exception table)에 별도의 fixup 코드가 있는 경우 해당 코드를 실행시킨다

 

do_bad_area()

arch/arm/mm/fault.c()

void do_bad_area(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
        struct task_struct *tsk = current;
        struct mm_struct *mm = tsk->active_mm;

        /*
         * If we are in kernel mode at this point, we
         * have no context to handle this fault with.
         */
        if (user_mode(regs))
                __do_user_fault(tsk, addr, fsr, SIGSEGV, SEGV_MAPERR, regs);
        else
                __do_kernel_fault(mm, addr, fsr, regs);
}

유저 모드에서 exception 된 경우 __do_user_fault() 함수를 호출하고 그 외의 모드는 __do_kernel_fault() 함수를 처리한다.

  • 코드 라인 10~11에서 유저 모드에서 exception되어 매핑되지 않은 영역에 접근하려 하는 경우 해당 유저 태스크에 SIGSEGV 시그널을 전송하여 태스크를 종료시킨다.
  • 코드 라인 12~13에서 유저 모드가 아닌 모드에서 exception되어 매핑되지 않은 영역에 접근하려 하는 경우 die() 처리를 한다. 단 ex_table(exception table)에 별도의 fixup 코드가 있는 경우 해당 코드를 실행시킨다.
    • get_user() 등의 매크로 함수에서 ex_table(exception table)에 fixup 코드를 등록하여 사용한다.

 

__do_user_fault()

arch/arm/mm/fault.c

/*
 * Something tried to access memory that isn't in our memory map..
 * User mode accesses just cause a SIGSEGV
 */
static void
__do_user_fault(unsigned long addr, unsigned int fsr, unsigned int sig, 
                int code, struct pt_regs *regs)
{
        struct task_struct *tsk = current;

        if (addr > TASK_SIZE)
                harden_branch_predictor();

#ifdef CONFIG_DEBUG_USER
        if (((user_debug & UDBG_SEGV) && (sig == SIGSEGV)) ||
            ((user_debug & UDBG_BUS)  && (sig == SIGBUS))) {
                printk(KERN_DEBUG "%s: unhandled page fault (%d) at 0x%08lx, code 0x%03x\n",
                       tsk->comm, sig, addr, fsr);
                show_pte(tsk->mm, addr);
                show_regs(regs);
        }
#endif
#ifndef CONFIG_KUSER_HELPERS
        if ((sig == SIGSEGV) && ((addr & PAGE_MASK) == 0xffff0000))
                printk_ratelimited(KERN_DEBUG
                                   "%s: CONFIG_KUSER_HELPERS disabled at 0x%08lx\n",
                                   tsk->comm, addr);
#endif

        tsk->thread.address = addr;
        tsk->thread.error_code = fsr;
        tsk->thread.trap_no = 14;
        force_sig_info(sig, code, (void __user *)addr);
}

유저 모드에서 커널 영역의 매핑되지 않은 페이지를 엑세스하려할 때 해당 유저 태스크에 인수로 요청 받은 sigSIGSEGV, SIGBUS, SIGKILL 중 하나 시그널을 전달한다.

 

__do_kernel_fault()

arch/arm/mm/fault.c

/*
 * Oops.  The kernel tried to access some page that wasn't present.
 */
static void
__do_kernel_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
                  struct pt_regs *regs)
{
        /*
         * Are we prepared to handle this kernel fault?
         */
        if (fixup_exception(regs))
                return;

        /*
         * No handler, we'll have to terminate things with extreme prejudice.
         */
        bust_spinlocks(1);
        pr_alert("8<--- cut here ---\n");
        pr_alert("Unable to handle kernel %s at virtual address %08lx\n",
                 (addr < PAGE_SIZE) ? "NULL pointer dereference" :
                 "paging request", addr);

        show_pte(KERN_ALERT, mm, addr);
        die("Oops", regs, fsr);
        bust_spinlocks(0);
        do_exit(SIGKILL);
}

유저 모드가 아닌 모드에서 커널 영역의 매핑되지 않은 페이지를 엑세스하려할 때 ex_table(exception table) 에 등록한 별도의 fixup 코드를 실행시킨다. 만일 등록된 별도의 fixup 코드가 없는 경우 “Unable to handle kernel…” 메시지와 함께 pte 정보를 출력하고 die() 처리한다.

  • 코드 라인 8~9에서 ex_table(exception table) 에 등록한 별도의 fixup 코드가 있는 경우 이를 실행하고 복귀한다.
  • 코드 라인 15~21에서 “Unable to handle kernel…” 메시지와 함께 pte 정보를 출력하고 die() 처리한다

 

Fault 처리 관련 -4-

do_page_fault()

arch/arm/mm/fault.c

static int __kprobes
do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
        struct task_struct *tsk;
        struct mm_struct *mm;
        int sig, code;
        vm_fault_t fault;
        unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;

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

        tsk = current;
        mm  = tsk->mm;

        /* Enable interrupts if they were enabled in the parent context. */
        if (interrupts_enabled(regs))
                local_irq_enable();

        /*
         * 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))
                flags |= FAULT_FLAG_USER;
        if ((fsr & FSR_WRITE) && !(fsr & FSR_CM))
                flags |= FAULT_FLAG_WRITE;

abort exception되어 fault 처리를 하는데 exception 되기 전의 모드에 따라 처리를 다음과 같이 수행한다.

  • 유저 모드
    • vma에 포함되지 않은 영역인 경우 태스크 die 처리
    • vma에 포함되었지만 페이지 테이블 엔트리가 present 플래그가 없어 즉, 매핑되지 않은 영역은 mm_fault 처리하여 실제 메모리를 할당하고 매핑시킨다.
      • lazy alloc, swap, file 매핑 등
  • 유저 모드가 아닌 모드(커널 모드)
    • vma에 포함되지 않은 영역인 경우 die 처리
    • vma에 포함되었지만 페이지 테이블 엔트리가 present 플래그가 없어 즉, 매핑되지 않은 영역은 mm_fault 처리하여 실제 메모리를 할당하고 매핑시킨다. (vmalloc 영역에서의 lazy alloc)

 

  • 코드 라인 8에서 allow retry와 killable 플래그를 기본 플래그로 대입한다.
  • 코드 라인 10~11에서 페이지 fault 처리에 앞서 커널 디버거 kprobe를 지원하고 동작중인 경우 kprobe용 fault 핸들러 함수를 처리할 수 있게 한다.
  • 코드 라인 13~14에서 현재 태스크의 메모리 디스크립터를 가리킨다.
  • 코드 라인 17~18에서 exception되기 전에 인터럽트가 가능한 상태인 경우 local irq를 enable한다.
  • 코드 라인 24~25에서 atomic 하게 처리해야 하는 스케쥴링하는 동안이거나 메모리 디스크립터가 지정되지 않은 경우 no_context: 레이블로 이동하여 커널 fault를 처리 한다. (die)
  • 코드 라인 27~28에서 user 모드에서 exception된 경우 user 플래그 표시를 한다.
  • 코드 라인 29~30에서 fault 상태 값에 FSR_WRITE가 있는 경우 write 플래그를 추가한다.

 

        /*
         * 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->ARM_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->ARM_pc))
                        goto no_context;
#endif
        }
        
        fault = __do_page_fault(mm, addr, fsr, flags, tsk);

        /* 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 ((fault & VM_FAULT_RETRY) && fatal_signal_pending(current)) { 
                if (!user_mode(regs))
                        goto no_context;
                return 0;
        }
        
        /*
         * Major/minor page fault accounting is only done on the
         * initial attempt. If we go through a retry, it is extremely
         * likely that the page will be found in page cache at that point.
         */

        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);
        if (!(fault & VM_FAULT_ERROR) && flags & FAULT_FLAG_ALLOW_RETRY) {
                if (fault & VM_FAULT_MAJOR) {
                        tsk->maj_flt++;
                        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1,
                                        regs, addr);
                } else {
                        tsk->min_flt++;
                        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1,
                                        regs, addr);
                }
                if (fault & VM_FAULT_RETRY) {
                        /* Clear FAULT_FLAG_ALLOW_RETRY to avoid any risk
                        * of starvation. */
                        flags &= ~FAULT_FLAG_ALLOW_RETRY;
                        flags |= FAULT_FLAG_TRIED;
                        goto retry;
                }
        }

        up_read(&mm->mmap_sem);
  • 코드 라인 6~10에서 매핑과 관련한 세마포어 락을 시도하여 실패하면 천천히 다시 락을 획득한다. 단 유저 모드가 아니면서 ex_table에도 해당 주소가 없는 경우는 no_context: 레이블로 이동하여 커널 fault를 처리 한다. (die)
  • 코드 라인 11~23에서 preemption point를 수행하며 유저 모드가 아니면서 CONFIG_DEBUG_VM 커널 옵션을 사용하는 경우 ex_table에도 해당 주소가 없는 경우는 no_context: 레이블로 이동하여 커널 fault를 처리 한다. (die)
  • 코드 라인 25~35에서 페이지 fault 처리를 수행하고 수행 후 fault retry 요청이 있지만 시그널 지연중인 경우는 그냥 함수를 빠져나간다. 단 커널 모드에서 exception된 경우 no_context 레이블로 이동한다.
  • 코드 라인 43에서 CONFIG_PERF_EVENTS 커널 옵션을 사용하는 경우 커널에서 제공하는 s/w perf 카운터 중 하나인 PERF_COUNT_SW_PAGE_FAULTS 카운터를 증가시킨다.
  • 코드 라인 44~61에서 vm_fault 에러이면서 retry를 허용한 경우 retry 플래그를 제거 하고, retry 중(FAULT_FLAG_TRIED)이라고 설정한다음 다시 한 번 시도한다. 그리고 major/minor fault인지 여부에 따라 maj_flt 또는 min_flt를 증가시키고 해당 perf 카운터도 증가시킨다.

 

        /*
         * Handle the "normal" case first - VM_FAULT_MAJOR
         */
        if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP | VM_FAULT_BADACCESS))))
                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;
        }

        if (fault & VM_FAULT_SIGBUS) {
                /*
                 * We had some memory, but were unable to
                 * successfully fix up this page fault.
                 */
                sig = SIGBUS;
                code = BUS_ADRERR;
        } else {
                /*
                 * Something tried to access memory that
                 * isn't in our memory map..
                 */
                sig = SIGSEGV;
                code = fault == VM_FAULT_BADACCESS ?
                        SEGV_ACCERR : SEGV_MAPERR;
        }

        __do_user_fault(addr, fsr, sig, code, regs);
        return 0;

no_context:
        __do_kernel_fault(mm, addr, fsr, regs);
        return 0;
}

유저 페이지 fault에 대한 처리를 수행한다.

  • 코드 라인 4~5에서 높은 확률로 fault 에러, badmap, badaccess가 없는 경우 함수를 종료한다.
  • 코드 라인 11~12에서 user 모드가 아닌 모드에서 exception된 경우 함수를 종료한다.
  • 코드 라인 14~22에서 OOM fault로 인해 매핑을 못한 경우 OOM 킬러가 동작하는 경우 OOM kill 처리를 수행한다.
    • 메모리를 많이 사용하는 태스크를 평가하여 kill 한다
  • 코드 라인 24~42에서 SIGBUS fault인 경우 시그널에 SIGBUS, code에 BUS_ADRERR을 담고 user fault 핸들러를 수행하고, SIGBUS가 아닌 경우 시그널에 SIGSEGV, code에 bad access 여부에 따라 SEGV_ACCERR 또는 SEGV_MAPERR를 담고 user fault 핸들러를 수행하고 복귀한다.
  • 코드 라인 44~46에서 no_context: 레이블이다. kernel fault 핸들러를 수행하고 복귀한다.

 

__do_page_fault()

arch/arm/mm/fault.c

static vm_fault_t __kprobes
__do_page_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
                unsigned int flags, struct task_struct *tsk)
{
        struct vm_area_struct *vma;
        vm_fault_t fault;

        vma = find_vma(mm, addr);
        fault = VM_FAULT_BADMAP;
        if (unlikely(!vma))
                goto out;
        if (unlikely(vma->vm_start > addr))
                goto check_stack;

        /*
         * Ok, we have a good vm_area for this
         * memory access, so we can handle it.
         */
good_area:
        if (access_error(fsr, vma)) {
                fault = VM_FAULT_BADACCESS;
                goto out;
        }

        return handle_mm_fault(vma, addr & PAGE_MASK, flags);

check_stack:
        /* Don't allow expansion below FIRST_USER_ADDRESS */
        if (vma->vm_flags & VM_GROWSDOWN &&
            addr >= FIRST_USER_ADDRESS && !expand_stack(vma, addr))
                goto good_area;
out:
        return fault;
}

유저 영역에 대한 fault 여부를 다음과 같이 확인한다.

  • 유저 태스크에 등록된 vma 영역을 벗어난 경우 VM_FAULT_BADMAP
  • 사용 권한이 없는 vma 영역인 경우 VM_FAULT_BADACCESS
  • vma 영역이지만 매핑만 하지 않은 경우이므로 물리 메모리를 할당하고 lazy alloc 처리, swap 또는 file 로딩 후 매핑처리한다.

 

  • 코드 라인 8~11에서 vma 영역에서 찾을 수 없는 경우 VM_FAULT_BADMAP을 반환한다.
  • 코드 라인 12~13에서 찾은 vma 영역 아래에 위치한 경우 check_stack 레이블로 이동한다.
  • 코드 라인 19~23에서 access 권한 에러인 경우 VM_FAULT_BADACCESS를 반환한다.
  • 코드 라인 25에서 vma 영역에 해당하지만 메모리 매핑이 아직 안된 페이지를 처리 하러 handle_mm_fault() 함수를 호출한다.
  • 코드 라인 27~31에서 check_stack: 레이블이다. vma가 밑으로 증가되는 스택이고 요청 주소가 유저 영역 하한 주소인 FIRST_USER_ADDRESS(arm에서는 2 페이지) 이상이고 스택이 확장된 스택이 아니면 good_area 레이블로 이동한다.

 


Undefined Instruction 핸들러

__und_usr

arch/arm/kernel/entry-armv.S

        .align  5
__und_usr:
        usr_entry uaccess=0

        mov     r2, r4
        mov     r3, r5

        @ r2 = regs->ARM_pc, which is either 2 or 4 bytes ahead of the
        @      faulting instruction depending on Thumb mode.
        @ r3 = regs->ARM_cpsr
        @
        @ The emulation code returns using r9 if it has emulated the
        @ instruction, or the more conventional lr if we are to treat
        @ this as a real undefined instruction
        @
        badr     r9, ret_from_exception

        @ IRQs must be enabled before attempting to read the instruction from
        @ user space since that could cause a page/translation fault if the
        @ page table was modified by another CPU.
        enable_irq

        tst     r3, #PSR_T_BIT                  @ Thumb mode?
        bne     __und_usr_thumb
        sub     r4, r2, #4                      @ ARM instr at LR - 4
1:      ldrt    r0, [r4]
 ARM_BE8(rev    r0, r0)                         @ little endian instruction

        @ r0 = 32-bit ARM instruction which caused the exception
        @ r2 = PC value for the following instruction (:= regs->ARM_pc)
        @ r4 = PC value for the faulting instruction
        @ lr = 32-bit undefined instruction function
        adr     lr, BSYM(__und_usr_fault_32)
        b       call_fpe

user 모드에서 undefined instruction exception을 만나 진입하게 되면 VFP 예외 처리, FPE 수행 등을 수행한 후 다시 user 모드로 복귀한다.

  • 코드 라인 3에서 전체 레지스터를 스택에 pt_regs 구조체 순서로 백업한다.
  • 코드 라인 5~6에서 exception된 주소가 담기 r4 레지스터를 r2 레지스터에 담고, cpsr을 담고 있는 r5 레지스터를 r3 레지스터에 담는다.
  • 코드 라인 16에서 레지스터 r9에 ret_from_exception 레이블의 주소를 담는다.
  • 코드 라인 21에서 irq를 enable한다.
  • 코드 라인 23~24에서 cpsr의 thumb 모드 비트가 설정된 경우 __und_usr_thumb 레이블로 이동한다.
  • 코드 라인 25~26에서 exception된 instruction 코드를 r0 레지스터로 읽어온다.
    • exception 당시 pc – 4의 주소이다.
  • 코드 라인 33~34에서 __und_usr_fault_32 레이블의 주소를 돌아갈 주소로 지정하기 위해 lr 레지스터에 대입하고 Floating Point 관련 예외 처리 또는 에뮬레이션을 수행하기 위해 call_fpe 레이블로 이동한다.

 

__und_usr_fault_32

arch/arm/kernel/entry-armv.S

__und_usr_fault_32:
        mov     r1, #4
        b       1f
__und_usr_fault_16_pan:
        uaccess_disable ip
__und_usr_fault_16:
        mov     r1, #2
1:      mov     r0, sp
        badr     lr, ret_from_exception
        b       __und_fault
ENDPROC(__und_usr_fault_32)
ENDPROC(__und_usr_fault_16)

r1 레지스터에 4를 담고 ret_from_exception 레이블 주소를 복귀 주소로 lr 레지스터에 저장한 후 fault 처리를 위해 __und_fault 레이블로 이동한다.

 

__und_invalid

arch/arm/kernel/entry-armv.S

__und_invalid:
        inv_entry BAD_UNDEFINSTR

        @
        @ XXX fall through to common_invalid
        @

허용하지 않은 모드에서 undefined instruction exception 핸들러에 진입하여 실패 처리를 위한 루틴이다. 이어서 common_invalid: 레이블을 계속 진행한다.

  • 처리가 유사한 __dabt_invalid 소스 설명 참고

 

__und_svc

arch/arm/kernel/entry-armv.S

        .align  5
__und_svc:
#ifdef CONFIG_KPROBES
        @ If a kprobe is about to simulate a "stmdb sp..." instruction,
        @ it obviously needs free stack space which then will belong to
        @ the saved context.
        svc_entry MAX_STACK_SIZE
#else
        svc_entry
#endif
        @
        @ call emulation code, which returns using r9 if it has emulated
        @ the instruction, or the more conventional lr if we are to treat
        @ this as a real undefined instruction
        @
        @  r0 - instruction
        @
#ifndef CONFIG_THUMB2_KERNEL
        ldr     r0, [r4, #-4]
#else
        mov     r1, #2
        ldrh    r0, [r4, #-2]                   @ Thumb instruction at LR - 2
        cmp     r0, #0xe800                     @ 32-bit instruction if xx >= 0
        blo     __und_svc_fault
        ldrh    r9, [r4]                        @ bottom 16 bits
        add     r4, r4, #2
        str     r4, [sp, #S_PC]
        orr     r0, r9, r0, lsl #16
#endif
        badr     r9, __und_svc_finish
        mov     r2, r4
        bl      call_fpe

        mov     r1, #4                          @ PC correction to apply
__und_svc_fault:
        mov     r0, sp                          @ struct pt_regs *regs
        bl      __und_fault

__und_svc_finish:
        get_thread_info tsk
        ldr     r5, [sp, #S_PSR]                @ Get SVC cpsr
        svc_exit r5                             @ return from exception
 UNWIND(.fnend          )
ENDPROC(__und_svc)

svc 모드에서 undefined instruction exception을 만나 진입하게 되면 VFP 예외 처리, FPE 수행 등을 수행한 후 다시 svc 모드로 복귀한다.

  • 코드 라인 3~10에서 전체 레지스터를 스택에 pt_regs(svc_pt_regs) 구조체 순서로 백업한다.
    • kprobes를 사용 시 MAX_STACK_SIZE(64) 바이트 만큼의 공간을 스택에 추가로 확보한다.
  • 코드 라인 19에서 exception된 instruction 코드를 r0 레지스터에 가져온다.
    • exception 당시 pc – 4의 주소이다.
  • 코드 라인 30~32에서 __und_svc_finish 레이블의 주소를 r9에 담고 r4를 r2에 담고 VFP 예외 처리 및 FPE 수행 등을 처리하기 위해 call_fpe 함수를 호출한다.
  • 코드 라인 34~37에서 r1에 4를 더하고, r0에 스택위치를 대입한  후 __und_fault() 함수를 호출한다.
  • 코드 라인 40~42에서 r5 레지스터에 백업해두었던 pt_regs의 psr 값을 담은 후 스택에 백업해 둔 레지스터들을 복구하고 svc 모드로 빠져나간다.

 

__und_fault

arch/arm/kernel/entry-armv.S

__und_fault:
        @ Correct the PC such that it is pointing at the instruction
        @ which caused the fault.  If the faulting instruction was ARM
        @ the PC will be pointing at the next instruction, and have to
        @ subtract 4.  Otherwise, it is Thumb, and the PC will be
        @ pointing at the second half of the Thumb instruction.  We
        @ have to subtract 2.
        ldr     r2, [r0, #S_PC]
        sub     r2, r2, r1
        str     r2, [r0, #S_PC]
        b       do_undefinstr
ENDPROC(__und_fault)

복귀 주소에 correction(4) 만큼을 뺀다. undefined 훅에 등록된 명령인 경우 정상적으로 함수를 리턴하고 그렇지 않은 경우 유저 모드인 경우 task의 kill 처리를 위한 시그널을 요청하고, 그렇지 않은 경우 시스템을 die 처리한다.

  • 코드 라인 8~10에서 스택에 위치한 pt_regs의 pc 값을 읽어 r1 (correction)값을 뺀 후 다시 pt_regs의 pc 위치에 저장한다.
  • 코드 라인 11에서 설치된 undefined 훅에 등록된 명령인 경우 함수를 리턴하고 그렇지 않은 경우 유저 모드인 경우 task의 kill 처리를 위한 시그널을 요청하고, 그렇지 않은 경우 시스템을 die 처리한다.

 

do_undefinstr()

arch/arm/kernel/traps.c

asmlinkage void do_undefinstr(struct pt_regs *regs)
{
        unsigned int instr;
        siginfo_t info;
        void __user *pc;

        pc = (void __user *)instruction_pointer(regs);

        if (processor_mode(regs) == SVC_MODE) {
#ifdef CONFIG_THUMB2_KERNEL
                if (thumb_mode(regs)) {
                        instr = __mem_to_opcode_thumb16(((u16 *)pc)[0]);
                        if (is_wide_instruction(instr)) {
                                u16 inst2;
                                inst2 = __mem_to_opcode_thumb16(((u16 *)pc)[1]);
                                instr = __opcode_thumb32_compose(instr, inst2);
                        }
                } else
#endif
                        instr = __mem_to_opcode_arm(*(u32 *) pc);
        } else if (thumb_mode(regs)) {
                if (get_user(instr, (u16 __user *)pc))
                        goto die_sig;
                instr = __mem_to_opcode_thumb16(instr);
                if (is_wide_instruction(instr)) { 
                        unsigned int instr2;
                        if (get_user(instr2, (u16 __user *)pc+1))
                                goto die_sig;
                        instr2 = __mem_to_opcode_thumb16(instr2);
                        instr = __opcode_thumb32_compose(instr, instr2);
                }
        } else {
                if (get_user(instr, (u32 __user *)pc))
                        goto die_sig;
                instr = __mem_to_opcode_arm(instr);
        }

        if (call_undef_hook(regs, instr) == 0)
                return;

die_sig:
#ifdef CONFIG_DEBUG_USER
        if (user_debug & UDBG_UNDEFINED) {
                pr_info("%s (%d): undefined instruction: pc=%p\n",
                        current->comm, task_pid_nr(current), pc);
                __show_regs(regs);
                dump_instr(KERN_INFO, regs);
        }
#endif
        arm_notify_die("Oops - undefined instruction", regs, 
                       SIGILL, ILL_ILLOPC, pc, 0, 6);
}
NOKPROBE_SYMBOL(do_undefinstr)

undefined 훅에 등록된 명령인 경우 정상적으로 함수를 리턴한다. 만일 등록되지 않은 경우 다음과 같이 처리한다.

  • 유저 모드인 경우 task의 kill 처리를 위한 시그널을 요청
  • 유저 모드가 아닌(커널) 경우 시스템을 die 처리한다.

 

  • 코드 라인 7에서 스택에 백업한 pt_regs 구조체의 pc 주소 값을 대입한다.
  • 코드 라인 9~20에서 exception 되기 전의 모드가 svc 모드가 아닌 경우 __mem_to_opcode_arm() 함수를 사용하여 명령어 값을 가져온다.
  • 코드 라인 32~36에서 exception 되기 전의 모드가 svc 모드가 아닌 경우 __mem_to_opcode_arm() 함수를 사용하여 명령어 값을 가져오되 단 usr 모드인 경우 die_sig 레이블로 이동한다.
  • 코드 라인 38~39에서 undefined hook이 설치된 경우 인스트럭션과 모드 등을 비교하여 해당 훅 함수를 호출한다. 호출 결과가 성공(0)인 경우 함수를 빠져나간다.
  • 코드 라인 41~51에서 “Oops – undefined instruction” 메시지를 출력하며 die() 함수를 호출한다.
    • 커널인 경우 시스템 die, 유저 모드인 경우 태스크만 die 처리한다.

 

Undefined Hook

call_undef_hook()

arch/arm/kernel/traps.c

static int call_undef_hook(struct pt_regs *regs, unsigned int instr)
{
        struct undef_hook *hook;
        unsigned long flags;
        int (*fn)(struct pt_regs *regs, unsigned int instr) = NULL;

        raw_spin_lock_irqsave(&undef_lock, flags);
        list_for_each_entry(hook, &undef_hook, node)
                if ((instr & hook->instr_mask) == hook->instr_val &&
                    (regs->ARM_cpsr & hook->cpsr_mask) == hook->cpsr_val)
                        fn = hook->fn;
        raw_spin_unlock_irqrestore(&undef_lock, flags);

        return fn ? fn(regs, instr) : 1;
}

undefined hook이 설치된 경우 인스트럭션과 모드 등을 비교하여 해당 훅 함수를 호출한다.  성공=0, 매칭된 훅 함수가 없거나 실패=1

  • register_undef_hook() 함수를 통해 hook이 추가된다.
  • CONFIG_TLS_REG_EMUL 커널 옵션을 사용하는 경우 SMP를 사용하는 ARMv6+ 이전 아키텍처를 위해 TLS 에뮬레이션 목적으로 hook를 하나 설치한다.
    • late_initcall(arm_mrc_hook_init); -> get_tp_trap() 함수

 


Die 처리

arm_notify_die()

arch/arm/kernel/traps.c

void arm_notify_die(const char *str, struct pt_regs *regs,
                int signo, int si_code, void __user *addr,
                unsigned long err, unsigned long trap)
{
        if (user_mode(regs)) {
                current->thread.error_code = err;
                current->thread.trap_no = trap;

                force_sig_info(signo, si_code, addr);
        } else {
                die(str, regs, err);
        }
}

exception 되기 전의 모드가 유저 모드인 경우 태스크에 signal을 보내 해당 태스크만 죽이고 커널 모드인 경우 die() 함수를 호출한다.

 

참고

 

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다