Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save YangSiJun528/e86ee87bb0305eb52dcee257bd92eced to your computer and use it in GitHub Desktop.

Select an option

Save YangSiJun528/e86ee87bb0305eb52dcee257bd92eced to your computer and use it in GitHub Desktop.
[한국어 번역] 베어 메탈 printf - OS 없이 C 표준 라이브러리 사용하기.md

이 글은 Bare metal printf - C standard library without OS를 한국어로 번역한 글입니다.

AI 모델 deepseek r1을 사용하여 변역하였으여, 부정확한 내용이 있을 수 있습니다.


베어 메탈 printf - OS 없이 C 표준 라이브러리 사용하기

게시일: 2025년 4월 26일 오후 12:00

오늘은 Newlib을 활용하여 베어 메탈 시스템에서 사용할 수 있는 간결한 C 표준 라이브러리를 만드는 방법을 살펴보겠습니다. 작은 예제를 통해 UART 기본 기능을 구현하고 이를 Newlib에 전달하여 완전한 printf 기능을 구축하는 과정을 다룰 것입니다. 대상 플랫폼은 RISC-V이지만, 다른 플랫폼에도 동일한 개념이 적용될 수 있습니다.

목차

  • 소프트웨어 추상화와 C 표준 라이브러리
  • 베어 메탈에서의 C 표준 라이브러리
  • Newlib 개념
  • 크로스 컴파일 툴체인
  • 자동화된 RISC-V 툴체인 빌드
  • 메모리 및 UART 기본 기능 구현
  • 애플리케이션 예제: 입력과 출력
  • 'Gotcha' 순간
  • 앱 실행
  • 결론

소프트웨어 추상화와 C 표준 라이브러리

일반적인 최종 사용자 시스템(예: Mac 또는 Linux 노트북)에서 printf를 실행하면 복잡한 과정이 발생합니다. 애플리케이션 프로세스는 printf 함수를 호출하며, 이는 대부분 동적으로 링크됩니다. 여러 C 함수 계층을 거친 후 운영체제 커널에 시스템 호출이 발생합니다. 커널은 출력을 다양한 하위 시스템(터미널, 의사 터미널 등)을 통해 라우팅하며, 최종적으로 화면에 문자를 렌더링하기 위해 여러 추상화 계층을 거칩니다. printf가 템플릿 기반으로 출력 문자열을 포맷팅하는 과정은 더욱 복잡합니다.

반면 베어 메탈 시스템에서는 이러한 추상화 계층이 존재하지 않으며 스택이 훨씬 단순합니다.


베어 메탈에서의 C 표준 라이브러리

베어 메탈 환경에서는 C 함수를 지원하는 하위 계층이 없습니다. 일반적인 시스템에서는 프로세스가 시스템 호출을 통해 커널에 출력을 전달하지만, 베어 메탈에서는 전달할 커널이 없습니다. 여기서 목표는 UART와 같은 간단한 I/O 장치로 출력하는 printf 기능을 구현하는 것입니다.

이때 Newlib이 등장합니다. Newlib은 베어 메탈에서 C 표준 라이브러리를 활성화하는 데 적합한 솔루션으로, 사용자 정의 C 표준 라이브러리를 구축하기 위한 키트로 생각할 수 있습니다.


Newlib 개념

Newlib은 전체 C 표준 라이브러리를 처음부터 구현할 필요 없이 몇 가지 기본 프리미티브를 구현하도록 요구합니다. 예를 들어 _write 함수는 단일 문자를 출력 스트림에 쓰는 기본 기능을 구현하며, Newlib은 이를 기반으로 printf와 같은 복잡한 기능을 구축합니다.

Newlib은 다양한 플랫폼에 대한 사전 구현도 제공합니다. Linux를 대상으로 할 경우 시스템 호출을 사용할 수 있으며, 최소 구성에서는 기본 프리미티브가 오류를 반환하도록 설정할 수 있습니다.


크로스 컴파일 툴체인

RISC-V 대상으로 Newlib을 사용하는 크로스 컴파일 툴체인을 설정해야 합니다. 이 툴체인은 다음을 충족해야 합니다:

  1. 호스트 플랫폼에서 RISC-V 명령어 생성
  2. C 표준 라이브러리 기능 호출 시 Newlib 사용

일반적인 Linux 배포판의 GCC는 호스트 플랫폼용으로 빌드되므로, RISC-V 대상 툴체인을 별도로 구성해야 합니다. 이 과정은 복잡할 수 있지만, 자동화된 스크립트를 사용해 시간을 절약할 수 있습니다.


자동화된 RISC-V 툴체인 빌드

RISC-V 툴체인 저장소를 클론한 후 다음 명령어로 구성합니다:

./configure --prefix=/opt/riscv-newlib --enable-multilib --disable-gdb --with-cmodel=medany
  • --enable-multilib: 다양한 RISC-V 변형 지원
  • --disable-gdb: GDB 빌드 생략
  • --with-cmodel=medany: 64비트 주소 처리 지원

빌드 및 설치:

sudo make

메모리 및 UART 기본 기능 구현

UART 드라이버 구현 (uart.c):

#define UART_BASE 0x10000000
#define UART_THR  (*(volatile char *)(UART_BASE + 0x00))
#define UART_LSR  (*(volatile char *)(UART_BASE + 0x05))

void uart_putc(char c) {
    while ((UART_LSR & UART_LSR_TX_IDLE) == 0);
    UART_THR = c;
    if (c == '\n') {
        while ((UART_LSR & UART_LSR_TX_IDLE) == 0);
        UART_THR = '\r';
    }
}

Newlib 시스템 호출 구현 (syscalls.c):

#include <errno.h>
#include <sys/stat.h>

int _write(int fd, const void *buf, size_t count) {
    // stdout/stderr 처리
    for (size_t i = 0; i < count; i++) {
        uart_putc(((char *)buf)[i]);
    }
    return count;
}

힙 관리 (_sbrk 구현):

extern char _end;
extern char _stack_bottom;

void* _sbrk(int incr) {
    static char *heap_end = &_end;
    char *prev_heap_end = heap_end;
    
    if (heap_end + incr > &_stack_bottom) {
        errno = ENOMEM;
        return (void*)-1;
    }
    heap_end += incr;
    return prev_heap_end;
}

애플리케이션 예제

메인 프로그램 (main.c):

#include <stdio.h>

int main(void) {
    printf("Hello from RISC-V UART!\n");
    char buffer[100];
    printf("Type something: ");
    scanf("%s", buffer);
    printf("You typed: %s\n", buffer);
    while(1);
    return 0;
}

링커 스크립트 (link.ld):

MEMORY {
  RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 64M
}

SECTIONS {
  .text : { *(.text.init) *(.text) }
  .bss : {
    _bss_start = .;
    *(.bss)
    _bss_end = .;
  }
  _stack_top = ORIGIN(RAM) + LENGTH(RAM);
}

'Gotcha' 순간

--with-cmodel=medany 플래그는 64비트 주소 공간을 올바르게 처리하기 위해 필수적입니다. 이 없으면 높은 메모리 주소를 처리하는 명령어가 생성되지 않아 링크 오류가 발생할 수 있습니다.


앱 실행

Makefile을 사용해 빌드 및 QEMU 실행:

CC = /opt/riscv-newlib/bin/riscv64-unknown-elf-gcc
CFLAGS = -march=rv64imac_zicsr -mabi=lp64 -mcmodel=medany -specs=nosys.specs

firmware.elf: main.o uart.o syscalls.o startup.o
    $(CC) $(CFLAGS) -T link.ld -nostartfiles -o $@ $^

run: firmware.elf
    qemu-system-riscv64 -machine virt -nographic -bios $<

실행 결과:

Hello from RISC-V UART!
Type something: foo
You typed: foo

결론

Newlib을 사용하면 베어 메탈 환경에서도 표준 C 라이브러리 기능을 활용할 수 있습니다. UART 드라이버 구현과 시스템 호출 재정의를 통해 printf 및 기타 고수준 함수를 사용할 수 있게 되었습니다. 이 접근법은 다양한 임베디드 프로젝트에 적용 가능하며, 추가 기능 구현을 통해 시스템 확장이 가능합니다.

더 많은 정보를 원하시면 트위터링크드인을 팔로우해 주세요!

태그: 임베디드, RISC-V, 베어메탈, Newlib, C표준라이브러리

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment