01 · QUESTION
무엇을 확인할 것인가
예외가 들어왔을 때 어느 stack을 선택하고, user register를 어디까지 저장하며, kernel이 다시 sleep 가능한 context라고 판단하는 시점은 언제인가?
모든 architecture는 hardware가 제공한 최소 상태를 kernel entry frame으로 확장하고 exception class를 해석한 뒤 공통 irq, fault, syscall 계층으로 보낸다. 다만 hardware가 자동 저장하는 값, vector 주소 계산법, privilege 전환 때 stack을 바꾸는 방법은 서로 다르다.
assembly entry가 pt_regs를 완성하기 전에는 C ABI와 scheduler의 일반 가정을 적용할 수 없다. NMI와 같은 재진입 경로, user access 차단, speculation 방어, lockdep/RCU 진입은 정해진 순서로만 호출해야 한다.
02 · CONTRACT
공통 규약과 아키텍처별 구현
x86-64의 아래 entry_64.S 원본 발췌와 IDT 그림은 IDT 진입 경로를 설명한다. FRED가 활성화된 시스템은 entry_64_fred.S·entry_fred.c로 진입하며, #PF 주소도 read_cr2() 대신 fred_event_data(regs)에서 가져온다.
| architecture | 핵심 mechanism | 실패 형태 | 확인할 상태 |
|---|---|---|---|
| arm64 | VBAR_EL1 vector slot과 EL0/EL1 sync, IRQ, FIQ, SError 분기 | 잘못된 vector offset, 불완전한 register save 또는 PAN 복원 누락은 return 직후 register corruption이나 kernel user-memory 접근 허용으로 나타난다. | VBAR_EL1·ELR_EL1·SPSR_EL1과 pt_regs를 먼저 본다. 동기 abort에서는 ESR_EL1.EC를 확인하고, ESR_ELx_FnV가 설정되지 않는 등 FAR 유효성이 확인될 때만 FAR_EL1을 fault 주소로 사용한다. IRQ는 GIC INTID를 본다. |
| x86-64 | IDT gate와 entry_64.S의 idtentry macro (FRED 비활성 경로) | error-code 유무를 잘못 맞추면 모든 frame offset이 한 word씩 밀린다. SWAPGS 판단 실패는 per-CPU 주소를 user GSBASE로 해석하는 치명적 오류를 만든다. | IDT/FRED 선택, vector·error_code·CS·RSP와 unwind 결과를 확인한다. #PF 주소는 IDT 경로의 CR2 또는 FRED 경로의 event data에서 확인한다. |
| RISC-V | STVEC가 가리키는 handle_exception과 SCAUSE dispatch | kernel trap에서 user stack으로 오인하거나 sscratch를 늦게 복원하면 nested trap이 잘못된 task와 stack을 사용한다. | STVEC, SCAUSE interrupt bit와 code, SEPC, STVAL, SSTATUS.SPP/SPIE, sscratch와 pt_regs.sp를 기록한다. |
03 · DIAGRAMS
세 그림으로 먼저 읽기
arm64
- mechanism
- VBAR_EL1 vector slot과 EL0/EL1 sync, IRQ, FIQ, SError 분기
- state
- 예외 진입은 복귀 위치·상태를 ELR_EL1·SPSR_EL1에 남기고,
entry.S가 x0–x30과 SP를pt_regs에 저장한다. 동기 경로의el0t_64_sync_handler()는 ESR_EL1로 SVC·abort 등을 분기하고, abort handler가 FAR_EL1을 읽는다. 단 FAR 주소는ESR_ELx_FnV등 유효성 조건을 확인한 경우에만 해석한다. IRQ handler는 ESR_EL1·FAR_EL1을 새 IRQ의 원인·주소로 읽지 않는다. - checkpoint
- VBAR_EL1·ELR_EL1·SPSR_EL1과
pt_regs를 먼저 본다. 동기 abort에서는 ESR_EL1.EC를 확인하고,ESR_ELx_FnV가 설정되지 않는 등 FAR 유효성이 확인될 때만 FAR_EL1을 fault 주소로 사용한다. IRQ는 GIC INTID를 본다.
x86-64
- mechanism
- IDT gate와 entry_64.S의 idtentry macro (FRED 비활성 경로)
- state
- 64-bit IDT 경로는 SS, RSP, RFLAGS, CS, RIP를 hardware frame에 저장하고, 일부 exception은 error code도 push한다. privilege 전환이나 IST 선택은 사용할 stack을 바꾼다. entry macro는 나머지 register를 저장해
pt_regs형태를 맞춘다. 아래의 일반idtentry발췌와 IST/paranoid 전용 경로는 구분해야 한다. FRED가 활성화되면 별도 진입점과 FRED frame을 사용한다. - checkpoint
- IDT/FRED 선택, vector·error_code·CS·RSP와 unwind 결과를 확인한다. #PF 주소는 IDT 경로의 CR2 또는 FRED 경로의 event data에서 확인한다.
RISC-V
- mechanism
- STVEC가 가리키는 handle_exception과 SCAUSE dispatch
- state
- trap 진입 시 hardware는 SEPC, SCAUSE, STVAL과 SSTATUS만 갱신하고 일반 register는 저장하지 않는다. assembly가
sscratch와tp를 이용해 user/kernel trap을 구별하고 task kernel stack의 pt_regs에 x1-x31을 저장한다. - checkpoint
- STVEC, SCAUSE interrupt bit와 code, SEPC, STVAL, SSTATUS.SPP/SPIE, sscratch와 pt_regs.sp를 기록한다.
sscratch 교환 순서가 재진입 가능한 시점을 정한다.arm64
- 동기 abort
- ELR_EL1·SPSR_EL1·
pt_regs와 ESR_EL1.EC를 확인한다. FAR_EL1은ESR_ELx_FnV가 0인 등 주소 유효성이 확인될 때만 fault 주소로 쓴다. - IRQ
- ELR_EL1·SPSR_EL1·
pt_regs와 GIC INTID를 본다. ESR_EL1·FAR_EL1을 이번 IRQ의 원인·주소로 해석하지 않는다.
x86-64
- #PF
- RIP·CS·RFLAGS·error code와 fault 주소를 본다. 주소는 IDT면
read_cr2(), FRED면fred_event_data(regs)다. - 외부 IRQ
- 선택된 IDT/FRED 경로의 frame과 vector·APIC 상태를 본다. CR2를 이번 IRQ의 주소로 해석하지 않는다.
RISC-V
- page fault
- SEPC·SCAUSE·STVAL을 함께 사용해 fault를 분류한다.
- interrupt
- SCAUSE interrupt bit와 code 및 로컬/외부 controller 상태를 본다. STVAL 하나로 interrupt source를 판정하지 않는다.
04 · SOURCE
Linux 6.18.37 원본 코드와 주요 구문 해설
소스 위치를 고정된 숫자로 복사하지 않고 Linux v6.18.37 tree에서 함수 선언을 다시 찾아 발췌했습니다. 원문 전체 발췌를 보존하고, 개별 해설은 근거가 있는 구문에만 붙였습니다.
arm64 · Linux 6.18.37
VBAR_EL1 vector slot과 EL0/EL1 sync, IRQ, FIQ, SError 분기
예외 진입은 복귀 위치·상태를 ELR_EL1·SPSR_EL1에 남기고, entry.S가 x0–x30과 SP를 pt_regs에 저장한다. 동기 경로의 el0t_64_sync_handler()는 ESR_EL1로 SVC·abort 등을 분기하고, abort handler가 FAR_EL1을 읽는다. 단 FAR 주소는 ESR_ELx_FnV 등 유효성 조건을 확인한 경우에만 해석한다. IRQ handler는 ESR_EL1·FAR_EL1을 새 IRQ의 원인·주소로 읽지 않는다.
원본 코드: arch/arm64/kernel/entry-common.c:752-834
752}
753
754static void noinstr el0_fpac(struct pt_regs *regs, unsigned long esr)
755{
756 arm64_enter_from_user_mode(regs);
757 local_daif_restore(DAIF_PROCCTX);
758 do_el0_fpac(regs, esr);
759 arm64_exit_to_user_mode(regs);
760}
761
762asmlinkage void noinstr el0t_64_sync_handler(struct pt_regs *regs)
763{
764 unsigned long esr = read_sysreg(esr_el1);
765
766 switch (ESR_ELx_EC(esr)) {
767 case ESR_ELx_EC_SVC64:
768 el0_svc(regs);
769 break;
770 case ESR_ELx_EC_DABT_LOW:
771 el0_da(regs, esr);
772 break;
773 case ESR_ELx_EC_IABT_LOW:
774 el0_ia(regs, esr);
775 break;
776 case ESR_ELx_EC_FP_ASIMD:
777 el0_fpsimd_acc(regs, esr);
778 break;
779 case ESR_ELx_EC_SVE:
780 el0_sve_acc(regs, esr);
781 break;
782 case ESR_ELx_EC_SME:
783 el0_sme_acc(regs, esr);
784 break;
785 case ESR_ELx_EC_FP_EXC64:
786 el0_fpsimd_exc(regs, esr);
787 break;
788 case ESR_ELx_EC_SYS64:
789 case ESR_ELx_EC_WFx:
790 el0_sys(regs, esr);
791 break;
792 case ESR_ELx_EC_SP_ALIGN:
793 el0_sp(regs, esr);
794 break;
795 case ESR_ELx_EC_PC_ALIGN:
796 el0_pc(regs, esr);
797 break;
798 case ESR_ELx_EC_UNKNOWN:
799 el0_undef(regs, esr);
800 break;
801 case ESR_ELx_EC_BTI:
802 el0_bti(regs);
803 break;
804 case ESR_ELx_EC_MOPS:
805 el0_mops(regs, esr);
806 break;
807 case ESR_ELx_EC_GCS:
808 el0_gcs(regs, esr);
809 break;
810 case ESR_ELx_EC_BREAKPT_LOW:
811 el0_breakpt(regs, esr);
812 break;
813 case ESR_ELx_EC_SOFTSTP_LOW:
814 el0_softstp(regs, esr);
815 break;
816 case ESR_ELx_EC_WATCHPT_LOW:
817 el0_watchpt(regs, esr);
818 break;
819 case ESR_ELx_EC_BRK64:
820 el0_brk64(regs, esr);
821 break;
822 case ESR_ELx_EC_FPAC:
823 el0_fpac(regs, esr);
824 break;
825 default:
826 el0_inv(regs, esr);
827 }
828}
829
830static void noinstr el0_interrupt(struct pt_regs *regs,
831 void (*handler)(struct pt_regs *))
832{
833 arm64_enter_from_user_mode(regs);
834 주요 구문 해설
원본 83줄은 그대로 표시하며, 검토한 71개 구문에 설명을 붙였습니다.
static void noinstr el0_fpac(struct pt_regs *regs, unsigned long esr)64비트 사용자 모드의 포인터 인증 실패(FPAC)를 처리하는 진입 함수입니다. regs는 저장 레지스터, esr은 실패 명령의 예외 정보이며 noinstr는 민감한 진입 구간의 계측 삽입을 막습니다.
arm64_enter_from_user_mode(regs);사용자 모드에서 FPAC 예외로 들어온 사실을 문맥 추적과 RCU 진입 상태에 반영합니다. 포인터 인증 실패 처리기를 호출하기 전에 커널 진입 상태를 갖춥니다.
local_daif_restore(DAIF_PROCCTX);DAIF를 일반 커널 프로세스 문맥의 마스크 상태로 복원합니다. 진입 직후의 강한 예외 차단 상태를 풀고 포인터 인증 실패를 처리할 준비를 합니다.
do_el0_fpac(regs, esr);포인터 인증 실패를 사용자 SIGILL/ILL_ILLOPN으로 전달합니다. 저장된 PC와 ESR로 실패 위치·원인을 남기며 실패한 포인터 연산을 정상 실행으로 이어가지 않습니다.
arm64_exit_to_user_mode(regs);FPAC 예외 처리를 마친 뒤 시그널·재스케줄 등 사용자 복귀 전 작업을 처리합니다. 단순 C 반환과 달리 사용자 모드로 돌아갈 상태를 정리하는 경로입니다.
asmlinkage void noinstr el0t_64_sync_handler(struct pt_regs *regs)EL0에서 발생한 AArch64 동기 예외의 C 분배기입니다. 어셈블리 진입 코드가 만든 pt_regs를 받아 ESR의 예외 클래스에 맞는 처리기를 선택합니다.
unsigned long esr = read_sysreg(esr_el1);ESR_EL1에서 예외 원인과 부가 정보를 읽습니다. 아래 switch는 이 값의 EC 필드로 사용자 모드 동기 예외를 분류합니다.
switch (ESR_ELx_EC(esr)) {ESR의 Exception Class를 읽어 system call, instruction/data abort, alignment, debug와 FP/SVE trap을 나누는 중심 분기다.
case ESR_ELx_EC_SVC64:AArch64 SVC64를 Linux syscall entry로 전달하는 경계다.
el0_svc(regs);64비트 사용자 코드의 SVC 예외를 시스템 호출 진입 경로로 넘깁니다. regs에 저장된 시스템 호출 번호와 인자를 사용하여 서비스 함수를 선택합니다.
break;시스템 호출 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_DABT_LOW:낮은 예외 레벨 EL0에서 발생한 데이터 abort 클래스입니다. 데이터 주소 변환·권한 등 접근 오류를 el0_da로 보냅니다.
el0_da(regs, esr);사용자 데이터 접근에서 발생한 abort를 처리합니다. ESR과 예외 주소를 이용해 페이지 폴트 처리 또는 오류 시그널 전달로 이어집니다.
break;데이터 abort 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_IABT_LOW:EL0에서 명령을 가져오다가 발생한 instruction abort 클래스입니다. 실행 대상 주소의 매핑·권한 실패를 데이터 접근과 구분합니다.
el0_ia(regs, esr);사용자 명령어 인출 중 발생한 abort를 처리합니다. 실행할 주소의 매핑·권한 문제를 데이터 접근 오류와 구분하는 경로입니다.
break;명령 abort 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_FP_ASIMD:FP 또는 Advanced SIMD 접근 자체가 트랩된 클래스입니다. 부동소수점 산술 결과의 오류인 FP_EXC64와 다른 원인입니다.
el0_fpsimd_acc(regs, esr);사용자 FP/SIMD 접근 트랩을 do_fpsimd_acc로 넘깁니다. 이 버전은 FPSIMD를 지원하지 않으면 SIGILL을 보내고, 지원하는데도 트랩되면 비정상 커널 상태로 BUG를 일으킵니다. 지연된 레지스터 복원을 수행하는 경로가 아닙니다.
break;FP/SIMD 접근 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_SVE:SVE 명령 접근이 트랩된 클래스입니다. 태스크의 SVE 사용 상태를 준비하거나 미지원 접근을 거부하는 처리로 보냅니다.
el0_sve_acc(regs, esr);사용자 SVE 접근 트랩을 처리하여 처음 사용에 필요한 벡터 저장 공간과 레지스터 상태를 준비합니다. SVE 미지원 또는 32비트 호환 태스크이면 SIGILL, 저장 공간 확보 실패이면 SIGKILL로 처리합니다.
break;SVE 접근 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_SME:SME 접근이 트랩된 클래스입니다. SME가 아직 비활성인지, 허용되지 않은 모드에서 명령을 사용했는지 ESR로 구분하는 처리에 들어갑니다.
el0_sme_acc(regs, esr);SME 비활성 상태에서의 첫 접근이면 SVE·SME 저장 공간을 확보하고 태스크의 SME 사용 상태와 벡터 길이를 반영합니다. 지원하지 않거나 잘못된 실행 모드에서의 접근이면 SIGILL을 보내는 별도 분기가 있습니다.
break;SME 접근 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_FP_EXC64:AArch64 부동소수점 연산 예외 클래스입니다. 0 나누기·overflow 같은 연산 오류를 SIGFPE로 알리는 경로입니다.
el0_fpsimd_exc(regs, esr);부동소수점 연산에서 발생한 산술 예외를 처리합니다. FP/SIMD 장치에 접근하려다 난 트랩과 달리 연산 오류를 사용자 시그널로 전달하는 경우입니다.
break;부동소수점 산술 예외 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_SYS64:AArch64 시스템 명령·시스템 레지스터 접근 트랩의 클래스입니다. 다음 WFx와 같은 el0_sys 처리기를 공유합니다.
case ESR_ELx_EC_WFx:WFI·WFE 등 대기 명령의 트랩 클래스입니다. 앞의 SYS64와 함께 시스템 명령 에뮬레이션·허용 여부 처리로 합류합니다.
el0_sys(regs, esr);사용자 시스템 레지스터 접근 또는 WFx 트랩을 처리합니다. ESR에 인코딩된 명령 정보를 해석하여 허용된 에뮬레이션이나 미지원 명령 처리를 선택합니다.
break;시스템 명령 트랩 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_SP_ALIGN:스택 포인터 정렬 규칙을 위반한 예외입니다. regs->sp를 문제 주소로 전달하는 전용 처리기로 들어갑니다.
el0_sp(regs, esr);사용자 스택 포인터의 정렬 오류를 처리합니다. 주소 변환 실패가 아니라 SP 정렬 규칙 위반에 해당하는 예외입니다.
break;SP 정렬 예외 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_PC_ALIGN:명령 주소 PC가 요구 정렬을 만족하지 않은 예외입니다. 잘못된 분기 목표 주소를 PC 정렬 처리 경로로 보냅니다.
el0_pc(regs, esr);사용자 프로그램 카운터의 정렬 오류를 처리합니다. 정렬되지 않은 명령 주소로 분기한 경우를 별도의 PC 정렬 예외로 전달합니다.
break;PC 정렬 예외 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_UNKNOWN:하드웨어가 명령 원인을 다른 클래스로 분류하지 못한 예외입니다. 미정의 명령 에뮬레이션·오류 시그널 처리 경로를 선택합니다.
el0_undef(regs, esr);분류되지 않은 사용자 명령 예외를 미정의 명령 처리기로 넘깁니다. 지원되지 않거나 잘못된 명령을 실행한 상황의 에뮬레이션·시그널 처리를 맡깁니다.
break;미정의 명령 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_BTI:BTI의 간접 분기 착지점 검사를 위반한 클래스입니다. 지정된 종류의 간접 분기가 허용되지 않은 위치에 도착한 상황을 처리합니다.
el0_bti(regs);BTI가 요구하는 유효한 분기 착지점을 위반한 예외를 처리합니다. 간접 분기가 허용되지 않은 명령 위치에 도착한 상황입니다.
break;BTI 위반 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_MOPS:MOPS 메모리 복사·설정 명령 실행 중 발생한 클래스입니다. 진행 레지스터를 조정하여 prologue에서 재시작할 수 있게 하는 전용 경로입니다.
el0_mops(regs, esr);메모리 복사·설정 명령인 MOPS의 예외를 처리합니다. ESR을 해석해 사용자 레지스터를 MOPS prologue부터 다시 시작할 수 있는 상태로 조정하고, single-step 중이면 현재 단계를 끝냅니다.
break;MOPS 예외 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_GCS:guarded control stack 검사에서 발생한 클래스입니다. 제어 흐름 보호 스택의 위반을 일반 페이지 폴트와 구분하여 처리합니다.
el0_gcs(regs, esr);guarded control stack 검사에서 발생한 예외를 처리합니다. 보호된 반환 흐름과 실제 제어 흐름이 맞지 않는 등의 GCS 오류를 일반 메모리 폴트와 구분합니다.
break;GCS 위반 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_BREAKPT_LOW:EL0의 하드웨어 breakpoint 클래스입니다. 실행 주소가 디버그 레지스터에 설정된 감시 조건과 일치한 경우입니다.
el0_breakpt(regs, esr);사용자 하드웨어 breakpoint 예외를 처리합니다. 디버그 레지스터에 설정한 실행 주소 감시가 일치했을 때의 경로입니다.
break;하드웨어 breakpoint 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_SOFTSTP_LOW:EL0의 single-step 디버그 클래스입니다. 한 명령 실행을 마쳤거나 일시 중지한 breakpoint를 지나가는 디버그 처리를 선택합니다.
el0_softstp(regs, esr);사용자 single-step 예외를 처리합니다. 디버거가 한 명령씩 실행하도록 설정한 뒤 발생하는 디버그 이벤트를 전달합니다.
break;single-step 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_WATCHPT_LOW:EL0의 하드웨어 watchpoint 클래스입니다. 감시 대상 데이터 주소의 읽기·쓰기 이벤트를 FAR_EL1과 함께 처리합니다.
el0_watchpt(regs, esr);사용자 watchpoint 예외를 처리합니다. 설정한 데이터 주소에 대한 읽기·쓰기가 감시 조건과 일치한 경우입니다.
break;watchpoint 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_BRK64:사용자 AArch64 BRK 명령으로 발생한 소프트웨어 breakpoint 클래스입니다. ESR의 BRK 정보와 레지스터 문맥을 전용 처리기로 전달합니다.
el0_brk64(regs, esr);사용자 AArch64 BRK 명령 예외를 처리합니다. 소프트웨어 breakpoint 명령의 immediate와 레지스터 문맥을 디버그 처리에 넘깁니다.
break;BRK 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
case ESR_ELx_EC_FPAC:포인터 인증 실패를 직접 보고하는 FPAC 클래스입니다. 실패를 SIGILL로 전달하는 el0_fpac 경로를 선택합니다.
el0_fpac(regs, esr);포인터 인증 실패를 FPAC 처리기로 넘겨 SIGILL/ILL_ILLOPN을 전달합니다. 실패한 명령 PC와 ESR을 사용하므로 나중에 잘못된 포인터를 역참조해서 발생하는 일반 주소 오류와 구분됩니다.
break;FPAC 전용 처리를 마쳤으므로 예외 클래스 switch를 종료합니다. 아래의 다른 예외 처리기를 연달아 실행하지 않게 하는 C 제어문이며 CPU의 예외 복귀 명령은 아닙니다.
default:앞에서 지원한 어느 예외 클래스에도 맞지 않는 경우입니다. 알 수 없는 동기 예외를 bad_el0_sync로 처리하는 기본 경로로 보냅니다.
el0_inv(regs, esr);이 switch가 지원하지 않는 사용자 동기 예외를 잘못된 예외 경로로 처리합니다. 정상 처리기로 분류할 수 없는 ESR과 레지스터 상태를 다룹니다.
static void noinstr el0_interrupt(struct pt_regs *regs,사용자 실행을 중단하고 들어온 IRQ/FIQ를 처리하는 공통 함수입니다. regs는 중단된 사용자 문맥이며 아래의 handler 인자로 실제 컨트롤러 진입 함수를 받습니다.
void (*handler)(struct pt_regs *))pt_regs 포인터를 받아 반환값 없이 인터럽트를 처리하는 함수 포인터 인자입니다. IRQ에는 handle_arch_irq, FIQ에는 handle_arch_fiq 등을 넘겨 공통 진입·복귀 처리를 공유합니다.
arm64_enter_from_user_mode(regs);사용자 실행을 중단하고 인터럽트 처리에 진입했음을 커널 문맥 추적에 반영합니다. 이 사용자 진입 경로는 이어서 irq_enter_rcu와 지정된 IRQ/FIQ 처리 함수를 실행할 상태를 갖춥니다.
x86-64 · Linux 6.18.37
IDT gate와 entry_64.S의 idtentry macro (FRED 비활성 경로)
64-bit IDT 경로는 SS, RSP, RFLAGS, CS, RIP를 hardware frame에 저장하고, 일부 exception은 error code도 push한다. privilege 전환이나 IST 선택은 사용할 stack을 바꾼다. entry macro는 나머지 register를 저장해 pt_regs 형태를 맞춘다. 아래의 일반 idtentry 발췌와 IST/paranoid 전용 경로는 구분해야 한다. FRED가 활성화되면 별도 진입점과 FRED frame을 사용한다.
원본 코드: arch/x86/entry/entry_64.S:317-401
317.endm
318
319/**
320 * idtentry - Macro to generate entry stubs for simple IDT entries
321 * @vector: Vector number
322 * @asmsym: ASM symbol for the entry point
323 * @cfunc: C function to be called
324 * @has_error_code: Hardware pushed error code on stack
325 *
326 * The macro emits code to set up the kernel context for straight forward
327 * and simple IDT entries. No IST stack, no paranoid entry checks.
328 */
329.macro idtentry vector asmsym cfunc has_error_code:req
330SYM_CODE_START(\asmsym)
331
332 .if \vector == X86_TRAP_BP
333 /* #BP advances %rip to the next instruction */
334 UNWIND_HINT_IRET_ENTRY offset=\has_error_code*8 signal=0
335 .else
336 UNWIND_HINT_IRET_ENTRY offset=\has_error_code*8
337 .endif
338
339 ENDBR
340 ASM_CLAC
341 cld
342
343 .if \has_error_code == 0
344 pushq $-1 /* ORIG_RAX: no syscall to restart */
345 .endif
346
347 .if \vector == X86_TRAP_BP
348 /*
349 * If coming from kernel space, create a 6-word gap to allow the
350 * int3 handler to emulate a call instruction.
351 */
352 testb $3, CS-ORIG_RAX(%rsp)
353 jnz .Lfrom_usermode_no_gap_\@
354 .rept 6
355 pushq 5*8(%rsp)
356 .endr
357 UNWIND_HINT_IRET_REGS offset=8
358.Lfrom_usermode_no_gap_\@:
359 .endif
360
361 idtentry_body \cfunc \has_error_code
362
363_ASM_NOKPROBE(\asmsym)
364SYM_CODE_END(\asmsym)
365.endm
366
367/*
368 * Interrupt entry/exit.
369 *
370 + The interrupt stubs push (vector) onto the stack, which is the error_code
371 * position of idtentry exceptions, and jump to one of the two idtentry points
372 * (common/spurious).
373 *
374 * common_interrupt is a hotpath, align it to a cache line
375 */
376.macro idtentry_irq vector cfunc
377 .p2align CONFIG_X86_L1_CACHE_SHIFT
378 idtentry \vector asm_\cfunc \cfunc has_error_code=1
379.endm
380
381/**
382 * idtentry_mce_db - Macro to generate entry stubs for #MC and #DB
383 * @vector: Vector number
384 * @asmsym: ASM symbol for the entry point
385 * @cfunc: C function to be called
386 *
387 * The macro emits code to set up the kernel context for #MC and #DB
388 *
389 * If the entry comes from user space it uses the normal entry path
390 * including the return to user space work and preemption checks on
391 * exit.
392 *
393 * If hits in kernel mode then it needs to go through the paranoid
394 * entry as the exception can hit any random state. No preemption
395 * check on exit to keep the paranoid path simple.
396 */
397.macro idtentry_mce_db vector asmsym cfunc
398SYM_CODE_START(\asmsym)
399 UNWIND_HINT_IRET_ENTRY
400 ENDBR
401 ASM_CLAC주요 구문 해설
원본 85줄은 그대로 표시하며, 검토한 37개 구문에 설명을 붙였습니다.
.endmC 예외 처리기를 호출하고 error_return으로 이어지는 idtentry_body 매크로 정의를 끝냅니다. 이 줄 자체는 실행될 명령을 생성하지 않습니다.
.macro idtentry vector asmsym cfunc has_error_code:req간단한 IDT 진입 코드를 만드는 매크로를 정의합니다. vector는 예외 번호, asmsym은 진입 심볼, cfunc은 C 처리기이고 has_error_code는 하드웨어 오류 코드 유무를 반드시 지정해야 합니다.
SYM_CODE_START(\asmsym)이번 매크로 호출에서 지정한 asmsym 이름으로 진입 코드 심볼을 시작합니다. IDT가 가리킬 실제 명령 주소의 경계입니다. 이 표기는 빌드 도구에 코드 심벌과 정렬을 알려 주며 그 자체가 런타임 함수 호출은 아닙니다.
.if \vector == X86_TRAP_BP생성 중인 진입점이 int3 breakpoint인지 어셈블 시점에 검사합니다. int3는 저장 RIP가 이미 다음 명령을 가리키므로 스택 추적 힌트가 다릅니다.
UNWIND_HINT_IRET_ENTRY offset=\has_error_code*8 signal=0int3용 IRET 진입 프레임 위치를 unwinder에 알려 줍니다. 오류 코드가 있으면 그 8바이트를 offset에 반영하고 signal=0으로 저장 RIP가 다음 명령임을 구분합니다.
.elseint3 이외 예외의 unwind 정보를 생성하는 분기입니다. 실행 중 조건 분기가 아니라 매크로 확장 때 선택됩니다.
UNWIND_HINT_IRET_ENTRY offset=\has_error_code*8일반 예외의 하드웨어 IRET 프레임 위치를 기록합니다. has_error_code*8만큼의 오류 코드 슬롯을 반영하며 이 지시문 자체는 스택을 이동하지 않습니다.
.endif예외 종류별 unwind 힌트 선택을 끝냅니다. 뒤의 실제 진입 명령들은 두 경우 모두 생성됩니다.
ENDBRIBT 구성에 맞는 ENDBR 진입 표식을 넣습니다. 간접 분기의 유효한 착지점임을 CPU에 알리는 명령이며 지원·빌드 조건에 따라 매크로가 달라집니다.
ASM_CLACSMAP이 요구하는 경우 AC 플래그를 지워 커널의 사용자 메모리 접근을 기본 차단 상태로 만듭니다. 중단된 코드가 열어 둔 접근 허용을 예외 처리기가 그대로 물려받지 않게 합니다.
cld방향 플래그 DF를 0으로 만듭니다. 이후 C 코드와 문자열 명령이 증가 방향이라는 호출 규약의 전제에서 실행되도록 합니다.
.if \has_error_code == 0하드웨어가 오류 코드를 스택에 넣지 않는 예외인지 어셈블 시점에 검사합니다. 해당 경우 공통 프레임을 맞추기 위한 슬롯을 아래에서 추가합니다.
pushq $-1 /* ORIG_RAX: no syscall to restart */오류 코드가 없는 예외에 ORIG_RAX 슬롯 값 -1을 쌓습니다. 공통 pt_regs 배치를 맞추고 재시작할 시스템 호출이 없음을 나타냅니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.
.endif오류 코드가 없는 경우의 슬롯 추가를 끝냅니다. 이미 하드웨어 오류 코드가 있는 예외에는 같은 슬롯을 중복 생성하지 않습니다.
.if \vector == X86_TRAP_BPint3 예외이면 커널에서 발생했을 때 CALL 에뮬레이션용 여유 공간을 만드는 코드를 생성합니다. 다른 예외에는 이 특별 처리가 들어가지 않습니다.
testb $3, CS-ORIG_RAX(%rsp)저장된 CS의 하위 두 비트로 int3가 사용자 모드에서 발생했는지 검사합니다. 커널 int3이면 CALL 명령 에뮬레이션용으로 스택에 여섯 워드 공간을 만들고, 사용자 예외이면 그 공간 생성을 건너뜁니다. 메모리 피연산자가 있는 TEST 명령이지 함수 호출이 아닙니다.
jnz .Lfrom_usermode_no_gap_\@저장 CS의 권한 비트가 사용자 모드를 나타내면 여유 공간 생성을 건너뜁니다. 라벨의 매크로 확장 번호는 여러 진입점 사이의 이름 충돌을 막습니다.
.rept 6다음 pushq를 여섯 번 생성하도록 어셈블러에 지시합니다. 실행 중 루프가 아니라 스택 프레임 여섯 워드를 복사할 여섯 명령이 생깁니다.
pushq 5*8(%rsp)현재 RSP보다 40바이트 위의 워드를 스택에 복사합니다. 반복할 때마다 RSP가 내려가므로 여섯 워드 프레임을 차례로 복제하여 커널 int3의 CALL 에뮬레이션에 필요한 공간을 만듭니다. PUSHQ는 rsp를 8바이트 낮춘 뒤 새 스택 위치에 피연산자 값을 기록합니다.
.endr여섯 pushq를 생성하는 반복 지시를 마칩니다. 뒤의 명령은 한 번만 이어집니다.
UNWIND_HINT_IRET_REGS offset=8프레임을 복제한 뒤의 IRET 레지스터 배치를 unwinder에 알려 줍니다. offset 8은 추가 슬롯을 반영하는 메타데이터이며 실제 레지스터 복원은 아닙니다.
.Lfrom_usermode_no_gap_\@:사용자 int3가 여유 공간 생성을 건너뛰어 도착하는 공통 위치입니다. 커널 int3도 프레임 준비를 마치면 여기로 이어집니다.
.endifint3 전용 스택 처리 코드 생성을 끝냅니다. 아래 공통 몸체는 모든 idtentry 종류가 사용합니다.
idtentry_body \cfunc \has_error_code진입 문맥을 정리하고 지정 C 처리기를 호출한 뒤 error_return으로 가는 공통 몸체를 펼칩니다. cfunc과 오류 코드 유무를 전달해 처리기 인자와 프레임을 맞춥니다.
_ASM_NOKPROBE(\asmsym)이 진입 심볼에 kprobe를 설치하지 못하도록 금지 목록에 넣습니다. 레지스터·스택이 아직 예외 진입 중인 코드에서 추가 breakpoint 예외가 발생하지 않게 합니다.
SYM_CODE_END(\asmsym)생성한 asmsym 코드 심볼의 범위와 크기를 마칩니다. 실제 예외 복귀 흐름은 위 공통 몸체가 error_return으로 연결합니다.
.endmIDT 진입 코드를 생성하는 idtentry 매크로 정의를 끝냅니다. 이 지점에서 특정 예외 처리기를 새로 실행하지 않습니다.
+ The interrupt stubs push (vector) onto the stack, which is the error_code원문 주석의 한 줄입니다. IRQ stub이 vector 번호를 예외 프레임의 오류 코드 자리에 쌓고 공통 진입점으로 이동한다는 배치를 설명하며 실행 코드가 아닙니다.
.macro idtentry_irq vector cfunc외부 IRQ용 진입점 생성 매크로를 정의합니다. vector와 C 처리기 cfunc을 받아 공통 idtentry 형식을 IRQ에 맞춰 재사용합니다.
.p2align CONFIG_X86_L1_CACHE_SHIFTIRQ 진입점 주소를 L1 캐시 라인 크기인 2^CONFIG_X86_L1_CACHE_SHIFT 경계에 맞춥니다. 자주 실행되는 인터럽트 진입 명령의 정렬을 위한 지시문입니다.
idtentry \vector asm_\cfunc \cfunc has_error_code=1asm_ 접두사의 진입 심볼을 만들고 IRQ vector가 이미 쌓인 슬롯을 has_error_code=1로 취급합니다. 실제 예외 오류 코드 대신 vector 번호를 C 처리기의 두 번째 인자로 전달할 수 있게 합니다.
.endm외부 IRQ용 진입점 생성 매크로를 끝냅니다. 앞 idtentry 확장으로 만들어진 코드가 실제 진입 경로입니다.
.macro idtentry_mce_db vector asmsym cfuncmachine check와 debug 예외의 특수 진입 매크로입니다. 사용자 진입은 정상 경로로, 커널 진입은 임의의 중간 CPU 상태도 처리하는 paranoid 경로로 나눕니다.
SYM_CODE_START(\asmsym)machine check 또는 debug의 어셈블리 진입 심볼을 시작합니다. 매크로 인자 asmsym에 따라 해당 벡터가 가리킬 주소가 정해집니다. 이 표기는 빌드 도구에 코드 심벌과 정렬을 알려 주며 그 자체가 런타임 함수 호출은 아닙니다.
UNWIND_HINT_IRET_ENTRY하드웨어 IRET 진입 프레임이 있는 현재 스택 위치를 unwinder에 표시합니다. 예외 도중 스택을 추적할 기준이며 IRET를 실행하는 명령이 아닙니다.
ENDBRmachine check/debug 진입점에도 IBT용 ENDBR 표식을 둡니다. 지원하는 구성에서 이 주소를 유효한 간접 분기 목표로 표시합니다.
ASM_CLACmachine check/debug 처리에 들어가기 전에 SMAP 사용자 접근 허용 상태를 지웁니다. 중단된 문맥의 AC 상태와 무관하게 안전한 기본 접근 상태로 시작합니다.
RISC-V · Linux 6.18.37
STVEC가 가리키는 handle_exception과 SCAUSE dispatch
trap 진입 시 hardware는 SEPC, SCAUSE, STVAL과 SSTATUS만 갱신하고 일반 register는 저장하지 않는다. assembly가 sscratch와 tp를 이용해 user/kernel trap을 구별하고 task kernel stack의 pt_regs에 x1-x31을 저장한다.
원본 코드: arch/riscv/kernel/entry.S:88-182
88 REG_L a2, TASK_TI_A2(tp)
89.Lnew_vmalloc_restore_context_a1:
90 REG_L a1, TASK_TI_A1(tp)
91.Lnew_vmalloc_restore_context_a0:
92 REG_L a0, TASK_TI_A0(tp)
93.endm
94
95
96SYM_CODE_START(handle_exception)
97 /*
98 * If coming from userspace, preserve the user thread pointer and load
99 * the kernel thread pointer. If we came from the kernel, the scratch
100 * register will contain 0, and we should continue on the current TP.
101 */
102 csrrw tp, CSR_SCRATCH, tp
103 bnez tp, .Lsave_context
104
105.Lrestore_kernel_tpsp:
106 csrr tp, CSR_SCRATCH
107
108#ifdef CONFIG_64BIT
109 /*
110 * The RISC-V kernel does not eagerly emit a sfence.vma after each
111 * new vmalloc mapping, which may result in exceptions:
112 * - if the uarch caches invalid entries, the new mapping would not be
113 * observed by the page table walker and an invalidation is needed.
114 * - if the uarch does not cache invalid entries, a reordered access
115 * could "miss" the new mapping and traps: in that case, we only need
116 * to retry the access, no sfence.vma is required.
117 */
118 new_vmalloc_check
119#endif
120
121 REG_S sp, TASK_TI_KERNEL_SP(tp)
122
123#ifdef CONFIG_VMAP_STACK
124 addi sp, sp, -(PT_SIZE_ON_STACK)
125 srli sp, sp, THREAD_SHIFT
126 andi sp, sp, 0x1
127 bnez sp, handle_kernel_stack_overflow
128 REG_L sp, TASK_TI_KERNEL_SP(tp)
129#endif
130
131.Lsave_context:
132 REG_S sp, TASK_TI_USER_SP(tp)
133 REG_L sp, TASK_TI_KERNEL_SP(tp)
134 addi sp, sp, -(PT_SIZE_ON_STACK)
135 REG_S x1, PT_RA(sp)
136 REG_S x3, PT_GP(sp)
137 REG_S x5, PT_T0(sp)
138 save_from_x6_to_x31
139
140 /*
141 * Disable user-mode memory access as it should only be set in the
142 * actual user copy routines.
143 *
144 * Disable the FPU/Vector to detect illegal usage of floating point
145 * or vector in kernel space.
146 */
147 li t0, SR_SUM | SR_FS_VS
148
149 REG_L s0, TASK_TI_USER_SP(tp)
150 csrrc s1, CSR_STATUS, t0
151 csrr s2, CSR_EPC
152 csrr s3, CSR_TVAL
153 csrr s4, CSR_CAUSE
154 csrr s5, CSR_SCRATCH
155 REG_S s0, PT_SP(sp)
156 REG_S s1, PT_STATUS(sp)
157 REG_S s2, PT_EPC(sp)
158 REG_S s3, PT_BADADDR(sp)
159 REG_S s4, PT_CAUSE(sp)
160 REG_S s5, PT_TP(sp)
161
162 /*
163 * Set the scratch register to 0, so that if a recursive exception
164 * occurs, the exception vector knows it came from the kernel
165 */
166 csrw CSR_SCRATCH, x0
167
168 /* Load the global pointer */
169 load_global_pointer
170
171 /* Load the kernel shadow call stack pointer if coming from userspace */
172 scs_load_current_if_task_changed s5
173
174#ifdef CONFIG_RISCV_ISA_V_PREEMPTIVE
175 move a0, sp
176 call riscv_v_context_nesting_start
177#endif
178 move a0, sp /* pt_regs */
179
180 /*
181 * MSB of cause differentiates between
182 * interrupts and exceptions주요 구문 해설
원본 95줄은 그대로 표시하며, 검토한 51개 구문에 설명을 붙였습니다.
REG_L a2, TASK_TI_A2(tp)new_vmalloc 검사에 빌려 쓴 a2를 thread_info의 임시 슬롯에서 복원합니다. 이 경로는 즉시 재시도로 해결하지 못하여 일반 예외 처리로 이어질 때 원래 레지스터 값을 되살립니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
.Lnew_vmalloc_restore_context_a1:new_vmalloc 검사에서 a1까지 사용한 경로가 되돌아오는 라벨입니다. a2 복원이 불필요한 분기는 여기로 바로 와 a1·a0만 복원합니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
REG_L a1, TASK_TI_A1(tp)new_vmalloc 검사 중 사용한 a1을 복원합니다. 어디까지 임시 레지스터를 사용했는지에 따라 해당 복원 라벨부터 실행합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
.Lnew_vmalloc_restore_context_a0:new_vmalloc 검사에서 a0만 빌려 쓴 경로의 복원 위치입니다. 사용하지 않은 a1·a2 슬롯을 읽지 않고 원래 a0만 되돌립니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
REG_L a0, TASK_TI_A0(tp)new_vmalloc 검사에 사용한 a0를 복원하고 매크로를 빠져나갑니다. 일반 예외 프레임에 검사 도중 바뀐 임시 값이 저장되는 일을 막습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
.endm새 vmalloc 매핑으로 설명할 수 있는 예외인지 검사하는 매크로 정의를 끝냅니다. 실제 삽입은 아래 new_vmalloc_check 호출 위치에서 이루어집니다.
SYM_CODE_START(handle_exception)공통 예외 진입 코드 handle_exception의 심볼을 시작합니다. 아직 C pt_regs 프레임이 없으므로 처음부터 스택과 tp를 구분하여 준비합니다. 이 표기는 빌드 도구에 코드 심벌과 정렬을 알려 주며 그 자체가 런타임 함수 호출은 아닙니다.
csrrw tp, CSR_SCRATCH, tptp와 sscratch를 교환해 user-origin trap이면 current task pointer를 얻고 kernel-origin이면 별도 경로를 선택한다.
bnez tp, .Lsave_contextCSR_SCRATCH와 교환한 tp가 0이 아니면 사용자 모드에서 들어온 경우로 보고 저장 경로로 갑니다. 커널 진입에서는 scratch가 0이므로 아래에서 원래 커널 tp를 되찾습니다.
.Lrestore_kernel_tpsp:커널 문맥에서 예외가 났을 때 tp와 sp를 복구·점검하는 위치입니다. 사용자 진입은 앞 분기로 이 구간을 건너뜁니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
csrr tp, CSR_SCRATCH진입 시 교환으로 CSR_SCRATCH에 들어간 원래 커널 tp를 다시 읽습니다. 이후 thread_info 접근과 스택 저장이 현재 태스크를 기준으로 동작하게 합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
#ifdef CONFIG_64BIT64비트 RISC-V에서는 새 vmalloc 매핑의 TLB 반영을 늦출 수 있습니다. 해당 매핑을 아직 보지 못해 trap이 난 경우 new_vmalloc_check가 필요한 무효화 또는 접근 재시도를 처리합니다. 32비트 빌드에는 이 경로가 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
new_vmalloc_check커널 페이지 폴트가 새 vmalloc 매핑을 아직 관측하지 못한 결과인지 검사합니다. 해당하면 필요한 SFENCE.VMA와 재시도로 해결하고, 아니면 임시 레지스터를 복원한 뒤 일반 예외 저장으로 이어갑니다.
#endif108행에서 시작한 선택 구간을 마칩니다. 64비트 RISC-V에서는 새 vmalloc 매핑의 TLB 반영을 늦출 수 있습니다. 해당 매핑을 아직 보지 못해 trap이 난 경우 new_vmalloc_check가 필요한 무효화 또는 접근 재시도를 처리합니다. 32비트 빌드에는 이 경로가 없습니다.
REG_S sp, TASK_TI_KERNEL_SP(tp)커널에서 예외가 발생했을 때의 sp를 현재 태스크의 kernel_sp 슬롯에 보관합니다. 뒤의 스택 넘침 검사에서 sp를 계산용으로 사용한 뒤 원래 값으로 돌리기 위한 저장입니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
#ifdef CONFIG_VMAP_STACKVMAP_STACK은 가상 매핑된 커널 스택과 보호 영역으로 스택 넘침을 잡는 기능입니다. 켜지면 pt_regs를 놓을 위치의 정렬 비트를 검사해 범위를 벗어났을 때 전용 overflow handler로 보내고, 꺼지면 이 추가 검사를 생략합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
addi sp, sp, -(PT_SIZE_ON_STACK)예외 프레임을 쌓았을 때의 SP를 계산하기 위해 PT_SIZE_ON_STACK만큼 뺍니다. 이 단계는 VMAP_STACK 넘침 검사이며 원래 SP는 앞에서 별도 슬롯에 저장했습니다.
srli sp, sp, THREAD_SHIFT계산한 SP를 THREAD_SHIFT만큼 오른쪽으로 옮겨 스택 정렬 경계를 나타내는 비트를 아래로 내립니다. 태스크 스택이 가드 영역으로 넘어갔는지 판별할 준비입니다.
andi sp, sp, 0x1검사에 필요한 비트 하나만 남깁니다. VMAP_STACK의 정렬·가드 배치에서 이 값이 1이면 프레임을 놓을 위치가 유효 스택 범위를 벗어난 것입니다.
bnez sp, handle_kernel_stack_overflow스택 넘침 비트가 켜져 있으면 전용 overflow 처리로 분기합니다. 그 상태에서 일반 예외 프레임을 계속 쌓아 메모리를 더 손상시키지 않게 합니다.
REG_L sp, TASK_TI_KERNEL_SP(tp)넘침 검사에 사용하기 전의 커널 sp를 다시 읽습니다. 검사용 비트 계산값 대신 실제 스택 주소로 예외 프레임을 만들게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
#endif123행에서 시작한 선택 구간을 마칩니다. VMAP_STACK은 가상 매핑된 커널 스택과 보호 영역으로 스택 넘침을 잡는 기능입니다. 켜지면 pt_regs를 놓을 위치의 정렬 비트를 검사해 범위를 벗어났을 때 전용 overflow handler로 보내고, 꺼지면 이 추가 검사를 생략합니다.
.Lsave_context:사용자 진입과 점검을 마친 커널 진입이 레지스터 저장을 시작하는 공통 라벨입니다. 이제 tp는 현재 태스크의 thread_info를 가리킵니다. 레이블은 현재 위치를 가리키는 이름으로, 별도의 CPU 명령을 추가하지 않습니다.
REG_S sp, TASK_TI_USER_SP(tp)예외 직전의 sp를 user_sp라는 임시 슬롯에 저장합니다. 사용자 진입이면 사용자 스택 주소이고, 커널 예외이면 중단된 커널 스택 주소입니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_L sp, TASK_TI_KERNEL_SP(tp)예외 프레임을 쌓을 커널 스택 주소를 sp로 읽습니다. 다음 명령이 PT_SIZE_ON_STACK만큼 공간을 확보하여 pt_regs를 만듭니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
addi sp, sp, -(PT_SIZE_ON_STACK)커널 스택에서 pt_regs 크기만큼 공간을 확보합니다. 아래 REG_S 명령들은 줄어든 sp를 기준으로 예외 직전 레지스터를 저장합니다.
REG_S x1, PT_RA(sp)hardware가 저장하지 않은 integer register를 Linux pt_regs layout에 맞춰 쌓기 시작한다.
REG_S x3, PT_GP(sp)중단된 코드의 gp(x3)를 pt_regs의 PT_GP 슬롯에 저장합니다. 커널의 전역 포인터로 바꾸기 전에 복귀에 필요한 원래 값을 보존합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_S x5, PT_T0(sp)중단된 코드의 임시 레지스터 t0(x5)를 pt_regs에 저장합니다. 뒤에서 상태 비트 마스크를 만드는 데 t0를 덮어써도 원래 값은 복원할 수 있습니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
save_from_x6_to_x31x6부터 x31까지 일반 레지스터를 pt_regs 슬롯에 저장하는 매크로를 펼칩니다. 이후 C 처리기가 레지스터를 써도 예외 복귀에 필요한 원래 값이 보존됩니다.
li t0, SR_SUM | SR_FS_VS사용자 메모리 접근 SUM과 FP·벡터 상태 비트 FS/VS를 함께 끌 마스크를 t0에 넣습니다. 커널의 허용된 접근·연산 구간 외에서 잘못 사용하면 트랩되게 준비합니다.
REG_L s0, TASK_TI_USER_SP(tp)앞서 임시 저장한 예외 직전 sp를 s0로 가져옵니다. 현재 sp는 예외 프레임을 가리키므로 원래 스택 주소를 별도로 프레임에 기록해야 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
csrrc s1, CSR_STATUS, t0CSR_STATUS의 기존 값을 s1에 받으면서 t0가 가리키는 SUM·FS·VS 비트를 지웁니다. 복귀용 원래 상태는 보존하고 현재 커널 실행에는 접근 제한을 적용합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
csrr s2, CSR_EPC예외가 발생한 명령 PC를 CSR_EPC에서 s2로 읽습니다. 아래 PT_EPC에 보관해 재시도·시스템 호출 진행·복귀 주소 조정에 사용합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
csrr s3, CSR_TVALCSR_TVAL에서 예외 부가 정보를 s3로 읽습니다. 페이지 폴트에서는 문제 주소 등 원인별 자료이며 아래 PT_BADADDR에 저장합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
csrr s4, CSR_CAUSEtrap 원인과 interrupt bit를 읽어 exception table 또는 interrupt dispatch로 보낸다.
csrr s5, CSR_SCRATCH진입 순간의 원래 tp를 CSR_SCRATCH에서 s5로 읽습니다. 사용자에서 왔으면 사용자 스레드 포인터, 커널에서 왔으면 기존 커널 tp이며 pt_regs에 보관합니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
REG_S s0, PT_SP(sp)예외 직전 스택 포인터를 pt_regs의 PT_SP에 기록합니다. 예외 복귀와 디버깅에서 중단된 문맥의 스택을 찾는 값입니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_S s1, PT_STATUS(sp)SUM·FP·벡터 허용 비트를 지우기 전에 읽어 둔 상태값을 PT_STATUS에 저장합니다. 커널 안에서는 접근을 제한하되 복귀할 때는 원래 실행 상태를 복원할 수 있게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_S s2, PT_EPC(sp)CSR_EPC에서 얻은 예외 발생 PC를 PT_EPC에 저장합니다. 처리기가 명령을 재시도하거나 복귀 위치를 조정할 기준입니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_S s3, PT_BADADDR(sp)CSR_TVAL 값을 PT_BADADDR에 저장합니다. 페이지 폴트 등의 처리에서 문제가 된 주소나 예외 종류에 따른 부가 정보를 사용합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_S s4, PT_CAUSE(sp)CSR_CAUSE 값을 PT_CAUSE에 보관합니다. 인터럽트 여부와 예외 원인 번호에 따라 뒤의 처리 경로를 선택할 자료입니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
REG_S s5, PT_TP(sp)진입 때 CSR_SCRATCH로 옮겨 둔 원래 tp를 PT_TP에 저장합니다. 사용자 스레드 포인터와 커널 태스크 포인터를 전환해도 중단된 코드의 tp를 복원할 수 있게 합니다. REG_L/REG_S의 접근 폭은 XLEN에 맞춰 RV32에서는 4바이트, RV64에서는 8바이트입니다.
csrw CSR_SCRATCH, x0CSR_SCRATCH를 0으로 만들어 이후 커널에서 중첩 예외가 나면 커널 진입임을 식별하게 합니다. 원래 tp는 이미 pt_regs에 저장했으므로 이 값을 지워도 복귀 자료는 남습니다. CSR은 CPU의 제어·상태 레지스터이며 일반 RAM의 변수와 구분됩니다.
load_global_pointer필요한 구성에서 커널의 전역 데이터 접근용 gp를 로드합니다. 사용자 gp는 이미 pt_regs에 저장했으며 Shadow Call Stack 구성에서는 gp의 용도에 맞는 별도 경로를 사용합니다.
scs_load_current_if_task_changed s5진입 전 tp(s5)와 현재 커널 tp가 다를 때 현재 태스크의 보호 스택 포인터를 gp로 로드합니다. 사용자 진입이면 복원하고 같은 태스크의 커널 중첩 진입이면 진행 중 SCS 위치를 유지합니다.
#ifdef CONFIG_RISCV_ISA_V_PREEMPTIVE커널의 RISC-V Vector 사용 중에도 선점을 허용하는 구성입니다. 예외 진입 때 pt_regs를 전달하여 벡터 문맥 중첩을 기록하므로 중단된 커널 벡터 상태를 올바르게 보존할 수 있습니다. 설정이 없으면 이 중첩 관리 호출은 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
move a0, sp현재 sp가 가리키는 pt_regs 주소를 C 함수 첫 인자 a0로 옮깁니다. 다음 벡터 중첩 진입 처리기가 중단된 문맥의 상태를 검사할 수 있게 합니다.
call riscv_v_context_nesting_start선점 가능한 커널 벡터 연산 중 예외가 중첩되었음을 기록합니다. 저장 pt_regs의 벡터 상태와 중첩 깊이를 관리하여 안쪽 예외 처리와 바깥 벡터 상태가 섞이지 않도록 합니다.
#endif174행에서 시작한 선택 구간을 마칩니다. 커널의 RISC-V Vector 사용 중에도 선점을 허용하는 구성입니다. 예외 진입 때 pt_regs를 전달하여 벡터 문맥 중첩을 기록하므로 중단된 커널 벡터 상태를 올바르게 보존할 수 있습니다. 설정이 없으면 이 중첩 관리 호출은 없습니다.
move a0, sp /* pt_regs */공통 예외·인터럽트 C 처리기에 전달할 pt_regs 포인터를 a0에 다시 넣습니다. 앞의 함수 호출이 a0를 변경했을 수 있으므로 실제 분배 전에 인자를 새로 준비합니다.
05 · WORKED EXAMPLE
숫자로 검산하기
user page fault 한 건의 entry frame 비교
user PC 0x400812에서 VA 0x7fff0000을 load하다 translation fault가 발생했다고 가정한다.
- hardware이 page fault에서는 arm64가 ELR/ESR과 유효성을 확인한 FAR를, x86이 RIP/CS/RFLAGS와 #PF 주소(IDT면 CR2, FRED면 event data)를, RISC-V가 SEPC/SCAUSE/STVAL을 제공한다.
- stackarm64와 RISC-V는 해당 entry stack에
pt_regs를 만들고, x86은 IDT 경로에서 privilege frame과 필요 시 IST stack을, FRED 경로에서 FRED frame을 사용한다. - classification세 경로 모두 user-origin과 read fault를 판별한 뒤 architecture page-fault handler에서 mm fault core로 넘어간다.
- returnPTE가 복구되면 저장된 PC를 그대로 재실행한다. signal이 필요하면 동일 frame을 signal setup이 수정한다.
결론fault address 하나만 비교하지 말고 origin privilege, saved PC, error class, stack 종류와 register frame 완성 시점을 함께 추적해야 한다.
06 · DEEP DIVE
주요 동작 상세 분석
공통 커널 코드와 아키텍처별 함수의 역할 구분
모든 architecture는 hardware가 제공한 최소 상태를 kernel entry frame으로 확장하고 exception class를 해석한 뒤 공통 irq, fault, syscall 계층으로 보낸다. 다만 hardware가 자동 저장하는 값, vector 주소 계산법, privilege 전환 때 stack을 바꾸는 방법은 서로 다르다.
assembly entry가 pt_regs를 완성하기 전에는 C ABI와 scheduler의 일반 가정을 적용할 수 없다. NMI와 같은 재진입 경로, user access 차단, speculation 방어, lockdep/RCU 진입은 정해진 순서로만 호출해야 한다.
arm64: VBAR_EL1 vector slot과 EL0/EL1 sync, IRQ, FIQ, SError 분기
예외 진입은 복귀 위치·상태를 ELR_EL1·SPSR_EL1에 남기고, entry.S가 x0–x30과 SP를 pt_regs에 저장한다. 동기 경로의 el0t_64_sync_handler()는 ESR_EL1로 SVC·abort 등을 분기하고, abort handler가 FAR_EL1을 읽는다. 단 FAR 주소는 ESR_ELx_FnV 등 유효성 조건을 확인한 경우에만 해석한다. IRQ handler는 ESR_EL1·FAR_EL1을 새 IRQ의 원인·주소로 읽지 않는다.
DAIF mask, PAN/UAO, MTE tag check, speculation barrier와 context tracking 순서가 user memory 접근 가능 시점을 결정한다. 디버깅할 때는 VBAR_EL1·ELR_EL1·SPSR_EL1과 pt_regs를 먼저 본다. 동기 abort에서는 ESR_EL1.EC를 확인하고, ESR_ELx_FnV가 설정되지 않는 등 FAR 유효성이 확인될 때만 FAR_EL1을 fault 주소로 사용한다. IRQ는 GIC INTID를 본다.
x86-64: IDT gate와 entry_64.S의 idtentry macro (FRED 비활성 경로)
64-bit IDT 경로는 SS, RSP, RFLAGS, CS, RIP를 hardware frame에 저장하고, 일부 exception은 error code도 push한다. privilege 전환이나 IST 선택은 사용할 stack을 바꾼다. entry macro는 나머지 register를 저장해 pt_regs 형태를 맞춘다. 아래의 일반 idtentry 발췌와 IST/paranoid 전용 경로는 구분해야 한다. FRED가 활성화되면 별도 진입점과 FRED frame을 사용한다.
SWAPGS, CR3/KPTI 전환, speculation 방어와 irqentry_enter 순서가 NMI 재진입과 user GSBASE 안전성을 좌우한다. 디버깅할 때는 IDT/FRED 선택, vector·error_code·CS·RSP와 unwind 결과를 확인한다. #PF 주소는 IDT 경로의 CR2 또는 FRED 경로의 event data에서 확인한다.
RISC-V: STVEC가 가리키는 handle_exception과 SCAUSE dispatch
trap 진입 시 hardware는 SEPC, SCAUSE, STVAL과 SSTATUS만 갱신하고 일반 register는 저장하지 않는다. assembly가 sscratch와 tp를 이용해 user/kernel trap을 구별하고 task kernel stack의 pt_regs에 x1-x31을 저장한다.
SUM 차단, FS/VS 상태, interrupt enable bit와 sscratch 교환 순서가 재진입 가능한 시점을 정한다. 디버깅할 때는 STVEC, SCAUSE interrupt bit와 code, SEPC, STVAL, SSTATUS.SPP/SPIE, sscratch와 pt_regs.sp를 기록한다.
객체를 사용하고 해제하는 주체와 시점을 확인한다
pt_regs는 현재 kernel stack 또는 별도 exception stack에 존재하며 handler와 signal/ptrace가 참조하는 동안 유지된다. nested exception은 바깥 frame을 덮지 않도록 stack 경계와 nesting counter를 보존해야 한다.
주소나 레지스터 값만 확인하면 갱신되지 않은 상태를 놓칠 수 있다. 상태를 만드는 코드, 다른 코드에 전달하는 시점, 사용하는 코드, 해제하는 시점을 함께 기록한다.
최대 지연 시간은 명령 하나의 실행 시간만으로 결정되지 않는다
entry latency는 register save, privilege 경계 방어, vector dispatch, interrupt masking과 cache miss의 합이다. NMI/FIQ 급 경로는 일반 IRQ보다 우선하지만 handler가 긴 critical section에 막히면 tail latency가 늘어난다.
평균값뿐 아니라 인터럽트를 비활성화한 시간, 다른 CPU의 응답 시간, 펌웨어 호출 시간, 재시도 횟수를 따로 확인해야 최대 지연의 원인을 찾을 수 있다.
07 · FAILURE
실패를 어떤 증거로 나눌 것인가
| 분류 | 관찰되는 결과 | 첫 확인값 |
|---|---|---|
| arm64 | 잘못된 vector offset, 불완전한 register save 또는 PAN 복원 누락은 return 직후 register corruption이나 kernel user-memory 접근 허용으로 나타난다. | VBAR_EL1·ELR_EL1·SPSR_EL1과 pt_regs를 먼저 본다. 동기 abort에서는 ESR_EL1.EC를 확인하고, ESR_ELx_FnV가 설정되지 않는 등 FAR 유효성이 확인될 때만 FAR_EL1을 fault 주소로 사용한다. IRQ는 GIC INTID를 본다. |
| x86-64 | error-code 유무를 잘못 맞추면 모든 frame offset이 한 word씩 밀린다. SWAPGS 판단 실패는 per-CPU 주소를 user GSBASE로 해석하는 치명적 오류를 만든다. | IDT/FRED 선택, vector·error_code·CS·RSP와 unwind 결과를 확인한다. #PF 주소는 IDT 경로의 CR2 또는 FRED 경로의 event data에서 확인한다. |
| RISC-V | kernel trap에서 user stack으로 오인하거나 sscratch를 늦게 복원하면 nested trap이 잘못된 task와 stack을 사용한다. | STVEC, SCAUSE interrupt bit와 code, SEPC, STVAL, SSTATUS.SPP/SPIE, sscratch와 pt_regs.sp를 기록한다. |
08 · LAB
재현과 계측 절차
- QEMU에서 의도적인 user read fault, kernel fault, breakpoint를 발생시켜 각 entry frame과 C handler까지 function graph를 수집한다.
- entry 직전과 irqentry_enter 이후의 RCU/context-tracking 상태를 tracepoint로 비교한다.
- 동일한 workload에서 세 architecture의 tracepoint 이름, CPU 번호, PC, stack pointer와 address-space identifier를 같은 열로 기록한다.
- 소스만 읽고 끝내지 않고 최종
vmlinux의objdump -dr,readelf -SW결과로 선택된 alternative와 section 배치를 확인한다.
09 · REFERENCES
원문 위치
- arm64arch/arm64/kernel/entry-common.c:752-834
- x86-64arch/x86/entry/entry_64.S:317-401
- RISC-Varch/riscv/kernel/entry.S:88-182
- arm64 vector·IRQarch/arm64/kernel/entry.S:519-603entry-common.c:830-864
- arm64 abortentry-common.c:559-578
- x86 FRED·#PFentry_64_fred.S:14-62fault.c:1483-1490
- arm64 FAR 유효성esr.h:107-109fault.c:808-838
Linux kernel source: GPL-2.0-only. 이 글의 코드 발췌는 Linux v6.18.37 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.