Handling Non-Maskable interrupts

마스크 불가능 인터럽트 처리기

리눅스 커널에서의 인터럽트 및 인터럽트 처리기 챕터의 6번째 파트입니다. 지난 파트에서 우리는 일반적 보호 결함예외, 분리 예외, 무효한 opcode예외 등등 일부 예외 처리기의 구현을 봤습니다. 이전 파트에서 썻듯이 이파트에서는 나머지 예외의 구현을 볼 것입니다. 다음 처리기의 구현을 봅시다:

이 파트에서 시작해봅시다.

마스크 불가능 인터럽트 처리기

마스크 불가능인터럽트는 표준 마스킹 기술에 의해 무시되지 않는 하드웨어 인터럽트입니다. 일반적인 방법으로 마스크 불가능한 인터럽트는 다음 두 가지 방법 중 하나로 생성될 수 있습니다;

  • 외부 하드웨어는 CPU에서 마스크 불가능 인터럽트 을 지정

  • 프로세서는 시스템 버스 또는 APIC 직렬 버스에서 전달 모드 NMI로 메시지를 수신

프로세서가 이러한 소스 중 하나에서 NMI를 받으면, 프로세서는 번호가 2(첫 번째 파트의 표 참조)인 인터럽트 벡터에 의해 지정된 NMI처리기를 호출하여 즉시 처리합니다. 우리는 이미 arch/x86/kernel/traps.c소스 코드 파일에서 정의된 trap_init함수에서 인터럽트 디스크립터 테이블벡터 번호, nmi인터럽트 처리기의 주소 및 NMI_STACK인터럽트 스택 테이블 엔트리으로 채웠습니다:

set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);

이전 파트에서 우리는 모든 인터럽트 처리기의 엔트리 포인트가 다음과 같이 정의되는 것을 봤습니다:

.macro idtentry sym do_sym has_error_code:req paranoid=0 shift_ist=-1
ENTRY(\sym)
...
...
...
END(\sym)
.endm

arch/x86/entry/entry_64.S어셈블리 소스 코드 파일의 매크로 입니다. 그러나 Non-Maskable인터럽트 처리기는 이 매크로로 정의되지 않습니다:

ENTRY(nmi)
...
...
...
END(nmi)

동일한 arch/x86/entry/entry_64.S어셈블리 파일 에 자체 엔트리 포인트가 있습니다. 여기에 들어가서 Non-Maskable인터럽트 처리기가 어떻게 작동하는지 이해해봅시다. nmi처리기의 호출에서 시작합니다:

PARAVIRT_ADJUST_EXCEPTION_FRAME

이 매크로는 우리가 다른 챕터에서 보게 될 반 가상화와 관련되었기 때문에 이 파트에서는 자세히 다루지 않을 것입니다. 다음으로 스택에 rdx레지스터의 내용을 저장합니다:

pushq    %rdx

그리고 마스크 불가능 인터럽트가 발생했을 때 cs가 커널 세그먼트가 아닌 확인이 할당됩니다:

cmpl    $__KERNEL_CS, 16(%rsp)
jne    first_nmi

__KERNEL_CS매크로는 arch/x86/include/asm/segment.h에 정의됐으며 글로벌 디스크립터 테이블에서 두번째 설명을 보여줍니다:

#define GDT_ENTRY_KERNEL_CS    2
#define __KERNEL_CS    (GDT_ENTRY_KERNEL_CS*8)

GDT의 자세한 내용은 리눅스 커널 부팅 프로세스 챕터의 두번째 파트에서 읽을 수 있습니다. cs가 커널 세그먼트가 아니라면 이것은 NMI가 중첩되지 않음을 의미하고 우리는 first_nmi레이블로 넘어갈 수 있습니다. 이 경우를 생각해봅시다. 먼저 현재 스택 포인터의 주소를 rdx에 넣고 1first_nmi레이블의 스택에 넣습니다:

first_nmi:
    movq    (%rsp), %rdx로
    pushq    $1

왜 스택에 1을 넣을까요? 이에 대한 답은 We allow breakpoints in NMIs입니다. x86_64에서, 다른 아키텍쳐와 마찬가지로 CPU의 첫 NMI가 완료되기 전에 다른 NMI는 실행되지 않습니다. NMI인터럽트는 다른 인터럽츠처럼 iret명령이 완료되고 예외는 이것을 수행합니다. NMI처리기가 페이지 결함 또는 브레이크포인트 또는 iret명령을 사용하는 다른 예외를 사용하는 경우. NMI컨텍스트에서 이런 일이 일어나면, CPU는 NMI컨텍스트를 떠나고 새로운 NMI가 발생할 것입니다. iretNMIs를 다시 실행해 이러한 예외에서 벗어나는데 사용되고 우리는 중첩된 마스크 불가능 인터럽트를 얻을 것입니다. 문제는 NMI처리기가 예외가 트리거 될때의 상태로 돌아가지 않고, 대신 새로운 NMIs가 실행중인 NMI처리기를 선점할 수 있는 상태로 돌아갑니다. 첫 NMI처리기가 완료되기 전에 다른 NMI가 오면, 새로운 NMI는 선점된 NMIs스택 전체에 씁니다. 우리는 이전 NMI스택의 맨 위에서 사용중인 다음 NMI에서 중첩된 NMIs를 얻을 수 있습니다. 이것은 중첩된 마스크 불가능 인터럽트가 이전의 마스크 불가능 인터럽트의 스택을 손상시켜서 실행할 수 없다는 것을 의미합니다. 이것이 임시 변수를 위해 스택에 공간을 할당한 이유입니다. 우리는 이전 NMI가 실행될 때 변수가 설정됐는지 확인할 것이고 중첩된 NMI가 아니라면 제거할 것입니다. 우리는 non-maskable 현재 실행되었음을 나타내기 위해 스택에서 이전에 할당된 공간애 1을 넣습니다. NMI 또는 다른 예외가 일어났을 때 다음과 같은 스택 프레임이 있다는 것을 기억하십시오:

+------------------------+
|         SS             |
|         RSP            |
|        RFLAGS          |
|         CS             |
|         RIP            |
+------------------------+

또한 예외가 있는 경우 에러 코드가 있습니다. 따라서 이러한 모든 조작 후에 스택 프레임은 다음과 같습니다:

+------------------------+
|         SS             |
|         RSP            |
|        RFLAGS          |
|         CS             |
|         RIP            |
|         RDX            |
|          1             |
+------------------------+

다음 단계에서 우리는 스택의 또 다른 40바이트를 할당합니다:

subq    $(5*8), %rsp

그리고 .rept어셈블리 지시문의 공간이 할당된 다음 오리지널 스택 프레임의 복사본을 넣습니다:

.rept 5
pushq    11*8(%rsp)
.endr

우리는 오리지널 스택 프레임의 복사본이 필요합니다. 일반적으로 인터럽트 스택의 두 복사본이 필요합니다. 처음은 copied인터럽트 스택: saved스택 프레임과 copied스택 프레임입니다. 이제 우리는 오리지널 스택 프레임을 할당된 40바이트(copied스택 프레임) 다음에 위치한 saved스택 프레임에 넣습니다. 이 스택 프레임은 중첩된 NMI가 바꿀수 있는 copied스택 프레임을 수정하는데 사용됩니다. 두 번째 copied스택 프레임은 중첩된 NMIs가 트리거된 두번째 NMI를 첫 번째 NMI가 아는 것을 허용하고 우리가 첫 번째 NMI처리기를 반복해야 함을 알립니다. 우리는 오리지널 스택 프레임의 첫 복사본을 만들었습니다. 이제 두 번째 복사본을 만들 차례입니다:

addq    $(10*8), %rsp

.rept 5
pushq    -6*8(%rsp)
.endr
subq    $(5*8), %rsp

이 모든 조작 후의 스택 프레임은 다음과 같습니다:

+-------------------------+
| original SS             |
| original Return RSP     |
| original RFLAGS         |
| original CS             |
| original RIP            |
+-------------------------+
| temp storage for rdx    |
+-------------------------+
| NMI executing variable  |
+-------------------------+
| copied SS               |
| copied Return RSP       |
| copied RFLAGS           |
| copied CS               |
| copied RIP              |
+-------------------------+
| Saved SS                |
| Saved Return RSP        |
| Saved RFLAGS            |
| Saved CS                |
| Saved RIP               |
+-------------------------+

그런 다음 이전 예외 처리기에서 이미 했던 것처럼 더미 오류 코드를 스택에 넣고 스택의 범용 레지스터를 위한 공간을 할당합니다:

pushq    $-1
ALLOC_PT_GPREGS_ON_STACK

우리는 이미 인터럽트 챕터의 세 번째 파트에서 ALLOC_PT_GREGS_ON_STACK매크로의 구현을 봤습니다. 이 매크로는 arch/x86/entry/calling.h에서 정의되었으며 rdi에서 또 다른 범용 레지스터를 위한 스택에 120바이트를 r15로 할당합니다:

.macro ALLOC_PT_GPREGS_ON_STACK addskip=0
addq    $-(15*8+\addskip), %rsp
.endm

일반 레지스터를 위한 공간 할당 후 paranoid_entry의 호출을 볼 수 있습니다:

call    paranoid_entry

이전 파트에서 이 레이블을 떠올릴 수 있습니다. 스텍에서 범용 레지스터를 넣고, MSR_GS_BASE모델 특정 레지스터를 읽고 그것의 값을 확인합니다. MSR_GS_BASE의 값이 음수면, 커널 모드로 가고 paranoid_entry를 반환합니다. 다른 방법은 우리가 사용자 모드로 갔고 커널 gs에서 사용자 gs를 바꾸기 위해 swapgs명령을 실행해야 함을 의미합니다:

ENTRY(paranoid_entry)
    cld
    SAVE_C_REGS 8
    SAVE_EXTRA_REGS 8
    movl    $1, %ebx
    movl    $MSR_GS_BASE, %ecx
    rdmsr
    testl    %edx, %edx
    js    1f
    SWAPGS
    xorl    %ebx, %ebx
1:    ret
END(paranoid_entry)

swapgs명령 후 ebx레지스터를 0으로 설정했습니다. 다음에 우리는 이 레지스터의 내용을 확인하고 다른 방식으로 01을 포함한 ebx 대신 실행됬는지 확인합니다. 다음 단계에서 NMI처리기가 page fault을 일으키고 컨드롤 레지스터의 값을 바꾸기 때문에 r12레지스터의 cr2컨트롤 레지스터의 값을 저장합니다:

movq    %cr2, %r12

이제 실제 NMI처리기를 호출할 차례입니다. rdi에서 pt_regs의 주소, rsi의 에러코드를 넣고 do_nmi처리기를 호출합니다:

movq    %rsp, %rdi
movq    $-1, %rsi
call    do_nmi

우리는 이 파트에서 do_nmi의 후반부로 돌아갈 것이지만, 지금은 do_nmi의 실행이 끝난 후에 무슨 일이 일어나는지 보겠습니다. do_nmi처리기가 끝난 다음 cr2레지스터를 검사하는데 do_nmi이 수행되는 동안 페이지 결함이 생길 수 있고, 만약 그것을 얻으면 오리지널 cr2를 복원하며 다른 방법으로는 레이블 1로 점프합니다. 그런 다음 ebx 레지스터(swapgs명령을 사용한 경우 0을, 그렇지 않은 경우 1을 가짐)의 내용을 테스트 하고 1을 가지거나 nmi_restore레이블로 점프한 경우 SWAPGS_UNSAFE_STACK을 실행합니다. SWAPGS_UNSAFE_STACK매크로는 swapgs명령을 확장합니다. nmi_restore레이블에서 우리는 범용 레지스터를 복구하고, 이 레지스터를 위한 스택에 할당된 공간을 제거하며, 임시 변수를 지우고 INTERRUPT_RETURN매크로를 사용해 인터럽트 처리기를 종료합니다:

    movq    %cr2, %rcx
    cmpq    %rcx, %r12
    je    1f
    movq    %r12, %cr2
1:
    testl    %ebx, %ebx
    jnz    nmi_restore
nmi_swapgs:
    SWAPGS_UNSAFE_STACK
nmi_restore:
    RESTORE_EXTRA_REGS
    RESTORE_C_REGS
    /* Pop the extra iret frame at once */
    REMOVE_PT_GPREGS_FROM_STACK 6*8
    /* Clear the NMI executing stack variable */
    movq    $0, 5*8(%rsp)
    INTERRUPT_RETURN

INTERRUPT_RETURNarch/x86/include/irqflags.h에 정의되었으며 iret명령을 확장합니다. 이것이 전부입니다.

이제 이전 NMI인터럽트가 실행을 끝내지 않았을 때 다른 NMI인터럽트가 일어난 경우를 고려해봅시다. 이 파트의 도입부부터 우리는 사용자 공간에서 온 것을 확인하고 이 경우 first_nmi으로 점프한 것을 떠올릴 수 있습니다:

cmpl    $__KERNEL_CS, 16(%rsp)
jne    first_nmi

이 경우 이것은 매번 첫 NMI입니다. 왜냐하면 첫 NMI가 페이지 결함, 브레이크 포인트 또는 다른 예외를 잡은 것으로 인해 커널 모드가 실행되기 때문입니다. 사용자 공간에서 오지 않은 경우, 우선 임시 변수를 테스트하십시오:

cmpl    $1, -8(%rsp)
je    nested_nmi

그것이 1로 설정됐으면 우리는 nested_nmi레이블로 점프합니다. 1이 아니면 IST스택을 테스트합니다. 중첩된 NMIs의 경우 repeat_nmi 위에 있는지 확인합니다. 무시하는 경우, 다른 방법으로 end_repeat_nmi보다 위인지 확인하고 nested_nmi_out레이블로 점프합니다.

이제 do_nmi예외 처리기를 살펴보겠습니다. 이 함수는 arch/x86/kernel/nmi.c소스 코드 파일에 정의되었으며 모든 예외처리기는 두 매개변수를 가집니다:

  • pt_regs의 주소;

  • 에러 코드.

do_nminmi_nesting_preprocess함수의 호출로 시작하며 nmi_nesting_postprocess의 호출과 함께 끝납니다. nmi_nesting_preprocess함수는 디버그 스택에서 작동하지 않는지 확인하고 디버그 스택에서 update_debug_stack per-cpu변수가 1로 설정됐으면 arch/x86/kernel/cpu/common.cdebug_stack_set_zero함수를 호출합니다 이 함수는 CPU당 debug_stack_use_ctr변수를 증가시키고 새로운 Interrupt Descriptor Table을 로드합니다:

static inline void nmi_nesting_preprocess(struct pt_regs *regs)
{
        if (unlikely(is_debug_stack(regs->sp))) {
                debug_stack_set_zero();
                this_cpu_write(update_debug_stack, 1);
        }
}

nmi_nesting_postprocess함수는 nmi_nesting_preprocess에서 설정한 CPU당 update_debug_stack변수를 확인하고 디버그 스택을 리셋하거나 다른 말로 본래의 Interrupt Descriptor Table을 로드합니다. nmi_nesting_preprocess함수의 호출 이후, do_nmi에서 nmi_enter의 호출을 볼 수 있습니다. nmi_enter는 인터럽티드 프로세스의 lockdep_recursion필드를 증가시키고, 점유한 카운터를 업데이트 하며 NMI에 관한 RCU서브시스템을 알립니다. 또한 nmi_exit함수와 비슷한 nmi_enter도 있지만 그 반대도 마찬가지입니다. nmi_enter 이후 irq_stat구조체의 __nmi_count를 증가시키고 default_do_nmi함수를 호출합니다. 먼저 모든 default_do_nmi에서 이전 nmi의 주소를 확인하고 마지막 nmi의 주소를 실제 주소로 업데이트 합니다:

if (regs->ip == __this_cpu_read(last_nmi_rip))
    b2b = true;
else
    __this_cpu_write(swallow_nmi, false);

__this_cpu_write(last_nmi_rip, regs->ip);

그 다음 CPU특정 NMIs를 처리해야 합니다:

handled = nmi_handle(NMI_LOCAL, regs, b2b);
__this_cpu_add(nmi_stats.normal, handled);

그리고 비특정 NMIs는 reason에 따릅니다:

reason = x86_platform.get_nmi_reason();
if (reason & NMI_REASON_MASK) {
    if (reason & NMI_REASON_SERR)
        pci_serr_error(reason, regs);
    else if (reason & NMI_REASON_IOCHK)
        io_check_error(reason, regs);

    __this_cpu_add(nmi_stats.external, 1);
    return;
}

이것이 전부입니다.

범위 초과 예외

다음 예외는 BOUND범위를 초과한 예외입니다. 이 BOUND명령은 첫 번째 피연산자(배열 인덱스)가 두 번째 피연산자(바운드 피연산자)에 지정된 배열의 범위 내에 있는지 확인합니다. 인덱스가 bound 내에 없으면 BOUND범위는 예외가 초과됐거나 #BR이 발생한 것입니다. #BR예외의 처리기는 arch/x86/kernel/traps.c에 정의된 do_bounds함수입니다. do_bounds처리기는 exception_enter함수의 호출로 시작하며 exception_exit의 호출로 끝납니다:

prev_state = exception_enter();

if (notify_die(DIE_TRAP, "bounds", regs, error_code,
               X86_TRAP_BR, SIGSEGV) == NOTIFY_STOP)
    goto exit;
...
...
...
exception_exit(prev_state);
return;

이전 컨텍스트의 상태를 얻은 후에, 예외를 notify_die체인에 추가하고 NOTIFY_STOP을 반환하면 예외에서 돌아옵니다. notify체인 및 context tracking함수에 대한 것은 이전 파트에서 읽을 수 있습니다. 다음 단계에서 IF플래그를 확인하는 contidional_sti함수가 비활성화됐으면 인터럽트를 활성화하고 그 값에 따르는 local_irq_enable을 호출합니다:

conditional_sti(regs);

if (!user_mode(regs))
    die("bounds", regs, error_code);

그리고 사용자 모드에서 오지 않았으면 die함수와 함께 SIGSEGV신호를 보내는지 확인하십시오. 다음으로 우리는 MPX의 활성화 여부를 확인하고 이 기능이 비활성화 됐으면 exit_trap레이블로 점프합니다:

if (!cpu_feature_enabled(X86_FEATURE_MPX)) {
    goto exit_trap;
}

여기서 우리는 `do_trap`함수를 실행합니다(자세한 내용은 이전 파트에서 찾을 수 있습니다):

```C
exit_trap:
    do_trap(X86_TRAP_BR, SIGSEGV, "bounds", regs, error_code, NULL);
    exception_exit(prev_state);

MPX기능이 활성화된 경우 get_xsave_field_ptr함수로 BNDSTATUS을 확인하고 이것이 0이면, 그것은MPX가 이 예외의 원인이 아닌 것을 의미합니다:

bndcsr = get_xsave_field_ptr(XSTATE_BNDCSR);
if (!bndcsr)
        goto exit_trap;

이 모든 것 이후에도 MPX가 이 예외의 원인일 때 한 가지 방법이 있습니다. 이 파트에서는 인텔 메모리 보호 확장에 대한 자세한 내용은 다루지 않지만 다른 챕터에서 살펴볼 것입니다.

보조프로세서 예외 및 SIMD예외

다음 두 가지 예외는 x87 FPU 부동 소수점 오류 또는 #MFSIMD부동 소수점 예외 또는 #XF입니다. 첫 번째 예외는 x87 FPU이 부동 소수점 오류를 감지했을 때 일어납니다. 예를 들어 0으로 나누기, 숫자 오버플로 등이 있습니다. 두 번째 예외는 프로세서가 SSE/SSE2/SSE3 SIMD 부동 소수점 예외를 감지했을 때 일어납니다. 이것은 x87 FPU와 비슷합니다. 이 예외를 위한 처리기는 do_coprocessor_errorarch/x86/kernel/traps.c에서 정의된 do_simd_coprocessor_error가 있으며 서로 매우 유사합니다. 둘 다 동일한 소스 코드 파일에서 math_error함수를 호출하지만 다른 벡터 번호를 전달합니다. do_coprocessor_errormath_errorX86_TRAP_MF벡터 번호를 전달합니다:

dotraplinkage void do_coprocessor_error(struct pt_regs *regs, long error_code)
{
    enum ctx_state prev_state;

    prev_state = exception_enter();
    math_error(regs, error_code, X86_TRAP_MF);
    exception_exit(prev_state);
}

그리고 do_simd_coprocessor_errormath_error함수로 X86_TRAP_XF를 전달합니다:

dotraplinkage void
do_simd_coprocessor_error(struct pt_regs *regs, long error_code)
{
    enum ctx_state prev_state;

    prev_state = exception_enter();
    math_error(regs, error_code, X86_TRAP_XF);
    exception_exit(prev_state);
}

먼저 모든 math_error함수는 현재 중단된 작업, fpu의 주소, 예외를 설명하는 문자열을 정의하고, 그것을 notify_die체인에 더하며 NOTIFY_STOP이 반환되면 예외 처리기에서 반환합니다:

    struct task_struct *task = current;
    struct fpu *fpu = &task->thread.fpu;
    siginfo_t info;
    char *str = (trapnr == X86_TRAP_MF) ? "fpu exception" :
                        "simd exception";

    if (notify_die(DIE_TRAP, str, regs, error_code, trapnr, SIGFPE) == NOTIFY_STOP)
        return;

그 후에 커널 모드에서 왔는지 확인하고 그렇다면 fixup_exception함수로 예외를 고치려 노력할 것입니다. 예외의 오류 코드와 벡터 번호로 작업을 채울 수 없거나 죽은 경우:

if (!user_mode(regs)) {
    if (!fixup_exception(regs)) {
        task->thread.error_code = error_code;
        task->thread.trap_nr = trapnr;
        die(str, regs, error_code);
    }
    return;
}

사용자 모드에서 온 경우 fpu상태를 저장하고 예외의 벡터 번호로 작업 구조체를 채우고 신호의 숫자, errno, 예외가 발생한 곳의 주소, 신호 코드로 siginfo_t을 채웁니다:

fpu__save(fpu);

task->thread.trap_nr    = trapnr;
task->thread.error_code = error_code;
info.si_signo        = SIGFPE;
info.si_errno        = 0;
info.si_addr        = (void __user *)uprobe_get_trap_addr(regs);
info.si_code = fpu__exception_code(fpu, trapnr);

그런 다음 신호 코드를 확인하고 0이 아닌 경우 반환합니다:

if (!info.si_code)
    return;

또는 마지막에 SIGFPE신호를 보냅니다:

force_sig_info(SIGFPE, &info, task);

이것이 전부입니다.

결론

인터럽트 및 인터럽트 처리챕터의 6번째 파트의 끝으로 우리는 이 파트에서 non-maskable인터럽트, SIMDx87 FPU부동 소수점 예외와 같은 몇몇 예외 처리기의 구현을 봤습니다. 결과적으로 우리는 이 파트에서 trap_init함수를 끝냈고 다음 파트로 향할 것입니다. 다음은 외부 인터럽트와 init/main.cearly_irq_init함수입니다.

질문이나 제안사항이 있다면 코멘트를 남기거나 트위터로 보내주십시오.

영어는 모국어가 아니어서 모든 불편함 점은 정말 죄송합니다. 실수를 발견하면 linux-insides에서 수정 사항이 포함된 PR을 보내주십시오.

링크

Last updated