예외 처리 메커니즘 분석

예외 처리 기본 원리

프로그램에서 예외가 발생하면 현재 실행 중인 프로그램 P의 상태를 저장한 후 예외 처리 프로그램으로 제어를 전달해야 합니다.

상태 저장 관점에서 명령어 집합 상태기계 S = {<R,M>}를 고려해봅시다. 여기서 R은 레지스터, M은 메모리를 의미합니다.

예외 처리 프로그램과 P는 서로 다른 프로그램이며, 각자의 메모리 공간을 사용합니다. 따라서 예외 처리 프로그램이 P의 메모리를 임의로 수정하지 않는다면 실질적인 저장 작업이 필요 없습니다. 하지만 레지스터는 단 하나만 존재하며, P와 예외 처리 프로그램이 공유합니다. 따라서 P의 레지스터 상태를 반드시 저장해야 합니다.

레지스터 저장 위치

레지스터를 저장할 수 있는 위치는 다음과 같은 방법들이 있습니다:

  1. 레지스터에 저장: 새로운 레지스터 세트를 추가하여 P의 레지스터 상태를 복사합니다.
  2. 메모리에 저장: 메모리에 저장하려면 빈 메모리 공간을 찾아야 합니다.
  3. 스택에 저장: 스택 공간이 있다면 이 방법이 가능합니다.

저장 주체

레지스터 저장 작업은 다음 두 주체에 의해 수행될 수 있습니다:

  • 하드웨어 저장: CPU 상태기계의 제어 하에 저장
  • 소프트웨어 저장: 명령어를 통해 CPU를 제어하여 저장

따라서 레지스터 저장 설계는 총 4가지 방법으로 나뉩니다:

레지스터 메모리
하드웨어 저장 새 레지스터에 저장 메모리에 저장
소프트웨어 저장 새 레지스터에 저장 메모리에 저장

RISC-V 제어 상태 레지스터(CSR)

CSR은 프로세서 상태를 제어하고 반영하기 위한 특수 레지스터입니다(예: mepc).

  • 하드웨어가 특정 이벤트 발생 시 자동으로 CSR을 업데이트하거나 CSR에서 값을 읽어 사용할 수 있습니다.
  • 소프트웨어는 CSR 명령어를 통해 CSR에 접근할 수 있습니다.
  • 각 CSR은 소프트웨어에서 접근 가능한 고유 번호(CSR 주소 공간)를 가집니다.

RISC-V에서 주로 사용되는 CSR에는 다음과 같은 것들이 있습니다:

  • mtvec: 예외 발생 시 프로세서가 점프해야 할 주소 저장
  • mepc: 예외가 발생한 명령어 주소 저장. 예외 처리 반환 시 원래 프로그램 실행 위치로 돌아가기 위해 사용
  • mstatus: 프로세서 상태 저장
  • mcause: 예외 종류 저장

간단한 예외 처리 프로그램 예제

#include <klib.h>
void 예외_처리기() {
  uintptr_t 프로그램_카운터;
  asm volatile ("csrr %0, mepc" : "=r"(프로그램_카운터)); 
  printf("예외 발생 주소: %p\n", 프로그램_카운터);
  while (1);
}

int main() {
  asm volatile ("csrw mtvec, %0" : :"r"(예외_처리기));
  asm volatile (".word 0");
  printf("프로그램 계속 실행됨\n");
  while (1);
}

예외 코드

RISC-V에서 정의된 주요 예외 코드는 다음과 같습니다:

0 - 명령어 주정렬 오류
1 - 명령어 접근 오류
2 - 불법 명령어
3 - 중단점
4 - 로드 주정렬 오류
5 - 로드 접근 오류
6 - 저장/AMO 주정렬 오류
7 - 저장/AMO 접근 오류
8 - 사용자 모드 환경 호출
9 - 슈퍼바이저 모드 환경 호출
11 - 머신 모드 환경 호출
12 - 명령어 페이지 오류
13 - 로드 페이지 오류
15 - 저장/AMO 페이지 오류

예외 상태에서 복귀

진단 결과 문제가 크지 않다면 P를 계속 실행할 수 있습니다. 이 경우 예외 처리 프로그램에서 P의 이전 상태(레지스터)를 복원해야 합니다.

  • RISC 아키텍처는 load 명령어를 사용하여 메모리에 저장된 내용을 레지스터로 복원합니다.
  • 예외 처리 프로그램과 P는 서로 다른 프로그램이므로 ret/jal 명령어로 직접 반환할 수 없습니다.
  • jalr 명령어는 반환 주소를 레지스터에 써야 하지만, 이는 P의 상태를 변경하므로 P에서 해당 레지스터를 사용하면 오류가 발생합니다.

이러한 문제를 해결하기 위해 RISC-V는 mret라는 특수 반환 명령어를 제공합니다. 이 명령어는 mepc에 저장된 주소로 점프합니다.

uintptr_t 프로그램_카운터, 예외_원인;
asm volatile ("csrr %0, mepc" : "=r"(프로그램_카운터));
asm volatile ("csrr %0, mcause" : "=r"(예외_원인));
printf("예외 원인: %p, 발생 주소: %p\n", 예외_원인, 프로그램_카운터);

if (예외_원인 == 2) {
  asm volatile ("csrw mepc, %0; mret" : : "r"(프로그램_카운터 + 4));
}
while (1);

하드웨어 구현

단일 주기 구현에서의 예외 처리

예외 처리를 구현하기 위해 다음 단계를 거칩니다:

  1. CSR 구현
  2. CSR 읽기/쓰기 명령어 추가 및 명령어 예외 시 일반 레지스터(GPR)와 CSR 간 데이터 교환 구현
  3. 예외 트리거 구현: 디코딩 시 불법 명령어, ecall 명령어 등을 인식하고, 예외 이벤트 감지 시 mepc, mcause 등 CSR을 업데이트한 후 mtvec에 저장된 주소로 점프
  4. mret 명령어 구현: mepc에 저장된 주소로 점프

예외 처리 상태기계 모델

예외 처리 상태에서 상태기계 모델은 확장되어야 합니다: R = {PC, GPR, CSR}. 여기서 명령어 실행은 항상 성공하는 것은 아니며, 다음과 같은 함수를 정의합니다:

f_ex : S → {0,1}, 임의의 상태 S에 대해:

  • f_ex(S) = 0: 현재 명령어 의미에 따라 상태 전환
  • f_ex(S) = 1: raise_intr(예외 번호)라는 특수 명령어를 실행하고 다음 상태로 업데이트

예외(f_ex(S)=1)가 발생하면, 예외 번호와 예외 발생 PC 값을 CSR 레지스터에 저장합니다.

시스템 호출 및 특권 레벨

현대 컴퓨터는 다중 사용자 및 다중 작업을 지원하지만, 여러 프로그램이 동일한 자원을 경쟁할 수 있습니다. 이를 위한 해결책은 하드웨어가 특권 레벨 보호를 제공하는 것입니다.

이는 자원 관리 프로그램을 높은 특권 레벨(운영 시스템의 본질)에 두고, 사용자 프로그램을 낮은 특권 레벨에 배치하는 것을 의미합니다. 사용자 프로그램은 일반 명령어만 실행하여 계산을 수행하고, 시스템 호출 요청을 통해 서비스를 제공받을 수 있습니다.

시스템 호출 발생

유일한 합법적인 방법은 자발적 예외(트랩)를 발생시키는 명령어를 실행하는 것입니다:

  • RISC-V에서는 ecall 명령어
  • 운영 시스템은 mcause를 통해 이 예외가 합법적인 요청임을 확인(예외 번호 통해)

사용자 프로그램이 어떤 서비스를 요청하는지 지정하기 위해 시스템 호출에도 매개변수가 필요합니다. 이는 레지스터를 통해 전달됩니다!

RISC-V Linux 규약에서는 a7 레지스터에 시스템 호출 번호를, a0, a1 등에 각각 1, 2번째 매개변수를 전달합니다.

컨텍스트 스위칭 구현

am-kernels/kernels/yield-os/yield-os.c는 운영 시스템 프로토타입을 구현합니다. 콜백 함수를 다음과 같이 수정합니다:

  • 현재 프로그램 컨텍스트 저장
  • 다른 프로그램 컨텍스트 복원

이 과정은 다음과 같습니다: 프로그램 A에서 예외 발생 → A의 컨텍스트 저장 → 예외 처리 → B의 컨텍스트 복원 → 예외에서 복귀

이를 통해 A에서 B로 전환되는 효과를 구현합니다.

CTE(컨텍스트 트랩 엔진) 초기화

bool cte_init(Context*(*처리기)(이벤트, 컨텍스트*)) {
  asm volatile("csrw mtvec, %0" : : "r"(__am_asm_트랩));
  사용자_처리기 = 처리기;
  return true;
}

예외 처리 흐름

선택 후 다음과 같은 예외 관련 작업이 시작됩니다:

  1. IOE 초기화 → CTE(콜백 함수 등록) → hello_intr() → yield(자발적 트랩 트리거) → mtvec 읽기 → __am_asm_트랩 시작 → 레지스터 내용 스택에 쓰기 → __am_irq_처리기() 호출 → 이벤트 상태 할당 → 처리기 함수 실행(putch(y)) → 스택에서 내용 복원

while 루프 때문에 y가 계속 출력됩니다. 자발적 트랩이 발생할 때마다 y가 출력됩니다.

컨텍스트 스위칭 원리

컨텍스트 스위칭은 본질적으로 스택 포인터 전환입니다. 다른 프로세스의 컨텍스트 구조를 찾기 위해 프로세스 제어 블록(PCB)이 필요합니다:

typedef union {
  uint8_t 스택[스택_크기];
  struct {
    컨텍스트 *컨텍스트_포인터;
  };
} PCB;

컨텍스트 스위칭 시 PCB의 컨텍스트 포인터를 CTE의 __am_irq_처리기 함수에 반환합니다. 프로세스를 전환하고 실행하기 위해 kcontext 함수를 사용합니다:

컨텍스트* kcontext(메모리_영역 k스택, void (*진입점)(void *), void *인자);

kcontext의 목적은 프로세스를 전환하고 실행시키는 것입니다. main 함수에서 cte 초기화, 콜백 함수 schedule, 두 작업을 위한 컨텍스트 생성을 수행합니다.

현재까지의 흐름은 다음과 같습니다:

  1. main 함수에서 cte 초기화, 콜백 함수 등록, kcontext로 두 작업의 컨텍스트 생성
  2. main 함수에서 yield 호출로 예외 발생 → __am_asm_트랩() 진입
  3. __am_asm_트랩()에서 현재 컨텍스트 저장 및 __am_irq_처리기() 호출
  4. __am_irq_처리기()에서 콜백 함수 schedule 실행하여 작업 전환, 다음 작업의 컨텍스트 반환
  5. __am_asm_트랩()에서 새로 전환된 컨텍스트 복원 및 작업 함수 f로 반환

이러한 메커니즘을 통해 다중 작업 환경에서의 컨텍스트 스위칭이 구현됩니다.

태그: RISC-V 예외 처리 컨텍스트 스위칭 시스템 호출 하드웨어 구현

7월 15일 02:33에 게시됨