Exception -4- (ARM32 VFP & FPE)

<kernel v5.4>

VFP(Vector Floating Point)

  • VFP는 반정도(half precision), 단정도(single precision) 및 배정도(double precision) 고정 소수점 연산을 지원하는 보조 연산 장치이다.
  • SIMD(Single Instruction Multiple Data)라고도 불린다.

 

버전별 특징

  • VPFv1
    • ARM10에서 사용
    • VFP 예외를 트래핑해야 한다. (VFP 지원 코드가 필요 하다)
    • gcc 컴파일 시: -mfpu=vfp
  • VFPv2
    • ARM11, ARMv5, ARMv6에서 사용
      • ARM10200E가 제공하는 VFP10 수정 버전 1
      • ARM926E/946E/966E에 대해 별도로 허가된 옵션으로 사용되는 VFP9-S
      • ARM1136JF-S, ARM1176JZF-S 및 ARM11 MPCore에서 제공되는 VFP11
    • VFP 예외를 트래핑해야 한다. (VFP 지원 코드가 필요 하다)
    • 16개의 64-bit FPU 레지스터
    • gcc 컴파일 시: -mfpu=vfp
  • VFPv3
    • ARMv7인 Cortex-A8부터 사용하고 NEON으로 불린다.
      • VFPv3-D16
        • 오직 16개의 64-bit FPU 레지스터
      • VFPv3-F16 variant
        • 일반적이지 않지만 단정도 고정 소수점 연산을 지원한다.
    • VFP에서 벡터 기능이 퇴색하고(deprecated) 파이프라인화되어 advanced SIMD로 진보되어 VFPv2의 2배 성능을 확보하였다.
      • 실제 배정도 고정 소수점 연산을 지원하는 scalar 명령셋과 지원하지 않는 vector 명령셋으로 구분된다.
        • 단 ARMv8에서는 vector 명령셋에서도 배정도 고정 소수점 연산을 지원한다.
        • 기존의 벡터 명령도 advanced SIMD와 같이 병렬 처리된다.
    • VFP 예외를 트래핑할 필요가 없다는 점을 제외하고 VFPv2와 동일하다. (VFP 지원 코드가 필요 없다)
    • 32개의 64-bit FPU 레지스터
    • 단정도 고정 소수점 연산(optional)
    • gcc 컴파일 시: -mfpu=neon
    • 참고: NEON | arm

 

GCC로 FP 코드 연동

  • -mfloat-abi=
    • soft
      • Floating Point 라이브러리를 호출하도록 생성하고 호출 방법으로 Floating Poing ABI 규격의 calling convention을 사용한다.
    • hard
      • -mfpu에서 지정한 VFP 하드웨어 코드를 생성하고 호출 방법 역시 각자의 방법을 사용한다.
    • softfp
      • -mfpu에서 지정한 VFP 하드웨어 코드를 생성하되 호출 방법은 soft 방식을 따른다.
  • -mfpu=
    • vfpv2’, ‘vfpv3’, ‘vfpv3-fp16’, ‘vfpv3-d16’, ‘vfpv3-d16-fp16’, ‘vfpv3xd’, ‘vfpv3xd-fp16’, ‘neon-vfpv3’, ‘neon-fp16’, ‘vfpv4’, ‘vfpv4-d16’, ‘fpv4-sp-d16’, ‘neon-vfpv4’, ‘fpv5-d16’, ‘fpv5-sp-d16’, ‘fp-armv8’, ‘neon-fp-armv8’ 및 ‘crypto-neon-fp-armv8’
    • neon
      • VFP 코드를 생성할 경우에 NEON(‘vfpv3’ alias)용으로 생성한다.
    • vfp
      • VFP 코드를 생성할 경우에 VFPv2(‘vfpv2’ alias)용으로 생성한다.
  • 참고: ABI(Application Binary Interface) | 문c

 

VFP 지원 코드

하드웨어 VFP 만으로는 처리할 수 없어서 아래와 같은 상황에서 VFP 지원을 받아 소프트웨어적으로 처리한다.

  • NaN 관련된 부동 소수점 연산
  • 비정규 값과 관련된 부동 소수점 연산
  • 부동 소수점 오버플로
  • 부동 소수점 언더플로
  • 정확하지 않은 결과
  • 0으로 나누기 오류
  • 잘못된 연산

 


FPE(Floating Point Emulation)

  • VFP가 없는 arm 아키텍처에서 커널이 제공하는 라이브러리를 통해 소프트 에뮬레이션 방법으로 동작한다.
  • arm에서는 VFP가 여러 가지 아키텍처에 따라 다르므로 특별히 고성능을 요구하지 않는 경우 호환 목적의 코드를 만들기 위해 FPE를 사용하기도 한다.

 

Undefined instruction으로부터 진입

call_fpe

arch/arm/kernel/entry-armv.S

call_fpe:
        get_thread_info r10                     @ get current thread
#ifdef CONFIG_NEON
        adr     r6, .LCneon_arm_opcodes
2:      ldr     r5, [r6], #4                    @ mask value
        ldr     r7, [r6], #4                    @ opcode bits matching in mask
        cmp     r5, #0                          @ end mask?
        beq     1f
        and     r8, r0, r5
        cmp     r8, r7                          @ NEON instruction?
        bne     2b
        mov     r7, #1
        strb    r7, [r10, #TI_USED_CP + 10]     @ mark CP#10 as used
        strb    r7, [r10, #TI_USED_CP + 11]     @ mark CP#11 as used
        b       do_vfp                          @ let VFP handler handle this
1:
#endif
        tst     r0, #0x08000000                 @ only CDP/CPRT/LDC/STC have bit 27
        tstne   r0, #0x04000000                 @ bit 26 set on both ARM and Thumb-2
        reteq   lr
        and     r8, r0, #0x00000f00             @ mask out CP number
 THUMB( lsr     r8, r8, #8              )
        mov     r7, #1
        add     r6, r10, #TI_USED_CP
 ARM(   strb    r7, [r6, r8, lsr #8]    )       @ set appropriate used_cp[]
 THUMB( strb    r7, [r6, r8]            )       @ set appropriate used_cp[]
#ifdef CONFIG_IWMMXT
        @ Test if we need to give access to iWMMXt coprocessors
        ldr     r5, [r10, #TI_FLAGS]
        rsbs    r7, r8, #(1 << 8)               @ CP 0 or 1 only
        movscs  r7, r5, lsr #(TIF_USING_IWMMXT + 1)
        bcs     iwmmxt_task_enable      
#endif
 ARM(   add     pc, pc, r8, lsr #6      )
 THUMB( lsl     r8, r8, #2              )
 THUMB( add     pc, r8                  )
        nop

instruction이 VFPv3(NEON) 또는 소프트 지원이 필요한 VFPv1/v2 아키텍처에서 동작되는 경우 각각 커널의 VFP  소프트 지원 또는  FPE(Floating Point Emulation) 라이브러리를 통해 해당 예외 처리를 수행한다.

  • 코드 라인 3~15에서 instruction이 VFP이고 지원되지 않는 아키텍처에서 undefined exception으로 진입한 경우이므로 이에 대응하는 VFP 소프트 지원 라이브러리를 통해 해당 명령을 수행한다.
    • 코드 라인 3~8에서 instruction이 VFP 명령어인지 비교하기 위해 LCneon_arm_opcodes 테이블의 값들과 비교하는데 테이블의 끝까지 비교해도 일치하지 않은 경우 이므로 레이블 1로 이동한다. 즉 VFP 명령이 아닐 경우 보조 프로세서 명령 여부를 찾을 계획이다.
    • 코드 라인 9~11에서 instruction(r0)을 mask(r5)하여 opcode(r7) 값과 비교하여 같지 않은 경우 다음 테이블 엔트리와 비교하기 위해 레이블 2로 이동하여 루프를 돈다.
    • 코드 라인 12~15에서 보조 프로세서 인수 전달용 구조체 curr->used_cp[] 배열의 10번째와 11번째에 1을 저장한 후 do_vfp 레이블로 이동한다.
  • 코드 라인 15~33에서 VFP가 없는 아키텍처에서 undefined exception으로 진입한 경우이므로 이에 대응하기 위해 FPE(Floating Point Emulation)를 호출하여 지원한다.
    • 코드 라인 15~19에서 보조프로세서관련 명령이 아닌 경우 fault 처리를 위해 lr(__und_usr_fault_32 )주소로 복귀한다.
      • 보조 프로세서 관련 명령
        • CDP(Coprocessor Data oPerations), CPRT(COprocessor Register Transfer)
        • LDC(Load Data from Coprocessor) 및 STC(Store To Coprocessor) 등
    • 코드 라인 20~25에서 CP 번호를 r8 레지스터에 대입하고 이 값을 인덱스로 curr->used_cp[]에 1을 저장한다.
    • 코드 라인 26~32에서 보조프로세서가 iWMMXt인지 판단되는 경우 iwmmxt_task_enable 레이블로 이동한다.
    • 코드 라인 33에서 보조 프로세서 번호에 해당하는 테이블 위치로 jump 한다.
      • CP 번호가 1~2번인 경우 FPE(Floating Point Emulation)를 처리하러 do_fpe 레이블로 이동한다.
      • CONFIG_CRUNCH 커널 옵션이 설정되고 CP 번호가 4~6번인 경우 MaverickCrunch를 처리하러 crunch_task_enable 레이블로 이동한다.
      • CONFIG_VFP가 설정되고 CP번호가 10~11번인 경우 VFP를 처리하러 do_vfp 레이블로 이동한다.
      • 이 외의 경우에는 fault 처리를 위해 lr(__und_usr_fault_32) 주소로 복귀한다.

 

CP 별(보조프로세서 인덱스) jump 테이블

        ret.w   lr                              @ CP#0
        W(b)    do_fpe                          @ CP#1 (FPE)
        W(b)    do_fpe                          @ CP#2 (FPE)
        ret.w   lr                              @ CP#3
#ifdef CONFIG_CRUNCH
        b       crunch_task_enable              @ CP#4 (MaverickCrunch)
        b       crunch_task_enable              @ CP#5 (MaverickCrunch)
        b       crunch_task_enable              @ CP#6 (MaverickCrunch)
#else
        ret.w   lr                              @ CP#4
        ret.w   lr                              @ CP#5
        ret.w   lr                              @ CP#6
#endif
        ret.w   lr                              @ CP#7
        ret.w   lr                              @ CP#8
        ret.w   lr                              @ CP#9
#ifdef CONFIG_VFP
        W(b)    do_vfp                          @ CP#10 (VFP)
        W(b)    do_vfp                          @ CP#11 (VFP)
#else
        ret.w   lr                              @ CP#10 (VFP)
        ret.w   lr                              @ CP#11 (VFP)
#endif
        ret.w   lr                              @ CP#12
        ret.w   lr                              @ CP#13
        ret.w   lr                              @ CP#14 (Debug)
        ret.w   lr                              @ CP#15 (Control)

 

.LCneon_arm_opcodes

arch/arm/kernel/entry-armv.S

#ifdef CONFIG_NEON
        .align  6

.LCneon_arm_opcodes:
        .word   0xfe000000                      @ mask
        .word   0xf2000000                      @ opcode

        .word   0xff100000                      @ mask
        .word   0xf4000000                      @ opcode

        .word   0x00000000                      @ mask
        .word   0x00000000                      @ opcode

NEON instruction을 구분하기 위한 mask와 opcode이다.

  • 마지막 word 두 개는 종료를 구분하기 위한 값이다.

 

 

ARM instruction set format

bit 27~26이 설정된 명령이 보조 프로세서 관련 명령이다.

  • STC, STC2, LDC, LDC2, MCRR, MCRR2, MRRC, MRRC2, CDP, CDP2, MCR, MCR2, MRC, MRC2

 

Advanced SIMD (NEON) instruction set format

ARMv7에서 동작하는 Floating Point 장치이다.

  • VLD1~4, VST1~4, VADD, VSUB, VDIV, …)
  • 참고: VFP Instruction Set Quick Reference | arm – 다운로드 pdf

 

VFP 소프트 지원 호출 함수

do_vfp

arch/arm/vfp/entry.S

@ VFP entry point.
@
@  r0  = instruction opcode (32-bit ARM or two 16-bit Thumb)
@  r2  = PC value to resume execution after successful emulation
@  r9  = normal "successful" return address
@  r10 = this threads thread_info structure
@  lr  = unrecognised instruction return address
@  IRQs enabled.
@
ENTRY(do_vfp)
        inc_preempt_count r10, r4
        ldr     r4, .LCvfp
        ldr     r11, [r10, #TI_CPU]     @ CPU number
        add     r10, r10, #TI_VFPSTATE  @ r10 = workspace
        ldr     pc, [r4]                @ call VFP entry point
ENDPROC(do_vfp)

VFP 소프트 지원용 핸들러 함수를 호출한다.

  • VFPv1 및 VFPv2 아키텍처에서는 고정 소수 연산의 예외처리를 소프트웨어의 도움을 받아 처리하게 되어 있다.
  • .LCvfp
    • vfp/vfpmodule.c – core_initcall(vfp_init)을 통해 초기화된다.
      • 초기화 전에는 .LCvfp 값은 vfp_null_entry() 함수 주소를 가리킨다.
      • 초기화가 정상적으로 완료되면 .LCvfp 값은 vfp_support_entry() 함수 주소를 가리킨다.

 

vfp_null_entry()

arch/arm/vfp/entry.S

ENTRY(vfp_null_entry)
        dec_preempt_count_ti r10, r4
        ret     lr
ENDPROC(vfp_null_entry)

preemption 카운터를 감소시키고 복귀한다.

 

FPE 호출 함수

do_fpe

arch/arm/kernel/entry-armv.S

do_fpe:
        ldr     r4, .LCfp
        add     r10, r10, #TI_FPSTATE           @ r10 = workspace
        ldr     pc, [r4]                        @ Call FP module USR entry point

FPE(Floating Point Emulation) 핸들러 함수를 호출한다.

  • .LCfp
    • nwfpe/fpmodule.c – module_init(fpe_init)을 통해 초기화된다.
      • 초기화되면 nwfpe_enter: 레이블 주소를 가리킨다.

 

참고

 

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() 함수를 호출한다.

 

참고

 

Kernel-provided User Helpers

 

커널 메모리의 코드 및 데이터 일부를 유저가 직접 호출 또는 접근할 수 있도록 유일하게 허용한 페이지이다.

  • Posix syscall을 사용하지 않고 직접 유저 공간에서 호출하므로 매우 빠른 속도가 요구되는 코드를 수행할 수 있다.
  • 하이 벡터를 사용하는 프로세서에서 CONFIG_KUSER_HELPERS 커널 옵션을 사용하여 제공한다.
    • 로우 벡터에서는 kuser helper 코드를 지원하지 않는다.
  • arm에서 가상 주소 0xffff_0000로 시작하는 페이지를 사용하는 하이 벡터 페이지의 사용하지 않는 윗 부분을 이용한다.
    • 현재 0xffff_0f60 ~ 0xffff0fff 까지 범위에서 사용하고 있다.

 

코드 위치

현재 커널은 하이벡터 페이지의 가장 윗 부분을 이용하여 다음 4개의 함수와 1개의 상수 값을 제공한다.

  • __kuser_cmpxchg64()
  • __kuser_memory_barrier()
  • __kuser_cmpxchg()
  • __kuser_get_tls()
  • __kuser_helper_version 상수

 

코드가 위치한 주소는 다음 그림과 같다.

 

Kernel provided User Helper 버전 확인

가상 주소: 0xffff_0ffc

 

사용 방법

#define __kuser_helper_version (*(int32_t *)0xffff0ffc)

void check_kuser_version(void)
{
        if (__kuser_helper_version < 2) {
                fprintf(stderr, "can't do atomic operations, kernel too old\n");
                abort();
        }
}

버전 값이 2보다 작은 경우 커널이 너무 오래되어서 atomic operation을 지원하지 않는 것을 알 수 있다.

 

__kuser_cmpxchg64() 함수

가상 주소: 0xffff_0f60

 

사용방법

typedef int (__kuser_cmpxchg64_t)(const int64_t *oldval,
                                  const int64_t *newval,
                                  volatile int64_t *ptr);
#define __kuser_cmpxchg64 (*(__kuser_cmpxchg64_t *)0xffff0f60)

int64_t atomic_add64(volatile int64_t *ptr, int64_t val)
{
        int64_t old, new;

        do {
                old = *ptr;
                new = old + val;
        } while(__kuser_cmpxchg64(&old, &new, ptr));

        return new;
}

64bit 값이 담긴 ptr 포인터 주소에 val 값을 atomic하게 더한다.

  • __kuser_cmpxchg64() 함수는 64bit 현재 값이 oldval과 같은 경우 newval을 atomic 하게 저장하고 성공리에 0을 반환한다. 그 외의 값은 oldval과 newval이 달라 저장하지 않은 경우이다.

 

 3가지 구현  방법

Kernel-provided User Helper code 중 kuser_cmpxchg64() 함수는 시스템에 따라 3 가지의 구현 중 하나를 사용한다.

  1. fastpath
    • ARMv6K를 포함하여 이후 버전의 ARM 아키텍처는 user space에서 직접 cmpxchg 및 cmpxchg64 atomic operation을 수행할 수 있다.
      • ARM SMP 시스템에서 사용하는 atomic operation
        • ARMv6: swp
        • ARMv7: ldrex/strex이 권장 사용되며, swp는 호환목적으로 s/w emulation 방법을 사용하여 구현되어 있다.(비권장)
  2. slowpath for SMP
    • fastpath를 지원하지 않는 SMP 아키텍처를 사용하는 경우 CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG 커널 옵션을 사용하여 arm syscall을 사용하여 커널로 진입하여 64bit cmpxchg atomic operation을 수행하는 방법이다.
  3. slowpath for UP
    • UP 시스템에서 atomic을 포기하고 순서대로 진행하는 방법으로 인터럽트가 커널에서 호출되는 경우에 한하여 fixup을 수행하여 보강한다.

 

NEEDS_SYSCALL_FOR_CMPXCHG

  • arm 아키텍처로 집중해서 이 커널 옵션을 보면 ARMv6 이전 SMP 프로세서(아직은 이렇게 만든 SoC가 없는 것 같다. 하지만 미래에 어떤 회사가 만들지 모르는 법이므로…)는 직접 user space에서 atomic하게 cmpxchg를 수행할 수 없다. 따라서 느리더라도 POSIX call을 사용하여 커널에 진입한 후 처리하도록 이 방법을 사용하여야 한다.

 

__kuser_cmpxchg64:

user space에서 64 bit atomic operation을 처리하기 위해 이러한 기능에 대한 아키텍처의 지원 여부에 따라 구현 방법을 3가지로 달리 구현되었다.

1) 커널에 위탁 처리하는 방법

arch/arm/kernel/entry-armv.S

/*
 * Due to the length of some sequences, __kuser_cmpxchg64 spans 2 regular
 * kuser "slots", therefore 0xffff0f80 is not used as a valid entry point.
 */

__kuser_cmpxchg64:                              @ 0xffff0f60

#if defined(CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG)

        /*
         * Poor you.  No fast solution possible...
         * The kernel itself must perform the operation.
         * A special ghost syscall is used for that (see traps.c).
         */
        stmfd   sp!, {r7, lr}
        ldr     r7, 1f                  @ it's 20 bits
        swi     __ARM_NR_cmpxchg64
        ldmfd   sp!, {r7, pc}
1:      .word   __ARM_NR_cmpxchg64

CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG 커널 옵션은 주로 user space에서 atomic operation을 지원하지 못하는 SMP 아키텍처에서 사용한다. atomic operation이 필요한 경우 커널은 인터럽트되지 않도록 블럭한 후 처리할 수 있기 때문에 user space에서의 kuser_cmpxchg64 요청은 arm syscall을 사용하여 커널에 위탁하여 처리하고 그 결과를 내려보내주게 구현되었다.

  • 코드 라인 15에서 r7, lr 레지스터 사용해야 하므로 잠시 스택에 보관한다.
  • 코드 라인 16~17에서 r7에 __ARM_NR_cmpxchg64에 해당하는 arm syscall 넘버를 대입한 후 swi swi 명령을 사용하여 arm syscall 호출을 수행한다.
  • 코드 라인 18에서 스택으로 부터 r7을 복구하고, lr 주소로 jump 한다.

 

2) atomic이 지원되는 아키텍처에서 직접 수행하는 방법

#elif defined(CONFIG_CPU_32v6K)

        stmfd   sp!, {r4, r5, r6, r7}
        ldrd    r4, r5, [r0]                    @ load old val
        ldrd    r6, r7, [r1]                    @ load new val
        smp_dmb arm
1:      ldrexd  r0, r1, [r2]                    @ load current val
        eors    r3, r0, r4                      @ compare with oldval (1)
        eoreqs  r3, r1, r5                      @ compare with oldval (2)
        strexdeq r3, r6, r7, [r2]               @ store newval if eq
        teqeq   r3, #1                          @ success?
        beq     1b                              @ if no then retry
        smp_dmb arm
        rsbs    r0, r3, #0                      @ set returned val and C flag
        ldmfd   sp!, {r4, r5, r6, r7}
        usr_ret lr

CONFIG_CPU_32v6K는 user space에서도 atomic operation을 사용할 수 있는 armv6 이상의 SMP 아키텍처에서 사용하는 커널 옵션이다. 이러한 경우 user space에서 직접 ldrex 및 strex를 사용하여 atomic operation을 수행할 수 있다.

  • 코드 라인 3에서 r4~r7까지 레지스터를 스택에 백업한다.
  • 코드 라인 4에서 r0(old)가 가리키는 주소에서 double word(64bit) 값을 읽어서 r4와 r5 레지스터에 저장한다.
    • ldr r4, [r0]; ldr r5, [r0, #4]와 동일한 결과다.
  • 코드 라인 5에서 r1(new)가 가리키는 주소에서 double word(64bit) 값을 읽어서 r6와 r7 레지스터에 저장한다.
  • 코드 라인 6에서 다음 ldrexd를 호출하기 전에 dmb를 사용하여 메모리에 대해 order 문제가 생기지 않도록 한다.
  • 코드 라인 7에서 r2(ptr)가 가리키는 주소에서 double word(64bit) 값을 읽어서 r0와 r1 레지스터에  저장한다.
  • 코드 라인 8~10에서 읽은 ptr 값(r0, r1)과 old 값(r4, r5)가 비교를 하여 같은 경우 r2 주소에 new(r6, r7) 값을 저장하되 결과를 r3에 저장한다.
  • 코드 라인 11~12에서 저장 시 이 캐시 라인이 exclusive되지 않아 결과(r3)이 실패(#1)한 경우 다시 레이블 1로 이동하여 성공할 때까지 반복한다.
    • 다른 cpu에서 이 exclusive한 캐시라인에 접근하는 경우 open되어 strex 명령을 수행 시 실패를 얻게된다.
  • 코드 라인 13에서 다시 메모리 배리어를 사용하여 order 문제가 생기지 않도록 한다.
  • 코드 라인 14에서 0에서 r3(0=기록한 경우, 그 외=oldval과 newval이 달라 기록하지 않은 경우)와 캐리(C)까지 뺀 후 r0 레지스터에 담는다.
  • 코드 라인 15~16에서 스택에 백업해둔 레지스터들을 복구하고 lr 주소로 복귀한다.

 

3) UP 시스템에서 atomic 구현

#elif !defined(CONFIG_SMP)

#ifdef CONFIG_MMU

        /*
         * The only thing that can break atomicity in this cmpxchg64
         * implementation is either an IRQ or a data abort exception
         * causing another process/thread to be scheduled in the middle of
         * the critical sequence.  The same strategy as for cmpxchg is used.
         */
        stmfd   sp!, {r4, r5, r6, lr}
        ldmia   r0, {r4, r5}                    @ load old val
        ldmia   r1, {r6, lr}                    @ load new val
1:      ldmia   r2, {r0, r1}                    @ load current val
        eors    r3, r0, r4                      @ compare with oldval (1)
        eoreqs  r3, r1, r5                      @ compare with oldval (2)
2:      stmeqia r2, {r6, lr}                    @ store newval if eq
        rsbs    r0, r3, #0                      @ set return val and C flag
        ldmfd   sp!, {r4, r5, r6, pc}

        .text
kuser_cmpxchg64_fixup:
        @ Called from kuser_cmpxchg_fixup.
        @ r4 = address of interrupted insn (must be preserved).
        @ sp = saved regs. r7 and r8 are clobbered.
        @ 1b = first critical insn, 2b = last critical insn.
        @ If r4 >= 1b and r4 <= 2b then saved pc_usr is set to 1b.
        mov     r7, #0xffff0fff
        sub     r7, r7, #(0xffff0fff - (0xffff0f60 + (1b - __kuser_cmpxchg64)))
        subs    r8, r4, r7
        rsbcss  r8, r8, #(2b - 1b)
        strcs   r7, [sp, #S_PC]
#if __LINUX_ARM_ARCH__ < 6
        bcc     kuser_cmpxchg32_fixup
#endif
        ret     lr
        .previous
#endif

        kuser_pad __kuser_cmpxchg64, 64

user space에서 atomic이 구현되지 않는 UP 아키텍처에서 __kuser_cmpxchg64 함수의 기능 구현은 atomic과 관련 없이 단순하게 구현되어 있다. 그 구현 루틴 아래에 위치한 kuser_cmpxchg64_fixup 루틴을 살펴보기 전에는 그 어떠한 atomic 관련한 부가 루틴도 찾아 볼 수가 없다. atomic에 대한 보장을 위해 자세한 것은 kuser_cmpxchg64_fixup 레이블에서 설명하기로 한다.

  • 코드 라인 11에서 r4, r5, r6, lr 레지스터를 스택에 백업해둔다.
  • 코드 라인 12에서 r0위치에 있는 64bit old 값을 r4와 r5 레지스터에 로드한다.
  • 코드 라인 13에서 r1 위치에 있는 64bit new 값을 r6와 lr 레지스터에 로드한다.
  • 코드 라인 14에서 r2 위치에 있는 64bit ptr 값을 r0와 r1 레지스터에 로드한다.
  • 코드 라인 15~17에서 old 값과 ptr 값을 비교하여 같은 경우 new 값을 ptr에 저장한다.
  • 코드 라인 18에서 0에서 r3(0=기록한 경우, 그 외=old 값과 new 값이 달라 기록하지 않은 경우)와 캐리(C)까지 뺀 후 r0 레지스터에 담는다.
  • 코드 라인 19에서 스택에 백업해둔 레지스터들을 복구하고 lr 주소로 복귀한다.
kuser_cmpxchg64_fixup:

user space에서 atomic이 구현되지 않는 UP 아키텍처에서 __kuser_cmpxchg64 루틴을 수행 시 atomic하게 처리해야 하는 구간 즉, 레이블 1과 레이블 2 사이를 수행하는 도중에 irq, fiq, dabt 등을 만나게 되는 경우 해당 exception을 처리한 후 되돌아갈 주소를 atomic 구간의 가장 윗 부분으로 바꾸기 위해 스택의 pt_regs의 pc를 조작하는 방법을 사용한다.

  • 코드 라인 28~29에서 atomic operation이 시작되는 레이블 1:의 가상 주소 값을 알아온다.
    • r7 <- ffff0f60 + (1b – __kuser_cmpxchg64)를 대입하고 싶지만 operand에 사용하는 값의 크기 제한으로 인해 두 개의 명령을 사용하였다.
  • 코드 라인 30~32에서 인터럽트 되었을 때의 pc 주소가 담긴 r4 레지스터 값이 atomic 하게 처리할 구간 범위(레이블 1: ~ 레이블 2:)인 경우 스택에 저장해 둔 pt_regs 구조체 중 pc 위치에 레이블 1 주소를 저장하여 다시 인터럽트 복귀 시 atomic operation을 다시 시도하게 변경한다.
  • 코드 라인 33~35에서 arm 아키텍처가 버전 6보다 이전인 경우에는 위의 atomic opeation 수행 도중 인터럽트 된 것이 아니라면  kuser_cmpxchg32 명령 수행 도중에 발생한 일인지 확인하여 역시 같은 방식으로 복귀 주소를 변경하게 한다.

 

__kuser_memory_barrier:

arch/arm/kernel/entry-armv.S

__kuser_memory_barrier:                         @ 0xffff0fa0
        smp_dmb arm
        usr_ret lr

        kuser_pad __kuser_memory_barrier, 32

SMP 시스템에서 메모리 배리어를 사용하여 이전 로직의 메모리 액세스와 연관하여 order 문제가 생기지 않도록 막는다.

 

__kuser_cmpxchg:

user space에서 32 bit atomic operation을 처리하기 위해 이러한 기능에 대한 아키텍처의 지원 여부에 따라 구현 방법을 3가지로 달리 구현되었다.

1) 커널에 위탁 처리하는 방법

arch/arm/kernel/entry-armv.S

__kuser_cmpxchg:                                @ 0xffff0fc0

#if defined(CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG)

        /*
         * Poor you.  No fast solution possible...
         * The kernel itself must perform the operation.
         * A special ghost syscall is used for that (see traps.c).
         */
        stmfd   sp!, {r7, lr}
        ldr     r7, 1f                  @ it's 20 bits
        swi     __ARM_NR_cmpxchg
        ldmfd   sp!, {r7, pc}
1:      .word   __ARM_NR_cmpxchg

CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG 커널 옵션은 주로 user space에서 atomic operation을 지원하지 못하는 SMP 아키텍처에서 사용한다. atomic operation이 필요한 경우 커널은 인터럽트되지 않도록 블럭한 후 처리할 수 있기 때문에 user space에서의 kuser_cmpxchg 요청은 arm syscall을 사용하여 커널에 위탁하여 처리하고 그 결과를 내려보내주게 구현되었다.

  • 코드 라인 10에서 r7, lr 레지스터 사용해야 하므로 잠시 스택에 보관한다.
  • 코드 라인 11~12에서 r7에 __ARM_NR_cmpxchg64에 해당하는 arm syscall 넘버를 대입한 후 swi swi 명령을 사용하여 arm syscall 호출을 수행한다.
  • 코드 라인 13에서 스택으로 부터 r7을 복구하고, lr 주소로 jump 한다.

 

2) UP 시스템에서 atomic 구현

#elif __LINUX_ARM_ARCH__ < 6

#ifdef CONFIG_MMU

        /*
         * The only thing that can break atomicity in this cmpxchg
         * implementation is either an IRQ or a data abort exception
         * causing another process/thread to be scheduled in the middle
         * of the critical sequence.  To prevent this, code is added to
         * the IRQ and data abort exception handlers to set the pc back
         * to the beginning of the critical section if it is found to be
         * within that critical section (see kuser_cmpxchg_fixup).
         */
1:      ldr     r3, [r2]                        @ load current val
        subs    r3, r3, r0                      @ compare with oldval
2:      streq   r1, [r2]                        @ store newval if eq
        rsbs    r0, r3, #0                      @ set return val and C flag
        usr_ret lr

        .text
kuser_cmpxchg32_fixup:
        @ Called from kuser_cmpxchg_check macro.
        @ r4 = address of interrupted insn (must be preserved).
        @ sp = saved regs. r7 and r8 are clobbered.
        @ 1b = first critical insn, 2b = last critical insn.
        @ If r4 >= 1b and r4 <= 2b then saved pc_usr is set to 1b.
        mov     r7, #0xffff0fff
        sub     r7, r7, #(0xffff0fff - (0xffff0fc0 + (1b - __kuser_cmpxchg)))
        subs    r8, r4, r7
        rsbcss  r8, r8, #(2b - 1b)
        strcs   r7, [sp, #S_PC]
        ret     lr
        .previous

#else
#warning "NPTL on non MMU needs fixing"
        mov     r0, #-1
        adds    r0, r0, #0
        usr_ret lr
#endif

user space에서 atomic이 구현되지 않는 UP 아키텍처에서 __kuser_cmpxchg 함수의 기능 구현은 atomic과 관련 없이 단순하게 구현되어 있다. 그 구현 루틴 아래에 위치한 kuser_cmpxchg_fixup 루틴을 살펴보기 전에는 그 어떠한 atomic 관련한 부가 루틴도 찾아 볼 수가 없다. atomic에 대한 보장을 위해 자세한 것은 kuser_cmpxchg_fixup 레이블에서 설명하기로 한다.

  • 코드 라인 14에서 r2(ptr)위치에 있는 값을 r3 레지스터에 로드한다.
  • 코드 라인 15~16에서 r0(old) 레지스터 값과 r3 레지스터 값을 비교하여 같은 경우 r1(new) 레지스터 값을 r2(ptr) 레지스터가 가리키는 주소에 저장한다.
  • 코드 라인 17~18은 0에서 r3(0=기록한 경우, 그 외=old 값과 new 값이 달라 기록하지 않은 경우)와 캐리(C)까지 뺀 후 r0 레지스터에 담고 lr 주소로 복귀한다.
kuser_cmpxchg_fixup:

user space에서 atomic이 구현되지 않는 UP 아키텍처에서 __kuser_cmpxchg 루틴을 수행 시 atomic하게 처리해야 하는 구간 즉, 레이블 1과 레이블 2 사이를 수행하는 도중에 irq, fiq, dabt 등을 만나게 되는 경우 해당 exception을 처리한 후 되돌아갈 주소를 atomic 구간의 가장 윗 부분으로 바꾸기 위해 스택의 pt_regs의 pc를 조작하는 방법을 사용한다.

  • 코드 라인 27~28에서 atomic operation이 시작되는 레이블 1:의 가상 주소 값을 알아온다.
    • r7 <- ffff0fc0 + (1b – __kuser_cmpxchg)를 대입하고 싶지만 operand에 사용하는 값의 크기 제한으로 인해 두 개의 명령을 사용하였다.
  • 코드 라인 29~32에서 인터럽트 되었을 때의 pc 주소가 담긴 r4 레지스터 값이 atomic 하게 처리할 구간 범위(레이블 1: ~ 레이블 2:)인 경우 스택에 저장해 둔 pt_regs 구조체 중 pc 위치에 레이블 1 주소를 저장하여 다시 인터럽트 복귀 시 atomic operation을 다시 처음 부터 시도하게 변경하고 lr 주소로 복귀한다.

 

3) atomic이 지원되는 아키텍처에서 직접 수행하는 방법

#else

        smp_dmb arm
1:      ldrex   r3, [r2]
        subs    r3, r3, r0
        strexeq r3, r1, [r2]
        teqeq   r3, #1
        beq     1b
        rsbs    r0, r3, #0
        /* beware -- each __kuser slot must be 8 instructions max */
        ALT_SMP(b       __kuser_memory_barrier)
        ALT_UP(usr_ret  lr)

#endif

        kuser_pad __kuser_cmpxchg, 32

user space에서도 atomic operation을 사용할 수 있는 armv6 이상의 SMP 아키텍처에서 사용하는 커널 옵션이다. 이러한 경우 user space에서 직접 ldrex 및 strex를 사용하여 atomic operation을 수행할 수 있다.

  • 코드 라인 3에서 메모리 배리어를 사용하여 이전 로직의 메모리 액세스와 연관하여 order 문제가 생기지 않도록 막는다.
  • 코드 라인 4~6에서 r2(ptr)가 가리키는 주소에서 값을 읽은 값을 r3 레지스터에 저장한다. 이 값과 r0(old) 값을 비교하여 같은 경우 r1(new) 값을 r2(ptr)가 가리키는 주소에 저장한다. 저장 결과는 r3에 담는다. (성공=0, 실패=1)
  • 코드 라인 7~8에서 저장 시 이 캐시 라인이 exclusive되지 않아 결과(r3)이 실패(#1)한 경우 다시 레이블 1로 이동하여 성공할 때까지 반복한다.
    • 다른 cpu에서 이 exclusive한 캐시라인에 접근하는 경우 open되어 strex 명령을 수행 시 실패를 얻게된다.
  • 코드 라인 9는 0에서 r3(0=기록한 경우, 그 외=oldval과 newval이 달라 기록하지 않은 경우)와 캐리(C)까지 뺀 후 r0 레지스터에 담는다.
  • 코드 라인 11~12에서 복귀를 하되 SMP 시스템인 경우 다시 메모리 배리어를 사용하여 order 문제가 생기지 않도록 한다.

 

__kuser_get_tls:

arch/arm/kernel/entry-armv.S

__kuser_get_tls:                        @ 0xffff0fe0
        ldr     r0, [pc, #(16 - 8)]     @ read TLS, set in kuser_get_tls_init
        usr_ret lr
        mrc     p15, 0, r0, c13, c0, 3  @ 0xffff0fe8 hardware TLS code
        kuser_pad __kuser_get_tls, 16
        .rep    3
        .word   0                       @ 0xffff0ff0 software TLS value, then
        .endr

TLS(Thread Local Storage) 값을 알아온다.  2가지의 구현을 사용한다.

  • S/W TLS
    • 0xffff_0ff0 주소에 TLS 값을 보관해두고 그 값을 읽어 사용한다.
  • H/W TLS
    • user 에서 읽기만 가능한 TPIDRURO 레지스터에서 값을 읽어 사용한다.

 

  • 코드 라인 2~3에서 0xfff_0ff0 위치에서 TLS 값을 가져와 r0 레지스터에 담고 lr 주소로 복귀한다.
  • 코드 라인 4에는 TPIDRURO 레지스터에서 값을 읽어 r0 레지스터에 담는 코드를 두었다.
    • H/W TLS 레지스터를 지원하는 경우 이 코드를 복사하여 코드 라인 2의 주소인 0xffff_0fe0에 복사하기 위해 사용된다.
      • setup_arch() -> paging_init() -> devicemaps_init() -> early_trap_init() -> kuser_init() 함수 내부에 다음 코드를 찾을 수 있다.
        • memcpy(vectors + 0xfe0, vectors + 0xfe8, 4);

 

매크로 함수

usr_ret 매크로

arch/arm/kernel/entry-armv.S

        .macro  usr_ret, reg
#ifdef CONFIG_ARM_THUMB
        bx      \reg
#else
        ret     \reg
#endif
        .endm

\reg 주소로 복귀한다.

 

kuser_pad 매크로

arch/arm/kernel/entry-armv.S

        .macro  kuser_pad, sym, size
        .if     (. - \sym) & 3
        .rept   4 - (. - \sym) & 3
        .byte   0
        .endr
        .endif
        .rept   (\size - (. - \sym)) / 4
        .word   0xe7fddef1
        .endr
        .endm

현재 주소 위치 – sym 주소가 4바이트 단위로 정렬되지 않은 경우 정렬을 위해 0으로 채운다.(0~3 바이트) 그 이후 sym 주소부터 size 만큼의 공간 중 현재 위치부터 0xe7fddef1 값으로 채운다.

  • 예) kuser_pad __kuser_memory_barrier, 32
    • __kuser_memory_barrier 함수 위치 부터 32 바이트 공간내에서 빈 자리를 0xe7fddef1 값으로 채운다.
      • 0xffff0fa0: 0xf57ff05b 0xe12fff1e 0xe7fddef1 0xe7fddef1
      • 0xffff0fb0: 0xe7fddef1 0xe7fddef1 0xe7fddef1 0xe7fddef1

 

참고

Exception -2- (ARM32 Handler 1)

<kernel v5.4>

Exception Entry

32bit ARM 프로세스에 exception이 발생하면 가상 벡터 주소의 각 exception을 담당하는 8개의  엔트리로 jump를 하게된다.

가장 첫 exception 엔트리가 reset 벡터가 있고 그 뒤로 나머지 7개의 exception 엔트리가 위치한다. 다음 8개의 exception 별로 호출되는 부분을 알아보자.

  • Reset
    • vector_rst 레이블로 jump 하고 swi 0을 호출하여 swi exception을 발생하게 한 후 이어서 vector_und: 레이블로 jump 한다.
  • Undefined
    • vector_und 레이블로 jump 하고 exception 당시의 프로세스 모드에 따라 __und_usr, __und_svc 또는 __und_invalid 레이블로 점프한다.
    • exception 처리를 마치고 돌아가는 주소는 exception 당시의 pc 위치로 복귀한다.
  • SWI
    • 벡터 테이블 바로 다음 페이지에 위치한 첫 엔트리에 vector_swi 레이블의 위치가 담겨있고 이 위치로 jump 한다.
    • exception 처리를 마치고 돌아가는 주소는 exception 당시의 pc-4 위치로 복귀한다.
  • Prefetch Abort
    • vector_pabt 레이블로 jump 하고 exception 당시의 프로세스 모드에 따라 __pabt_usr, __pabt_svc 또는 __pabt_invalid 레이블로 점프한다.
    • exception 처리를 마치고 돌아가는 주소는 exception 당시의 pc-4 위치로 복귀한다.
  • Data Abort
    • vector_dabt 레이블로 jump 하고 exception 당시의 프로세스 모드에 따라 __dabt_usr, __dabt_svc 또는 __dabt_invalid 레이블로 점프한다.
    • exception 처리를 마치고 돌아가는 주소는 exception 당시의 pc-8 위치로 복귀한다.
  • Address
    • vector_addrexcptn 레이블로 jump 한다. 이 주소는 현재 사용하지 않으므로 이 곳으로 진입하면 매우 위험하다.
  • IRQ
    • vector_irq 레이블로 jump 하고 exception 당시의 프로세스 모드에 따라 __irq_usr, __irq_svc 또는 __irq_invalid 레이블로 점프한다.
    • exception 처리를 마치고 돌아가는 주소는 exception 당시의 pc-4 위치로 복귀한다.
  • FIQ
    • vector_fiq 레이블로 jump 하고 exception 당시의 프로세스 모드에 따라 __fiq_usr, __fiq_svc 또는 __fiq_abt  레이블로 점프한다.
    • exception 처리를 마치고 돌아가는 주소는 exception 당시의 pc-4 위치로 복귀한다.

 

Exception시 모드 전환

아래 그림은 exception 발생 시 모드의 변환과 주요 처리 내용을 보여준다.

  • swi에서 syscall 처리 후 전체 레지스터 중 r0는 제외한다. (r0는 syscall에 대한 결과 값)

 

Context 저장 및 복원

exception으로 인해 context(레지스터들)를 보호하기 위해 스택에 저장한다.

  • 리눅스 커널은 AEABI 규칙을 사용하므로 sp 주소 증가시 8바이트 정렬하여야 하다.

 

다음 그림은 커널(svc) 및 유저(usr) 동작 시 exception이 발생하여 context를 저장하는 크기를 비교하여 보여준다.

 

svc_pt_regs 구조체

arch/arm/include/asm/ptrace.h

struct svc_pt_regs {
        struct pt_regs regs;
        u32 dacr;
        u32 addr_limit;
};

커널(svc)에서 exception이 발생 시 dacr, addr_limit 및 pt_regs를 포함한 svc_pt_regs 구조체를 사용하여 저장과 복원을 한다.

 

pt_regs 구조체

arch/arm/include/asm/ptrace.h

struct pt_regs {
        unsigned long uregs[18];
};

context 백업 및 복원에 사용하는 레지스터들이다.

 

C 루틴에서 참고되는 매크로 레지스터들

include/uapi/asm/ptrace.h

#define ARM_cpsr        uregs[16]
#define ARM_pc          uregs[15]
#define ARM_lr          uregs[14]
#define ARM_sp          uregs[13]
#define ARM_ip          uregs[12]
#define ARM_fp          uregs[11]
#define ARM_r10         uregs[10]
#define ARM_r9          uregs[9]
#define ARM_r8          uregs[8]
#define ARM_r7          uregs[7]
#define ARM_r6          uregs[6]
#define ARM_r5          uregs[5]
#define ARM_r4          uregs[4]
#define ARM_r3          uregs[3]
#define ARM_r2          uregs[2]
#define ARM_r1          uregs[1]
#define ARM_r0          uregs[0]
#define ARM_ORIG_r0     uregs[17]
  • uregs[0] ~ uregs[15]는 r0~r15 레지스터를 보관하는데 사용한다.
  • uregs[16]의 경우 cpsr 레지스터를 보관한다.
  • uregs[17]의 경우 호출 당시의 r0 레지스터를 보관한다.

 

Assembly 루틴에서 참고되는 매크로 상수들

arch/arm/kernel/asm-offsets.c

  DEFINE(S_R0,                  offsetof(struct pt_regs, ARM_r0));
  DEFINE(S_R1,                  offsetof(struct pt_regs, ARM_r1));
  DEFINE(S_R2,                  offsetof(struct pt_regs, ARM_r2));
  DEFINE(S_R3,                  offsetof(struct pt_regs, ARM_r3));
  DEFINE(S_R4,                  offsetof(struct pt_regs, ARM_r4));
  DEFINE(S_R5,                  offsetof(struct pt_regs, ARM_r5));
  DEFINE(S_R6,                  offsetof(struct pt_regs, ARM_r6));
  DEFINE(S_R7,                  offsetof(struct pt_regs, ARM_r7));
  DEFINE(S_R8,                  offsetof(struct pt_regs, ARM_r8));
  DEFINE(S_R9,                  offsetof(struct pt_regs, ARM_r9));
  DEFINE(S_R10,                 offsetof(struct pt_regs, ARM_r10));
  DEFINE(S_FP,                  offsetof(struct pt_regs, ARM_fp));
  DEFINE(S_IP,                  offsetof(struct pt_regs, ARM_ip));
  DEFINE(S_SP,                  offsetof(struct pt_regs, ARM_sp));
  DEFINE(S_LR,                  offsetof(struct pt_regs, ARM_lr));
  DEFINE(S_PC,                  offsetof(struct pt_regs, ARM_pc));
  DEFINE(S_PSR,                 offsetof(struct pt_regs, ARM_cpsr));
  DEFINE(S_OLD_R0,              offsetof(struct pt_regs, ARM_ORIG_r0));
  DEFINE(PT_REGS_SIZE,          sizeof(struct pt_regs));
  DEFINE(SVC_DACR,              offsetof(struct svc_pt_regs, dacr));
  DEFINE(SVC_ADDR_LIMIT,        offsetof(struct svc_pt_regs, addr_limit));
  DEFINE(SVC_REGS_SIZE,         sizeof(struct svc_pt_regs));
  • PT_REGS_SIZE
    • pt_regs 구조체 사이즈
  • SVC_REGS_SIZE
    • svc_pt_regs 구조체 사이즈

 


공통 핸들러

유저 모드에서 진입 시 레지스터 백업

usr_entry 매크로

arch/arm/kernel/entry-armv.S (THUMB 코드 생략)

/*
 * User mode handlers
 *
 * EABI note: sp_svc is always 64-bit aligned here, so should PT_REGS_SIZE
 */
#if defined(CONFIG_AEABI) && (__LINUX_ARM_ARCH__ >= 5) && (PT_REGS_SIZE & 7)
#error "sizeof(struct pt_regs) must be a multiple of 8"
#endif
        .macro  usr_entry, trace=1, uaccess=1
 UNWIND(.fnstart        )
 UNWIND(.cantunwind     )       @ don't unwind the user space
        sub     sp, sp, #PT_REGS_SIZE
 ARM(   stmib   sp, {r1 - r12}  )

 ATRAP( mrc     p15, 0, r7, c1, c0, 0)
 ATRAP( ldr     r8, .LCcralign)

        ldmia   r0, {r3 - r5}
        add     r0, sp, #S_PC           @ here for interlock avoidance
        mov     r6, #-1                 @  ""  ""     ""        ""

        str     r3, [sp]                @ save the "real" r0 copied
                                        @ from the exception stack

 ATRAP( ldr     r8, [r8, #0])

        @
        @ We are now ready to fill in the remaining blanks on the stack:
        @
        @  r4 - lr_<exception>, already fixed up for correct return/restart
        @  r5 - spsr_<exception>
        @  r6 - orig_r0 (see pt_regs definition in ptrace.h)
        @
        @ Also, separately save sp_usr and lr_usr
        @
        stmia   r0, {r4 - r6}
 ARM(   stmdb   r0, {sp, lr}^                   )

        .if \uaccess
        uaccess_disable ip
        .endif

        @ Enable the alignment trap while in kernel mode
 ATRAP( teq     r8, r7)
 ATRAP( mcrne   p15, 0, r8, c1, c0, 0)

        @
        @ Clear FP to mark the first stack frame
        @
        zero_fp

        .if     \trace
#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_off
#endif
        ct_user_exit save = 0
        .endif
        .endm

유저 프로세스에 exception이 발생한 경우이다. 레지스터들을 스택에 백업한다.

  • 해당 exception 모드(fiq, irq, pabt, dabt, und)에서 3개의 레지스터를 3 word로 구성된 mini stack에 백업한다.
  • svc 모드로 전환하고 다음과 같이 처리한다.
    • 스택에 18개의 레지스터를 보관할 수 있는 pt_regs 구조체 사이즈만큼 키운 후 레지스터들을 백업한다.
    • alignment trap을 enable하기 위해 cr_alignment 값을 읽어 SCTLR에 저장한다.
    • 그 외 irq disable된 기간 및 컨텍스트  트래킹 등을 수행한다.

 

  • 코드 라인 4에서 스택을 pt_regs 구조체 사이즈만큼 키운다. (grows down)
  • 코드 라인 5에서 r1~r12 레지스터들의 정보를 스택의 pt_regs 영역에 저장한다.
    • stmib (Store Memory Increment Before)를 사용하여 sp를 워드만큼 먼저 증가 시킨 후 레지스터들을 저장한다.
  • 코드 라인 7~8에서 SCTLR 값을 r7 레지스터에 읽어오고 전역 변수 cr_alignment 주소를 r8 레지스터에 대입한다.
    • ATRAP() 매크로는 CONFIG_ALIGNMENT_TRAP 커널 옵션이 사용되는 아키텍처에서 사용된다. (대부분의 arm에 적용됨)
    • 잠시 후 매크로 종료전에 cr_alignment 값과 SCTLR 값을 비교하여 alignment trap을 enable하기 위해 cr_alignment 값을 읽어서 SCTLR에 저장한다.
  • 코드 라인 10에서 mini 스택에서 백업한 값들을 r3~r5 레지스터에 로드한다.
  • 코드 라인 11에서 r0에  스택의 &ptregs.pc 주소를 알아온다.
  • 코드 라인 12에서 r6에 -1 값을 대입한다.
  • 코드 라인 14에서 r3(original r0) 레지스터 값을 pt_regs의 가장 첫 엔트리(r0)에 저장한다.
  • 코드 라인 17에서 전역 변수 cr_alignment 값을 읽어서 r8 레지스터에 저장한다.
  • 코드 라인 28에서 교정된 lr 값, psr 값, -1 값을 pt_regs 구조체의 pc, psr, old-rq에 순서대로 저장한다.
  • 코드 라인 29에서 sp와 lr 값을 pt_regs 구조체의 sp와 lr 위치에 그대로 저장한다.
  • 코드 라인 31~33에서 커널에서 유저 영역의 액세스를 제한한다. 그 전의 액세스 여부 값을 ip 레지스터에 담아온다.
  • 코드 라인 36~37에서 SCTLR 값을 읽은 r7 레지스터와 cr_alignment 값을 읽은 r8 레지스터 값을 비교해서 다른 경우에만 cr_alignment 값을 SCTLR에 저장한다.
    • SCTLR을 저장하는데 약 100 사이클 정도의 시간이 걸리므로 성능을 위해 변경 사항이 있는 경우에만 저장한다.
  • 코드 라인 42에서 fp 레지스터를 0으로 설정한다.
    • gcc 툴에서 tracing에 사용하는 fp 레지스터를 0으로 초기화한다.
    • exception 모드가 바뀌면 그 전 stack back trace 정보를 사용할 수 없으므로 이 시점에서 초기화한다.
  • 코드 라인 44~47에서 CONFIG_IRQSOFF_TRACER 커널 옵션이 사용되는 경우  얼마나 오랫동안 인터럽트가 disable 되었는지 그 주기를 알아보기 위한 트래킹 디버깅을 수행한다.
  • 코드 라인 48에서 컨텍스트 트래킹 디버깅이 enable된 경우에 수행된다.

 

다음 그림은 유저 모드에서 exception에 의해 해당 모드에 진입 시 레지스터들을 백업하는 usr_entry 매크로의 기능을 보여준다.

zero_fp 매크로

arch/arm/kernel/entry-header.S

        .macro  zero_fp
#ifdef CONFIG_FRAME_POINTER
        mov     fp, #0
#endif
        .endm

CONFIG_FRAME_POINTER 커널 옵션을 사용하는 경우 커널에서 문제가 발생했을 때 다양한 보고가 가능하도록 한다. 이 커널 옵션을 사용하지 않으면 보고되는 정보가 심각하게 제한된다.

 

trace_hardirqs_off()

kernel/trace/trace_irqsoff.c

void trace_hardirqs_off(void)
{
        if (!preempt_trace() && irq_trace())
                start_critical_timing(CALLER_ADDR0, CALLER_ADDR1);
}
EXPORT_SYMBOL(trace_hardirqs_off);

얼마나 오랫동안 인터럽트가 disable 되었는지 그 주기를 알아보기 위한 트래킹 디버깅을 수행한다.

 

ct_user_exit 매크로

arch/arm/kernel/entry-header.S

/*
 * Context tracking subsystem.  Used to instrument transitions
 * between user and kernel mode.
 */     
        .macro ct_user_exit, save = 1
#ifdef CONFIG_CONTEXT_TRACKING
        .if     \save
        stmdb   sp!, {r0-r3, ip, lr}
        bl      context_tracking_user_exit
        ldmia   sp!, {r0-r3, ip, lr}
        .else
        bl      context_tracking_user_exit
        .endif
#endif
        .endm

CONFIG_CONTEXT_TRACKING 커널 옵션을 사용하면서 전역 static key 변수 context_tracking_enabled이 설정된 경우 컨텍스트 트래킹에 관련한  후처리 디버그 활동을 수행한다.

 

SVC 모드에서 진입 시 레지스터 백업

svc_entry

arch/arm/kernel/entry-armv.S (THUMB 코드 생략)

/*
 * SVC mode handlers
 */
        .macro  svc_entry, stack_hole=0, trace=1, uaccess=1
 UNWIND(.fnstart                )
 UNWIND(.save {r0 - pc}         )
        sub     sp, sp, #(SVC_REGS_SIZE + \stack_hole - 4)
 SPFIX( tst     sp, #4          )
 SPFIX( subeq   sp, sp, #4      )
        stmia   sp, {r1 - r12}

        ldmia   r0, {r3 - r5}
        add     r7, sp, #S_SP - 4       @ here for interlock avoidance
        mov     r6, #-1                 @  ""  ""      ""       ""
        add     r2, sp, #(SVC_REGS_SIZE + \stack_hole - 4)
 SPFIX( addeq   r2, r2, #4      )
        str     r3, [sp, #-4]!          @ save the "real" r0 copied
                                        @ from the exception stack

        mov     r3, lr

        @
        @ We are now ready to fill in the remaining blanks on the stack:
        @
        @  r2 - sp_svc
        @  r3 - lr_svc
        @  r4 - lr_<exception>, already fixed up for correct return/restart
        @  r5 - spsr_<exception>
        @  r6 - orig_r0 (see pt_regs definition in ptrace.h)
        @
        stmia   r7, {r2 - r6}

        get_thread_info tsk
        ldr     r0, [tsk, #TI_ADDR_LIMIT]
        mov     r1, #TASK_SIZE
        str     r1, [tsk, #TI_ADDR_LIMIT]
        str     r0, [sp, #SVC_ADDR_LIMIT]

        uaccess_save r0
        .if \uaccess
        uaccess_disable r0
        .endif

        .if \trace
#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_off
#endif
        .endif
        .endm

커널(svc) 처리 중 exception이 발생한 경우이다. 레지스터들을 스택에 백업한다.

  • 해당 exception 모드(fiq, irq, pabt, dabt, und)에서 3개의 레지스터를 3 word로 구성된 mini stack에 백업한다.
  • svc 모드로 전환하고 다음과 같이 처리한다.
    • 스택을 18개의 레지스터를 포함하여 보관할 수 있는 svc_pt_regs 구조체 사이즈만큼 키운 후 레지스터들을 백업한다.
    • 그 외 irq disable된 기간 및 컨텍스트  트래킹 등을 수행한다. (for debug)

 

  • 코드 라인 4에서 스택을 svc_pt_regs 구조체 크기 + stack_hole – 4 만큼  확보한다.
    • stack_hole을 추가하는 이유
      • __und_svc() 레이블에서만 사용되는데 CONFIG_KPROBES 커널 옵션을 사용하고 kprobe를 이용하여 디버깅을 할 때 “stmdb sp!, {…}” 등의 문장에서 single step으로 디버깅을 하면 스택이 깨지는 문제가 발생하여 그러한 경우를 피하고자 64바이트(멀티 store 명령으로 최대 16개 레지스터를 저장할 수 있는 공간 크기)의 hole을 더 준비하였다.
      • 참고: ARM kprobes: don’t let a single-stepped stmdb corrupt the exception stack
    • -4를 하는 이유
      • r0를 제외한 스택 주소가 아래에서 설명하는 AEABI 규격으로 인해 정렬되어야 한다.루틴의 마지막 즈음에서 4 바이트를 증가시켜 짝수 워드로 정렬시킬 예정이므로 지금은 홀수 워드로 정렬되어야 한다.
  • 코드 라인 5~6에서 AEABI(ARM Embedded Application Binary Interface) 규격에 맞게 스택을 사용 시 64비트(8 바이트) 정렬을 해야 한다.
  • 코드 라인 7에서 r1에서 r12까지 레지스터를 모두 스택에 확보된 pt_regs에서 r1 위치부터 저장한다.
  • 코드 라인 9에서 기존 루틴에서 미니 스택에 저장해 놓은 old r0, 교정된 lr, spsr 값을 r3~r5 레지스터에 읽어 온다.
  • 코드 라인 10에서 r7 레지스터가 pt_regs의 sp 주소를 가리키게 한다.
  • 코드 라인 11에서 r6 레지스터에 -1을 대입한다.
  • 코드 라인 12~13에서 r2 레지스터가 stack_hole을 가리키게 한다. 만일 sp가 64비트 정렬을 한 경우라면 stack_hole 위치도 4 바이트만큼 위로 올린다. (stack_hole이 4바이트 커진다.)
  • 코드 라인 14에서 original r0를 읽어온 r3 레지스터의 내용을 pt_regs의 r0 주소에 저장한다. sp 주소는 4바이트 주소를 밑으로 이동시켜 정상적으로 sp가 pt_regs의 처음을 가리키게 한다.
  • 코드 라인 17~28에서 r3 레지스터에 lr_svc를 대입하고 sp_svc, lr_svc, lr_<exception>, spsr_<exception>, original r0 값이 담긴 r2~r6 레지스터 값을 pt_regs의 sp 주소부터 저장한다.
  • 코드 라인 30~34에서 현재 태스크의 thread_info.addr_limit 값을 읽어 스택에 위치한 svc_pt_regs.addr_limit에 백업하고, TASK 사이즈로 변경한다.
  • 코드 라인 36~39에서 유저 영역의 액세스 여부를 svc_pt_regs.dacr 에 백업하고, @uaccess가 요청된 경우 보안을 위해 커널에서 유저 영역의 액세스를 제한한다.
  • 코드 라인 41~44에서  CONFIG_TRACE_IRQFLAGS 커널 옵션이 사용되는 경우  얼마나 오랫동안 인터럽트가 disable 되었는지 그 주기를 알아보기 위한 트래킹 디버깅을 수행한다.

다음 그림과 같이 커널이 v4.9-rc1으로 버전업되면서 svc 모드에서 진입된 레지스터들을 백업하는데 기존 pt_regs를 사용하지 않고 svc_pt_regs를 사용한다.

 

레지스터 복구하고 서비스 모드로 복귀

svc_exit 매크로

arch/arm/kernel/entry-header.S (THUMB 코드 생략)

        .macro  svc_exit, rpsr, irq = 0
        .if     \irq != 0
        @ IRQs already off
#ifdef CONFIG_TRACE_IRQFLAGS
        @ The parent context IRQs must have been enabled to get here in
        @ the first place, so there's no point checking the PSR I bit.
        bl      trace_hardirqs_on
#endif
        .else
        @ IRQs off again before pulling preserved data off the stack
        disable_irq_notrace
#ifdef CONFIG_TRACE_IRQFLAGS
        tst     \rpsr, #PSR_I_BIT
        bleq    trace_hardirqs_on
        tst     \rpsr, #PSR_I_BIT
        blne    trace_hardirqs_off
#endif
        .endif
        ldr     r1, [sp, #SVC_ADDR_LIMIT]
        uaccess_restore
        str     r1, [tsk, #TI_ADDR_LIMIT]

        @ ARM mode SVC restore
        msr     spsr_cxsf, \rpsr
#if defined(CONFIG_CPU_V6) || defined(CONFIG_CPU_32v6K)
        @ We must avoid clrex due to Cortex-A15 erratum #830321
        sub     r0, sp, #4                      @ uninhabited address
        strex   r1, r2, [r0]                    @ clear the exclusive monitor
#endif
        ldmia   sp, {r0 - pc}^                  @ load r0 - pc, cpsr
        .endm

exception 전의 svc 모드로 다시 복귀하기 위해 백업해두었던 레지스터들을 복구한다.

  • 코드 라인 2~7에서 irq exception에 대한 ISR을 수행 후 종료 전에 hard irq에 대한 latency 트래킹을 수행한다.
    • __irq_svc에서 irq=1로 호출된다.
  • 코드 라인 9~18에서 irq=0으로 설정되는 경우 현재 cpu에 대해 irq를 mask하여 인터럽트가 진입하지 못하게 한다. @rpsr은 exception 되었을 때 SPSR 값이 담겨있다. 즉 복귀 전 모드에서 irq가 enable 상태인 경우 hardirq에 대한 trace on을 수행하고, 그렇지 않은 경우 trace off를 수행한다.
  • 코드 라인 19에서 스택에 위치한 svc_pt_regs.addr_limit 값을 r1 레지스터에 알아온다.
  • 코드 라인 20에서 커널에서 유저 모드 액세스 여부에 대한 값을 스택으로부터 복구한다.
  • 코드 라인 21에서 읽어온 addr_limit 값을 태스크의 thread_info.addr_limit에 기록한다.
  • 코드 라인 24에서 spsr에 \rpsr을 대입한다.
  • 코드 라인 25~29에서 strex로 clrex 기능을 수행한다. (erratum for Cortex-A15)
  • 코드 라인 30에서 스택으로부터 r0~pc까지 레지스터를 복구한다.
    • pc 위치에 이미 correction된 lr을 백업했었다.

 

허용하지 않은 모드에서 진입 시 레지스터 백업

inv_entry

arch/arm/kernel/entry-armv.S

/*
 * Invalid mode handlers
 */
        .macro  inv_entry, reason
        sub     sp, sp, #PT_REGS_SIZE
 ARM(   stmib   sp, {r1 - lr}           )
 THUMB( stmia   sp, {r0 - r12}          )
 THUMB( str     sp, [sp, #S_SP]         )
 THUMB( str     lr, [sp, #S_LR]         )
        mov     r1, #\reason
        .endm

스택에 레지스터들을 백업하기 위한 공간(struct pt_regs)을 확보하고  r1~r14(lr)까지 백업해둔다. r1 레지스터에는 reason 값을 대입한다.

    • pt_regs에 백업되지 않은 나머지 레지스터들은 common_invalid 레이블에서 백업을 계속 진행한다.

 

 

common_invalid

arch/arm/kernel/entry-armv.S

@
@ common_invalid - generic code for failed exception (re-entrant version of handlers)
@
common_invalid:
        zero_fp

        ldmia   r0, {r4 - r6}
        add     r0, sp, #S_PC           @ here for interlock avoidance
        mov     r7, #-1                 @  ""   ""    ""        ""
        str     r4, [sp]                @ save preserved r0
        stmia   r0, {r5 - r7}           @ lr_<exception>,
                                        @ cpsr_<exception>, "old_r0"

        mov     r0, sp
        b       bad_mode
ENDPROC(__und_invalid)

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

  • 코드 라인 2에서 fp 레지스터에 0을 대입한다.
  • 코드 라인 4에서 스택에 백업해두었던 old r0, 교정된 lr, spsr 값을 r4~r6 레지스터로 읽어온다.
  • 코드 라인 5에서 스택에 백업해두었던 pt_regs 구조체 영역에서 pc 주소를 r0에 대입한다.
  • 코드 라인 6에서 r7에 -1을 대입한다.
  • 코드 라인 7에서 old r0 값을 읽어온 r4 레지스터 값을 pt_regs의 가장 첫 위치 r0에 저장한다.
  • 코드 라인 8에서 교정된 lr, spsr, -1 값을 담고 있는 r5~r7 레지스터 값을 스택의 pt_regs 위치 중 old r0, cpsr, r15(pc) 주소에 저장한다.
  • 코드 라인 11에서 스택 값을 r0에 대입한다.
  • 코드 라인 12에서 bad_mode 레이블로 이동하여 “Oops” 출력 및 panic() 처리한다.

 

 

bad_mode()

arch/arm/kernel/traps.c

/*
 * bad_mode handles the impossible case in the vectors.  If you see one of
 * these, then it's extremely serious, and could mean you have buggy hardware.
 * It never returns, and never tries to sync.  We hope that we can at least
 * dump out some state information...
 */
asmlinkage void bad_mode(struct pt_regs *regs, int reason, unsigned int esr)
{
        console_verbose();

        pr_crit("Bad mode in %s handler detected on CPU%d, code 0x%08x -- %s\n",
                handler[reason], smp_processor_id(), esr,
                esr_get_class_string(esr));

        local_daif_mask();
        panic("bad mode");
}

허용하지 않은 모드에서 진입 시 “Oops – bad mode”를 출력하고 panic 처리 한다.

  • /proc/sys/kernel/panic” 초(secs) 만큼 대기하였다가 셀프 부팅한다. (0인 경우 그냥 부팅없이 정지해있다.)

 

kuser_cmpxchg_check

arch/arm/kernel/entry-armv.S

        .macro  kuser_cmpxchg_check
#if !defined(CONFIG_CPU_32v6K) && defined(CONFIG_KUSER_HELPERS) && \
    !defined(CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG)
#ifndef CONFIG_MMU
#warning "NPTL on non MMU needs fixing"
#else
        @ Make sure our user space atomic helper is restarted
        @ if it was interrupted in a critical region.  Here we
        @ perform a quick test inline since it should be false
        @ 99.9999% of the time.  The rest is done out of line.
        cmp     r4, #TASK_SIZE
        blhs    kuser_cmpxchg64_fixup
#endif
#endif
        .endm

Kernel-provided User Helper code 중 시스템이 POSIX syscall을 이용하여 cmpxchg를 이용하는 방식인 경우 커널 space에서 exception되어 이 루틴에 진입하게 되면 kuser_cmpchg64_fixup을 수행하고 온다.

  • r4에는 exception 처리 후 되돌아갈 주소(lr)이 담겨 있기 때문에 이 값을 TASK_SIZE와 비교하여 user space에서 진입하였는지 아니면 kernel space에서 진입하였는지 구분할 수 있다.
  • 참고: Kernel-provided User Helper | 문c

 

get_thread_info 매크로

include/asm/assembler.h

/*
 * Get current thread_info.
 */
        .macro  get_thread_info, rd
 ARM(   mov     \rd, sp, lsr #THREAD_SIZE_ORDER + PAGE_SHIFT    )
 THUMB( mov     \rd, sp                 )
 THUMB( lsr     \rd, \rd, #THREAD_SIZE_ORDER + PAGE_SHIFT       )
        mov     \rd, \rd, lsl #THREAD_SIZE_ORDER + PAGE_SHIFT
        .endm

현재 프로세스의 스택 하위에 위치한 thread_info 객체의 주소를 인수 @rd에 반환한다.

 


IRQ 핸들러

arm 아키텍처는 nested 인터럽트를 처리할 수 있게 설계되어 있다. 그러나 현재 까지 arm 리눅스에서는 FIQ를 제외하고는 중첩하여 처리하지 못하게 하였다. 당연히 arm 리눅스가 아닌 펌웨어 레벨의 os에서는 사용자가 nested 인터럽트를 구현하여 사용할 수 있다.

 

다음 그림은 irq exception이 발생한 후 인터럽트 컨트롤러의 인터럽트 핸들러까지 흐름을 보여준다.

  • CONFIG_GENERIC_IRQ_MULTI_HANDLER
    • 런타임에 등록하는 multi irq 핸들러이다.
    • Device Tree를 사용하는 경우 런타임에 해당 인터럽트 컨트롤러의 초기화 및 각 인트럽트에 대한 처리 방법 및 핸들러등을 준비해야 한다.
    • 전역 handle_arch_irq 변수는 set_handle_irq() 함수에 의해 핸들러 주소가 설정된다.
      • rpi2: Device Tree용 armctrl_of_init() 초기화 함수에서 두 개의 인터럽트 컨트롤러 초기화 함수를 호출하는데 그 중 부모 인터럽트 콘트롤러를 초기화할 때 핸들러로 bcm2836_arm_irqchip_handle_irq() 함수가 설정된다.

 

__irq_usr

arch/arm/kernel/entry-armv.S

        .align  5
__irq_usr:
        usr_entry
        kuser_cmpxchg_check
        irq_handler
        get_thread_info tsk
        mov     why, #0
        b       ret_to_user_from_irq
 UNWIND(.fnend          )
ENDPROC(__irq_usr)

유저 태스크 처리 중 인터럽트가 발생되어 irq exception 모드에서 3개의 레지스터를 3 word로 구성된 mini stack에 백업한 후 svc 모드에 진입하였고 그 이후 이 레이블에 진입을하면 관련 인터럽트 번호에 등록되어 있는 1개 이상의 ISR(Interrupt Service Routing)을 호출한다.

  • 코드 라인 3에서 전체 레지스터를 스택에 백업한다.
  • 코드 라인 4에서 atomic 연산을 지원하지 못하는 아키텍처에서 atomic 하게 처리해야 하는 구간에서 인터럽트를 맞이하고 복귀할 때 그 atomic operation 구간의 시작부분으로 다시 돌아가도록 pt_regs의 pc를 조작한다.
  • 코드 라인 5에서 관련 인터럽트 번호에 등록되어 있는 1개 이상의 ISR(Interrupt Service Routing)을 호출한다
  • 코드 라인 6에서 tsk 레지스터에 thread_info 객체의 주소를 알아온다.
  • 코드 라인 7~8에서  스택에 백업해둔 레지스터들을 다시 불러 읽은 후 user 모드로 복귀한다.

 

__irq_svc

arch/arm/kernel/entry-armv.S

        .align  5
__irq_svc:
        svc_entry
        irq_handler

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

        svc_exit r5, irq = 1                    @ return from exception
 UNWIND(.fnend          )
ENDPROC(__irq_svc)

커널 처리 중 인터럽트가 발생되어 irq exception 모드에서 3개의 레지스터를 3 word로 구성된 mini stack에 백업한 후 svc 모드에 진입하였고 그 이후 이 레이블에 진입을하면 관련 인터럽트 번호에 등록되어 있는 1개 이상의 처리 핸들러를 호출한다.

  • 코드 라인 3에서 전체 레지스터를 포함한 svc_pt_regs 구조체를 스택에 백업한다.
  • 코드 라인 4에서 관련 인터럽트 번호에 등록되어 있는 1개 이상의 ISR(Interrupt Service Routing)을 호출한다
  • 코드 라인 7~12에서 preempt 커널에서 현재 프로세스 컨텍스트가 preempt 허용(preempt 카운트가 0) 상태인 경우에 한해 리스케쥴(_TIF_NEED_RESCHED) 요청이 있는 경우 리스케쥴을 위해 svc_preempt 레이블로 이동한다.
    • thread_info->preempt가 0인 경우 preempt 가능한 상태이다.
    • flags에 _TIF_NEED_RESCHED 설정된 경우 리스케쥴 요청이 들어온 경우이다.
  • 코드 라인 15에서 스택에 백업해둔 레지스터들을 다시 불러 읽은 후 svc 모드로 복귀한다.

 

svc_preempt 레이블

arch/arm/kernel/entry-armv.S

#ifdef CONFIG_PREEMPT
svc_preempt:
        mov     r8, lr
1:      bl      preempt_schedule_irq            @ irq en/disable is done inside
        ldr     r0, [tsk, #TI_FLAGS]            @ get new tasks TI_FLAGS
        tst     r0, #_TIF_NEED_RESCHED
        reteq   r8                              @ go again
        b       1b
#endif

더 이상의 리스케쥴 요청이 없을 때까지 preemption 리스케쥴을 수행한다.

  • 코드 라인 3에서 r8 레지스터에 복귀할 주소를 담고 있는 lr 레지스터를 백업해둔다.
  • 코드 라인 4에서 현 태스크의 preemption을 포함한 리스케쥴을 수행한다.
    • 리스케쥴되어 현재 태스크보다 더 우선 순위 높은 태스크가 실행되는 경우 현재의 태스크는 잠든다. 그 후 깨어난 후 계속 진행한다.
  • 코드 라인 5~8에서 현재 프로세서 컨텍스트에 리스케쥴 요청이 있으면 다시 1: 레이블로 이동하여 계속 처리하고, 리스케쥴 요청이 없으면 루틴을 끝마치고 복귀한다.

 

irq_handler 매크로

arch/arm/kernel/entry-armv.S

/*
 * Interrupt handling.
 */
        .macro  irq_handler
#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
        ldr     r1, =handle_arch_irq
        mov     r0, sp
        badr    lr, 9997f
        ldr     pc, [r1]
#else
        arch_irq_handler_default
#endif
9997:
        .endm

아키텍처에 따른 irq 핸들러를 호출한다.

  • CONFIG_MULTI_IRQ_HANDLER 커널 옵션을 사용 여부에 따라
  • 설정한 경우 커널이 여러 개의 머신에 해당하는 IRQ 핸들러 함수를 같이 컴파일하고 실제 커널 부팅 시 IRQ 관련하여 선택한 호출 함수를 handle_arch_irq에 저장하여야 한다.
  • 설정하지 않은 경우 현재 커널이 IRQ 처리 방식을 고정한 경우로 컴파일 시 결정된 arch_irq_handler_default 매크로를 호출한다.

 

인터럽트 핸들러 for rpi2

 

ret_to_user_from_irq 및 no_work_pending 레이블

arch/arm/kernel/entry-common.S

ENTRY(ret_to_user_from_irq)
        ldr     r2, [tsk, #TI_ADDR_LIMIT]
        cmp     r2, #TASK_SIZE
        blne    addr_limit_check_failed
        ldr     r1, [tsk, #TI_FLAGS]
        tst     r1, #_TIF_WORK_MASK
        bne     slow_work_pending
no_work_pending:
        asm_trace_hardirqs_on save = 0

        /* perform architecture specific actions before user return */
        arch_ret_to_user r1, lr
        ct_user_enter save = 0

        restore_user_regs fast = 0, offset = 0
ENDPROC(ret_to_user_from_irq)

irq 처리를 마치고 유저로 복귀하기 전에 pending된 작업이 있으면 수행한 후 백업해 두었던 레지스터들을 다시 읽어 들인 후 복귀한다.

  • 코드 라인 2~4에서 thread_info->addr_limit값이 태스크 사이즈를 초과하는 경우 addr_limit_check_failed 레이블로 이동한다.
  • 코드 라인 5~7에서 thread_info->flags를 검사하여 _TIF_WORK_MASK에 속한 플래그가 있는 경우 slow_work_pending 레이블로 이동한다.
    • _TIF_WORK_MASK
      • _TIF_NEED_RESCHED | _TIF_SIGPENDING | _TIF_NOTIFY_RESUME | _TIF_UPROBE
  • 코드 라인 12에서 user process로 다시 복귀 전에 처리할 아키텍처 specific한 일이 있는 경우 수행한다.
    • rpi2: 없음
  • 코드 라인 13에서 CONFIG_CONTEXT_TRACKING 커널 옵션을 사용한 context 트래킹 디버그가 필요한 경우 수행한다.
  • 코드 라인 15에서 백업해 두었던 레지스터들을 읽어들이며 다시 user space로 복귀한다.

 

지연 작업 처리

slow_work_pending

arch/arm/kernel/entry-common.S

slow_work_pending:
        mov     r0, sp                          @ 'regs'
        mov     r2, why                         @ 'syscall'
        bl      do_work_pending
        cmp     r0, #0
        beq     no_work_pending
        movlt   scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE)
        ldmia   sp, {r0 - r6}                   @ have to reload r0 - r6
        b       local_restart                   @ ... and off we go
ENDPROC(ret_fast_syscall)

지연된 작업이 있는 경우 완료 시킨 후 no_work_pending 레이블로 이동하는데 pending 시그널을 처리하다 restart 에러가 발생한 경우 local_restart 레이블로 이동한다.

  • 코드 라인 2~4에서 do_work_pending() 함수를 호출하여 지연된 작업을 수행한다.
  • 코드 라인 5~6에서 더 이상 할 작업이 없는 경우 no_work_pending 레이블로 이동한다.
  • 코드 라인 7에서 결과가 음수인 경우 scno에 sys_restart_syscall() 함수를 처리하기 위한 syscall 인덱스 번호를 대입한다.
    • EABI(Embedded Application Binary Interface)를 사용하는 경우 0x0 – 0x0을 적용하여 0이된다.
    • OABI(Old Application Binary Interface)를 사용하는 경우 0x90000 – 0x90000을 적용하여 0이된다.
  • 코드 라인 8~9에서 스택으로부터 r0~r6 레지스터에 로드한 후 local_restart 레이블로 이동한다.

 

do_work_pending()

arch/arm/kernel/signal.c

asmlinkage int
do_work_pending(struct pt_regs *regs, unsigned int thread_flags, int syscall)
{
        /*
         * The assembly code enters us with IRQs off, but it hasn't
         * informed the tracing code of that for efficiency reasons.
         * Update the trace code with the current status.
         */
        trace_hardirqs_off();
        do {
                if (likely(thread_flags & _TIF_NEED_RESCHED)) {
                        schedule();
                } else {
                        if (unlikely(!user_mode(regs)))
                                return 0;
                        local_irq_enable();
                        if (thread_flags & _TIF_SIGPENDING) {
                                int restart = do_signal(regs, syscall);
                                if (unlikely(restart)) {
                                        /*
                                         * Restart without handlers.
                                         * Deal with it without leaving
                                         * the kernel space.
                                         */
                                        return restart;
                                }
                                syscall = 0;
                        } else if (thread_flags & _TIF_UPROBE) {
                                uprobe_notify_resume(regs);
                        } else {
                                clear_thread_flag(TIF_NOTIFY_RESUME);
                                tracehook_notify_resume(regs);
                                rseq_handle_notify_resume(NULL, regs);
                        }
                }
                local_irq_disable();
                thread_flags = current_thread_info()->flags;
        } while (thread_flags & _TIF_WORK_MASK);
        return 0;
}

지연된 작업이 있는 경우 완료될 때까지 처리한다. 정상 처리되면 0을 반환하는데 만일 pending 시그널을 처리하다 에러가 발생한 경우 restart 값을 반환한다.

  • 코드 라인 9에서 harirq 트레이스를 off한다.
  • 코드 라인 11~12에서 현재 태스크의 플래그들 중 리스케쥴 요청(_TIF_NEED_RESCHED 플래그)이 있는 경우 리스케쥴한다.
    •  인터럽트 처리를 빠져나가기 전에 이 코드가 실제 preemption 요청을 처리하는 곳이다.
  • 코드 라인 14~15에서 낮은 확률로 유저 모드 진입이 아닌 경우 그냥 빠져나간다.
  • 코드 라인 17~27에서 pending 시그널(_TIF_SIGPENDING 플래그) 이 있는 경우 시그널을 처리한다. 만일 restart 응답을 받은 경우 restart 값으로 함수를 빠져나간다.
  • 코드 라인 28~29에서 uprobe break point가 hit(_TIF_UPROBE 플래그)되어 진입된 경우이었던 경우 resume 관련 처리를 수행한다.
  • 코드 라인 30~34에서 4개의 pending 관련 비트들 중 마지막 플래그 TIF_NOTIFY_RESUME를 클리어한다. 이 플래그는 user로 되돌아가기 전에 호출할 콜백함수들을 수행하게 한다.
    • task_work_add() 함수에서 인수 notify가 설정되어 요청한 경우 task->task_works 리스트에 추가된 콜백 함수들을 수행한다.
  • 코드 라인 37~38에서 현재 프로세서의 플래그에 pending 작업이 존재한다고 표시된 경우 계속 루프를 돈다.
  • 코드 라인 39에서 펜딩 작업이 더 이상 없으므로 0을 반환한다.

 

restore_user_regs 매크로

arch/arm/kernel/entry-header.S (THUMB 코드 생략)

        .macro  restore_user_regs, fast = 0, offset = 0
        uaccess_enable r1, isb=0
        @ ARM mode restore
        mov     r2, sp
        ldr     r1, [r2, #\offset + S_PSR]      @ get calling cpsr
        ldr     lr, [r2, #\offset + S_PC]!      @ get pc
        tst     r1, #PSR_I_BIT | 0x0f
        bne     1f
        msr     spsr_cxsf, r1                   @ save in spsr_svc
#if defined(CONFIG_CPU_V6) || defined(CONFIG_CPU_32v6K)
        @ We must avoid clrex due to Cortex-A15 erratum #830321
        strex   r1, r2, [r2]                    @ clear the exclusive monitor
#endif
        .if     \fast
        ldmdb   r2, {r1 - lr}^                  @ get calling r1 - lr
        .else
        ldmdb   r2, {r0 - lr}^                  @ get calling r0 - lr
        .endif
        mov     r0, r0                          @ ARMv5T and earlier require a nop
                                                @ after ldm {}^
        add     sp, sp, #\offset + PT_REGS_SIZE
        movs    pc, lr                          @ return & move spsr_svc into cpsr
1:      bug     "Returning to usermode but unexpected PSR bits set?", \@
#elif defined(CONFIG_CPU_V7M)
        @ V7M restore.
        @ Note that we don't need to do clrex here as clearing the local
        @ monitor is part of the exception entry and exit sequence.
        .if     \offset
        add     sp, #\offset
        .endif
        v7m_exception_slow_exit ret_r0 = \fast
        .endm

백업해 두었던 레지스터들을 읽어들인 후 다시 user space로 복귀한다.

  • 코드 라인 2에서 커널의 유저 액세스를 허용하게 한다.
  • 코드 라인 4~9에서 스택에 백업해둔 pt_regs의 cpsr을 r1 레지스터를 통해 spsr 레지스터에 저장하고, pc 값을 lr 레지스터에 대입한다.
    • 만일 백업한 cpsr의 irq 비트가 마스크되어 있는 경우 1f 레이블에서 버그 출력을 한다.
  • 코드 라인 12에서 Cortex-A15 아키텍처에서 strex를 사용하여 clrex를 대신하였다.
  • 코드 라인 14~18에서 fast 요청이 있는 경우 스택에 백업해 둔 pt_regs의 r1~lr 까지의 레지스터를 읽어오고 fast 요청이 아닌 경우 r0 레지스터를 포함해서 불러온다.
  • 코드 라인 19에서 ARMv5T 및 그 이전 arm 아키텍처에서 multiple load 명령을 사용 후 nop을 사용해야 한다.
  • 코드 라인 21~22에서 스택에서 pt_regs를 제외시킨 후 user space로 복귀한다.

 

preempt_schedule_irq()

kernel/sched/core.c

/*
 * this is the entry point to schedule() from kernel preemption
 * off of irq context.
 * Note, that this is called and return with irqs disabled. This will
 * protect us against recursive calling from irq.
 */
asmlinkage __visible void __sched preempt_schedule_irq(void)
{
        enum ctx_state prev_state;

        /* Catch callers which need to be fixed */
        BUG_ON(preempt_count() || !irqs_disabled());

        prev_state = exception_enter();

        do {
                preempt_disable();
                local_irq_enable();
                __schedule(true);
                local_irq_disable();
                sched_preempt_enable_no_resched();
        } while (need_resched());

        exception_exit(prev_state);
}

리스케쥴 요청이 있는 동안 인터럽트는 허용하되 preemption은 허용되지 않게 한 후 스케쥴한다.

  • 코드 라인 8에서 리스케쥴 전에 context 트래킹 디버깅을 위한 선 처리 작업을 한다.
  • 코드 라인 10~14에서 인터럽트는 허용하되 preemption은 허용되지 active 구간을 증가시킨다. 않게 한 후 스케쥴한다. 완료 후 다시 인터럽트를 막고 preemption이 허용되도록 active 구간을 감소시킨다.
  • 코드 라인 15~16에서 리스케쥴 요청이 없을 때까지 반복한다.
  • 코드 라인 18에서리스케쥴이 완료되었으므로 context 트래킹 디버깅을 위한 후 처리 작업을 한다.

 


FIQ 핸들러

 

__fiq_usr

arch/arm/kernel/entry-armv.S

        .align  5
__fiq_usr:
        usr_entry trace=0
        kuser_cmpxchg_check
        mov     r0, sp                          @ struct pt_regs *regs
        bl      handle_fiq_as_nmi
        get_thread_info tsk
        restore_user_regs fast = 0, offset = 0
 UNWIND(.fnend          )
ENDPROC(__fiq_usr)

유저모드에서 fiq가 발생되어 fiq exception 모드로 진입하였고, 3개의 레지스터를 3 word로 구성된 mini stack에 백업한 후 svc 모드에 진입한 상황이다. 그 이후 이 레이블에 진입하여 관련 fiq 핸들러를 호출한다. (ARM에서는 기본적으로 fiq 관련 처리는 비어있는 상태이다)

  • 코드 라인 3에서 전체 레지스터를 스택에 백업한다. 속도를 중시하므로 trace를 제한한다.
  • 코드 라인 4에서 atomic 연산을 지원하지 못하는 아키텍처에서 atomic 하게 처리해야 하는 구간에서 인터럽트를 맞이하고 복귀할 때 그 atomic operation 구간의 시작부분으로 다시 돌아가도록 pt_regs의 pc를 조작한다.
  • 코드 라인 5~6에서 fiq 관련 등록된 ISR을 수행한다.
  • 코드 라인 7에서 tsk 레지스터에 thread_info 객체의 주소를 알아온다.
  • 코드 라인 8에서 스택에 백업해둔 레지스터들을 다시 불러 읽은 후 user 모드로 복귀한다.

 

handle_fiq_as_nmi()

arch/arm/kernel/traps.c

/*
 * Handle FIQ similarly to NMI on x86 systems.
 *
 * The runtime environment for NMIs is extremely restrictive
 * (NMIs can pre-empt critical sections meaning almost all locking is
 * forbidden) meaning this default FIQ handling must only be used in
 * circumstances where non-maskability improves robustness, such as
 * watchdog or debug logic.
 *
 * This handler is not appropriate for general purpose use in drivers
 * platform code and can be overrideen using set_fiq_handler.
 */
asmlinkage void __exception_irq_entry handle_fiq_as_nmi(struct pt_regs *regs)
{
        struct pt_regs *old_regs = set_irq_regs(regs);

        nmi_enter();

        /* nop. FIQ handlers for special arch/arm features can be added here. */

        nmi_exit();

        set_irq_regs(old_regs);
}

arm 아키텍처에서는 fiq 처리를 위한 루틴이 비어있다. 필요한 경우 임베디드 개발자가 추가하게 되어 있다.

 

nmi_enter()

include/linux/hardirq.h

#define nmi_enter()                                             \
        do {                                                    \
                arch_nmi_enter();                               \
                printk_nmi_enter();                             \
                lockdep_off();                                  \
                ftrace_nmi_enter();                             \
                BUG_ON(in_nmi());                               \
                preempt_count_add(NMI_OFFSET + HARDIRQ_OFFSET); \
                rcu_nmi_enter();                                \
                trace_hardirq_enter();                          \
        } while (0)

nmi 처리 시작 진입점에서 수행해야 할 일들을 처리한다. preempt count를 증가시켜 preemption되지 않도록 하고 nmi가 진행 중임을 rcu가 알 수 있도록 한다.

  • 코드 라인 3에서 nmi 처리 직전 아키텍처 specific 한 처리가 필요할 때 수행한다. (arm32는 없음)
  • 코드 라인 4에서 CONFIG_PRINTK_NMI를 지원하는 시스템에서 현재 cpu의 printk_context에 PRINTK_NMI_CONTEXT_MASK 마스크를 추가한다.
  • 코드 라인 5에서 lockdep 디버그를 끈다.
  • 코드 라인 6에서 nmi 처리 전에 대한 ftrace 트래킹을 지원한다.
  • 코드 라인 7에서 nmi 처리 중에 재진입된 경우 버그 처리한다.
  • 코드 라인 8에서 nmi 및 hard irq에 대한 preempt 카운터를 증가시킨다.
  • 코드 라인 9에서 nmi 처리 중인 것을 RCU도 알아야하기 때문에 &rcu_dynticks->dynticks를 1로 설정한다.
  • 코드 라인 10에서 nmi 처리 전 hard irq 처리에 대한 trace 출력을 한다.

 

nmi_exit()

include/linux/hardirq.h

#define nmi_exit()                                              \
        do {                                                    \
                trace_hardirq_exit();                           \
                rcu_nmi_exit();                                 \
                BUG_ON(!in_nmi());                              \
                preempt_count_sub(NMI_OFFSET + HARDIRQ_OFFSET); \
                ftrace_nmi_exit();                              \
                lockdep_on();                                   \
                printk_nmi_exit();                              \
                arch_nmi_exit();                                \
        } while (0)

nmi 처리 종료 후 수행해야 할 일들을 처리한다. rcu도 종료를 알 수 있도록 보고하고, 처리 전 증가된 preempt count도 감소시킨다.

  • 코드 라인 3에서 nmi 처리 후 hard irq 처리에 대한 trace 출력을 한다.
  • 코드 라인 4에서 nmi 처리 완료된 것을 RCU도 알아야하기 때문에 &rcu_dynticks->dynticks를 0으로 클리어한다.
  • 코드 라인 5에서 여전히 nmi 처리 중인 경우 버그 처리한다.
  • 코드 라인 6에서 nmi 및 hard irq에 대한 preempt 카운터를 감소시킨다.
  • 코드 라인 7에서 nmi 처리 완료에 대한 ftrace 트래킹을 지원한다.
  • 코드 라인 8에서 lockdep 디버그를 다시 켠다.
  • 코드 라인 9에서 CONFIG_PRINTK_NMI를 지원하는 시스템에서 현재 cpu의 printk_context에 PRINTK_NMI_CONTEXT_MASK 마스크를 제거한다.
  • 코드 라인 10에서 nmi 처리 완료 후 아키텍처 specific 한 처리가 필요할 때 수행한다. (arm32는 없음)

 

set_irq_regs()

include/asm-generic/irq_regs.h

static inline struct pt_regs *set_irq_regs(struct pt_regs *new_regs)
{
        struct pt_regs *old_regs;

        old_regs = __this_cpu_read(__irq_regs);
        __this_cpu_write(__irq_regs, new_regs);
        return old_regs;
}

pt_regs 레지스터들을 백업하고 기존 레지스터들을 반환한다.

 

__fiq_svc

arch/arm/kernel/entry-armv.S

        .align  5
__fiq_svc:
        svc_entry trace=0
        mov     r0, sp                          @ struct pt_regs *regs
        bl      handle_fiq_as_nmi
        svc_exit_via_fiq
 UNWIND(.fnend          )
ENDPROC(__fiq_svc)

커널 처리 중 fiq가 발생되어 fiq exception 모드로 진입하였고, 3개의 레지스터를 3 word로 구성된 mini stack에 백업한 후 svc 모드에 진입한 상황이다. 그 이후 이 레이블에 진입을하여 관련 ifiq 핸들러를 호출한다. (ARM에서는 기본적으로 fiq 관련 처리는 비어있는 상태이다)

  • 코드 라인 3에서 전체 레지스터를 스택에 백업한다. 속도를 중시하므로 trace를 제한한다.
  • 코드 라인 4~5에서 fiq 관련 등록된 ISR을 수행한다.
  • 코드 라인 6에서 스택에 백업해둔 레지스터들을 다시 불러 읽은 후 exception 전 해당 모드로 복귀한다.

 

svc_exit_via_fiq 매크로

arch/arm/kernel/entry-header.S (THUMB 코드 생략)

.       @
        @ svc_exit_via_fiq - like svc_exit but switches to FIQ mode before exit
        @
        @ This macro acts in a similar manner to svc_exit but switches to FIQ
        @ mode to restore the final part of the register state.
        @
        @ We cannot use the normal svc_exit procedure because that would
        @ clobber spsr_svc (FIQ could be delivered during the first few
        @ instructions of vector_swi meaning its contents have not been
        @ saved anywhere).
        @
        @ Note that, unlike svc_exit, this macro also does not allow a caller
        @ supplied rpsr. This is because the FIQ exceptions are not re-entrant
        @ and the handlers cannot call into the scheduler (meaning the value
        @ on the stack remains correct).
        @
.       .macro  svc_exit_via_fiq
        ldr     r1, [sp, #SVC_ADDR_LIMIT]
        uaccess_restore
        str     r1, [tsk, #TI_ADDR_LIMIT]
        @ ARM mode restore
        mov     r0, sp
        ldmib   r0, {r1 - r14}  @ abort is deadly from here onward (it will
                                @ clobber state restored below)
        msr     cpsr_c, #FIQ_MODE | PSR_I_BIT | PSR_F_BIT
        add     r8, r0, #S_PC
        ldr     r9, [r0, #S_PSR]
        msr     spsr_cxsf, r9
        ldr     r0, [r0, #S_R0]
        ldmia   r8, {pc}^
        .endm

exception 전의 모드로 다시 복귀하기 위해 백업해두었던 레지스터들을 복구한다. (종료 전에 fiq 모드로 switch하는 것만 제외하고 svc_exit와 유사하다)

  • 코드 라인 2에서 스택에 저장된 svc_pt_regs.addr_limit 값을 r1 레지스터에 읽어온다.
  • 코드 라인 3에서 스택에 저장된 유저 액세스 여부를 백업한다.
  • 코드 라인 4에서 읽어온 addr_limit 값을 태스크에 기록한다.
  • 코드 라인 6~7에서 스택으로 부터 r1 ~ r14 레지스터까지 복구한다.
  • 코드 라인 9에서 irq, fiq bit를 마스크한 상태로 fiq mode로 진입한다.
  • 코드 라인 10에서 r8에 스택에서 pt_regs의 pc 값을 읽어온다.
    • 가장 마지막에 복귀할 주소가 담긴다.
  • 코드 라인 11~12에서 스택에서 pt_regs의 psr 값을 읽어 spsr에 대입하여 기존 모드로 복귀한다.
  • 코드 라인 13에서 다시 스택에서 pt_regs의 r0 값을 읽어 r0 레지스터에 복구한다.
  • 코드 라인 14에서 복귀할 주소가 담긴 r8레지스터 값 주소로 jump 한다.

 

__fiq_abt

arch/arm/kernel/entry-armv.S (THUMB 코드 생략)

/*
 * Abort mode handlers
 */

@
@ Taking a FIQ in abort mode is similar to taking a FIQ in SVC mode
@ and reuses the same macros. However in abort mode we must also
@ save/restore lr_abt and spsr_abt to make nested aborts safe.
@
        .align 5
__fiq_abt:
        svc_entry trace=0

 ARM(   msr     cpsr_c, #ABT_MODE | PSR_I_BIT | PSR_F_BIT )
        mov     r1, lr          @ Save lr_abt
        mrs     r2, spsr        @ Save spsr_abt, abort is now safe
 ARM(   msr     cpsr_c, #SVC_MODE | PSR_I_BIT | PSR_F_BIT )
        stmfd   sp!, {r1 - r2}

        add     r0, sp, #8                      @ struct pt_regs *regs
        bl      handle_fiq_as_nmi

        ldmfd   sp!, {r1 - r2}
 ARM(   msr     cpsr_c, #ABT_MODE | PSR_I_BIT | PSR_F_BIT )
        mov     lr, r1          @ Restore lr_abt, abort is unsafe
        msr     spsr_cxsf, r2   @ Restore spsr_abt
 ARM(   msr     cpsr_c, #SVC_MODE | PSR_I_BIT | PSR_F_BIT )

        svc_exit_via_fiq
 UNWIND(.fnend          )
ENDPROC(__fiq_abt)

abt 모드에서 fiq exception이 발생되어 3개의 레지스터를 3 word로 구성된 mini stack에 백업한 후 svc 모드에 진입하였고 그 이후 이 레이블에 진입을하면 관련 fiq 핸들러를 수행한다. (ARM에서는 기본적으로 fiq 관련 처리는 비어있는 상태이다)

  • 코드 라인 3에서 전체 레지스터를 스택에 백업한다.
  • 코드 라인 5~9에서 abt 모드의 lr, spsr을 스택에 백업한다.
    • irq 및 fiq를 disable한 상태로 다시 abt 모드로 진입하여 abt 모드에서의 lr 및 spsr을 r1, r2 레지스터에 잠시 저장하고 svc 모드로 바꾼 후 스택에 백업한다.
  • 코드 라인 11~12에서 fiq 관련 등록된 ISR을 수행한다. (ARM에서는 기본적으로 등록되어 있지 않다)
  • 코드 라인 14~18에서 irq와 fiq를 금지한 상태로 abt 모드에 진입하고, 백업해 두었던 2개의 lr, spsr을 다시 복구한 후 svc 모드로 돌아온다.
  • 코드 라인 20에서 스택에 백업해둔 레지스터들을 다시 불러 읽은 후 exceptino 전 해당 모드로 복귀한다.

 


SWI 핸들러

arm에서 소프트 인터럽트 발생 시 호출되며 8개의 exception 벡터 주소가 있는 페이지의 바로 다음 페이지 첫 엔트리에 vector_swi 레이블 주소가 담겨 있고, 이 주소를 호출한다.

 

다음 그림은 유저 모드에서 syscall 호출 시 vector_swi 레이블부터 swi 처리에 대한 함수 호출 관계를 보여준다.

 

Fast syscall vs Slow syscall

syscall이 처리된 후 다시 유저로 복귀하기 전에 처리할 스케쥴 워크가 남아 있는 경우 이를 처리한다.  즉 곧바로 유저로 복귀하지 않고 우선 순위가 높아 먼저 처리할 태스크를 위해 스케쥴이 다시 재배치되면서 다른 태스크를 수행하고 돌아오게 된다. 이렇게 syscall 처리 후 곧바로 돌아오는 경우 fast syscall이라 하고 곧바로 돌아오지 못하고 preemption 되었다가 돌아오는 경우를 slow syscall이라한다. slow syscall 상태가 되면 유저 입장에서 보면 blocked 함수를 호출한 것처럼 느리게 처리된다.

  • Fast syscall
    • syscall 처리 후 곧바로 유저로 복귀
  • Slow syscall
    • syscall 처리 후 유저 복귀 전에 먼저 처리할 태스크를 수행하도록 스케쥴을 바꾼다.

 

vector_swi:

arch/arm/kernel/entry-common.S -1/2-

/*=============================================================================
 * SWI handler
 *-----------------------------------------------------------------------------
 */
        .align  5
ENTRY(vector_swi)
#ifdef CONFIG_CPU_V7M
        v7m_exception_entry
#else
        sub     sp, sp, #PT_REGS_SIZE
        stmia   sp, {r0 - r12}                  @ Calling r0 - r12
 ARM(   add     r8, sp, #S_PC           )
 ARM(   stmdb   r8, {sp, lr}^           )       @ Calling sp, lr
        mrs     saved_psr, spsr                 @ called from non-FIQ mode, so ok.
 TRACE( mov     saved_pc, lr            )
        str     saved_pc, [sp, #S_PC]           @ Save calling PC
        str     saved_psr, [sp, #S_PSR]         @ Save CPSR
        str     r0, [sp, #S_OLD_R0]             @ Save OLD_R0
#endif
        zero_fp
        alignment_trap r10, ip, __cr_alignment
        asm_trace_hardirqs_on save=0
        enable_irq_notrace
        ct_user_exit save=0

        /*
         * Get the system call number.
         */

#if defined(CONFIG_OABI_COMPAT)

        /*
         * If we have CONFIG_OABI_COMPAT then we need to look at the swi
         * value to determine if it is an EABI or an old ABI call.
         */
 USER(  ldr     r10, [saved_pc, #-4]    )       @ get SWI instruction
 ARM_BE8(rev    r10, r10)                       @ little endian instruction

#elif defined(CONFIG_AEABI)

        /*
         * Pure EABI user space always put syscall number into scno (r7).
         */
        /* Legacy ABI only. */
 USER(  ldr     scno, [saved_pc, #-4]   )       @ get SWI instruction
#endif

        /* saved_psr and saved_pc are now dead */

        uaccess_disable tbl

        adr     tbl, sys_call_table             @ load syscall table pointer

#if defined(CONFIG_OABI_COMPAT)
        /*
         * If the swi argument is zero, this is an EABI call and we do nothing.
         *
         * If this is an old ABI call, get the syscall number into scno and
         * get the old ABI syscall table address.
         */
        bics    r10, r10, #0xff000000
        eorne   scno, r10, #__NR_OABI_SYSCALL_BASE
        ldrne   tbl, =sys_oabi_call_table
#elif !defined(CONFIG_AEABI)
        bic     scno, scno, #0xff000000         @ mask off SWI op-code
        eor     scno, scno, #__NR_SYSCALL_BASE  @ check OS number
#endif
        get_thread_info tsk
        /*
         * Reload the registers that may have been corrupted on entry to
         * the syscall assembly (by tracing or context tracking.)
         */
 TRACE( ldmia   sp, {r0 - r3}           )

 

소프트 인터럽트 핸들러로 user space에서 POSIX 시스템 콜 호출을 하는 경우 커널에서 “sys_”로 시작하는 syscall 함수 및 arm용 syscall 함수를 호출한다.  (thumb 소스는 생략하였다)

  • 리눅스는 3개의 ABI(Application Binary Interface) 관련 모드를 준비하였다.
    • CONFIG_OABI_COMPAT 커널 옵션은 Old ABI(legacy ABI) 및 AEABI 두  방식을 동시에 지원하기 위해 제공한다.
    • CONFIG_AEABI 커널 옵션은 AEABI 방식만 제공한다.
    • 위 두 커널 옵션을 사용하지 않는 경우 OABI를 지원한다.
    • 참고: ABI(Application Binary Interface) | 문c

 

  • 코드 라인 6~7에서 PT_REGS_SIZE만큼 스택을 키운다. (grows down) 그런 후 r0~r12까지의 레지스터 값들을 스택에 저장한다.
  • 코드 라인 8~9에서 스택에 비워둔 레지스터 저장 장소 중 sp와 lr 레지스터 값을 해당 위치에 저장한다.
  • 코드 라인 10에서 스택의 pt_regs.psr 값을 읽어와 spsr에 대입한다.
  • 코드 라인 12~14에서 스택에 있는 pt_regs 구조체의 pc 위치에 돌아갈 주소가 담긴 lr을 저장하고, psr 위치에는 현재 모드의 spsr을 저장한다. 마지막으로 old_r0 위치에 r0 레지스터를 저장하다.
  • 코드 라인 16에서 fp 레지스터에 0을 대입한다
  • 코드 라인 17에서 CONFIG_ALIGNMENT_TRAP 커널 옵션을 사용하는 경우 alignment trap 기능을 적용한다.
  • 코드 라인 19에서 local irq를 enable 한다.
  • 코드 라인 20에서 컨텍스트 트래킹에 관련한  후처리 디버그 활동을 수행한다.
  • 코드 라인 26~63에서 ABI 모드에 따라 swi 호출당시의 syscall 번호를 scno(r7)에 대입하고 tbl(r8)에 syscall 테이블 주소를  대입한다.
    • CONFIG_OABI_COMPAT: AEABI 및 OABI 두 모드를 동시에 지원한다.
      • swi 호출 당시의 숫자 부분만을 떼어 r10에 대입하고 그 값에 따라
        • 0인 경우 AEABI로 인식하여 아무것도 수행하지 않는다. (AEABI 규약에 의거 swi 0 호출 전에 r7에 syscall 번호가 담겨온다)
        • 0이 아닌 경우 OABI로 인식하여 r10에서 __NR_OABI_SYSCALL_BASE(0x900000)값을 더한 후 scno(r7)에 대입하고 tbl(r8)에 sys_oabi_call_table을 지정한다.
    • CONFIG_AEABI: AEABI 모드만 지원한다.
      • 아무것도 수행하지 않는다. (AEABI 규약에 의거 swi 0 호출 전에 r7에 syscall 번호가 담겨온다)
    • CONFIG_OABI
      • swi 호출 당시의 숫자 부분만을 떼어 scno(r7)에 대입하고 __NR_SYSCALL_BASE(0x900000)값을 더한다.
  • 코드 라인 64에서 tsk(r9) 레지스터에 현재 cpu의 thread_info 주소를 알아온다.

 

arch/arm/kernel/entry-common.S -2/2-

local_restart:
        ldr     r10, [tsk, #TI_FLAGS]           @ check for syscall tracing
        stmdb   sp!, {r4, r5}                   @ push fifth and sixth args

        tst     r10, #_TIF_SYSCALL_WORK         @ are we tracing syscalls?
        bne     __sys_trace

        invoke_syscall tbl, scno, r10, __ret_fast_syscall

        add     r1, sp, #S_OFF
2:      cmp     scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
        eor     r0, scno, #__NR_SYSCALL_BASE    @ put OS number back
        bcs     arm_syscall
        mov     why, #0                         @ no longer a real syscall
        b       sys_ni_syscall                  @ not private func

#if defined(CONFIG_OABI_COMPAT) || !defined(CONFIG_AEABI)
        /*
         * We failed to handle a fault trying to access the page
         * containing the swi instruction, but we're not really in a
         * position to return -EFAULT. Instead, return back to the
         * instruction and re-enter the user fault handling path trying
         * to page it in. This will likely result in sending SEGV to the
         * current task.
         */
9001:
        sub     lr, saved_pc, #4
        str     lr, [sp, #S_PC]
        get_thread_info tsk
        b       ret_fast_syscall
#endif
ENDPROC(vector_swi)
local_restart 레이블
  • 코드 라인 2에서 현재 태스크의 플래그 값을 r10 레지스터로 읽어온다.
  • 코드 라인 3에서 syscall 호출 시의 5번째 인수와 6번째 인수를 스택에 저장한다.
  • 코드 라인 5~6에서 syscall tracing이 요청된 경우 트레이스 관련 함수를 호출하여 처리한다.
    • __sys_trace 레이블에서 slow_syscall 루틴이 동작한다. (코드 설명 생략)
  • 코드 라인 8에서 syscall 번호가 syscall 처리 범위 이내인 경우 syscall 테이블에서 해당 syscall 번호에 해당하는 “sys_”로 시작하는 함수로 jump 하고 수행이 완료된 후 ret_fast_syscall 레이블 주소로 이동한다.
    • 0번 syscall은 커널 내부 사용목적의 syscall 함수이다. (sys_restart_syscall())
  • 코드 라인 10~15에서 syscall 번호가 ARM용 syscall 범위이내인 경우 arm_syscall() 함수를 호출한다. 만일 범위를 벗어난 경우 sys_ni_syscall() 함수를 호출하여 private한 syscall을 처리하게 한다.  만일 특별히 private syscall을 등록하지 않은 경우 -ENOSYS 에러로 함수를 빠져나온다.
    • 커널 4.0 기준 5개의 ARM syscall 함수들이 등록되어 있다.
  • 코드 라인 27~30에서 OABI 사용 시 다른 cpu에 의해 swi 명령이 있는 페이지를 mkold한 상태와 동시에 현재 cpu에서 swi 호출 시 panic 되는 경우를 막기 위해 다시 한번 해당 명령으로 복귀한다.

 

alignment_trap 매크로

arch/arm/kernel/entry-header.S

        .macro  alignment_trap, rtmp1, rtmp2, label
#ifdef CONFIG_ALIGNMENT_TRAP
        mrc     p15, 0, \rtmp2, c1, c0, 0
        ldr     \rtmp1, \label
        ldr     \rtmp1, [\rtmp1]
        teq     \rtmp1, \rtmp2
        mcrne   p15, 0, \rtmp1, c1, c0, 0
#endif
        .endm

CONFIG_ALIGNMENT_TRAP 커널 옵션을 사용하는 경우 alignment trap 기능을 적용한다.

  • alignment trap을 사용하면 정렬되지 않은 주소 및 데이터에 접근 할 때 prefect abort 또는 data abort exception이 발생한다.
  • 기존에 저장해 두었던 cr_alignment 값과 SCTLR 값이 다른 경우에만 SCTLR에 cr_alignment 값을 저장하는 방식으로 성능 저하를 막는다.

 

ret_fast_syscall 레이블

arch/arm/kernel/entry-common.S

/*
 * This is the fast syscall return path.  We do as little as possible here,
 * such as avoiding writing r0 to the stack.  We only use this path if we
 * have tracing, context tracking and rseq debug disabled - the overheads
 * from those features make this path too inefficient.
 */
ret_fast_syscall:
__ret_fast_syscall:
 UNWIND(.fnstart        )
 UNWIND(.cantunwind     )
        disable_irq_notrace                     @ disable interrupts
        ldr     r2, [tsk, #TI_ADDR_LIMIT]
        cmp     r2, #TASK_SIZE
        blne    addr_limit_check_failed
        ldr     r1, [tsk, #TI_FLAGS]            @ re-check for syscall tracing
        tst     r1, #_TIF_SYSCALL_WORK | _TIF_WORK_MASK
        bne     fast_work_pending


        /* perform architecture specific actions before user return */
        arch_ret_to_user r1, lr

        restore_user_regs fast = 1, offset = S_OFF
 UNWIND(.fnend          )
ENDPROC(ret_fast_syscall)

syscall 처리를 마치고 복귀하기 전에 pending된 작업이 있으면 수행한 후 백업해 두었던 레지스터들을 다시 읽어 들인 후 복귀한다.

 

invoke_syscall 매크로

arch/arm/kernel/entry-header.S

        .macro  invoke_syscall, table, nr, tmp, ret, reload=0
#ifdef CONFIG_CPU_SPECTRE
        mov     \tmp, \nr
        cmp     \tmp, #NR_syscalls              @ check upper syscall limit
        movcs   \tmp, #0
        csdb
        badr    lr, \ret                        @ return address
        .if     \reload
        add     r1, sp, #S_R0 + S_OFF           @ pointer to regs
        ldmiacc r1, {r0 - r6}                   @ reload r0-r6
        stmiacc sp, {r4, r5}                    @ update stack arguments
        .endif
        ldrcc   pc, [\table, \tmp, lsl #2]      @ call sys_* routine
#else
        cmp     \nr, #NR_syscalls               @ check upper syscall limit
        badr    lr, \ret                        @ return address
        .if     \reload
        add     r1, sp, #S_R0 + S_OFF           @ pointer to regs
        ldmiacc r1, {r0 - r6}                   @ reload r0-r6
        stmiacc sp, {r4, r5}                    @ update stack arguments
        .endif
        ldrcc   pc, [\table, \nr, lsl #2]       @ call sys_* routine
#endif
        .endm

@nr 번호가 syscall 범위에 있는 경우 해당하는 syscall 함수를 호출한 후 @ret 주소로 복귀한다. 범위 밖인 경우 그냥 빠져나온다.

 

sys_ni_syscall()

kernel/sys_ni.c

/*
 * Non-implemented system calls get redirected here.
 */                    
asmlinkage long sys_ni_syscall(void)
{
        return -ENOSYS;
}

범위 밖의 syscall 요청이 수행되는 루틴이다. private syscall이 필요한 경우 이곳에 작성되는데 없으면 -ENOSYS 에러를 반환한다.

 

참고