환경 설정
QEMU 다운
WSL - Ubuntu 22.04 환경에서 진행하였다. 다음과 같은 패키지를 다운받자.
sudo apt update && sudo apt install -y clang llvm lld qemu-system-riscv32 curl실제 OS 개발의 일련의 절차는 다음과 같다. 코드 수정 - 빌드 - USB 굽기 - PC 끄기 - USB 꼽고 바이오스 진입해서 부팅.. 이러한 과정을 실제로 하기에는 학습 효율이 너무 떨어지기 때문에, QEMU라는 일종의 컴퓨터 Emulator를 사용하여, 내 PC안에서 가상의 컴퓨터를 즉시 실행함으로써, 내가만든 OS가 하드웨어 위에서 어떻게 돌아가는지 테스트해볼 수 있다.Why QEMU?
OpenSBI 다운
OpenSBI 펌웨어를 다운로드한다. 다운로드 위치는 내가 코드를 작생할 폴더에 아래 명령어를 실행하면된다.
curl -LO https://github.com/qemu/qemu/raw/v8.0.4/pc-bios/opensbi-riscv32-generic-fw_dynamic.bin컴퓨터가 켜지자 마자 커널이 바로 실행될 수는 없다. 누군가가 하드웨어를 초기화하고 커널을 메모리에 올려서 실행해야하는데, 이 일련의 절차를 부트로더가 수행해주어야하고, 그 역할을 OpenSBI가 한다.Why OpenSBI?
QEMU를 실행할 때, 일종의 RISC-V 전용 가짜 BIOS인 OpenSBI를 올려줘야 내가 만든 OS가 돌아가기 위해 부트스트랩을 하게 되는 것이다.
더 설명하자면, 실제 컴퓨터가 켜질 때, ROM에 삽입된 BIOS/UEFI라는 펌웨어 코드를 실행하는데, 우리는 OpenSBI를 사용함으로써, QEMU라는 가상 컴퓨터 내부에서 컴퓨터가 켜질 때, BIOS의 역할을 OpenSBI 파일(.bin)이 수행하라고 주문하는 것이다.
실제로라면 생산공장에서 메인보드에 BIOS를 구워야하지만, 우리는 OS를 만드는게 목표이므로, QEMU 옵션인 -bios opensbi.bin 을 사용하여 마치 실제로 메인보드에 BIOS가 구워져 있는 것처럼 체험해 볼 수 있다.
QEMU virt machine
이 에서는 QEMU의 virt 를 사용한다. 이름에서도 알 수 있듯이 실제로 존재하는 하드웨어가 아니라 가상 머신 전용 하드웨어 세트(컴퓨터)라고 보면 된다.
왜 QEMU와 OpenSBI를 사용하나요?
이 스터디의 목적은 OS기 때문에, 하드웨어 설계나 BIOS 설계같은 것들은 하지 않는다.
- 하드웨어는 QEMU에게 맡긴다.
- BIOS와 같은 부트스트랩은 OpenSBI에게 맡긴다.
이렇게 함으로써 나는 부트스트랩이 되고 난 후의 프로그램인 커널을 만드는데 집중하면서 OS의 본질을 공부하는데 더 집중할 수 있게 된다.
OpenSBI 관련해서 더 알고 싶다면 [QEMU] 임베디드 리눅스 시스템 구축 프로젝트 - OpenSBI 을 참고하면 많은 도움이 될 것 같다.
RISC-V
Why RISC-V?
- 학습용으로 x86, arm보다 적절함.
- 오픈소스여서 문서화가 잘 되어있음.
RISC-V 입문 전 꼭 알아야할 개념
다음 개념들은 RISC-V 어셈블리를 입문할 때 알아둬야하는 개념이다.
- 레지스터란 무엇인가
- 사칙연산 (add, sub, mul, div 등)
- 메모리 접근 (lw, sw 등)
- 분기 명령 (beq, bne, blt, bgez 등)
- 함수 호출
- 스택의 구조
해당 글 에 매우 잘 설명되어 있다.
CPU 모드
CPU 모드 종류
RISC-V에서는 3가지 CPU모드가 있다. 권한은 M > S > U 라고 보면 된다.
- M-Mode(Machine Mode): 모든 하드웨어 자원에 직접 접근하여 시스템 초기화, 메모리 관리, 인터럽트 처리를 수행, OpenSBI
- S-Mode(Supervisor Mode): 하드웨어에 직접 접근하는 대신 OpenSBI가 제공하는 인터페이스(SBI)를 통해 필요한 기능을 요청, 커널
- U-Mode(User Mode): 일반 애플리케이션에서 실행되는 모드
CPU 모드 전환 흐름
U-mode ──ecall/fault──> S-mode ──ecall/fault──> M-mode
<─────sret────── <─────mret──────정상적인 경우 (V=1, R=1, U=1PTE(Page Table Entry)에 존재하는 값으로, V(페이지가 Valid한지 여부), R(페이지를 Read할 수 있는지), U(페이지가 U-mode 에서 접근가능한지)와 같은 정보를 저장하는 Bit로 페이지 테이블에 존재한다.) Page Fault가 발생하는 경우 (V=0) U-Mode (사용자 프로그램) MMU논리 주소를 물리 주소로 변환해주며 메모리 보호나 캐시 관리 등 CPU가 메모리에 접근하는 것을 총 관리해주는 하드웨어
가 페이지 테이블을 확인 -> V=0 -> S-Mode (OS 커널 트랩 핸들러) U-mode (사용자 프로그램 복귀) OS 개발은? -> S-Mode의 4-11까지의 내용을 개발하는 것이라고 볼 수 있다유저 어플리케이션 (U-Mode)에서
lw s0, 0(s1)명령어를 수행한다고 생각해보자. 어떤 일이 발생할까?Page Fault 처리 흐름
lw s0, 0(s1) 실행 시도Page Fault 발생lw 실행 중단, 하드웨어가 일련의 과정1. Page Fault 발생 감지
2. sepc <- 현재 PC 저장
3. scause <- 원인 코드 저장
4. stval <- 문제된 주소 저장
5. sstatus.SPP <- 현재 모드(U) 저장
6. 모드 비트를 S-mode로 변경
7. PC <- stvec 값으로 설정 (점프)
8. stvec 주소의 명령어 실행 시작
하드웨어가 자동적으로 일련의 과정을 수행함.을 수행 후 S-Mode로 전환scause를 확인 -> Load Page Fault임을 파악stval을 확인 -> 문제가 된 가상 주소 파악sret 실행 -> sepc에 저장된 주소(원래 lw 명령어)로 복귀, U-mode로 전환lw s0, 0(s1)가 처음부터 다시 실행
물론 이 과정은 메모리 스왑이 구현되어 있는 OS에 관련된 과정이고, 1000줄로 만드는 OS에서는 해당 과정처럼 작동하지 않는다.
특권 명령 (Privileged instructions)
U-mode에서 실행할 수 없는 명령으로, csrr/csrw, sret, sfence.vma등 이 있다.
각각에 대해 설명하자면,
| opcode | overview |
|---|---|
csrr/csrw rd, csr | csrControl and Status Register로, CPU 내부 상태와 제어를 담당하는 레지스터, sepc, scause, stval 등이 있음. 값을 읽거나 씀 |
sret | 트랩 핸들러 복귀(PC, CPU모드 등 복원) |
sfence.vma | TLBTranslation Lookaside Buffer. 최근에 사용된 가상 주소를 물리 주소 변환한 결과를 캐싱해두는 하드웨어. MMU 안에 있음. 초기화 |
인라인 어셈블리 (inline assembly)
C 코드 중간에 RISC-V 어셈블리를 직접 삽입하는 방법으로 다음과 같은 형태로 작성할 수 있다.
__asm__ __volatile__("어셈블리 명령어"
: 출력 operand
: 입력 operand
: 변경되는 레지스터
);예시 CSR 읽기 – scause를 읽어서 C 변수에 저장하는 경우
uint32_t value;
__asm__ __volatile__("csrr %0, scause" : "=r"(value));여기서 %0은 첫 번째 operand이고, "=r"(value)는 결과를 레지스터를 통해 value 변수에 할당해라는 뜻
CSR 쓰기 – stvec에 트랩 핸들러 주소를 등록하는 경우
__asm__ __volatile__("csrw stvec, %0" : : "r"(handler_addr));보통은 이런 인라인 어셈블리를 매번 쓰기 번거로우니까, 매크로나 함수로 감싸서 사용한다.
#define READ_CSR(reg) \
({ \
uint32_t __val; \
__asm__ __volatile__("csrr %0, " #reg \
: "=r"(__val)); \
__val; \
})
#define WRITE_CSR(reg, val) \
__asm__ __volatile__("csrw " #reg ", %0" \
: : "r"(val))이렇게 만들어두면 커널 코드에서 깔끔하게 쓸 수 있다.
uint32_t cause = READ_CSR(scause);
WRITE_CSR(stvec, handler_addr);참고 자료
- OS in 1000 Lines 01. 환경설정
- OS in 1000 Lines 02. RISC-V 입문
- [QEMU] 임베디드 리눅스 시스템 구축 프로젝트 - OpenSBI overview
- [QEMU] 임베디드 리눅스 시스템 구축 프로젝트 - OpenSBI
- KVM과 QEMU란? (Linux 가상화 솔루션)
- Control and Status Registers (CSRs)
- [운영체제] TLB(Translation Lookaside Buffer), 캐시사상기법 (직접사상, 연관사상), 페이지크기
- [OS] 요구 페이징
- [운영체제] 페이지 테이블(TLB, 계층/해시/역페이지 테이블)