-
RISC-V 어셈블리 프로그래밍을 웹 기반 인터랙티브 에뮬레이터로 학습할 수 있도록 만든 오픈 튜토리얼로, 저수준 컴퓨터 구조 개념을 실습 중심으로 설명
-
RV32I_Zicsr 명령어 집합을 중심으로, 산술·비트 연산·분기·메모리 접근·예외 처리 등 RISC-V의 핵심 구조를 단계별로 시각화
-
에뮬레이터 조작, 레지스터 상태 확인, 메모리 접근, 시스템 콜 구현 등을 통해 실제 CPU 동작을 체험할 수 있도록 구성
- 후반부에서는 Machine/User 모드 전환, 예외 처리 루틴, 간단한 운영체제 커널 구현까지 다루며, RISC-V 특유의 오픈 표준과 확장성을 강조
- RISC-V의 구조적 단순성과 공개 표준이 교육·연구·임베디드 시스템 개발에 적합함을 보여주는 실습형 자료
개요
- 이 튜토리얼은 Nick Morgan의 Easy 6502에서 영감을 받아 제작된 RISC-V 어셈블리 입문서로, 저수준 프로그래밍 개념을 알고 있지만 RISC-V에 익숙하지 않은 사용자를 대상으로 함
- RISC-V는 UC Berkeley에서 시작된 오픈 RISC 아키텍처로, 전 세계 연구자·엔지니어·학생들이 참여하는 활발한 커뮤니티를 형성
- 주요 특징은 간결한 설계, 공개 표준, 커뮤니티 중심 개발로, 누구나 자유롭게 코어와 칩을 설계 가능
- 본 튜토리얼은 32비트 RV32I_Zicsr 명령어 집합을 다루며, Rust의 riscv32i-unknown-none-elf 타깃과 호환
- 다루는 명령어는 총 45개로, 산술·논리·분기·메모리·CSR 조작 등 CPU의 기본 기능을 모두 포함
첫 번째 RISC-V 프로그램
- 에뮬레이터를 통해 addi와 ebreak 명령어를 실행하며, 레지스터 a0 값이 0x00000123으로 변하는 과정을 시각적으로 확인
-
Start, Run, Step, Dump 버튼으로 코드 실행·단계별 추적·어셈블 결과 확인 가능
-
Dump는 심볼 테이블과 명령어 인코딩(리틀엔디언 형식)을 표시해, 실제 메모리 주소와 기계어의 관계를 보여줌
프로세서 상태
-
프로그램 카운터(pc) 는 현재 실행 중인 명령어의 주소를 가리킴
-
일반 레지스터(x1~x31) 은 32비트 데이터를 저장하며, x0은 항상 0을 반환하는 제로 레지스터로 쓰기 무시
- RV32I에는 플래그 레지스터가 없으며, 오버플로·캐리 처리는 프로그래머가 직접 관리
명령어 문법
- 명령어는 명령어명, 피연산자1, 피연산자2, ... 형태로 구성
- 예: addi x10, x0, 0x123 → x10 = x0 + 0x123
- 즉시값(immediate)은 12비트 2의 보수 범위 [-2048, 2047] 내에서 사용
-
li, mv 등 의사명령어(pseudoinstruction) 는 실제 명령어 조합으로 확장되어 편의 제공
연산 명령어
-
산술 연산: add, addi, sub 등으로 덧셈·뺄셈 수행
-
addi는 즉시값을 더하고, sub는 두 레지스터의 차를 계산
-
비트 연산: and, or, xor 및 즉시값 버전(andi, ori, xori) 지원
-
xori rd, rs1, -1은 비트 반전(not) 효과
-
비교 연산: slt, sltu, slti, sltiu로 부호·무부호 비교 수행
-
a < b는 slt, a >= b는 slt 후 xori 1로 구현
-
시프트 연산: sll, srl, sra 및 즉시값 버전 존재
- 산술 시프트(sra)는 부호 비트를 유지, 논리 시프트(srl)은 0으로 채움
- RV32I에는 곱셈·나눗셈이 없으며, 확장 명령어(M 확장)로 제공
큰 수 다루기
-
lui 명령어는 상위 20비트를 로드하고, addi로 하위 12비트를 채워 32비트 상수를 구성
-
%hi(), %lo() 매크로로 상·하위 비트를 자동 계산 가능
분기와 점프
-
분기(branch) : 조건에 따라 점프 (beq, bne, blt, bge, bltu, bgeu)
-
점프(jump) : 무조건 점프 (jal, jalr)
-
jal은 다음 명령어 주소를 rd에 저장해 함수 호출에 사용
-
ret은 jalr x0, 0(x1)의 의사명령어로 복귀 수행
-
라벨(label) 은 코드 내 주소 식별자로, 루프·조건문 구현에 활용
메모리 접근
-
메모리 영역: 0x40000000~0x400FFFFF (1MiB)
-
로드/스토어 명령어:
-
lw / sw : 32비트 단위
-
lb, lbu, lh, lhu : 부호 확장/제로 확장 지원
-
sb, sh : 바이트·하프워드 저장
-
엔디언: 리틀엔디언 사용 (.byte 0x1,0x2,0x3,0x4 → .word 0x04030201)
-
메모리 정렬: 워드는 4바이트, 하프워드는 2바이트 정렬 필요
메모리 매핑 I/O
- 주소 0x10000000에 32비트 쓰기(sw) 시 하위 8비트가 출력창에 문자로 표시
- 이를 이용해 Hello World를 출력 가능
함수와 호출 규약
-
레지스터 별칭:
-
a0~a7: 인자/반환값
-
t0~t6: 임시
-
s0~s11: 호출 보존
-
ra: 복귀 주소, sp: 스택 포인터
- 함수 호출 시 인자는 a0~a7, 반환값은 a0 사용
- 호출된 함수는 s 레지스터와 sp를 보존해야 함
스택 구조
- 스택은 낮은 주소 방향으로 성장, 16바이트 정렬 유지
- 함수 진입 시 sp 감소로 공간 확보, 종료 시 복원
-
ra, s 레지스터를 스택에 저장·복원하여 중첩 호출 지원
- 예시: 재귀 피보나치 함수 구현
숫자 라벨
-
1:과 같은 숫자 라벨을 사용해 1b(backward), 1f(forward)로 간단히 참조 가능
위치 독립 코드 (PIC)
-
auipc 명령어는 pc + (imm << 12)를 계산해 PC 상대 주소 지정 지원
-
%pcrel_hi(), %pcrel_lo() 매크로로 위치 독립 주소 계산
-
la, call 의사명령어는 auipc와 jalr 조합으로 구현
특권 아키텍처 기초
-
Privilege Level: Machine(3), User(0) 두 단계
-
CSR(Control and Status Register) : 시스템 제어용 특수 레지스터
- 조작 명령어: csrrw, csrrs, csrrc 및 즉시값 버전
- 읽기 전용 접근은 csrr 의사명령어 사용
카운터와 상태
-
cycle, instret은 각각 클록 사이클과 명령어 실행 수를 카운트
- RV32에서는 cycleh, instreth로 상위 32비트 제공
- 현재 특권 레벨은 CSR로 직접 읽을 수 없으며, 설계상 가상화 조건을 충족하기 위함
예외 처리
- 예외 발생 시 mcause, mepc, mtval, mstatus에 정보 저장 후 mtvec으로 점프
-
mret 명령어는 예외 복귀 시 사용, mepc로 복귀 주소 설정
- 예외 코드 예시:
- 2: Illegal instruction
- 3: Breakpoint
- 8: Environment call from User mode
User 모드 전환과 시스템 콜
-
mret으로 Machine → User 모드 전환
-
ecall은 시스템 콜 트리거, ebreak은 디버그용 브레이크포인트
-
mscratch CSR은 커널 스택 포인터 저장소로 활용, User 모드에서 접근 불가
최소 운영체제 구현
- 기능:
-
a7=1: putchar (문자 출력)
-
a7=2: exit (종료)
- 예외 처리 루틴은 U모드 예외 시 레지스터 상태를 스택에 저장하고 trap_main 호출
-
trap_main은 mcause를 확인해 시스템 콜 분기, a0에 반환값 저장 후 mret으로 복귀
의사 코드 개요
-
trap_main은 mcause==8일 때 do_syscall 호출
-
do_syscall은 a7 값에 따라 sys_putchar 또는 sys_exit 실행
-
sys_putchar는 0x10000000 주소로 문자 출력
-
sys_exit은 ebreak으로 종료
생략 및 단순화된 부분
- 실제 RISC-V 어셈블리와 완전히 동일하지 않으며, 일부 의사명령어·CSR·확장 기능은 생략
- 다루지 않은 주제:
- 64비트 아키텍처
- 압축 명령어
- 인터럽트, 메모리 보호, 가상 메모리 등 고급 OS 기능
참고 자료 및 라이선스