Trap은 프로그램 실행 중 발생하는 특정 이벤트로 인해 CPU가 정상적인 명려어 실행을 멈추고, 해당 이벤트를 처리하기 위한 특수한 코드를 실행하도록 강제되는 상황을 의미한다. 이 장에서는 세 가지 종류의 trap에 대해 설명한다.
1. Trap이란?
1) 시스템 콜(system call)
유저 프로그램이 커널에 도움을 요청할 때 발생한다.
유저 프로그램은 ecall 명령어를 실행하여 커널에 진입하고, 커널은 요청을 처리한 후 유저 프로그램으로 돌아온다.
2) 예외(exception)
프로그램이 잘못된 동작을 수행할 때 발생한다.
ex. 0으로 나누기, 잘못된 가상 주소 접근 등이 예외 사오항에 해당한다.
3) 장치 인터럽트(device interrupt)
외부 장치가 CPU에게 특정 작업이 완료되었음을 알리기 위해 신호를 보낼 때 발생한다.
ex. 디스크가 데이터 읽기/쓰기 작업을 완료했을 때 인터럽트를 발생시켜 CPU에게 알린다.
인터럽트는 프로그램이 예상하지 못한 시점에 발생하기 때문에, 이를 투명하게 처리하는 것이 중요하다.
2. Trap의 처리 과정
1) trap 발생 시 제어 흐름 전환 : Trap이 발생하면 CPU는 현재 실행 중인 프로그램의 제어 흐름을 커널로 전환한다. 이를 통해 커널이 trap을 처리할 수 있다.
2) 커널에서 상태 저장 : 커널은 현재 레지스터 값과 프로그램 상태를 저장한다. 이렇게 저장된 상태는 trap 처리가 완료된 후 원래 프로그램으로 복귀시 사용된다.
3) 적절한 핸들러 실행 : 커널은 trap의 종류에 따라 적절한 핸들러를 실행한다.
4) 원래 상태 복원 후 복귀 : 핸들러가 작업을 완료하면, 커널은 저장된 레지스터 값과 상태를 복원하고 프로그램으로 돌아간다. 프로그램은 trap이 발생하기 이전 지점부터 정상적으로 실행을 재개한다.
3. xv6에서의 trap 처리
xv6는 모든 종류의 trap을 커널에서 처리한다.
1) system call : 유저 프로그램이 커널 기능을 요청할 때 trap이 발생하고, 커널에서 이를 처리하는 것이 자연스럽다.
2) device interrupt : 장치에 대한 접근을 커널에서만 허용하여 보안을 유지하고, 여러 프로세스 간에 장치를 효율적으로 공유할 수 있다.
3) exception : xv6는 사용자 프로그램에서 발생한 모든 예외에 대한 프로그램을 종료하는 방식으로 처리한다.
4. trap 처리의 단계
1) 하드웨어가 수행하는 작업 : trap이 발생하면 CPU는 현재 실행 중인 명령어를 멈추고, 자동으로 특정 레지스터에 trapr과 관련된 정보를 저장한다. 그런 다음 벡터 주소로 제어를 이동시켜, 해당 위치에 있는 핸들러 코드를 실행한다.
2) 어셈블리 핸들러 실행 : xv6에서는 어셈블리 코드로 작성된 초기 핸들러가 실행된다. 이 코드는 커널 c 코드가 trap을 처리할 수 있도록 필요한 환경을 설정한다. 예를 들어, 현재 상태를 저장하고 적절한 c 함수로 제어를 넘긴다.
3) c 함수에서 trap 처리 : 어셈블리 핸들러가 호출한 c 함수는 trap의 종류를 식별하고, 적절한 처리를 수행한다.
4) 시스템 콜 또는 장치 드라이버 호출 : trap이 시스템 콜에 의한 것이라면 시스템 콜 핸들러가 호출되어, 커널이 요청된 작업을 수행한다. 장치 인터럽트에 의한 것이라면 장치 드라이버 코드가 호출된다.
5. trap 핸들러의 종류
xv6는 trap을 처리할 때 세 가지 종류의 핸들러를 사용한다.
1) 유저 공간에서 발생한 trap : 유저 프로그램의 요청에 따라 커널 기능을 수행하거나, 예외가 발생한 프로그램을 종료한다.
2) 커널 공간에서 발생한 trap : 커널 코드에서 예외가 발생했을 때 처리한다.
ex. 커널이 잘못된 가상 주소를 참조한 경우.
3) 타이머 인터럽트 : 주기적으로 발생하는 타이머 인터럽트를 처리한다. xv6는 타이머 인터럽트를 통해 프로세스 스케줄링을 수행한다.
6. 벡터와 핸들러
벡터 : trap이 발생했을 때 CPU가 제어를 넘기는 어셈블리 코드의 시작 위치를 벡터라고 한다. 이 벡터에 위치한 초기 핸들러는 trap을 처리할 준비를 하고, 이후 C 함수로 제어를 넘긴다.
핸들러 : trap을 실제로 처리하는 어셈블리 코드 또는 C 코드를 핸들러라고 한다. xv6에서는 초기 핸들러는 어셈블리 코드로 작성되며, 이후 C 코드가 호출되어 trap을 처리한다.
RISC-V Trap Machine
1. RISC-V의 Trap 관련 레지스터
1) stvec : 커널이 trap handler 주소를 기록하는 레지스터
2) spec : trap 발생 시 프로그램 카운터(PC)를 저장하는 레지스터. trap 처리가 끝난 후 sret 명령어로 이 레지스터 값을 PC로 복원하여 프로그램 실행을 재개.
3) scause : trap의 원인 코드를 저장하는 레지스터.(시스템 콜, exception 등)
4) sscratch : trap 핸들러가 임시로 레지스터 값을 저장할 때 사용하는 레지스터. 초기 trap 처리 단계에서 임시로 하나의 레지스터 값을 저장하는 용도로 사용.
5) sstatus : CPU 상태를 나타내는 레지스터. (SIE : 인터럽트 허용 여부, SSP : trap 발생 시의 모드 use/supervisor)
2. RISC-V Trap 처리 단계
1) 인터럽트 확인 : Trap이 인터럽트일 경우, sstatus의 SIE 비트가 설정되어 있어야 인터럽트를 처리한다. SIE 비트가 클리어되어 있다면 Trap을 무시한다.
2) 인터럽트 비활성화 : sstatus의 SIE 비트를 클리어하여 인터럽트를 비활성화한다.
3) 현재 PC 저장 : 현재 실행 중이던 PC를 spec레지스터에 저장한다.
4) 현재 모드 저장 : 현재 모드를 sstatus의 SSP 비트에 저장한다.
5) Trap 원인 설정 : Trap 원인을 scause 레지스터에 기록한다.
6) 모드 전환 : 모드를 supervisor 모드로 변경한다.
7) trap 핸들러로 점프 : stvec 레지스터에 기록된 주소로 PC를 변경하여 trap 핸들러 실행을 시작한다.
Traps from user space
1. Trap 발생과 처리 흐름 : user program에 trap이 발생하면 xv6는 다음 순서로 이를 처리한다.
1) trap 발생 -> uservec 실행 : stvec에 등록된 핸들러가 uservec(어셈블리 코드)로 지정되어 있으므로, CPU가 해당 핸들러로 점프한다. Trap 발생 시 페이지 테이블 전환이 이루어지지 않기 때문에, 유저 페이지 테이블에서 uservec이 유효한 가상 주소로 매핑되어 있어야 한다. 이를 위해 TRAMPOLINE page가 사용된다.
2) uservec 핸들러 : 레지스터 저장
- uservec은 레지스터 32개를 모두 Trapframe에 저장한다. 이때 sscratch를 이용하여 임시로 a0값을 저장하고 trapframe 주소를 a0에 로드하여 나머지 레지스터 값을 저장한다.
3) usertrap 호출 : 레지스터를 저장한 후, 페이지 테이블을 커널 페이지 테이블로 전환하고, usertrap을 호출한다.
2. usertrap의 역할
Trap 원인 분석 : scause를 읽어 Trap의 원인을 판별한다. 시스템 콜이면 syscall(), 장치 인터럽트면 devintr()을 호출하고, 그 외의 exception에는 프로세스를 종료한다.
시스템 콜 처리 : syscall() 함수를 호출하여 커널이 유저 프로그램의 요청을 처리한다. 처리 후, spec 값을 수정하여 프로그램이 ecall 명령어 다음 명령어로부터 실행을 재개하도록 설정한다.
3. 유저 모드 복귀
Trap 처리가 끝나면 유저 프로그램을 복귀하기 위해 usertrapret -> uerret 경로로 복귀한다.
1) usertrapret : RISC-V 제어 레지스터(stvec, spec)를 설정하여 다음 Trap시 uservec으로 점프하도록 준비한다. 이후 userret을 호출하여 유저 페이지 테이블로 전환한다.
2) userret : satp 레지스터를 유저 페이지 테이블로 전환하여 유저 주소 공간을 활성화한다. Trapframe에서 레지스터 값을 복원하고, sret명령어로 유저 모드로 복귀한다.
Code : Calling system calls
1. 시스템콜 호출 과정
1) 유저 프로그램에서 ecall 실행 : 유저 프로그램이 ecall 명령어로 커널에 시스템 콜 요청. a0~a6 레지스터에 인자, a7레지스터에 시스템 콜 번호를 설정한다.
2) trap 발생 -> syscall 함수 호출
uesrvec -> usertrap을 거쳐 syscall()함수가 호출된다.
syscall()은 Trapframe에 저장된 a7 레지스터 값(syscall 번호)를 읽고, syscalls 배열에서 해당하는 핸들러를 호출한다.
3) 시스템 콜 처리
ex. sys_exec 호출 시 syscalls[a7] --> sys_exec를 가리키므로 이를 호출하여 실행한다. sys_exec 함수가 종료되면 반환 값을 Trapframe의 a0에 저장하여 유저 프로그램이 해당 값을 반환받도록 한다.
2. 오류 처리 : 유효하지 않은 시스템 콜 번호가 들어오면, syscall()은 오류 메시지를 출력하고 -1을 반환한다.
System call arguments
1. 시스템 콜 인자 흐름
유저 프로그램이 시스템 콜을 호출할 때, 인자는 RISC-V의 C 호출 규약에 따라 레지스터(a0~a6)에 저장된다. Trap이 발생하면, 커널은 유저 레지스터 값을 Trapframe에 저장하고, 이를 기반으로 인자를 처리한다.
1) Trapframe에 저장된 인자 접근 : argint, argaddr, argfd 함수는 Trapframe에 저장된 레지스터값을 읽어 각각 정수, 포인터, 파일 디스크립터 인자를 가져온다. 이 함수들은 argw라는 공통 함수를 호출한다.
2) 유저 메모리 접근 문제 해결
시스템 콜 인자로 포인터가 전달될 경우, 두 가지 문제가 발생할 수 있다.
- 유효하지 않은 포인터나 커널 메모리를 참조하는 포인터일 수 있다.
- 유저와 커널의 페이지 테이블이 다르기 때문에 커널이 일반적인 메모리 접근으로 유저 주소를 사용할 수 없다.
이를 해결하기 위해서 Xv6는 fetchstr, copyinstr, copyout같은 함수를 사용한다.
- fetchstr : 유저 메모리에서 문자열을 안전하게 가져오는 함수. copyinstr를 호출하여 유저 페이지 테이블에서 문자열을 복사한다.
- copyinstr : 유저 페이지 테이블에서 주어진 가상 주소(srcva)에 대응하는 물리주소(pa0)를 찾고, 해당 물리 주소에서 문자열을 읽어 커널 메모리로 복사한다.
- copyout : 커널 메모리에서 유저 메모리로 데이터를 복사한다.
Traps from kernel space
1. 커널 공간에서 발생하는 Trap : 커널이 실행 중일 때 Trap이 발생하면, 커널 페이지 테이블이 이미 활성화되어 있다. 이 경우 stvec 레지스터는 kernelvec을 가리키려, Trap이 발생하면 CPU가 kernelvec으로 점프한다.
2. kernelvec에서 하는 일
1) 32개 레지스터 저장
2) kerneltrap 호출
3) 타이머 인터럽트 처리 : 현재 실행중인 스레드가 사용자 프로세스의 커널 스레드라면, yield()를 호출하여 다른 스레드에게 실행 기회를 준다.
4) 복귀 : kerneltrap이 끝난 후, kernelvec은 저장된 레지스터를 복원하고 sret 명령어를 통해 trap 이전으로 복귀한다.
Page-Fault Exceptions
1. page fault란?
CPU가 가상 주소를 접근할 때 페이지 테이블에 유효한 매핑이 없거나 권한이 부족한 경우 발생하는 예외.
1) Load 페이지 폴트 : load 명령어로 메모리를 읽을 때 발생
2) Store 페이지 폴트 : store 명령어로 메모리를 쓸 때 발생
3) Instruction 페이지 폴트 : 실행할 명령어가 있는 주소를 번역할 수 없을 때 발생
2. Copy-on-Write(COW) fork
페이지 폴트를 활용하여 부모와 자식 프로세스가 초기 메모리를 공유하도록 구현하는 기법이다.
초기에는 부모와 자식이 같은 물리 메모리를 읽기 전용으로 공유하며, 쓰기 요청이 발생할 때만 페이지 폴트 예외가 발생하여 새로운 페이지를 할당하고 복사한다.
장점 : 일반적인 fork보다 빠르다(메모리 복사 x). fork 후 즉시 exec로 이어지는 경우, 대부분의 메모리는 복사되지 않고 해제되기 때문에 효율적이다.
3. Lazy Allocation : 유저 프로그램이 sbrk로 메모리를 요청할 때 즉시 물리 메모리를 할당하지 않고, 페이지 폴트가 발생할 때 할당하는 기법이다.
4. Demanding Paging : 실행 시 모든 메모리를 미리 로드하지 않고, 필요한 페이지에 대해서만 페이지 폴트 시 로드하는 기법이다.
장점 : 프로그램 실행 초기 응답 시간을 줄일 수 있다. RAM이 부족한 경우에도 동적으로 필요한 페이지만 로드하여 효율적으로 메모리를 사용할 수 있다.
5. Paging to Disk : RAM에 부족한 메모리를 보조 저장장치(디스크)에 저장하여 메모리 부족 상황을 해결하는 기법이다. 디스크에 저장된 페이지는 PTE를 유요하지 않은 상태로 표시하고, 해당 페이지에 접근할 때 페이지 폴트를 발생시켜 다시 디스크에서 RAM으로 로드한다.
Real World
1. Trampouline과 Trapframe의 복잡성 이유
RISC-V는 Trap 처리 시 최소한의 작업만 수행하여 Trap 처리를 빠르게 할 수 있도록 설계되었다.
CPU가 Trap을 처리할 때, 현재 유저 페이지 테이블과 유저 레지스터 상태에서 시작하기 때문에, 초기 몇 개의 Trap 핸들러 명령어는 유저 환경에서 실행된다.
이 시점에서 커널은 현재 실행 중인 프로세스와 커널 페이지 테이블의 주소를 모르기 때문에 보호된 장소를 제공한다.
- sscratch : Trap 핸들러가 유저 레지스터 값을 임시로 저장할 수 있도록 사용
- 특수 페이지 엔트리(PTE_U가 없는 엔트리) : 유저 페이지 테이블에 커널 메모리를 매핑하되, 유저가 접근하지 못하도록 보호.
2. Trampouline page 제거 가능성 : 현대 운영체제에서는 user page table에 커널 메모리를 항상 매핑하는 방법으로 트램폴린 페이지를 없앨 수 있다.
'computer security > RISC-V' 카테고리의 다른 글
Chapter 6. Locking (7) | 2025.01.14 |
---|---|
Chapter 5. Interrupts and device drivers (6) | 2025.01.13 |
Chapter 3. Page tables (6) | 2025.01.07 |
chapter 2. Operating system organization (7) | 2025.01.06 |
chapter 1. operating system interfaces (8) | 2025.01.04 |