2025. 2. 3. 00:48ㆍCS/OS
- 한글 번역이 생겼습니다!
1. SBI로 출력
SBI를 OS의 API 정도로 소개했었다.
SBI의 function을 호출하기 위해선 ecall
명령어를 사용한다.
가. kernel.c
#include "kernel.h"
typedef unsigned char uint8_t;
typedef unsigned int uint32_t;
typedef uint32_t size_t;
extern char __bss[], __bss_end[], __stack_top[];
void *memset(void *buf, char c, size_t n) {
uint8_t *p = (uint8_t *) buf;
while (n--)
*p++ = c;
return buf;
}
struct sbiret sbi_call(long arg0, long arg1, long arg2, long arg3,
long arg4, long arg5, long fid, long eid) {
register long a0 __asm__("a0") = arg0;
register long a1 __asm__("a1") = arg1;
register long a2 __asm__("a2") = arg2;
register long a3 __asm__("a3") = arg3;
register long a4 __asm__("a4") = arg4;
register long a5 __asm__("a5") = arg5;
register long a6 __asm__("a6") = fid;
register long a7 __asm__("a7") = eid;
__asm__ __volatile__("ecall"
: "=r"(a0), "=r"(a1)
: "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(a4),
"r"(a5), "r"(a6), "r"(a7)
: "memory");
return (struct sbiret){.error = a0, .value = a1};
}
void putchar(char ch) {
sbi_call(ch, 0, 0, 0, 0, 0, 0, 1 /* Console Putchar */);
}
void kernel_main(void) {
memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);
const char *s = "\n\nHello World!\n";
for (int i = 0; s[i] != '\0'; i++) {
putchar(s[i]);
}
for (;;) {
__asm__ __volatile__("wfi");
}
}
__attribute__((section(".text.boot")))
__attribute__((naked))
void boot(void) {
__asm__ __volatile__(
"mv sp, %[stack_top]\n" // Set the stack pointer
"j kernel_main\n" // Jump to the kernel main function
:
: [stack_top] "r" (__stack_top) // Pass the stack top address as %[stack_top]
);
}
수정되거나 추가한 코드만 살펴보면…
#include "kernel.h"
extern char __bss[], __bss_end[], __stack_top[];
struct sbiret sbi_call(long arg0, long arg1, long arg2, long arg3, long arg4,
long arg5, long fid, long eid) {
register long a0 __asm__("a0") = arg0;
register long a1 __asm__("a1") = arg1;
register long a2 __asm__("a2") = arg2;
register long a3 __asm__("a3") = arg3;
register long a4 __asm__("a4") = arg4;
register long a5 __asm__("a5") = arg5;
register long a6 __asm__("a6") = fid;
register long a7 __asm__("a7") = eid;
__asm__ __volatile__("ecall"
: "=r"(a0), "=r"(a1)
: "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(a4), "r"(a5),
"r"(a6), "r"(a7)
: "memory");
return (struct sbiret){.error = a0, .value = a1};
}
void putchar(char ch) {
sbi_call(ch, 0, 0, 0, 0, 0, 0, 1 /* Console Putchar */);
}
void kernel_main(void) {
const char *s = "\n\nHello World!\n";
for (int i = 0; s[i] != '\0'; i++) {
putchar(s[i]);
}
for (;;) {
__asm__ __volatile__("wfi");
}
}
register long a0 __asm__("a0") = arg0;
: C 변수a0
를 RISC-V의a0
레지스터에 직접 매핑fid
: Function ID - SBI 호출할 특정 기능을 식별하는 번호eid
: Extension ID - SBI 확장 기능을 식별하는 번호 (예: 1은 Console Putchar)
SBI 호출 시 Function ID (fid)와 Extension ID (eid)는 SBI 사양에서 미리 정의된 번호들.
이러한 ID들은 RISC-V SBI 표준에 의해 미리 정의되어 있어서 개발자가 임의로 정하는 것이 아닙니다.
1
번 즉 Console Putchar
는 인자로 받은 문자를 디버그 콘솔에 출력해 주는 함수다.
__asm__ __volatile__("ecall"
: "=r"(a0), "=r"(a1)
: "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(a4), "r"(a5),
"r"(a6), "r"(a7)
: "memory");
return (struct sbiret){.error = a0, .value = a1};
ecall
명령어를 실행하여 SBI 호출a0
와a1
레지스터의 출력을 C 변수에 저장a0
~a7
레지스터의 값을 입력으로 전달memory
메모리를 수정할 수 있음을 컴파일러에 알림- 실행 후
a0(error)
와a1(value)
값을sbiret
구조체로 반환합니다. (C 언어 compound literal 문법 - 쉽게 말해 익명 구조체)
ecall
명령어를 실행하면, CPU 실행 모드가 커널 모드(S-Mode)에서 OpenSBI 모드(M-Mode)로 전환되어 OpenSBI 처리 루틴이 동작한다.
처리가 끈나면 다시 커널 모드(S-Mode)로 돌아온다.
애플리케이션이 커널에 시스템 콜을 호출할 때도 ecall
이 사용된다.
상위 권한 레벨로의 함수 호출 역할.
void putchar(char ch) {
sbi_call(ch, 0, 0, 0, 0, 0, 0, 1 /* Console Putchar */);
}
void kernel_main(void) {
const char *s = "\n\nHello World!\n";
for (int i = 0; s[i] != '\0'; i++) {
putchar(s[i]);
}
for (;;) {
__asm__ __volatile__("wfi");
}
}
putchar
: SBI 호출을 통해 단일 문자를 콘솔에 출력.
: Console Putchar (Extension ID 1)을 사용하여 구현.
"Hello World!" 문자열을 한 글자씩 순회하면서 putchar
를 호출하여 출력.
무한 루프에서 wfi
(wait for interrupt) 명령어를 실행하여 시스템을 대기 상태로 유지.
나. kernel.h
#pragma once
struct sbiret {
long error;
long value;
};
kernel.h
에 반환하는 구조체를 정의한다.
#pragma once
: 헤더 파일의 중복 포함을 방지하는 전처리기 지시문.
: 한 소스 파일에서 동일한 헤더 파일이 여러 번 포함되는 것을 방지.
: 컴파일 시간을 단축하고 중복 정의로 인한 오류를 예방.
다. 실행
./run.sh
...
Hello World!
동작 과정
- 커널에서
ecall
명령어를 실행 → CPU는 OpenSBI가 부팅 시점에 설정해 둔 M-Mode 트랩 핸들러(mtvec 레지스터)로 점프 - 레지스터를 저장한 뒤, C로 작성된 트랩 핸들러가 호출
eid
에 따라, 해당 SBI 기능을 처리하는 함수가 실행- 8250 UART용 디바이스 드라이버가 문자를 QEMU에 전송
- QEMU의 8250 UART 에뮬레이션이 이 문자를 받아서 표준 출력
- 터미널 에뮬레이터가 문자를 화면에 표시
즉 Console Putchar
함수를 부르는 것은 OpenSBI에 구현된 디바이스 드라이버를 호출하는 것!
2. printf 함수
%d
(10진수), %x
(16진수), %s
(문자열)만 지원하는 단순한 버전의 아주 원시적인 printf
를 구현해보자.
포맷 문자열과 값을 받아서 출력하면 된다.
가. common.c
이 printf
는 추후 유저 모드 프로그램에서도 활용할 수 있게 커널과 유저에서 공유할 common.c
라는 파일에 작성한다.
common.c
:
#include "common.h"
void putchar(char ch);
void printf(const char *fmt, ...) {
va_list vargs;
va_start(vargs, fmt);
while (*fmt) {
if (*fmt == '%') {
fmt++; // Skip '%'
switch (*fmt) { // Read the next character
case '\0': // '%' at the end of the format string
putchar('%');
goto end;
case '%': // Print '%'
putchar('%');
break;
case 's': { // Print a NULL-terminated string.
const char *s = va_arg(vargs, const char *);
while (*s) {
putchar(*s);
s++;
}
break;
}
case 'd': { // Print an integer in decimal.
int value = va_arg(vargs, int);
if (value < 0) {
putchar('-');
value = -value;
}
int divisor = 1;
while (value / divisor > 9)
divisor *= 10;
while (divisor > 0) {
putchar('0' + value / divisor);
value %= divisor;
divisor /= 10;
}
break;
}
case 'x': { // Print an integer in hexadecimal.
int value = va_arg(vargs, int);
for (int i = 7; i >= 0; i--) {
int nibble = (value >> (i * 4)) & 0xf;
putchar("0123456789abcdef"[nibble]);
}
}
}
} else {
putchar(*fmt);
}
fmt++;
}
end:
va_end(vargs);
}
포맷 문자열을 한 글자씩 순회하면서 %
를 만나면 전달받은 값을 대신 출력하고 이외의 문자는 그대로 출력.
그런데 va_list
, va_start
, va_arg
, va_end
는 뭘까?
나. 가변 인자 함수
- 가변 인자 함수
: Variadic Function
: 인자의 개수가 고정되지 않은 함수를 말함. (예,printf
)
가변 인자 함수는 최소한 하나의 고정 매개변수를 가져야 하며, 이후에 ...
으로 가변 인자를 표시.
void example(int count, ...) {
va_list args;
va_start(args, count); // count가 마지막 고정 인자
for (int i = 0; i < count; i++) {
int value = va_arg(args, int); // 각 인자를 int 타입으로 가져옴
// value 처리
}
va_end(args);
}
가변 인자를 처리하는 과정:
va_list
로 가변 인자 목록을 저장할 변수를 선언va_start
로 가변 인자 처리 시작 (마지막 고정 인자를 기준점으로 사용)va_arg
로 각 가변 인자를 순차적으로 접근va_end
로 가변 인자 처리 종료
가변 인자 매크로들 | 설명 | 문법 |
va_list | 가변 인자들의 목록을 저장하는 타입. printf와 같이 인자의 개수가 고정되지 않은 함수에서 인자들을 처리하기 위해 사용. | va_list ap; |
va_start | 가변 인자 처리를 시작하기 위해 va_list를 초기화. 마지막 고정 매개변수를 기준으로 가변 인자의 시작 위치를 설정. | va_start(ap, last_arg); |
va_arg | va_list에서 다음 인자를 가져오는 매크로. 인자의 타입을 지정하면 해당 타입으로 값을 반환. | va_arg(ap, type); |
va_end | 가변 인자 처리를 종료하고 va_list를 정리. 메모리 누수를 방지하기 위해 반드시 호출해야 함. | va_end(ap); |
va_list
, va_start
, va_arg
, va_end
와 같은 것들은 원래 <stdarg.h>
헤더에 정의되어 있다.
해당 자료는 표준 라이브러리에 의존하지 않고 컴파일러 빌트인 기능을 직접 활용했다.
common.h
에 추가:
#pragma once
#define va_list __builtin_va_list
#define va_start __builtin_va_start
#define va_end __builtin_va_end
#define va_arg __builtin_va_arg
void printf(const char *fmt, ...);
__builtin_
으로 시작하는 식별자들은 컴파일러에서 제공하는 빌트인 기능이다.
복잡한 것은 컴파일러가 알아서 해주므로, 매크로를 정의만 해두면 알아서 돌아간다.
다. kernel.c와 run.sh 수정
#include "kernel.h"
#include "common.h"
void kernel_main(void) {
printf("\n\nHello %s\n", "World!");
printf("1 + 2 = %d, %x\n", 1 + 2, 0x1234abcd);
for (;;) {
__asm__ __volatile__("wfi");
}
}
kernel.c
의 kernel_main
을 수정한다.
$CC $CFLAGS -Wl,-Tkernel.ld -Wl,-Map=kernel.map -o kernel.elf kernel.c common.c
run.sh
에 common.c
를 추가하여 링크한다.
$ ./run.sh
Hello World!
1 + 2 = 3, 1234abcd
printf
해금!
'CS > OS' 카테고리의 다른 글
[1000줄 OS 구현하기] 커널 패닉 (0) | 2025.02.03 |
---|---|
[1000줄 OS 구현하기] C 표준 라이브러리 (0) | 2025.02.03 |
[1000줄 OS 구현하기] Boot (0) | 2025.01.21 |
[1000줄 OS 구현하기] RISC-V Assembly (0) | 2025.01.17 |
[1000줄 OS 구현하기] 시작하기 (0) | 2025.01.17 |