1. 예외
운영체제(OS)에서 예외(Exception)는 프로그램 실행 중에 발생하는 예기치 않은 상황이나 오류를 의미한다.
이는 하드웨어나 소프트웨어에서 발생할 수 있으며, 정상적인 프로그램의 흐름을 방해하는 사건입니다.
예외는 크게 다음과 같은 상황에서 발생할 수 있다:
- 하드웨어 인터럽트: 외부 장치로부터의 신호나 이벤트
- 소프트웨어 인터럽트: 시스템 콜과 같은 프로그램의 의도적인 요청
- 프로그램 오류: 0으로 나누기, 잘못된 메모리 접근, 페이지 폴트 등
운영체제는 이러한 예외 상황이 발생했을 때 적절한 예외 처리 루틴을 실행하여 시스템의 안정성을 유지해야 한다.
프로세스의 정상적인 실행을 방해하지 않으면서 문제를 해결하거나, 필요한 경우 프로그램을 안전하게 종료해야 한다.
가. 예외 처리 과정
RISC-V에서 예외는 다음과 같은 단계를 거쳐 처리한다.
- CPU는
medeleg(Machine Delegation) 레지스터를 확인하여 어떤 모드에서 예외를 처리할지 결정한다. 여기서는 OpenSBI가 이미 U-Mode와 S-Mode 예외를 S-Mode 핸들러에서 처리하도록 설정해 두었다. - CPU는 예외가 발생한 시점의 상태(각종 레지스터 값)를 여러 CSR(제어/상태 레지스터)들에 저장한다.
| 레지스터 | 줄임말 | 내용 |
| scause | Supervisor Cause | 예외 유형. 커널은 이를 읽어 어떤 종류의 예외인지 판단합니다. |
| stval | Supervisor Trap Value | 예외에 대한 부가 정보(예: 문제를 일으킨 메모리 주소). 예외 종류에 따라 다르게 사용됩니다. |
| sepc | Supervisor Exception Program Counter | 예외가 발생했을 때의 프로그램 카운터(PC) 값. |
| sstatus | Supervisor Status | 예외가 발생했을 때의 운영 모드(U-Mode/S-Mode 등). |
stvec(Supervisor Trap Vector)레지스터에 저장된 값이 프로그램 카운터로 설정되면서, 커널의 예외 핸들러(트랩 핸들러)로 점프한다.- 예외 핸들러는 일반 레지스터(프로그램 상태)를 별도로 저장한 뒤, 예외를 처리한다.
- 처리 후, 저장해 둔 실행 상태를 복원하고
sret명령어를 실행해 예외가 발생했던 지점으로 돌아가 프로그램을 재개한다.
나. 예외 핸들러 진입점
stvec 레지스터에 등록할 예외 핸들러 진입점이다.
예외 핸들러 진입점(entry point)은 커널에서 가장 까다롭고 실수하기 쉬운 부분 중 하나라고 한다.
원래의 일반 레지스터 값을 전부 스택에 저장하고, sp는 sscratch를 통해 우회적으로 저장한다.
kernel.c
__attribute__((naked))
__attribute__((aligned(4)))
void kernel_entry(void) {
__asm__ __volatile__(
"csrw sscratch, sp\n"
"addi sp, sp, -4 * 31\n"
"sw ra, 4 * 0(sp)\n"
"sw gp, 4 * 1(sp)\n"
"sw tp, 4 * 2(sp)\n"
"sw t0, 4 * 3(sp)\n"
"sw t1, 4 * 4(sp)\n"
"sw t2, 4 * 5(sp)\n"
"sw t3, 4 * 6(sp)\n"
"sw t4, 4 * 7(sp)\n"
"sw t5, 4 * 8(sp)\n"
"sw t6, 4 * 9(sp)\n"
"sw a0, 4 * 10(sp)\n"
"sw a1, 4 * 11(sp)\n"
"sw a2, 4 * 12(sp)\n"
"sw a3, 4 * 13(sp)\n"
"sw a4, 4 * 14(sp)\n"
"sw a5, 4 * 15(sp)\n"
"sw a6, 4 * 16(sp)\n"
"sw a7, 4 * 17(sp)\n"
"sw s0, 4 * 18(sp)\n"
"sw s1, 4 * 19(sp)\n"
"sw s2, 4 * 20(sp)\n"
"sw s3, 4 * 21(sp)\n"
"sw s4, 4 * 22(sp)\n"
"sw s5, 4 * 23(sp)\n"
"sw s6, 4 * 24(sp)\n"
"sw s7, 4 * 25(sp)\n"
"sw s8, 4 * 26(sp)\n"
"sw s9, 4 * 27(sp)\n"
"sw s10, 4 * 28(sp)\n"
"sw s11, 4 * 29(sp)\n"
"csrr a0, sscratch\n"
"sw a0, 4 * 30(sp)\n"
"mv a0, sp\n"
"call handle_trap\n"
"lw ra, 4 * 0(sp)\n"
"lw gp, 4 * 1(sp)\n"
"lw tp, 4 * 2(sp)\n"
"lw t0, 4 * 3(sp)\n"
"lw t1, 4 * 4(sp)\n"
"lw t2, 4 * 5(sp)\n"
"lw t3, 4 * 6(sp)\n"
"lw t4, 4 * 7(sp)\n"
"lw t5, 4 * 8(sp)\n"
"lw t6, 4 * 9(sp)\n"
"lw a0, 4 * 10(sp)\n"
"lw a1, 4 * 11(sp)\n"
"lw a2, 4 * 12(sp)\n"
"lw a3, 4 * 13(sp)\n"
"lw a4, 4 * 14(sp)\n"
"lw a5, 4 * 15(sp)\n"
"lw a6, 4 * 16(sp)\n"
"lw a7, 4 * 17(sp)\n"
"lw s0, 4 * 18(sp)\n"
"lw s1, 4 * 19(sp)\n"
"lw s2, 4 * 20(sp)\n"
"lw s3, 4 * 21(sp)\n"
"lw s4, 4 * 22(sp)\n"
"lw s5, 4 * 23(sp)\n"
"lw s6, 4 * 24(sp)\n"
"lw s7, 4 * 25(sp)\n"
"lw s8, 4 * 26(sp)\n"
"lw s9, 4 * 27(sp)\n"
"lw s10, 4 * 28(sp)\n"
"lw s11, 4 * 29(sp)\n"
"lw sp, 4 * 30(sp)\n"
"sret\n"
);
}
Code language: JavaScript (javascript)
"csrw sscratch, sp\n”
:sscratch(Supervisor Scratch) 레지스터는 RISC-V 아키텍처에서 제공하는 특수 레지스터로, 주로 예외 처리 시 임시 값을 저장하는 용도로 사용됨.
:sscratch레지스터를 임시 저장소로 이용해 예외 발생 시점의 스택 포인터를 저장한다.
: 예외를 처리한 후 복귀해야 한다."addi sp, sp, -4 * 31\n"
: 스택에 31개의 레지스터를 저장할 공간 확보
:sp를-4*31만큼 이동
: 4바이트만큼 이동하는 이유는 하나의 레지스터가 32개의 플립플롭으로 구성되어 있어서.
: RISC-V 아키텍처에서 우리가 저장하는 일반 레지스터들(ra,gp,tp,t0-t6,a0-a7,s0-s11)은 모두 32비트(32개의 플립플롭)로 구성되어 있다."sw ra, 4 * 0(sp)\n"…
: 모든 일반 레지스터(ra,gp,tp,t0-t6,a0-a7,s0-s11)를 스택에 순차적으로 저장.csrr a0, sscratch,sw a0, 4 * 30(sp)
:sscratch에 저장해둔 원래sp값을 복구하여 스택에 저장mv a0, sp
: 현재 스택 포인터sp를a0레지스터로 이동시켜handle_trap함수의 인자로 전달"call handle_trap\n”
: 현재 스택 포인터를 인자로 하여handle_trap함수 호출"lw ra, 4 * 0(sp)\n”…
:handle_trap함수 실행 후, 저장해둔 모든 레지스터 값을 복원sret명령어로 예외 처리 종료 및 원래 실행 지점으로 복귀

- tmi 1. 커널에서는 부동소수점(FPU) 레지스터를 사용하지 않으므로 여기서는 저장하지 않았습니다. 일반적으로 쓰레드 스위칭 시에만 부동소수점 레지스터를 저장 및 복원한다.
- tmi 2.
__attribute__((aligned(4)))는 함수 시작 주소를 4바이트 경계에 맞추기 위함.stvec레지스터는 예외 핸들러 주소뿐 아니라 하위 2비트를 모드 정보 플래그로 사용하기 때문에, 핸들러 주소가 4바이트 정렬이 되어 있어야 함.
- 레지스터 참고
| Register | ABI Name (alias) | Description |
| pc | pc | Program counter (where the next instruction is) |
| x0 | zero | Hardwired zero (always reads as zero) |
| x1 | ra | Return address |
| x2 | sp | Stack pointer |
| x3 | gp | Global pointer |
| x4 | tp | Thread pointer |
| x5 – x7 | t0 – t2 | Temporary registers |
| x8 | fp | Stack frame pointer |
| x10 – x11 | a0 – a1 | Function arguments/return values |
| x12 – x17 | a2 – a7 | Function arguments |
| x18 – x27 | s0 – s11 | Temporary registers saved across calls |
| x28 – x31 | t3 – t6 | Temporary registers |
다. handle_trap
kernel.c
void handle_trap(struct trap_frame *f) {
uint32_t scause = READ_CSR(scause);
uint32_t stval = READ_CSR(stval);
uint32_t user_pc = READ_CSR(sepc);
PANIC("unexpected trap scause=%x, stval=%x, sepc=%x\n", scause, stval, user_pc);
}
Code language: JavaScript (javascript)
scause: 예외 원인stval: 예외 부가정보 (예: 잘못된 메모리 주소 등)sepc: 예외가 일어난 시점의 PCPANIC(…): 디버깅을 위한 커널 패닉
kernel.h
#include "common.h"
struct trap_frame {
uint32_t ra;
uint32_t gp;
uint32_t tp;
uint32_t t0;
uint32_t t1;
uint32_t t2;
uint32_t t3;
uint32_t t4;
uint32_t t5;
uint32_t t6;
uint32_t a0;
uint32_t a1;
uint32_t a2;
uint32_t a3;
uint32_t a4;
uint32_t a5;
uint32_t a6;
uint32_t a7;
uint32_t s0;
uint32_t s1;
uint32_t s2;
uint32_t s3;
uint32_t s4;
uint32_t s5;
uint32_t s6;
uint32_t s7;
uint32_t s8;
uint32_t s9;
uint32_t s10;
uint32_t s11;
uint32_t sp;
} __attribute__((packed));
#define READ_CSR(reg) \
({ \
unsigned long __tmp; \
__asm__ __volatile__("csrr %0, " #reg : "=r"(__tmp)); \
__tmp; \
})
#define WRITE_CSR(reg, value) \
do { \
uint32_t __tmp = (value); \
__asm__ __volatile__("csrw " #reg ", %0" ::"r"(__tmp)); \
} while (0)
Code language: PHP (php)
struct trap_frame {…} __attribute__((packed));
: 성능 최적화를 위해 컴파일러가 자동으로 추가하는 정렬 패딩을 비활성화하는 GCC 컴파일러 지시어
: 구조체 멤버들 사이에 자동으로 추가되는 패딩을 없애서 메모리를 더 적게 사용
: 구조체의 각 멤버를 메모리상에서 연속적으로 배치#reg
: 문자열 연결을 위한 토큰 결합 연산자
: 매크로 인자 reg를 문자열 리터럴로 변환하여 어셈블리 코드에 직접 삽입할
: 만약READ_CSR(scause)라면#reg는"scause"라는 문자열로 변환되어"csrr %0, scause"와 같은 어셈블리 명령어를 생성.
kernel.c
void kernel_main(void) {
memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);
WRITE_CSR(stvec, (uint32_t) kernel_entry); // new
__asm__ __volatile__("unimp"); // new
Code language: JavaScript (javascript)
WRITE_CSR(stvec, (uint32_t) kernel_entry);
: 예외가 발생했을 때 실행할 핸들러의 주소를 설정하는 코드
:stvec(Supervisor Trap Vector) CSR에kernel_entry함수의 주소를 저장
: 이후 예외가 발생하면 CPU는 자동으로kernel_entry함수를 실행하게 됨__asm__ __volatile__("unimp");
:unimp명령어 unimplemented instruction
: illegal instruction 로 간주됨
: 일부러 예외를 일으킨다.
2. 실행
./run.sh
...
PANIC: kernel.c:37: unexpected trap scause=00000002, stval=00000000, sepc=8020010a
scause가 2는 “Illegal instruction” 예외를 의미.
stval는 예외 부가정보이고, sepc는 예외가 일어난 시점의 PC라고 했다.
$ llvm-addr2line -e kernel.elf 8020010a
/home/tiredi/Desktop/MyOS/08_exception/kernel.c:122
llvm-addr2line은 실행 파일의 주소를 소스 코드의 위치로 변환해 주는 디버깅 도구다.
8020010a에서 발생한 예외가 kernel.c 파일의 122번째 줄에서 발생했다고 알려준다.
122번째 줄은 __asm__ __volatile__("unimp");로 정확하게 동작했다.