And Brain said,

OS with Rust, OS 개발기 - 1. 운영체제를 넘어서(Beyond OS) 본문

IT/Rust

OS with Rust, OS 개발기 - 1. 운영체제를 넘어서(Beyond OS)

The Man 2023. 6. 4. 13:13
반응형

반응형
OS with Rust

1. 운영체제를 넘어서(Beyond OS)
2. 운영체제로 거듭나다(Reborn OS)

 

들어가기에 앞서,


무대 뒤의 복잡한 기계장치 없이 대형 공연을 진행하려 한다면 어떻게 될까요? 공연에 필요한 모든 요소, 무대 설정, 소품, 조명, 사운드 등을 직접 만들어내야 합니다. 운영체제 커널을 만드는 것은 이와 비슷하겠지요. 우리는 기존 운영체제에 종속되지 않는, 운영체제를 넘어서, 저희만의 운영체제를 만들 것입니다. 이제부터 여러분들은 저와 함께 운영체제의 많은 기능들, 스레드, 파일, 힙 메모리, 네트워크, 난수, 표준 출력 등에 의존하지 않고 모든 것을 직접 구성하게 될 것입니다. 재밌을 것 같지 않나요? 설레지 않나요?


두려워마세요! 우리에게는 Rust라는 아주 든든한 친구가 있습니다. 물론, 운영체제에 종속되선 안되기에 우리는 Rust의 표준 라이브러리의 대부분을 사용할 수 없지만, Rust가 제공하는 기능들은 여전히 우리에게 많은 도움을 줄 것입니다. 반복자, 클로저, 패턴 매칭, 옵션과 결과, 문자열 포매팅, 그리고 소유권 시스템 등 Rust의 기능들은 우리가 정의하지 않은 동작이나 메모리 안전성 문제를 걱정하지 않고도, 창의적이고 고수준의 커널을 만들 수 있도록 도와줄 것입니다.

 

자 그럼, 우리만의 새로운 무대를 만들어 볼까요? 운영체제 개발기, 바로 시작합시다.

 

 

Free Standing


새로운 운영체제를 만들기 위해서는 기존 운영체제에 종속되어선 안 됩니다. 그러려면 먼저, 기존 운영체제 없이도 실행할 수 있는 프리스탠딩(Free Standing) 혹은 베어메탈(Bare-metal)이라 불리는 실행 파일이 필요합니다. 이런 프리스탠딩 실행 파일을 만들기 위해선 Rust의 표준 라이브러리의 링크를 해제해야 합니다(!).

그럼, 새로운 cargo 크레이트를 만들어볼까요?

cargo new new_stage --bin

 

그리고, main.rs 파일을 수정해 줍니다.

// main.rs

#![no_std]

fn main() {
    println!("The curtain rises");
}

 

#![no_std]를 통해 표준 라이브러리의 링크를 해제합니다.

 

 

이제 cargo build 명령어만 쳐도 에러가 나오는 것을 확인하실 수 있을 겁니다.

 --> src/main.rs:4:5
  |
4 |     println!("The curtain rises");
  |     ^^^^^^^

 

println은 Rust의 표준 라이브러리에서 제공되는 것이기에 우리는 이제 println을 이용해 메시지를 출력할 수 없습니다.

 

println 호출 코드를 지운 후 크레이트를 다시 빌드해 본다면, 아직도 에러가 나는 것을 알 수 있습니다.

error: `#[panic_handler]` function required, but not found

error: language item required, but not found: `eh_personality`

 

Rust 컴파일러가 두 가지 에러를 내며, #[panic_handler] 함수와 language item 이 필요하다고 알려주고 있습니다.

 

첫 번째 에러부터 해결해 봅시다. Rust 컴파일러는 패닉이 일어날 경우 panic_handler 속성이 적용된 함수가 호출되도록 하는데, 표준 라이브러리 내에서는 기본적으로 이런 함수가 제공되지만, 우리는 panic이 일어날 시에 호출할 함수를 직접 작성해야 합니다.

 

#![no_std]

use core::panic::PanicInfo;

fn main() {}


// 이 함수는 panic 발생시 호출됩니다.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

 

 panic_handler attribute를 사용하여 panic 함수를 정의하였습니다. 이 함수는 PanicInfo라는 타입의 인자를 받아서 발산합니다. 발산하는 함수는 실제로는 값을 반환하지 않습니다. 이 경우, panic 함수는 패닉이 발생하면 프로그램이 무한 루프에 빠지도록 설계되었습니다.

이렇게 사용자 정의 panic_handler 함수를 추가하면, 운영체제 없이 실행되는 Rust 프로그램이 패닉이 발생했을 때 어떤 동작을 취할지 커스터마이징 할 수 있습니다.

 

 

두 번째 에러는 Rust 컴파일러가 'eh_personality' language item 이 필요하다고 알려주고 있습니다. Rust에서 Language Item은 특별한 종류의 아이템입니다. 이것들은 Rust 컴파일러에 의해 특별하게 인식되며, Rust 언어 자체의 작동 방식을 결정합니다. 앞선 panic_handler도 Language Item 중 하나입니다.

Language Item은 Rust의 많은 기능들을 구현하는 데 필요한 핵심적인 구성 요소입니다. 예를 들어, Drop 트레잇은 drop Language Item에 의해 제어되며, 이는 객체가 스코프를 벗어났을 때 어떻게 처리될지를 결정합니다.

 

Language Item은 일반적으로 표준 라이브러리에서 제공되지만, #![no_std] 특성을 사용하여 표준 라이브러리를 비활성화한 경우에는 사용자가 직접 제공해야 합니다. 이 경우 사용자는 해당 Language Item을 구현하는 함수나 트레잇을 제공해야 합니다. 이러한 접근 방식은 Rust가 임베디드 시스템이나 운영체제 커널 같은 매우 제한적인 환경에서도 실행될 수 있도록 합니다.

 

이 중, eh_personality는 Rust에서 스택 되감기(stack unwinding)를 처리하는 데 사용되는 특별한 언어 항목입니다. 여기서 "eh"는 "exception handling"을 의미합니다. 스택 되감기는 프로그램에서 예외가 발생했을 때 호출 스택을 안전하게 정리하는 과정을 말합니다.

프로그램에서 패닉이 발생하면, Rust는 스택 되감기 프로세스를 시작하여 호출 스택의 각 프레임에서 누적된 리소스를 안전하게 해제합니다. 이 과정에서 eh_personality 함수가 사용됩니다.

eh_personality 함수는 컴파일러가 생성하는 코드와 함께 작동하여 스택 되감기를 제어합니다. 각 스택 프레임에서 해제해야 하는 리소스가 있는지 확인하고, 필요한 경우 해당 리소스의 소멸자를 호출합니다.

 

Rust에서는 Cargo.toml 파일을 통해 패닉 동작을 구성할 수 있는데, panic = "abort"를 설정하면, 프로그램에서 패닉이 발생했을 때 스택 되감기(stack unwinding) 대신 프로세스를 즉시 중단합니다. 이렇게 설정하면 eh_personality를 구현할 필요가 없게 됩니다.

 

// Cargo.toml

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

이 설정은 개발(dev) 및 릴리스(release) 프로필 모두에 적용되며, 프로그램에서 패닉이 발생하면 즉시 프로세스가 종료됩니다. 단, 이렇게 설정하면 패닉 시점에서의 스택 트레이스(stack trace) 정보를 잃게 됩니다만, 실제로 eh_personality 함수를 구현하려면, 런타임 시스템에서 제공하는 다양한 API를 사용하여 스택 프레임을 검사하고, 필요한 경우 소멸자를 호출해야 합니다. 이는 매우 복잡한 작업이므로 현재로서는 이렇게 두 번째 에러만 해결하고 넘어갑시다.

 

이제 다시 빌드하게 되면 새로운 오류를 마주할 것입니다.

error: requires `start` lang_item

 

이번엔 컴파일러가 'start' language item이 필요하다고 말합니다.

 

어떤 프로그램이 실행될 때 가장 먼저 호출되는 함수는 무엇일까요? main 함수라 생각하셨다면 이는 틀렸습니다. 물론, main 함수는 대부분의 프로그램의 주요 진입 지점으로 간주되며, 사용자 정의 코드가 처음으로 실행되는 위치입니다. 하지만, 많은 프로그래밍 언어들은 main 함수가 호출되기 전에 여러 초기화 작업을 수행합니다. 

 

C++에서는 전역 객체의 생성자가 main 함수 호출 전에 실행되며, Java에서는 가비지 컬렉션과 같은 런타임 환경이, Go에서는 goroutine 스케줄러 같은 시스템이 먼저 실행됩니다. Rust 또한 마찬가지로, 스택과 힙과 같은 메모리 영역을 초기화하고, 커맨드 라인 인수와 환경 변수를 처리하는 C 런타임 라이브러리인 crt0 (C runtime zero)가 실행됩니다.

 

crt0는 작업을 마친 후 start language item으로 지정된 Rust 런타임의 실행 시작 함수를 호출하게 되는데, 우리는 표준 라이브러리를 사용하지 못하므로 여기서 crt0가 호출할 Rust 런타임의 실행 시작 함수를 직접 작성해야합니다. 즉, 우리가 직접 프로그램의 실행 시작 지점을 지정해줘야 합니다.

 

일단 먼저 #![no_main] 속성을 통해 main 함수를 날려버립시다.

#![no_std]
#![no_main]

use core::panic::PanicInfo;

// 이 함수는 panic 발생시 호출됩니다.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

 

그리고 프로그램 실행 시작 지점을 지정해 줍시다.

// main.rs

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

 

Rust에서 #[no_mangle] 속성은 해당 함수의 이름이 컴파일러에 의해 변경되지 않도록 지시합니다. 보통 함수를 컴파일하면 그 이름이 맹글링(mangling)되어 실제 바이너리에서는 원래의 이름이 아닌 다른 이름으로 나타나게 됩니다. 이것은 여러 함수나 변수가 같은 이름을 가질 수 있도록 하기 위한 것이지만, 우리는 링커 (linker)에 그 이름을 정확히 전달해야 하기에 #[no_mangle] 속성을 사용합니다.

 

또한, 이 함수는 C 언어의 호출 규약을 따르는 함수임을 나타냅니다(extern "C"). 그리고 일단은 무한 루프를 넣은 발산 함수로 놓아둡시다.

 

 

다시 빌드하게 되면 링커 에러를 마주하게 될 것입니다. 링커는 컴파일러가 생성한 코드들을 묶어 실행파일로 만드는 프로그램으로, 현재는 주어진 프로그램이 C 런타임 시스템을 이용할 것이라고 가정하고 있지만, 우리 크레이트는 그렇지 않기 때문에 나는 에러입니다.

이 링커 에러를 해결하려면 링커에게 C 런타임을 링크하지 말라고 알려줘야 합니다. 두 가지 방법이 있는데, 하나는 링커에 특정 인자들을 주는 것이고, 또 다른 하나는 크레이트 컴파일 대상 기기를 bare metal 기기로 설정하는 것입니다. 우리는 여기서 Bare Metal 시스템을 목표로 빌드할 것입니다.

 

기본적으로 Rust는 "호스트" 시스템에서 실행할 수 있는 실행 파일을 생성하도록 설계되어 있습니다. 예를 들어, Windows x86_64 환경에서 작동하는 컴퓨터를 사용하고 있다면, Rust는 그 컴퓨터에서 돌아갈 수 있는 '.exe' 파일을 만들어줍니다. 이러한 호스트 시스템의 특성은 "target triple"이라는 문자열을 통해 Rust에게 알려집니다. target triple은 컴퓨팅에서 특정한 하드웨어 플랫폼과 운영체제를 나타내는 문자열로 일반적으로 세 부분으로 구성되며, 각각 아키텍처, 벤더, 운영체제로 이루어져 있습니다. 이는 컴파일러나 툴체인에게 프로그램이 어떤 플랫폼에서 실행되어야 하는지를 알려주는 역할을 합니다.

 

그러나 이렇게 만들어진 Rust 컴파일러와 링커는 운영체제와 C 런타임이 있는 환경에서만 실행할 수 있습니다. 우리는 이러한 제약을 극복하기 위해 운영체제가 없는 시스템, 즉 "bare metal" 시스템에서도 프로그램을 실행할 수 있도록 Rust를 설정할 것입니다.

'Rust'에서는 이러한 bare metal 시스템을 대상으로 코드를 컴파일하는 기능을 제공합니다. 이를 크로스 컴파일링이라고 합니다. 우리는 'thumbv7em-none-eabihf' 라는 임베디드 ARM 시스템을 나타내는 target triple을 통해 bare metal 시스템을 목표로 프로그램을 빌드할 것입니다. 여기서 thumbv7em-none-eabihf은 각각 다음을 의미합니다.

 

thumbv7em : CPU 아키텍처를 가리킵니다. "thumbv7em"은 ARM v7 아키텍처의 Thumb 모드를 지원하는 프로세서를 가리키며, "em"은 "Embedded"를 의미합니다. 이는 이 아키텍처가 주로 임베디드 시스템에 사용된다는 것을 의미합니다.

none : 운영체제를 가리킵니다. 여기서 "none"은 이 target이 특정 운영체제에 의존하지 않는다는 것을 나타냅니다. 이는 프로그램이 "bare metal", 즉 운영체제 없이 직접 하드웨어에서 동작함을 의미합니다.

eabihf : ABI (Application Binary Interface)를 가리킵니다. "eabi"는 "Embedded Application Binary Interface"를 가리키며, 이는 ARM 아키텍처의 임베디드 소프트웨어를 위한 표준 ABI입니다. "hf"는 "hard float"를 의미하며, 이는 이 시스템이 하드웨어 부동소수점 연산을 지원한다는 것을 나타냅니다.

 

아직은 이해가 안 가셔도 괜찮습니다.

 

 

이제 이 target triple을 Rust 컴파일러에 알려주는 과정이 필요합니다.

 

rustup target add thumbv7em-none-eabihf

 

위 명령어를 실행하면, Rust 컴파일러는 이 target triple에 대한 정보를 다운로드하고 설치하게 됩니다.

그럼 이제 bare metal 시스템을 대상으로 프로그램을 빌드할 준비가 되었습니다. 

cargo build --target thumbv7em-none-eabihf

이제 빌드 명령을 실행하면, Rust의 빌드 시스템인 Cargo는 이 target triple을 목표로 프로그램을 컴파일합니다. 여기서 '--target' 옵션은 우리가 어떤 시스템을 대상으로 컴파일하고자 하는지 Cargo에 알려주는 역할을 합니다.

 

 

이제 에러없이 빌드가 성공적으로 이루어졌습니다.


이 세션에서 우리는 운영체제에 종속되지 않은 bare metal 시스템을 대상으로 Rust 코드를 컴파일하는 방법을 배웠습니다. 이는 우리가 운영체제 커널을 개발하는 데 아주 중요한 기법이며, 이를 통해 우리는 자신만의 운영체제를 빌드해 나갈 수 있습니다.

 

아직은 우리의 Free Standing 실행파일이 아무런 유의미한 작업을 하지 않습니다. 다음 포스트에서는 우리의 Free Standing 실행 파일을 최소한의 기능을 갖춘 운영체제 커널로 만들어봅시다.

 

Thanks for watching, Have a nice day.

 

References

https://os.phil-opp.com/

 

다음으로 ->

반응형
Comments