Interrupt handlers

예외

이것은 리눅스 커널에서 인터럽트 처리를 다루는 세 번째 파트입니다 chapter 그리고 이전 파트에서 우리는 arch/x86/kernel/setup.c 소스코드의 setup_arch 함수에서 멈췄습니다.

우리는 이미이 함수가 아키텍처 고유의 초기화를 실행한다는 것을 알고 있습니다. 우리의 경우 setup_arch 함수는 x86_64 아키텍처 관련 초기화를 합니다. setup_arch는 큰 기능이며, 이전 부분에서는 다음 두 가지 예외에 대한 두 가지 예외 처리기 설정을 중단했습니다.

  • #DB - 디버그 예외, 인터럽트 된 프로세스에서 디버그 핸들러로 제어를 전송합니다.

  • #BP - int 3 명령으로 인한 중단 점 예외.

이러한 예외는 x86_64 아키텍처가 kgdb를 통한 디버깅을 위해 조기 예외 처리를 할 수 있도록 합니다.

아시다시피 early_trap_init 함수에서 이러한 예외 처리기를 설정했습니다.

void __init early_trap_init(void)
{
        set_intr_gate_ist(X86_TRAP_DB, &debug, DEBUG_STACK);
        set_system_intr_gate_ist(X86_TRAP_BP, &int3, DEBUG_STACK);
        load_idt(&idt_descr);
}

arch/x86/kernel/traps.c에서, 우리는 이미 앞부분에서set_intr_gate_istset_system_intr_gate_ist 함수의 구현을 보았고 이제이 두 예외 핸들러의 구현에 대해 살펴볼 것입니다.

디버그 및 중단 점 예외

자, 우리는 #DB#BP 예외에 대해 early_trap_init 함수에 예외 핸들러를 설정했으며 이제는 구현을 고려할 차례입니다. 그러나이 작업을 수행하기 전에 먼저 이러한 예외에 대한 세부 정보를 살펴 보겠습니다.

첫 번째 예외인 #DB 또는 debug 예외는 디버그 이벤트가 발생할 때 발생합니다. 예를 들어 debug register의 내용을 변경해보십시오. 디버그 레지스터는 Intel 80386 프로세서에서 시작하여 x86 프로세서에 제공되는 특수 레지스터이며, 이 CPU 확장의 이름에서 알 수 있듯이 이 레지스터의 주 목적은 디버깅입니다.

이 레지스터를 사용하면 코드에서 중단 점을 설정하고 추적하기 위해 데이터를 읽거나 쓸 수 있습니다. 디버그 레지스터는 권한 모드에서만 액세스 할 수 있으며 다른 권한 수준에서 실행할 때 디버그 레지스터를 읽거나 쓰려고하면 일반 보호 오류 예외가 발생합니다. 그래서 우리는 #DB 예외에 set_intr_gate_ist를 사용했지만 set_system_intr_gate_ist는 사용하지 않았습니다.

# DB 예외의 verctor 수는 1 (우리는 X86_TRAP_DB로 전달)이며 사양에서 읽을 수 있듯이 이 예외에는 오류 코드가 없습니다.

+-----------------------------------------------------+
|Vector|Mnemonic|Description         |Type |Error Code|
+-----------------------------------------------------+
|1     | #DB    |Reserved            |F/T  |NO        |
+-----------------------------------------------------+

두 번째 예외는 프로세서가 int 3 명령을 실행할 때 발생하는 #BP 또는 breakpoint 예외입니다. DB 예외와 달리 #BP 예외는 사용자 공간에서 발생할 수 있습니다. 코드의 어느 곳에나 추가할 수 있습니다. 예를 들어 간단한 프로그램을 살펴보겠습니다.

// breakpoint.c
#include <stdio.h>

int main() {
    int i;
    while (i < 6){
        printf("i equal to: %d\n", i);
        __asm__("int3");
        ++i;
    }
}

이 프로그램을 컴파일하고 실행하면 다음과 같은 결과가 나타납니다.

$ gcc breakpoint.c -o breakpoint
i equal to: 0
Trace/breakpoint trap

그러나 gdb로 실행하면 중단 점이 표시되고 프로그램을 계속 실행할 수 있습니다.

$ gdb breakpoint
...
...
...
(gdb) run
Starting program: /home/alex/breakpoints 
i equal to: 0

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000000000400585 in main ()
=> 0x0000000000400585 <main+31>:    83 45 fc 01    add    DWORD PTR [rbp-0x4],0x1
(gdb) c
Continuing.
i equal to: 1

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000000000400585 in main ()
=> 0x0000000000400585 <main+31>:    83 45 fc 01    add    DWORD PTR [rbp-0x4],0x1
(gdb) c
Continuing.
i equal to: 2

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000000000400585 in main ()
=> 0x0000000000400585 <main+31>:    83 45 fc 01    add    DWORD PTR [rbp-0x4],0x1
...
...
...

이 순간부터 우리는 이 두 가지 예외에 대해 약간 알고 있으며 처리기를 고려할 수 있습니다.

예외 처리기 전 준비

앞에서 언급했듯이 set_intr_gate_istset_system_intr_gate_ist 함수는 두 번째 매개 변수에서 예외 처리기의 주소를 사용합니다. 두 가지 예외 처리기는 다음과 같습니다.

  • debug;

  • int3.

C 코드에는 이러한 기능이 없습니다. 이 모든 것은 커널의* .c / *. h 파일에서 찾을 수 있습니다. arch/x86/include/asm/traps.h 커널 헤더 파일 :

asmlinkage void debug(void);

그리고

asmlinkage void int3(void);

이 함수들의 정의에서 asmlinkage 지시어에 주목할 수 있습니다. 지시문은 gcc의 특수 지정자입니다. 실제로 어셈블리에서 호출되는 'C'함수의 경우 함수 호출 규칙을 명시 적으로 선언해야 합니다. 우리의 경우, asmlinkage 서술자로 만들어진 함수라면, gcc는 함수를 컴파일하여 스택에서 파라미터를 가져옵니다.

따라서 두 처리기 모두 arch / x86 / entry / entry_64.S 어셈블리 소스 코드 파일에 정의되어 있습니다. idtentry 매크로로 :

idtentry debug do_debug has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK

그리고

idtentry int3 do_int3 has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK

각 예외 처리기는 두 부분으로 구성 될 수 있습니다. 첫 번째 부분은 일반 부분이며 모든 예외 처리기에서 동일합니다. 예외 처리기는 스택에 범용 레지스터를 저장하고 사용자 공간에서 예외가 발생한 경우 커널 스택으로 전환하고 예외의 두 번째 부분으로 제어를 전송해야합니다. 매니저. 예외 처리기의 두 번째 부분은 특정 예외에 따라 특정 작업을 수행합니다. 예를 들어 페이지 오류 예외 처리기는 지정된 주소에 대한 가상 페이지를 찾아야하고 잘못된 opcode 예외 처리기는 SIGILL signal 등을 보내야합니다.

방금 봤 듯이, 예외 처리기는 arch / x86 / kernel / entry_64.Sidtentry 매크로 정의에서 시작합니다. 어셈블리 소스 코드 파일이므로이 매크로의 구현을 살펴 보겠습니다. 보시다시피, 'idtentry'매크로는 다섯 가지 인수를 취합니다.

  • sym - 예외 처리기의 엔트리가 될 .globl name으로 전역 기호를 정의합니다.

  • do_sym - 예외 핸들러의 2차 엔트리를 나타내는 심볼 이름;

  • has_error_code - 예외 오류 코드의 존재에 관한 정보.

마지막 두 매개 변수는 선택 사항입니다.

  • paranoid - 현재 모드를 확인하는 방법을 보여줍니다 (나중에 자세히 설명 할 것입니다).

  • shift_ist - Interrupt Stack Table에서 실행되는 예외임을 보여줍니다.

.idtentry 매크로의 정의는 다음과 같습니다.

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

idtentry매크로의 내부를 고려하기 전에 예외가 발생할 때 스택 상태를 알아야합니다. Intel® 64 및 IA-32 아키텍처 소프트웨어 개발자 매뉴얼 3A 에서 예외 발생시 스택 상태는 다음과 같습니다.

    +------------+
+40 | %SS        |
+32 | %RSP       |
+24 | %RFLAGS    |
+16 | %CS        |
 +8 | %RIP       |
  0 | ERROR CODE | <-- %RSP
    +------------+

이제 우리는 idtmacro의 구현을 고려할 수 있습니다. #DBBP 예외 핸들러는 모두 다음과 같이 정의됩니다.

idtentry debug do_debug has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK
idtentry int3 do_int3 has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK

이러한 정의를 살펴보면 컴파일러가 debugint3 이름을 가진 두 개의 루틴을 생성 할 것이고, 이들 예외 핸들러는 일부 준비 후에 do_debugdo_int3 보조 핸들러를 호출 할 것입니다. 세 번째 매개 변수는 오류 코드의 존재를 정의하며 예외가 없는 것처럼 볼 수 있습니다. 위의 다이어그램에서 볼 수 있듯이 프로세서는 예외가 제공하는 경우 오류 코드를 스택에 푸시합니다. 이 경우 debugint3 예외에는 오류 코드가 없습니다. 스택은 오류 코드를 제공하는 예외와 그렇지 않은 예외를 다르게 볼 수 있기 때문에 약간의 어려움이 발생할 수 있습니다. 그렇기 때문에 예외가 제공하지 않으면 'idtentry'매크로의 구현이 가짜 오류 코드를 스택에 넣는 것부터 시작합니다.

.ifeq \has_error_code
    pushq    $-1
.endif

그러나 가짜 오류 코드 일뿐입니다. 또한 -1은 유효하지 않은 시스템 호출 번호를 나타내므로 시스템 호출 재시작 로직이 트리거되지 않습니다.

idtentry 매크로 shift_istparanoid의 마지막 두 매개 변수는 Interrupt Stack Table의 스택에서 실행되는 예외 처리기를 알 수 있습니다. 이미 시스템의 각 커널 스레드에 자체 스택이 있다는 것을 알고있을 것입니다. 이러한 스택 외에도 시스템의 각 프로세서와 관련된 일부 특수 스택이 있습니다. 이러한 스택 중 하나는 예외 스택입니다. x86_64 아키텍처는 '인터럽트 스택 테이블'이라는 특별한 기능을 제공합니다. 이 기능을 사용하면 double fault 등과 같은 원자 예외와 같은 지정된 이벤트에 대해 새 스택으로 전환 할 수 있습니다. shift_ist 매개 변수를 사용하면 예외 처리기를 위해 IST 스택을 켜야하는지 알 수 있습니다.

두 번째 매개 변수 인 paranoid는 사용자 공간에서 예외 처리기가 아닌지 여부를 알 수있는 방법을 정의합니다. 이를 결정하는 가장 쉬운 방법은 CS 세그먼트 레지스터의 CPL 또는 Current Privilege Level을 통하는 것입니다. 3과 같으면 사용자 공간에서 왔고, 0이면 커널 공간에서 왔습니다.

testl $3,CS(%rsp)
jnz userspace
...
...
...
// we are from the kernel space

그러나 불행히도 이 방법은 100 % 보증하지 않습니다. 커널 문서에 설명 된대로 :

우리가 NMI / MCE / DEBUG / 슈퍼 아토믹 엔트리 컨텍스트에 있다면, 일반 항목에 CS를 쓴 직후에 트리거되었을 수 있습니다. 스택이지만 SWAPGS를 실행하기 전에 확인하는 유일한 안전한 방법 GS의 경우 느린 방법 인 RDMSR입니다.

다시 말해 NMIswapgs 명령의 중요 섹션에서 발생할 수 있습니다. 이런 식으로 CPU 별 영역의 시작에 대한 포인터를 저장하는 MSR_GS_BASE 모델 특정 레지스터의 값을 확인해야 합니다. 따라서 사용자 공간에서 왔는지 여부를 확인하려면 MSR_GS_BASE 모델 특정 레지스터의 값을 확인해야하며 음수이면 커널 공간에서 왔으며 다른 방법으로 사용자 공간에서 나왔습니다.

movl $MSR_GS_BASE,%ecx
rdmsr
testl %edx,%edx
js 1f

처음 두 줄의 코드에서 우리는 MSR_GS_BASE 모델 특정 레지스터의 값을 edx : eax 쌍으로 읽습니다. 사용자 공간에서 음의 값을 gs로 설정할 수 없습니다. 그러나 우리는 물리 메모리의 직접 매핑이 0xffff880000000000 가상 주소에서 시작한다는 것을 알고 있습니다. 이런 방식으로 MSR_GS_BASE0xffff880000000000부터 0xffffc7ffffffffff까지의 주소를 포함합니다. rdmsr 명령어가 실행 된 후, % edx 레지스터에서 가능한 가장 작은 값은 -30720이며, 부호 없는 4 바이트에서 0xffff8800입니다. 이것이 per-cpu 영역의 시작을 가리키는 커널 공간 gs가 음의 값을 포함하는 이유입니다.

스택에서 가짜 오류 코드를 푸시 한 후 다음과 같이 범용 레지스터를 위한 공간을 할당해야합니다.

ALLOC_PT_GPREGS_ON_STACK

arch/x86/entry/calling.h 헤더 파일에 정의 된 매크로는 스택에 15*8 바이트의 공간을 할당하여 범용 레지스터를 유지합니다.

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

따라서 ALLOC_PT_GPREGS_ON_STACK을 실행 한 후 스택은 다음과 같습니다.

     +------------+
+160 | %SS        |
+152 | %RSP       |
+144 | %RFLAGS    |
+136 | %CS        |
+128 | %RIP       |
+120 | ERROR CODE |
     |------------|
+112 |            |
+104 |            |
 +96 |            |
 +88 |            |
 +80 |            |
 +72 |            |
 +64 |            |
 +56 |            |
 +48 |            |
 +40 |            |
 +32 |            |
 +24 |            |
 +16 |            |
  +8 |            |
  +0 |            | <- %RSP
     +------------+

범용 레지스터를위한 공간을 할당 한 후 예외가 사용자 공간에서 발생했는지 여부를 이해하기 위해 몇 가지 검사를 수행하고, 그렇다면, 중단 된 프로세스 스택으로 돌아가거나 예외 스택을 유지해야합니다.

.if \paranoid
    .if \paranoid == 1
        testb    $3, CS(%rsp)
        jnz    1f
    .endif
    call    paranoid_entry
.else
    call    error_entry
.endif

물론 이 모든 경우를 고려해 봅시다.

사용자 공간에서의 예외 발생

첫 번째로 예외에 debugint3 예외와 같이 예외가 paranoid = 1인 경우를 생각해 봅시다. 이 경우 우리는 CS 세그먼트 레지스터에서 셀렉터를 확인하고 사용자 공간에서 왔거나 paranoid_entry가 다른 방식으로 호출되면 1f레이블로 점프합니다.

사용자 공간에서 예외 처리기로 온 첫 번째 경우를 고려해 봅시다. 위에서 설명한 바와 같이 우리는 1 레이블로 점프해야합니다. 1 라이블은

call    error_entry

모든 범용 레지스터를 스택의 이전에 할당 된 영역에 저장하는 루틴 :

SAVE_C_REGS 8
SAVE_EXTRA_REGS 8

이 두 매크로는 arch/x86/entry/calling.h 헤더 파일에 정의되어 있으며 범용 레지스터의 값을 스택의 특정 위치로 이동시킵니다.

.macro SAVE_EXTRA_REGS offset=0
    movq %r15, 0*8+\offset(%rsp)
    movq %r14, 1*8+\offset(%rsp)
    movq %r13, 2*8+\offset(%rsp)
    movq %r12, 3*8+\offset(%rsp)
    movq %rbp, 4*8+\offset(%rsp)
    movq %rbx, 5*8+\offset(%rsp)
.endm

SAVE_C_REGSSAVE_EXTRA_REGS를 실행하면 스택은 다음과 같이 보입니다.

     +------------+
+160 | %SS        |
+152 | %RSP       |
+144 | %RFLAGS    |
+136 | %CS        |
+128 | %RIP       |
+120 | ERROR CODE |
     |------------|
+112 | %RDI       |
+104 | %RSI       |
 +96 | %RDX       |
 +88 | %RCX       |
 +80 | %RAX       |
 +72 | %R8        |
 +64 | %R9        |
 +56 | %R10       |
 +48 | %R11       |
 +40 | %RBX       |
 +32 | %RBP       |
 +24 | %R12       |
 +16 | %R13       |
  +8 | %R14       |
  +0 | %R15       | <- %RSP
     +------------+

커널이 범용 레지스터를 스택에 저장 한 후 다음을 사용하여 사용자 공간에서 다시 왔는지 확인해야합니다.

testb    $3, CS+8(%rsp)
jz    .Lerror_kernelspace

문서에 설명 된 것처럼 %RIP가 잘린 것으로보고 된 경우 오류가 발생할 수 있기 때문입니다. 어쨌든 두 경우 모두 SWAPGS 명령이 실행되고 MSR_KERNEL_GS_BASEMSR_GS_BASE의 값이 교환됩니다. 이 시점부터 %gs 레지스터는 커널 구조의 기본 주소를 가리 킵니다. 따라서 SWAPGS명령이 호출되었으며 error_entry라우팅의 주요 지점이었습니다.

이제 우리는 idtentry 매크로로 돌아갈 수 있습니다. error_entry 호출 후 다음과 같은 어셈블러 코드가 표시 될 수 있습니다.

movq    %rsp, %rdi
call    sync_regs

여기에 우리는sync_regs의 첫 번째 인자가 될 스택 포인터 %rdi 레지스터의 기본 주소를 넣습니다 (x86_64 ABI) arch/x86/kernel/traps.c 소스 코드 파일에 정의 된이 함수를 호출하고 호출하십시오.

asmlinkage __visible notrace struct pt_regs *sync_regs(struct pt_regs *eregs)
{
    struct pt_regs *regs = task_pt_regs(current);
    *regs = *eregs;
    return regs;
}

이 함수는 arch/x86/include/asm/processor.h에 정의 된 task_ptr_regs 매크로의 결과를 가져옵니다. 헤더 파일을 스택 포인터에 저장하고 반환하고, task_ptr_regs 매크로는 일반 커널 스택에 대한 포인터를 나타내는 thread.sp0의 주소로 확장됩니다.

#define task_pt_regs(tsk)       ((struct pt_regs *)(tsk)->thread.sp0 - 1)

우리가 사용자 공간에서 왔을 때, 이것은 예외 처리기가 실제 프로세스 컨텍스트에서 실행될 것임을 의미합니다. sync_regs에서 스택 포인터를 얻은 후 스택을 전환합니다.

movq    %rax, %rsp

The last two steps before an exception handler will call secondary handler are:

  1. Passing pointer to pt_regs structure which contains preserved general purpose registers to the %rdi register:

movq    %rsp, %rdi

as it will be passed as first parameter of secondary exception handler.

  1. Pass error code to the %rsi register as it will be second argument of an exception handler and set it to -1 on the stack for the same purpose as we did it before - to prevent restart of a system call:

.if \has_error_code
    movq    ORIG_RAX(%rsp), %rsi
    movq    $-1, ORIG_RAX(%rsp)
.else
    xorl    %esi, %esi
.endif

Additionally you may see that we zeroed the %esi register above in a case if an exception does not provide error code.

In the end we just call secondary exception handler:

call    \do_sym

which:

dotraplinkage void do_debug(struct pt_regs *regs, long error_code);

will be for debug exception and:

dotraplinkage void notrace do_int3(struct pt_regs *regs, long error_code);

will be for int 3 exception. In this part we will not see implementations of secondary handlers, because of they are very specific, but will see some of them in one of next parts.

We just considered first case when an exception occurred in userspace. Let's consider last two.

An exception with paranoid > 0 occurred in kernelspace

In this case an exception was occurred in kernelspace and idtentry macro is defined with paranoid=1 for this exception. This value of paranoid means that we should use slower way that we saw in the beginning of this part to check do we really came from kernelspace or not. The paranoid_entry routing allows us to know this:

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)

As you may see, this function represents the same that we covered before. We use second (slow) method to get information about previous state of an interrupted task. As we checked this and executed SWAPGS in a case if we came from userspace, we should to do the same that we did before: We need to put pointer to a structure which holds general purpose registers to the %rdi (which will be first parameter of a secondary handler) and put error code if an exception provides it to the %rsi (which will be second parameter of a secondary handler):

movq    %rsp, %rdi

.if \has_error_code
    movq    ORIG_RAX(%rsp), %rsi
    movq    $-1, ORIG_RAX(%rsp)
.else
    xorl    %esi, %esi
.endif

The last step before a secondary handler of an exception will be called is cleanup of new IST stack fram:

.if \shift_ist != -1
    subq    $EXCEPTION_STKSZ, CPU_TSS_IST(\shift_ist)
.endif

You may remember that we passed the shift_ist as argument of the idtentry macro. Here we check its value and if its not equal to -1, we get pointer to a stack from Interrupt Stack Table by shift_ist index and setup it.

In the end of this second way we just call secondary exception handler as we did it before:

call    \do_sym

The last method is similar to previous both, but an exception occured with paranoid=0 and we may use fast method determination of where we are from.

Exit from an exception handler

After secondary handler will finish its works, we will return to the idtentry macro and the next step will be jump to the error_exit:

jmp    error_exit

routine. The error_exit function defined in the same arch/x86/entry/entry_64.S assembly source code file and the main goal of this function is to know where we are from (from userspace or kernelspace) and execute SWPAGS depends on this. Restore registers to previous state and execute iret instruction to transfer control to an interrupted task.

That's all.

Conclusion

It is the end of the third part about interrupts and interrupt handling in the Linux kernel. We saw the initialization of the Interrupt descriptor table in the previous part with the #DB and #BP gates and started to dive into preparation before control will be transferred to an exception handler and implementation of some interrupt handlers in this part. In the next part we will continue to dive into this theme and will go next by the setup_arch function and will try to understand interrupts handling related stuff.

If you have any questions or suggestions write me a comment or ping me at twitter.

Please note that English is not my first language, And I am really sorry for any inconvenience. If you find any mistakes please send me PR to linux-insides.

Last updated