이미지 로딩 중...
AI Generated
2025. 11. 13. · 4 Views
Rust로 만드는 나만의 OS 1편 Rust 개발환경 세팅
OS 개발의 첫 걸음, Rust 개발 환경을 완벽하게 구축하는 방법을 알아봅니다. rustup을 통한 툴체인 관리부터 cargo를 활용한 프로젝트 빌드까지, 시스템 프로그래밍에 필요한 모든 것을 단계별로 설명합니다.
목차
1. rustup 설치
시작하며
여러분이 Rust로 OS를 개발하려고 할 때, 가장 먼저 막히는 부분이 무엇인가요? 바로 개발 환경 구축입니다.
일반 애플리케이션 개발과 달리 OS 개발은 여러 타겟 아키텍처를 지원해야 하고, 때로는 nightly 버전의 실험적 기능이 필요합니다. 이런 복잡한 요구사항을 수동으로 관리하면 엄청난 시간이 소모됩니다.
컴파일러 버전을 바꾸거나, 다른 타겟을 추가하거나, 여러 프로젝트마다 다른 Rust 버전을 사용해야 할 때마다 골치가 아픕니다. 바로 이럴 때 필요한 것이 rustup입니다.
rustup은 Rust의 공식 툴체인 관리자로, 이 모든 복잡한 작업을 명령어 하나로 해결해줍니다.
개요
간단히 말해서, rustup은 Rust 컴파일러와 관련 도구들을 설치하고 관리하는 도구입니다. OS 개발에서는 특히 중요합니다.
왜냐하면 bare-metal 환경을 위한 크로스 컴파일, nightly 기능 사용, 다양한 타겟 아키텍처 지원 등이 필수적이기 때문입니다. 예를 들어, x86_64 아키텍처용 OS를 개발하면서 동시에 ARM용 포팅도 진행해야 하는 경우에 매우 유용합니다.
기존에는 각 타겟마다 별도의 컴파일러를 다운로드하고 PATH를 수동으로 설정해야 했다면, 이제는 rustup 명령어 하나로 모든 것을 관리할 수 있습니다. rustup의 핵심 특징은 첫째, 여러 Rust 버전을 동시에 관리할 수 있고, 둘째, 프로젝트별로 다른 툴체인을 자동으로 전환할 수 있으며, 셋째, 크로스 컴파일 타겟을 쉽게 추가할 수 있다는 점입니다.
이러한 특징들이 OS 개발처럼 복잡한 환경에서 개발 생산성을 크게 향상시킵니다.
코드 예제
# Linux/macOS에서 rustup 설치
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 설치 후 PATH 환경변수 적용
source $HOME/.cargo/env
# 설치 확인
rustup --version
rustc --version
cargo --version
# 기본 툴체인 설정
rustup default stable
설명
이것이 하는 일: rustup은 공식 설치 스크립트를 통해 Rust 개발 환경 전체를 자동으로 구축합니다. 첫 번째로, curl 명령어를 통해 HTTPS로 안전하게 설치 스크립트를 다운로드하고 실행합니다.
이 과정에서 rustup 자체와 함께 rustc(컴파일러), cargo(빌드 도구), rust-std(표준 라이브러리)가 함께 설치됩니다. --tlsv1.2 옵션은 보안을 위해 최신 TLS 프로토콜만 사용하도록 보장합니다.
그 다음으로, source 명령어로 환경변수를 즉시 적용합니다. 이렇게 하지 않으면 터미널을 재시작해야만 rustup을 사용할 수 있습니다.
$HOME/.cargo/env 파일에는 cargo와 rustup의 실행 경로가 PATH에 추가되는 내용이 담겨 있습니다. 마지막으로, 각 도구의 버전을 확인하여 정상적으로 설치되었는지 검증합니다.
rustup default stable 명령어는 안정화 버전을 기본 툴체인으로 설정하는데, OS 개발에서는 나중에 nightly로 변경하게 될 것입니다. 여러분이 이 명령어들을 실행하면 완전한 Rust 개발 환경이 자동으로 구축되고, 언제든지 다른 버전으로 전환하거나 새로운 타겟을 추가할 수 있는 유연한 시스템을 갖추게 됩니다.
특히 OS 개발처럼 실험적 기능이 필요한 프로젝트에서는 이러한 유연성이 필수적입니다.
실전 팁
💡 Windows 사용자는 https://rustup.rs 에서 rustup-init.exe를 다운로드하여 실행하세요. WSL2를 사용한다면 Linux 방식으로 설치하는 것을 추천합니다.
💡 설치 중 "Customize installation" 옵션에서 기본 툴체인을 nightly로 선택할 수 있지만, 처음에는 stable로 설치 후 필요시 전환하는 것이 안전합니다.
💡 기업 방화벽 환경에서는 프록시 설정이 필요할 수 있습니다. HTTPS_PROXY 환경변수를 설정하거나 rustup의 --proxy 옵션을 사용하세요.
💡 설치 후 ~/.cargo/bin 디렉토리가 PATH에 자동으로 추가되는지 확인하세요. 추가되지 않았다면 shell 설정 파일(.bashrc, .zshrc 등)에 수동으로 추가해야 합니다.
💡 rustup은 자동 업데이트를 지원하므로 주기적으로 rustup update를 실행하여 최신 버전을 유지하세요. 특히 nightly를 사용하는 경우 버그 픽스가 자주 배포됩니다.
2. Rust 버전 관리
시작하며
여러분이 OS 개발 프로젝트를 진행하다 보면 이런 딜레마에 빠집니다. "inline assembly를 사용하려면 nightly가 필요한데, 안정성이 걱정된다." 또는 "새로운 기능을 테스트하고 싶은데 프로덕션 코드에 영향을 줄까 봐 두렵다." 이런 문제는 실제로 많은 시스템 프로그래머들이 겪는 고민입니다.
한 프로젝트에서는 최신 실험적 기능이 필요하고, 다른 프로젝트에서는 검증된 안정 버전이 필요한 상황이 동시에 발생하죠. 바로 이럴 때 필요한 것이 Rust의 릴리스 채널 시스템입니다.
stable, beta, nightly 세 가지 채널을 자유롭게 전환하면서 안정성과 최신 기능 사이의 균형을 맞출 수 있습니다.
개요
간단히 말해서, Rust는 세 가지 릴리스 채널(stable, beta, nightly)을 제공하며, 각각 다른 목적과 안정성 수준을 가지고 있습니다. OS 개발에서는 nightly 채널이 거의 필수입니다.
왜냐하면 asm!, global_asm!, naked 함수, allocator API 등 저수준 시스템 프로그래밍에 필요한 기능들이 아직 nightly에만 있기 때문입니다. 예를 들어, 부트로더를 작성하거나 인터럽트 핸들러를 구현할 때 이러한 기능들이 반드시 필요합니다.
기존에는 한 시스템에 하나의 Rust 버전만 설치할 수 있었다면, 이제는 여러 채널을 동시에 설치하고 프로젝트별로 자동 전환할 수 있습니다. Rust 릴리스 채널의 핵심 특징은 첫째, stable은 6주마다 릴리스되는 안정 버전이고, 둘째, beta는 다음 stable이 될 버전으로 최종 테스트 중이며, 셋째, nightly는 매일 빌드되는 최신 버전으로 실험적 기능을 포함합니다.
이러한 구조가 혁신과 안정성을 동시에 보장합니다.
코드 예제
# nightly 툴체인 설치
rustup install nightly
# 특정 날짜의 nightly 설치 (재현 가능한 빌드를 위해)
rustup install nightly-2024-01-15
# 프로젝트 디렉토리에서 nightly를 기본으로 설정
rustup override set nightly
# 전역 기본 툴체인 변경
rustup default nightly
# 현재 활성화된 툴체인 확인
rustup show
# 특정 명령어만 nightly로 실행
cargo +nightly build
설명
이것이 하는 일: rustup의 툴체인 관리 기능을 통해 여러 Rust 버전을 동시에 사용하고 프로젝트별로 자동 전환할 수 있습니다. 첫 번째로, rustup install nightly는 최신 nightly 버전을 다운로드하고 설치합니다.
이 과정에서 컴파일러뿐만 아니라 해당 버전의 표준 라이브러리와 cargo도 함께 설치됩니다. nightly-2024-01-15처럼 특정 날짜를 지정하면 해당 시점의 스냅샷을 설치할 수 있어서, 팀원들이 모두 동일한 빌드 환경을 사용할 수 있습니다.
그 다음으로, rustup override set nightly를 프로젝트 디렉토리에서 실행하면 rust-toolchain.toml 파일이 생성됩니다. 이 파일이 있으면 해당 디렉토리에서는 자동으로 nightly가 활성화되므로, 매번 +nightly를 붙일 필요가 없습니다.
이는 CI/CD 환경에서도 동일하게 작동합니다. 마지막으로, cargo +nightly build 같은 명령어를 사용하면 override 설정 없이도 일회성으로 특정 툴체인을 사용할 수 있습니다.
이는 여러 버전에서 동작을 비교 테스트할 때 매우 유용합니다. 여러분이 이러한 버전 관리 방식을 활용하면 메인 프로젝트는 nightly로 개발하면서, 의존성 라이브러리는 stable로 테스트하는 등 유연한 개발 워크플로우를 구축할 수 있습니다.
특히 OS 개발에서는 실험적 기능을 사용하면서도 코드베이스의 다른 부분은 안정적으로 유지할 수 있어 리스크를 최소화할 수 있습니다.
실전 팁
💡 nightly를 사용할 때는 rust-toolchain.toml 파일을 git에 커밋하여 팀원 모두가 동일한 버전을 사용하도록 하세요. 파일 내용: [toolchain] channel = "nightly-2024-01-15"
💡 nightly는 매일 업데이트되므로 어제 컴파일되던 코드가 오늘 안 될 수 있습니다. 안정적인 개발을 위해 특정 날짜의 nightly를 고정하는 것을 추천합니다.
💡 rustup component list --installed로 현재 툴체인에 설치된 컴포넌트를 확인하세요. rust-src, llvm-tools-preview 등은 OS 개발에 필수적입니다.
💡 CI/CD 파이프라인에서는 rustup update를 자동으로 실행하지 마세요. 예상치 못한 빌드 실패가 발생할 수 있으므로 수동으로 버전을 관리하는 것이 안전합니다.
💡 stable 버전에서 작동하는 코드를 작성한 후 nightly 전용 기능을 추가하면, 나중에 해당 기능이 stable로 올라왔을 때 마이그레이션이 쉬워집니다.
3. cargo 기본 사용법
시작하며
여러분이 OS 프로젝트를 시작하려고 할 때, "어떻게 프로젝트 구조를 만들어야 하지?", "빌드 설정은 어떻게 하지?", "의존성 관리는?" 같은 질문들이 머릿속을 가득 채웁니다. 이런 고민들은 모든 새로운 프로젝트의 시작점입니다.
C/C++에서는 Makefile을 작성하고, 헤더 파일 경로를 설정하고, 링커 옵션을 일일이 지정해야 했죠. 이 과정에서 많은 시간이 낭비되고 설정 오류가 빈번하게 발생합니다.
바로 이럴 때 필요한 것이 cargo입니다. cargo는 프로젝트 생성부터 빌드, 테스트, 문서화, 배포까지 Rust 개발의 모든 과정을 자동화해주는 공식 빌드 도구입니다.
개요
간단히 말해서, cargo는 Rust의 패키지 매니저이자 빌드 시스템으로, 프로젝트 전체 라이프사이클을 관리합니다. OS 개발에서 cargo가 중요한 이유는 복잡한 빌드 설정을 선언적으로 관리할 수 있기 때문입니다.
커스텀 타겟 스펙, 링커 스크립트, 빌드 스크립트 등을 체계적으로 구성할 수 있습니다. 예를 들어, bare-metal 환경을 위한 크로스 컴파일 설정도 Cargo.toml 파일 하나로 관리할 수 있습니다.
기존에는 복잡한 빌드 시스템을 수동으로 구축해야 했다면, 이제는 cargo new 명령어 하나로 표준화된 프로젝트 구조를 즉시 생성할 수 있습니다. cargo의 핵심 특징은 첫째, 의존성을 자동으로 다운로드하고 버전을 관리하며, 둘째, 증분 컴파일을 통해 빌드 속도를 최적화하고, 셋째, 테스트와 벤치마크를 통합 지원합니다.
이러한 기능들이 개발자가 비즈니스 로직에만 집중할 수 있게 해줍니다.
코드 예제
# 새 바이너리 프로젝트 생성
cargo new my_os --bin
# 프로젝트 디렉토리로 이동
cd my_os
# 디버그 빌드 (개발용)
cargo build
# 릴리스 빌드 (최적화)
cargo build --release
# 빌드하고 실행
cargo run
# 테스트 실행
cargo test
# 프로젝트 정리
cargo clean
설명
이것이 하는 일: cargo는 프로젝트 템플릿을 생성하고, 소스 코드를 컴파일하며, 실행 파일을 만드는 전체 과정을 자동화합니다. 첫 번째로, cargo new my_os --bin은 표준 프로젝트 구조를 생성합니다.
src/main.rs 파일, Cargo.toml 설정 파일, .gitignore 등이 자동으로 만들어지며, 이미 "Hello, world!" 프로그램이 작성되어 있어 즉시 빌드할 수 있습니다. --bin 플래그는 실행 가능한 바이너리를 만든다는 의미입니다(라이브러리는 --lib).
그 다음으로, cargo build 명령어는 소스 코드를 분석하고, 의존성을 다운로드하고, 컴파일을 수행합니다. 기본적으로 디버그 모드로 빌드되어 target/debug 디렉토리에 실행 파일이 생성됩니다.
디버그 빌드는 컴파일 속도가 빠르고 디버깅 정보를 포함하지만, 실행 속도는 느립니다. cargo build --release는 최적화 컴파일을 수행하여 target/release에 실행 파일을 생성합니다.
OS 개발에서 최종 커널 이미지를 만들 때는 항상 릴리스 모드를 사용해야 합니다. 최적화 레벨은 Cargo.toml의 [profile.release] 섹션에서 세밀하게 조정할 수 있습니다.
여러분이 cargo를 활용하면 복잡한 빌드 설정을 일일이 기억할 필요 없이, 간단한 명령어로 일관된 빌드 환경을 유지할 수 있습니다. 특히 팀 협업 시 모든 개발자가 동일한 빌드 프로세스를 사용하게 되어 "내 컴퓨터에서는 되는데" 같은 문제를 예방할 수 있습니다.
실전 팁
💡 cargo build -vv (verbose verbose)를 사용하면 컴파일 과정의 모든 세부사항을 볼 수 있습니다. 링커 오류나 빌드 스크립트 문제를 디버깅할 때 유용합니다.
💡 cargo check는 컴파일 없이 코드 검사만 수행하므로 매우 빠릅니다. 개발 중 빠른 피드백을 받고 싶을 때 cargo build 대신 사용하세요.
💡 RUST_BACKTRACE=1 cargo run을 실행하면 패닉 발생 시 전체 스택 트레이스를 볼 수 있습니다. 디버깅에 필수적인 정보를 제공합니다.
💡 cargo build --target 옵션으로 크로스 컴파일할 수 있습니다. OS 개발에서는 커스텀 타겟 스펙(JSON 파일)을 사용하게 됩니다.
💡 cargo tree 명령어로 의존성 트리를 시각화할 수 있습니다. 중복된 의존성이나 버전 충돌을 파악하는 데 도움이 됩니다.
4. 크로스 컴파일 타겟 추가
시작하며
여러분이 x86_64 아키텍처용 OS를 개발하려고 할 때, 이런 문제에 직면합니다. "내 개발 환경은 Linux인데, 어떻게 bare-metal x86_64를 위한 코드를 컴파일하지?" 또는 "표준 라이브러리 없이 어떻게 빌드하지?" 이런 상황은 시스템 프로그래밍의 핵심 도전 과제입니다.
일반적인 Rust 프로그램은 호스트 OS의 표준 라이브러리에 의존하지만, OS 커널은 그 어떤 것에도 의존할 수 없습니다. 순수하게 하드웨어 위에서 직접 실행되어야 하죠.
바로 이럴 때 필요한 것이 크로스 컴파일 타겟입니다. rustup을 통해 bare-metal 타겟을 추가하면, 호스트 환경과 완전히 독립적인 코드를 생성할 수 있습니다.
개요
간단히 말해서, 크로스 컴파일 타겟은 현재 실행 중인 플랫폼과 다른 플랫폼을 위한 코드를 생성할 수 있게 해주는 컴파일러 설정입니다. OS 개발에서 가장 중요한 타겟은 x86_64-unknown-none입니다.
이는 x86_64 아키텍처, OS 없음(unknown), 표준 라이브러리 없음(none)을 의미합니다. 예를 들어, 부트로더나 커널을 컴파일할 때 이 타겟을 사용하면 OS에 의존하지 않는 순수한 기계어 코드가 생성됩니다.
기존에는 GCC의 크로스 컴파일 툴체인을 수동으로 빌드해야 했다면, 이제는 rustup target add 명령어 하나로 필요한 모든 것이 설치됩니다. 크로스 컴파일의 핵심 특징은 첫째, 호스트 환경에서 타겟 환경을 위한 코드를 생성할 수 있고, 둘째, 타겟별 최적화와 ABI를 자동으로 적용하며, 셋째, 여러 타겟을 동시에 관리할 수 있다는 점입니다.
이러한 능력이 멀티플랫폼 OS 개발을 가능하게 합니다.
코드 예제
# OS 개발용 bare-metal 타겟 추가
rustup target add x86_64-unknown-none
# ARM 아키텍처용 타겟
rustup target add aarch64-unknown-none
# RISC-V 아키텍처용 타겟
rustup target add riscv64gc-unknown-none-elf
# 설치된 타겟 목록 확인
rustup target list --installed
# 특정 타겟으로 빌드
cargo build --target x86_64-unknown-none
# rust-src 컴포넌트 추가 (core 라이브러리 소스)
rustup component add rust-src
설명
이것이 하는 일: rustup의 타겟 관리 시스템을 통해 다양한 아키텍처와 환경을 위한 컴파일 능력을 추가합니다. 첫 번째로, rustup target add x86_64-unknown-none은 해당 타겟을 위한 pre-compiled 표준 라이브러리(정확히는 core와 compiler_builtins)를 다운로드합니다.
"unknown"은 OS가 없다는 의미이고, "none"은 C 런타임 라이브러리가 없다는 의미입니다. 이는 우리가 만들 OS가 모든 것을 직접 제공해야 함을 의미합니다.
그 다음으로, 여러 아키텍처 타겟을 추가하여 멀티플랫폼 지원을 준비할 수 있습니다. aarch64는 64비트 ARM, riscv64gc는 RISC-V 64비트입니다.
각 타겟은 서로 다른 명령어 세트와 ABI를 가지므로, 동일한 Rust 코드라도 완전히 다른 기계어가 생성됩니다. rustup component add rust-src는 매우 중요합니다.
이는 Rust 표준 라이브러리의 소스 코드를 설치하는데, 나중에 커스텀 타겟을 사용할 때 cargo가 core 라이브러리를 타겟에 맞게 재컴파일할 수 있게 해줍니다. OS 개발에서는 거의 필수입니다.
여러분이 이러한 타겟들을 설치하면 단일 코드베이스에서 여러 아키텍처를 지원하는 OS를 개발할 수 있습니다. --target 플래그만 바꿔서 빌드하면 각 아키텍처에 최적화된 바이너리가 자동으로 생성됩니다.
Linux 커널이 여러 아키텍처를 지원하는 것과 비슷한 방식이지만, Rust의 타입 시스템 덕분에 훨씬 안전합니다.
실전 팁
💡 rustup target list로 사용 가능한 모든 타겟을 볼 수 있습니다. *-none이나 *-elf로 끝나는 것들이 bare-metal 타겟입니다.
💡 커스텀 타겟이 필요하면 JSON 파일로 타겟 스펙을 정의할 수 있습니다. rustc --print target-spec-json로 기존 타겟의 스펙을 출력하여 참고하세요.
💡 cargo build --target을 매번 입력하기 귀찮다면 .cargo/config.toml에 [build] target = "x86_64-unknown-none"을 추가하세요.
💡 llvm-tools-preview 컴포넌트를 설치하면 objdump, objcopy 같은 바이너리 분석 도구를 사용할 수 있습니다. rustup component add llvm-tools-preview
💡 타겟별로 다른 최적화 레벨을 적용하려면 Cargo.toml의 [profile.release] 섹션에서 설정하거나, 타겟별 설정 파일을 사용하세요.
5. cargo.toml 설정
시작하며
여러분이 OS 프로젝트의 빌드 설정을 구성할 때, "panic 핸들러는 어떻게 설정하지?", "링커는 어떻게 지정하지?", "최적화 옵션은?" 같은 수많은 결정을 내려야 합니다. 이런 설정들은 단순한 빌드 옵션이 아닙니다.
잘못 설정하면 커널이 부팅 중에 크래시하거나, 예상치 못한 동작을 할 수 있습니다. C 프로젝트에서 Makefile을 작성할 때 겪었던 복잡함을 기억하시나요?
바로 이럴 때 필요한 것이 Cargo.toml의 체계적인 설정 시스템입니다. 선언적 방식으로 프로젝트의 모든 측면을 명확하게 정의할 수 있습니다.
개요
간단히 말해서, Cargo.toml은 Rust 프로젝트의 메타데이터, 의존성, 빌드 설정을 담고 있는 중앙 설정 파일입니다. OS 개발에서 Cargo.toml의 역할은 특히 중요합니다.
panic 전략, 최적화 레벨, LTO(Link Time Optimization), 코드 모델 등을 설정해야 합니다. 예를 들어, panic이 발생했을 때 스택 언와인딩을 할 수 없으므로 abort 전략을 사용해야 하고, 코드 크기를 최소화하기 위해 특별한 최적화 설정이 필요합니다.
기존에는 컴파일러 플래그를 빌드 스크립트에 나열해야 했다면, 이제는 구조화된 TOML 형식으로 읽기 쉽고 유지보수하기 쉬운 설정을 작성할 수 있습니다. Cargo.toml의 핵심 특징은 첫째, 프로필별로 다른 빌드 설정을 적용할 수 있고, 둘째, 의존성의 버전과 기능을 세밀하게 제어할 수 있으며, 셋째, 빌드 스크립트와의 통합을 지원한다는 점입니다.
이러한 기능들이 복잡한 OS 프로젝트를 체계적으로 관리할 수 있게 합니다.
코드 예제
[package]
name = "my_os"
version = "0.1.0"
edition = "2021"
[dependencies]
# bare-metal 환경에서는 대부분의 의존성을 사용할 수 없음
[profile.dev]
panic = "abort" # 스택 언와인딩 비활성화
[profile.release]
panic = "abort"
lto = true # Link Time Optimization 활성화
opt-level = "z" # 코드 크기 최적화
설명
이것이 하는 일: Cargo.toml은 컴파일러와 링커에게 OS 개발에 적합한 방식으로 코드를 생성하도록 지시합니다. 첫 번째로, [package] 섹션은 프로젝트의 기본 정보를 정의합니다.
edition = "2021"은 Rust 2021 에디션을 사용한다는 의미로, 최신 언어 기능과 개선사항을 활용할 수 있습니다. 버전 관리는 semantic versioning을 따르며, 이는 나중에 crates.io에 배포할 때 중요합니다.
그 다음으로, [profile.dev]와 [profile.release] 섹션에서 panic = "abort"를 설정합니다. 기본 panic 전략은 "unwind"인데, 이는 스택을 역순으로 풀면서 각 프레임의 소멸자를 실행합니다.
하지만 OS 커널에서는 이런 복잡한 런타임 지원이 없으므로, 즉시 중단(abort)하도록 설정합니다. lto = true는 Link Time Optimization을 활성화합니다.
이는 전체 프로그램 최적화를 수행하여 함수 인라이닝, 죽은 코드 제거 등을 더 공격적으로 수행합니다. 빌드 시간이 늘어나지만 최종 바이너리가 작아지고 빠라집니다.
opt-level = "z"는 실행 속도보다 코드 크기를 우선시하는 최적화로, 부트로더처럼 크기 제약이 있는 경우 유용합니다. 여러분이 이런 설정을 제대로 하면 컴파일러가 bare-metal 환경에 적합한 코드를 생성합니다.
특히 panic = "abort" 설정은 필수인데, 이것 없이는 링커가 언와인딩 관련 심볼을 찾지 못해 빌드가 실패합니다. 또한 LTO와 크기 최적화를 통해 부트로더의 512바이트 제약이나 초기 커널의 크기 제한을 만족시킬 수 있습니다.
실전 팁
💡 opt-level은 0(최적화 없음), 1, 2, 3(속도 최적화), "s"(약간의 크기 최적화), "z"(최대 크기 최적화) 중 선택할 수 있습니다.
💡 codegen-units = 1을 설정하면 병렬 컴파일을 포기하는 대신 더 나은 최적화를 얻을 수 있습니다. 릴리스 빌드에 권장됩니다.
💡 [dependencies]에 default-features = false를 설정하면 의존성의 std 기능을 비활성화할 수 있습니다. no_std 환경에서 필수입니다.
💡 [profile.release]에 strip = true를 추가하면 디버그 심볼을 제거하여 바이너리 크기를 더 줄일 수 있습니다.
💡 overflow-checks = true를 dev 프로필에 추가하면 정수 오버플로우를 런타임에 검사합니다. 개발 중 버그를 조기에 발견하는 데 도움이 됩니다.
6. no_std 환경 이해
시작하며
여러분이 첫 OS 코드를 작성하려고 main.rs를 열었을 때, 즉시 이런 의문이 듭니다. "println!도 사용할 수 없다고?", "Vec나 String은?", "파일 I/O는 어떻게 하지?" 이런 충격은 모든 OS 개발자가 겪는 통과의례입니다.
일반 Rust 프로그램에서 당연하게 사용하던 표준 라이브러리의 거의 모든 것이 OS 위에서 실행되는 것을 전제로 만들어졌습니다. 하지만 여러분은 지금 그 OS를 만들고 있습니다!
바로 이럴 때 필요한 것이 no_std 모드입니다. 표준 라이브러리 없이 core 라이브러리만으로 프로그래밍하는 방법을 배워야 합니다.
개요
간단히 말해서, no_std는 Rust 표준 라이브러리(std) 없이 프로그래밍하는 모드로, OS나 임베디드 시스템 개발에 사용됩니다. OS 개발에서 no_std는 필수입니다.
왜냐하면 std는 힙 할당, 파일 시스템, 네트워킹, 스레딩 같은 OS 서비스에 의존하기 때문입니다. 예를 들어, std::vec::Vec은 힙 메모리를 할당하는데, 여러분이 만들 OS 커널에는 아직 메모리 할당자가 구현되지 않았을 수 있습니다.
기존에는 C로 OS를 개발할 때 표준 라이브러리를 사용하지 않는 것이 일반적이었다면, Rust에서는 명시적으로 #![no_std]를 선언하여 core 라이브러리만 사용하겠다고 표시합니다. no_std의 핵심 특징은 첫째, core 라이브러리는 사용할 수 있어 기본 타입과 트레이트가 제공되고, 둘째, alloc 라이브러리를 추가하면 메모리 할당자를 직접 구현한 후 힙 자료구조를 사용할 수 있으며, 셋째, OS 의존적인 모든 기능을 직접 구현해야 한다는 점입니다.
이러한 제약이 오히려 완전한 제어권을 제공합니다.
코드 예제
#![no_std] // 표준 라이브러리 비활성화
#![no_main] // Rust의 기본 main 진입점 비활성화
use core::panic::PanicInfo;
// panic 핸들러 직접 구현 (필수)
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {} // 무한 루프로 중단
}
// 커스텀 진입점
#[no_mangle] // 이름 맹글링 방지
pub extern "C" fn _start() -> ! {
loop {}
}
설명
이것이 하는 일: no_std 속성들은 Rust 런타임의 OS 의존성을 제거하고, 개발자가 모든 것을 직접 제어할 수 있게 합니다. 첫 번째로, #![no_std]는 컴파일러에게 std 대신 core를 사용하도록 지시합니다.
core는 std의 부분집합으로 OS 없이 작동하는 기본 기능만 포함합니다. Option, Result, Iterator, 기본 타입(i32, bool 등), 트레이트(Clone, Copy 등)는 모두 core에 있으므로 사용할 수 있습니다.
그 다음으로, #![no_main]은 Rust의 기본 런타임 초기화를 비활성화합니다. 일반 Rust 프로그램은 main 함수 전에 여러 초기화 작업(커맨드라인 인자 파싱, 환경변수 설정 등)을 수행하는데, 이는 모두 OS 서비스를 필요로 합니다.
#[no_mangle] extern "C" fn _start()는 링커가 찾을 수 있는 표준 진입점을 제공합니다. #[panic_handler]는 매우 중요합니다.
panic이 발생했을 때 호출되는 함수를 정의해야 하는데, 반환 타입이 !인 것에 주목하세요. 이는 "never type"으로, 이 함수가 절대 반환하지 않음을 의미합니다.
간단한 구현은 무한 루프지만, 실제로는 에러 메시지를 화면에 출력하거나 시스템을 리셋하는 코드를 작성하게 됩니다. 여러분이 이 코드를 컴파일하면 어떤 OS 서비스에도 의존하지 않는 순수한 바이너리가 생성됩니다.
이는 부트로더나 BIOS/UEFI에서 직접 로드하여 실행할 수 있습니다. 처음에는 제약이 크게 느껴지겠지만, 이것이 바로 완전한 제어권을 가진다는 의미입니다.
모든 메모리 접근, 모든 I/O 작업을 여러분이 직접 설계할 수 있습니다.
실전 팁
💡 core::fmt::Write 트레이트를 구현하면 println!과 유사한 포맷팅 기능을 직접 만들 수 있습니다. VGA 버퍼나 시리얼 포트에 출력하는 데 사용됩니다.
💡 alloc 크레이트를 사용하려면 GlobalAlloc 트레이트를 구현한 메모리 할당자를 제공해야 합니다. 간단한 범프 할당자(bump allocator)부터 시작하세요.
💡 compiler_builtins는 자동으로 링크되어 memcpy, memset 같은 저수준 함수를 제공합니다. 직접 구현할 필요가 없습니다.
💡 no_std에서도 사용 가능한 크레이트가 많습니다. Cargo.toml에서 default-features = false로 설정하면 std 의존성을 제거할 수 있습니다.
💡 cfg(test) 속성을 사용하면 테스트 환경에서는 std를 사용하고 실제 빌드에서는 no_std를 사용하도록 조건부 컴파일할 수 있습니다.
7. rustc 컴파일 옵션
시작하며
여러분이 OS 커널을 빌드할 때, "왜 링커 에러가 발생하지?", "코드 모델은 무엇을 선택해야 하지?", "PIC는 활성화해야 하나?" 같은 저수준 질문들과 마주하게 됩니다. 이런 문제들은 일반 애플리케이션 개발에서는 거의 신경 쓸 일이 없지만, OS 개발에서는 핵심입니다.
잘못된 코드 모델을 선택하면 커널이 잘못된 메모리 주소에 로드되거나, 스택 레드존 때문에 인터럽트 처리 중 데이터가 손상될 수 있습니다. 바로 이럴 때 필요한 것이 rustc의 세밀한 컴파일 옵션들입니다.
.cargo/config.toml을 통해 타겟별 컴파일러 플래그를 정확하게 제어할 수 있습니다.
개요
간단히 말해서, rustc 컴파일 옵션은 코드 생성, 최적화, 링킹 등 컴파일 과정의 세부 사항을 제어하는 저수준 설정입니다. OS 개발에서 특히 중요한 옵션들이 있습니다.
disable-redzone은 스택 레드존을 비활성화하여 인터럽트 안전성을 보장하고, code-model은 코드가 메모리의 어디에 로드될지 결정하며, linker는 커스텀 링커 스크립트를 사용할 수 있게 합니다. 예를 들어, 커널이 상위 메모리(0xFFFFFFFF80000000 이상)에 로드되려면 kernel 코드 모델이 필요합니다.
기존에는 이런 옵션들을 매번 명령행에 입력해야 했다면, 이제는 .cargo/config.toml에 한 번 정의하면 프로젝트 전체에 자동으로 적용됩니다. rustc 옵션의 핵심 특징은 첫째, LLVM 백엔드의 강력한 기능에 접근할 수 있고, 둘째, 타겟별로 다른 설정을 적용할 수 있으며, 셋째, 링커와 어셈블러까지 완전히 제어할 수 있다는 점입니다.
이러한 제어력이 bare-metal 프로그래밍을 가능하게 합니다.
코드 예제
# .cargo/config.toml
[build]
target = "x86_64-unknown-none"
[target.x86_64-unknown-none]
rustflags = [
"-C", "code-model=kernel", # 상위 메모리 주소 사용
"-C", "link-arg=-nostartfiles", # 스타트업 파일 비활성화
"-C", "link-arg=-T", # 링커 스크립트 지정
"-C", "link-arg=linker.ld",
]
[unstable]
build-std = ["core", "compiler_builtins"] # core를 재컴파일
설명
이것이 하는 일: .cargo/config.toml은 프로젝트별 컴파일러 설정을 정의하여 모든 빌드에 일관되게 적용합니다. 첫 번째로, [build] target 설정은 기본 빌드 타겟을 지정하여 매번 --target을 입력할 필요를 없앱니다.
[target.x86_64-unknown-none] 섹션에서 해당 타겟을 위한 특수 설정을 정의할 수 있습니다. 그 다음으로, rustflags 배열에 컴파일러 플래그를 나열합니다.
-C code-model=kernel은 LLVM에게 코드가 상위 2GB 주소 공간에 위치할 것이라고 알려줍니다. 이는 64비트 OS 커널의 전형적인 메모리 레이아웃입니다.
기본 small 코드 모델은 하위 2GB만 가정하므로 커널에 부적합합니다. -C link-arg= 옵션들은 링커에 직접 전달되는 인자입니다.
-nostartfiles는 C 런타임 스타트업 코드(crt0.o 등)를 링크하지 않도록 하고, -T linker.ld는 커스텀 링커 스크립트를 사용하도록 지정합니다. 링커 스크립트는 각 섹션(.text, .data, .bss 등)이 메모리 어디에 배치될지 정확하게 제어합니다.
[unstable] build-std는 nightly 전용 기능으로, core 라이브러리를 타겟에 맞게 재컴파일합니다. 이는 커스텀 타겟 스펙을 사용할 때 필수입니다.
pre-compiled core가 없는 타겟의 경우 소스에서 직접 빌드해야 하기 때문입니다. 여러분이 이런 설정을 제대로 구성하면 cargo build 명령어 하나로 모든 복잡한 옵션이 자동으로 적용됩니다.
팀원들도 동일한 빌드 환경을 갖게 되고, CI/CD에서도 일관된 빌드가 보장됩니다. 특히 링커 스크립트를 통해 커널의 메모리 레이아웃을 정밀하게 제어할 수 있어, 부트로더가 커널을 올바른 주소에 로드하고 실행할 수 있게 됩니다.
실전 팁
💡 -C target-feature=+soft-float를 추가하면 하드웨어 부동소수점 연산을 비활성화합니다. 커널이 FPU 상태를 저장/복원하기 전에는 필수입니다.
💡 -Z emit-stack-sizes (nightly)를 사용하면 각 함수의 스택 사용량을 분석할 수 있습니다. 스택 오버플로우 디버깅에 유용합니다.
💡 링커 스크립트에서 PROVIDE 지시어로 심볼을 정의하면 Rust 코드에서 extern "C"로 참조할 수 있습니다. _kernel_start, _kernel_end 같은 경계 심볼을 만드는 데 사용됩니다.
💡 -C relocation-model=static을 설정하면 위치 독립 코드(PIC)를 비활성화합니다. 커널은 고정된 주소에 로드되므로 PIC가 불필요합니다.
💡 RUSTFLAGS 환경변수로 일시적으로 플래그를 추가할 수 있습니다. 실험적 옵션을 테스트할 때 config.toml을 수정하지 않고 시도할 수 있습니다.