컴퓨터의 전원을 켜면, 가장 먼저 수행되는 일은 Motherboard의 ROM에 저장되어있는 펌웨어 코드를 실행하는 것이다. → 이 코드는 몇가지 테스트(기기 안정성을 위해)를 진행하고, RAM을 확인한 다음, CPU를 비롯한 하드웨어의 초기화를 진행한다. 그리고 마지막으로 부트로더를 실행한다.
일반적으로 POST + BootLoader가 합쳐진 코드를 Motherboard의 fimrware 코드라고 하고, 이 firmware는 크게 두가지로 나뉜다.
본 구현에서는 BIOS만 지원할 예정이다. UEFI를 지원하고자 한다면 다음의 Github Issue
UEFI 표준을 사용하는 기기들도, 일단은 BIOS를 지원하기 때문에, 실질적으로 BIOS를 사용하면 모든 x86 시스템에서는 부팅을 시킬 수 있다.
잡설은 여기까지하고... 진짜 부팅과정을 살펴보면 다음과 같다.
Bootloader를 프로그래밍 하는 일은 쉬운일이 아니다.
만일, 운영체제들이 각기 다른 부트로더를 사용한다면, 운영체제를 설치할 때마다 부트로더를 설치해야한다.
그러나... Multiboot 역시 몇가지 단점을 가지고 있다.
thumbv7em-none-eabihf라는 bare-metal target으로 설정하였다.x86_64 CPU를 대상으로 하고 있기 때문에, 이 target은 적합하지 않으므로, 새로운 target-triple을 정의해야한다.x86_64-unknown-linux-gnu 환경에 대한 정보는 다음과 같다.{
"llvm-target": "x86_64-unknown-linux-gnu",
"data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}os 필드는 none으로, arch 필드를 x86_64로 설정하면 된다.{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float"
}ld.lld를 사용하도록 설정rust-lld)를 사용하도록 설정abort를 선택main.rs 파일은 다음과 같을 것이다.// src/main.rs
#![no_std] // Rust 표준 라이브러리를 링크하지 않도록 합니다
#![no_main] // Rust 언어에서 사용하는 실행 시작 지점 (main 함수)을 사용하지 않습니다
use core::panic::PanicInfo;
/// 패닉이 일어날 경우, 이 함수가 호출됩니다.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[no_mangle] // 이 함수의 이름을 mangle하지 않습니다
pub extern "C" fn _start() -> ! {
// 링커는 기본적으로 '_start' 라는 이름을 가진 함수를 실행 시작 지점으로 삼기에,
// 이 함수는 실행 시작 지점이 됩니다
loop {}
}
이제, build target에 대한 정보를 cargo에게 전달하기 위해, project root directory에 x86-64.json파일을 만들고 여기에 우리의 target(x86_64-unknown-none)에 대한 정보를 저장한다.
여기까지 되었다면, cargo에 우리가 만든 JSON파일을 전달함을 통해서 성공적으로 빌드할 수 있다.
> cargo build --target x86_64.json
error[E0463]: can't find crate for `core`
core라는 crate를 찾을 수 없기 때문이다.core crate는 Rust의 언어적인 기능을 제공하는 crate이다.core crate가 미리 컴파일된 상태로 rustc와 같이 배포된다는 것이다. 따라서, 일반적인 target triple을 사용하는 경우에는 core crate를 별도로 빌드할 필요가 없지만, 지금과 같이 새로운 target에 대해서 빌드를 진행할 경우에는 core crate를 별도로 빌드해야한다.이제 core crate를 빌드하기 위해서 build-std를 이용해야만 한다.
core나 다른 표준 라이브러리의 빌드를 사용하는 것이 아니라, 빌드시에 해당 crate들을 직점 재컴파일하는 기능이다.build-std를 사용하기 위해서는, cargo 설정 파일(.cargo/config.toml)에 다음과 같이 추가해주면 된다.
# .cargo/config.toml 에 들어갈 내용
[unstable]
build-std = ["core", "compiler_builtins"]
아마 대부분의 경우에는 .cargo폴더가 없을 것이다. 이 경우에는 직접 만들어주면 된다.
이 설정을 통해서, core와 compiler_builtins crate를 새로 컴파일하도록 설정하였다.
만일 compiler_builtins crate 사용에 문제가 있다면, rust-src component를 설치해주면 된다.
rustup component add rust-src여기까지 되었다면, 다음과 같이 빌드가 성공적으로 진행될 것이다.
> cargo build --target x86-64.json
Compiling core v0.0.0 (/home/bae/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core)
Compiling compiler_builtins v0.1.103
Compiling rustc-std-workspace-core v1.99.0 (/home/bae/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/rustc-std-workspace-core)
Compiling rust_os v0.1.0 (/home/bae/Github/rust_os)
Finished dev [unoptimized + debuginfo] target(s) in 4.85s
Rust는 놀랍게도 몇가지 built-in function에 대해서는 system의 c라이브러리에 의존하고 있다.
memset, memcpy, memcmp 등의 함수들이 그렇다.하지만, 우리의 커널은 c라이브러리를 제공할 수 없다.(당연하다. bare-metal 환경이니까)
#[no_mangle]compiler_builtins crate의 기본제공 함수를 사용이 crate는 memcpy, memset, memcmp 등의 함수를 제공한다.
다만, libc의 함수들과 충돌할 수 있으므로, 평시에는 사용하지 않도록 해제되어있던 것이다.
따라서, 이 crate의 함수를 사용하기 위해서는 compiler_builtins crate를 사용하도록 설정해주어야한다.
# .cargo/config.toml 에 들어갈 내용
[unstable]
build-std-features = ["compiler-builtins-mem"]
build-std = ["core", "compiler_builtins"]
현재 우리의 커널은 정말 아무것도 하지 않는다.
이를 위해서는 VGA Text Buffer를 사용해야 한다.
일단, 코드를 보고, 어떻게 동작하는지를 이해해보자.
static HELLO: &[u8] = b"Hello World!";
#[no_mangle]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop {}
}
vga_buffer라는 이름의 포인터를 만들었다.vga_buffer에 저장하고자 한다.unsafe 블록을 사용하여야 하는데, rust에서는 raw pointer를 사용하는 것을 막기 때문이다.(이는 rust compiler가 우리가 사용하는 raw pointer의 유효성을 검증할 능력이 없기 때문이다.)unsafe는 결국 프로그래머가 해당 코드가 안전함을 보장해야만 하기 떄문에, 결코 좋은 방법이 아니므로 남용해서는 안된다.vga_buffer에 HELLO의 byte들을 할당함을 통해서 이를 완성한다.커널을 실행시키기 위해서는 먼저, 커널을 부트로더와 연결해야한다.
bootloader라는 도구를 사용할 것이다.cargo add bootloader를 통해서 bootloader를 추가해주자.Cargo.toml에 다음과 같이 추가해주면 된다.# Cargo.toml 에 들어갈 내용
[dependencies]
bootloader = "0.9.23"
또, 우리의 커널을 부트로더가 인식할 수 있는 형태로 변환해야한다.
bootimage 도구를 사용할 것이다.cargo install bootimage를 통해서 설치할 수 있다.llvm-tools-preview가 필요하기 때문에, rustup component add llvm-tools-preview을 통해서 설치해준다.여기까지 완료되었다면, cargo bootimage를 통해서 커널을 디스크 이미지로 변환할 수 있다.
qemu-system-x86_64 -drive format=raw, file=target/x86-64/debug/bootimage-rust_os.bin으로 실행하거나
cargo run setting을 통해서 해결한다.
# .cargo/config.toml 에 들어갈 내용
[target.'cfg(target_os = "none")']
runner = "bootimage runner"