2025. 1. 17. 21:09ㆍCS/OS
1. RISC-V
RISC-V는 (”리스크 파이브"로 발음한다.) 축소 명령어 집합 컴퓨터 즉, RISC(Reduced Instruction Set Computer) 기반의 개발형 명령어 집합(ISA)이다.
대부분의 ISA와 달리 RISC-V ISA는 일부 목적으로는 자유로이 사용할 수 있으며, 누구든지 RISC-V 칩과 소프트웨어를 설계, 제조, 판매할 수 있게 허가되어 있다.
저자는 RISC-V를 CPU로 선택한 이유가 명세가 간단하고 초보자에게 적합하며, x86과 Arm과 함께 최근 주목받는 ISA이기 때문이라 한다.
RISC-V의 spec을 읽어볼 수 있다.
32비트 RISC-V를 사용한다.
조금의 수정을 통해서 64비트로 변경할 수 있지만 보다 복잡하고 주소를 읽기 어렵다는 단점이 있어 32비트로 진행한다고 한다.
출처 : https://ko.wikipedia.org/wiki/RISC-V
2. QEMU virt machine
이 책에서는 QEMU virt
머신을 사용한다.
QEMU는 가상화 소프트웨어이고, virt는 QEMU의 가상 머신 플랫폼으로, 실제 하드웨어를 모방한 가상의 하드웨어 플랫폼이다.
QEMU 안에서 동작하는 가상화된 CPU, 메모리, 등 각종 하드웨어들을 제공한다.
이를 이용하면 가상화된 환경에서 안전하게 실험할 수 있다.
또 디버깅 때 QEMU의 소스 코드를 읽거나, QEMU 프로세스에 디버거를 연결하여 문제파악에 사용한다.
3. RISC-V assembly
OS를 구현하기 위해서 RISC-V의 assembly를 알아야 한다.
많이 어렵지 않다고 하니 배워보자.
어셈블리어를 이해하는데 참고.
가. RISC-V의 Register 구성
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 |
나. Memory access
lw a0, (a1) // Read a word (32-bits) from address in a1
// and store it in a0. In C, this would be: a0 = *a1;
sw a0, (a1) // Store a word in a0 to the address in a1.
// In C, this would be: *a1 = a0;
여기서 (...)
는 C 언어에서의 포인터처럼 생각하면 된다.
li a0, 42 # a0 레지스터에 42를 직접 저장
반면에 li
(load immediate)는 즉시 값을 레지스터에 로드하는 명령어다.
이는 C 언어에서 변수에 직접 값을 할당하는 것과 비슷하다. (예: int a = 123;
)
다. Branch instructions
Branch instructions는 프로그램의 제어 흐름을 변경한다.
이는 if
, for
, while
문을 구현하는 데 사용된다.
bnez a0, <label> // Go to <label> if a0 is not zero
// If a0 is zero, continue here
<label>:
// If a0 is not zero, continue here
bnez
: branch if not equal to zero - 값이 0이 아닐 때 분기beq
: branch if equal - 두 값이 같을 때 분기blt
: branch if less than - 첫 번째 값이 두 번째 값보다 작을 때 분기
이들은 C언어의 goto
와 비슷하지만 조건이 있다는 점이 다르다.
다. Function calls
jal
(jump and link)과 ret
(return) 명령어는 함수를 호출하고 함수에서 반환하는 데 사용된다.
li a0, 123 // Load 123 to a0 register (function argument)
jal ra, <label> // Jump to <label> and store the return address
// in the ra register.
// After the function call, continue here...
// int func(int a) {
// a += 1;
// return a;
// }
<label>:
addi a0, a0, 1 // Increment a0 (first argument) by 1
ret // Return to the address stored in ra.
// a0 register has the return value.
함수 인자는 calling convention에 따라 a0
- a7
레지스터에 전달되며, 반환값은 a0
레지스터에 저장된다.
라. Stack
스택은 함수 호출과 지역 변수를 위해 사용되는 후입선출(LIFO, Last-In-First-Out) 메모리 공간이다.
스택은 아래쪽으로 확장되며, 스택 포인터 sp
는 스택의 맨 위를 가리킨다.
스택에 값을 저장하기 위해서는 스택 포인터를 감소시키고 값을 저장한다(일명 push 연산):
addi sp, sp, -4 // Move the stack pointer down by 4 bytes
// (i.e. stack allocation).
sw a0, (sp) // Store a0 to the stack
스택에서 값을 로드하려면, 값을 로드하고 스택 포인터를 증가시킨다(일명 pop 연산):
lw a0, (sp) // Load a0 from the stack
addi sp, sp, 4 // Move the stack pointer up by 4 bytes
// (i.e. stack deallocation).
C 언어에서는 컴파일러가 스택 연산을 자동으로 생성하므로, 직접 작성할 필요가 없다.
마. CPU modes
CPU는 각기 다른 privilege 권한을 가진 여러 모드를 가지고 있다.
RISC-V의 경우 세 가지 모드가 있다:
Mode | Overview |
M-mode | Mode in which OpenSBI (i.e. BIOS) operates. |
S-mode | Mode in which the kernel operates, aka. "kernel mode". |
U-mode | Mode in which applications operate, aka. "user mode". |
OpenSBI(Open Source Supervisor Binary Interface)는 RISC-V 하드웨어와 운영체제 사이의 인터페이스를 제공하는 펌웨어로, PC의 BIOS나 UEFI와 비슷한 역할을 한다.
바. Privileged instructions
CPU 명령어 중 privileged 명령어라고 분류되는 것들이 존재한다.
이는 user-mode 즉 일반 application에선 실행할 수 없다.
우리는 아래의 privileged instructions만 사용할 예정이다.
Opcode and operands | Overview | Pseudocode |
csrr rd, csr | Read from CSR | rd = csr; |
csrw csr, rs | Write to CSR | csr = rs; |
csrrw rd, csr, rs | Read from and write to CSR at once | tmp = csr; csr = rs; rd = tmp; |
sret | Return from trap handler (restoring program counter, operation mode, etc.) | |
sfence.vma | Clear Translation Lookaside Buffer (TLB) |
- CSR
: Control and Status Register
: a register that stores CPU settings.
: The list of CSRs can be found in RISC-V Privileged Specification.
명령어 | 의미 | 동작 | 예시 |
csrr | Control and Status Register Read | CSR에서 값을 읽어서 지정된 레지스터에 저장 | csrr rd, csr |
csrw | Control and Status Register Write | 레지스터의 값을 CSR에 쓰기 | csrw csr, rs |
csrrw | Control and Status Register Read and Write | CSR 읽기와 쓰기를 원자적으로 수행 | csrrw rd, csr, rs |
sret | Supervisor Return | 트랩 핸들러에서 복귀, PC와 CPU 모드 복원 | sret |
sfence.vma | Supervisor Fence Virtual Memory Address | TLB(Translation Lookaside Buffer) 초기화 | sfence.vma |
사. Inline assembly
C 코드 내부에서 assembly를 사용하는 문법을 "inline assembly"라고 한다.
uint32_t value;
__asm__ __volatile__("csrr %0, sepc" : "=r"(value));
1) 문법
__asm__ __volatile__("assembly" : output operands : input operands : clobbered registers);
구성 요소 | 설명 |
__asm__ | 인라인 어셈블리임을 나타냅니다. |
__volatile__ | 컴파일러가"assembly"코드를 최적화하지 않도록 지시합니다. |
"assembly" | 문자열 리터럴로 작성된 어셈블리 코드입니다. |
output operands | 어셈블리의 결과를 저장할 C 변수들입니다. |
input operands | 어셈블리에서 사용될 C 표현식들입니다 (예:123,x). |
clobbered registers | 어셈블리에서 내용이 변경되는 레지스터들입니다. 이를 명시하지 않으면 C 컴파일러가 해당 레지스터들의 내용을 보존하지 않아 버그가 발생할 수 있습니다. |
Output and input operands는 콤마로 구분되며, 각 operand는 constraint (C expression) 형식으로 작성된다.
Constraint는 operand의 타입을 지정하기 위해서 사용되며, 보통 output operand에는 =r (register)를 input operand에는 r을 사용한다.
어셈블리 코드에서 input과 output operand는 %0, %1, %2 와 같이 쓰고 순서대로 접근할 수 있다.
2) 예시
uint32_t value;
__asm__ __volatile__("csrr %0, sepc" : "=r"(value));
RISC-V의 sepc(Supervisor Exception Program Counter) CSR의 값을 읽어서 value 변수에 저장하는 인라인 어셈블리다.
uint32_t value;
: 32비트 부호 없는 정수형 변수를 선언__asm__
: 인라인 어셈블리임을 나타내냄__volatile__
: 컴파일러가 이 코드를 최적화하지 않도록 지시"csrr %0, sepc"
: CSR에서 값을 읽어와서%0
로 표시된 위치(여기서는value
변수)에 저장: "=r"(value)
:value
변수에 어셈블리 명령어의 실행 결과를 저장합니다
__asm__ __volatile__("csrw sscratch, %0" : : "r"(123));
이는 csrw
명령어를 사용하여 sscratch
CSR에 123
을 쓰는 것입니다. %0
는 123
을 포함하는 레지스터(r
제약조건)에 해당하며, 실제로는 아래와 같이 동작합니다.
li a0, 123 // Set 123 to a0 register
csrw sscratch, a0 // Write the value of a0 register to sscratch register
인라인 어셈블리에는 csrw
명령어만 작성되어 있다.
li
명령어는 "r"
제약 조건(레지스터의 값)을 만족시키기 위해 컴파일러가 자동으로 삽입한다.
'CS > OS' 카테고리의 다른 글
[1000줄 OS 구현하기] Boot (0) | 2025.01.21 |
---|---|
[1000줄 OS 구현하기] 시작하기 (0) | 2025.01.17 |