[Rust OS] 독립적인 러스트 바이너리

5 minute read

운영체제를 만들기 위해 운영체제 종속성을 제거하는 작업을 한다.

운영체제 커널을 만들 때는 운영체제의 기능을 사용하면 안 된다.

스레드, 파일, 힙 메모리, 네트워크, 난수. 표준 입출력 등등 운영체제의 추상화나 하드웨어를 사용하는 기능을 사용할 수 없다.

그러므로 러스트 표준 라이브러리도 사용 불가

그래도 반복자, 클로저, 패턴 매칭, 옵션, Result, 문자열 포맷팅 그리고 소유권 시스템 같은 기능은 사용해도 된다.

이 기능들을 사용하면 커널 코드를 표현력 있게 작성하고 비정상적인 작동을 막고 메모리 안전성을 보장받을 수 있다.

운영체제의 기능을 사용하지 않는 독립적인 러스트 바이너리를 만드는 법을 설명한다.

표준 라이브러리 기능 끄기

러스트 표준 라이브러리는 운영체제 기능을 사용하기 위해 C 표준 라이브러리인 libc를 사용한다.

main.rs 파일에 #![no_std] 코드 한 줄만 추가하면 표준 라이브러리를 제외하고 컴파일할 수 있다.

패닉 구현하기

표준 라이브러리 제거 시 패닉도 없어져서 패닉을 구현해야 한다.

use core::panic::PanicInfo;

/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

eh_personality 언어 항목

위 코드를 추가하고 실행하면 eh_personality 언어 항목이 없다고 에러가 난다.

eh_personality는 스택 되감기를 위해 사용되는데 운영체제 기능을 사용하므로 없애는 게 낫다.

Cargo.toml에 다음 코드를 추가한다.

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

start 속성

프로그램 시작 시 main 함수가 가장 먼저 실행되는 것은 아니다.

대부분의 언어는 런타임 시스템을 가지고 있어서 이 시스템이 가비지 컬랙팅을 수행하고(e.g. 자바), 소프트웨어 스레드(e.g. Go의 고루틴)를 실행한다.

main 함수 실행 전에 런타임이 실행되어야 한다.

일반적으로 표준 라이브러리를 사용하는 러스트 바이너리 실행 시 crt0 (C runtime zero)라는 C 런타임 라이브러리가 실행돼서 C 프로그램을 위한 환경 설정을 한다.

환경 설정은 스택을 생성하거나 인자들을 알맞은 레지스터에 배치하는 것 등등이다.

C 런타임은 Rust 런타임을 실행하는데, 이 런타임의 시작점이 바로 start 언어 항목이다.

러스트 런타임 매우 작아서, 스택 오버플로우 가드를 설정하거나 패닉 발생 시 백트레이스를 출력하는 등의 작은 일을 수행한다.

그 다음 런타임이 main 함수를 호출한다.

우리가 만들 독립 실행 파일은 러스트 런타임과 crt0을 사용할 수 없으므로, 프로그램 시작점을 직접 정의해야 한다.

start 언어 아이템은 어차피 crt0을 사용해야 하므로 crt0의 시작점을 덮어쓰기로 한다.

시작점 덮어쓰기

러스트 컴파일러에 일반적인 시작점을 안 쓸거라고 #![no_main] 속성을 추가한다.

main는 런타임이 없어서 main 함수는 지우고 운영체제 시작점인 _start 함수를 덮어쓴다.

#[no_mangle]
pub extern "C" fn _start() -> ! {
    loop {}
}

링커 에러

프로그램 실행 시 링커 에러가 발생한다.

링커는 생성된 코드를 실행파일로 합쳐주는 프로그램이다.

링커는 기본 설정에 따라 프로그램이 C 런타임을 사용한다고 가정해서 에러가 발생했다.

링커에게 C 런타임을 안 쓸거라고 알려주기 위해 인자를 넘기거나 베어 메탈 타깃을 만들 수 있다.

베어 메탈 타깃 만들기

러스트는 현재 운영체제(호스트 시스템)에 따라 실행파일을 생성한다.

x86_64 윈도우라면 x86_64 명령어를 사용하는 .exe 실행파일을 만든다.

러스트는 환경 구분을 위해 타깃 트리플을 사용한다.

rustc --version --verbose로 타깃 트리플을 확인할 수 있다.

host: x86_64-unknown-linux-gnu에서 CPU 아키텍처(x86_64), 판매자 (unknown), 운영체제(linux) 그리고 ABI (gnu)인 타깃 트리플을 확인할 수 있다.

러스트 컴파일러와 링커는 현재 운영체제가 리눅스거나 윈도우라면 C 런타임을 기본으로 사용할 거라고 가정한다.

베어 메탈 환경을 위한 트리플 타깃의 예로 임베디드 ARM 시스템을 뜻하는 thumbv7em-none-eabihf가 있다.

rustup target add thumbv7em-none-eabihf로 해당 타깃용 표준 라이브러리를 다운 받는다.

이제부터 cargo build --target thumbv7em-none-eabihf 명령어로 독립 실행파일을 생성할 수 있다.

근데 이거 안 쓰고 x86_64 베어 메탈 환경용 커스텀 타깃 사용할 거임.

링커 인자

링커에 인자를 잘 넘겨서 링커 에러를 해결할 수도 있다.

# Linux
cargo rustc -- -C link-arg=-nostartfiles
# Windows
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
# macOS
cargo rustc -- -C link-args="-e __start -static -nostartfiles"

근데 이렇게 생성된 바이너리는 _stack 함수 실행 시 스택 초기화 등 기대하는 게 많아서 설정이 더 필요하다.

출처

A Freestanding Rust Binary

Categories:

Updated: