[1000줄 OS 구현하기] RISC-V Assembly

2025. 1. 17. 21:09CS/OS

 

RISC-V 101 | OS in 1,000 Lines

 

operating-system-in-1000-lines.vercel.app

 


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 Ratified Specifications

The RISC-V open-standard instruction set architecture (ISA) defines the fundamental guidelines for designing and implementing RISC-V processors.

riscv.org

 

RISC-V의 spec을 읽어볼 수 있다.

 

32비트 RISC-V를 사용한다.

 

조금의 수정을 통해서 64비트로 변경할 수 있지만 보다 복잡하고 주소를 읽기 어렵다는 단점이 있어 32비트로 진행한다고 한다.

 

출처 : https://ko.wikipedia.org/wiki/RISC-V


2. QEMU virt machine

 

이 책에서는 QEMU virt 머신을 사용한다.

 

 

‘virt’ Generic Virtual Platform (virt) — QEMU documentation

‘virt’ Generic Virtual Platform (virt) The virt board is a platform which does not correspond to any real hardware; it is designed for use in virtual machines. It is the recommended board type if you simply want to run a guest such as Linux and do not

www.qemu.org

 

QEMU는 가상화 소프트웨어이고, virt는 QEMU의 가상 머신 플랫폼으로, 실제 하드웨어를 모방한 가상의 하드웨어 플랫폼이다.

 

QEMU 안에서 동작하는 가상화된 CPU, 메모리, 등 각종 하드웨어들을 제공한다.

 

이를 이용하면 가상화된 환경에서 안전하게 실험할 수 있다.

 

또 디버깅 때 QEMU의 소스 코드를 읽거나, QEMU 프로세스에 디버거를 연결하여 문제파악에 사용한다.

 


3. RISC-V assembly

 

OS를 구현하기 위해서 RISC-V의 assembly를 알아야 한다.

 

많이 어렵지 않다고 하니 배워보자.

 

 

RISC-V 101 | OS in 1,000 Lines

 

operating-system-in-1000-lines.vercel.app

 

 

[컴퓨터구조론] 마이크로-프로그램

1. 제어 유니트의 기능 명령어들을 인출하여 해독하고 실행하는 과정이 순차적으로 발생되도록 하기 위해서 그 순간마다 적절한 제어 신호들이 생성되어 해당 하드웨어 모듈로 보 내져야 한다.

ramen4598.tistory.com

 

어셈블리어를 이해하는데 참고.

 

가. 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)  

 

 

명령어 의미 동작 예시
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을 쓰는 것입니다. %0123을 포함하는 레지스터(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