LoginSignup
2
0

1000行のコードでOSがかけるらしいのでやってみた Part1

Last updated at Posted at 2023-12-19

概要

OSを1000行のソースコードで書くことができるという本を目にしたので、これに挑戦している記事です。
この記事を執筆している時点では完成させることができなかったため、Part1です。
この記事では、第5章のブートさせる箇所までを扱います。

こちらのページの内容をやっていきます。
https://operating-system-in-1000-lines.vercel.app/ja/welcome

この記事を書いている人間について

この記事ではもっぱらC言語で記述されたコードが書かれていますが私は普段Goを用いたサーバーサイドの開発を行っており、C言語には不慣れです。また、ITやソフトウェアに関する専門的な教育を受けたわけではないしがない職業プログラマーです。そのため、記述に不正確な箇所が含まれる場合があります。ご容赦ください。

OSとわたし

OS、オペレーティングシステムというものを初めて意識するのはいつでしょうか。
私の場合は、中学生の頃に学校のパソコン室のWindowsをKnoppixのLiveCDから消し飛ばしている友人の姿を見たときがOSを意識するきっかけな気がします。
他には「闘うプログラマー」というWindowsNTの開発秘話伝記をよんだことがあります。この書籍に登場するデイヴィッドカトラーは今でも現役のソフトウェア開発者で、私は尊敬しています。この本自体も熱くて良い内容でした。

C言語の公式情報

いままで僕が仕事で関わったことのある言語、PHP, JavaScript, Python, Goなどはどれも公式にドキュメントが存在し、容易にアクセスできました。しかし、C言語はISOで規格付けられている言語で、言語仕様などは購入しなければならないようです。
そんなことすら知りませんでした。また、Google検索もままならないため、わからない箇所はある程度ChatGPT(課金)にたよったりしたあとにまた調べていきます。

OSの実装

ここからが本題です。

環境

私は私物PCでUbuntuを使っているので、これらをインストールしました。
(もとのページ内に記述されていたaptコマンドでインストールするもののうち、curlはすでにあることが明らかなので除いた)

sudo apt update && sudo apt install -y clang llvm lld qemu-system-riscv32

clang

コンパイラフロントエンド、C言語の字句解析をして抽象構文木に変換してくれる

llvm

コンパイラバックエンド、clangがつくった抽象構文木をもとに機械語コードを作ってくれる

lld

リンカ、コンパイラがつくったコードをまとめて実行ファイルを作ってくれる

quemu

CPUのエミュレーター。私が普段使っているPCはx86-64のIntel製CPUを搭載していますが、そのアーキテクチャで動くOSをつくるのではなく、RISC Vというアーキテクチャで動くものを使います。そのためにQUEMUで動きをエミュレートさせます

ソースコードを書く環境

これは好みによるところで、私が参考にした本では特に指定されていません。
私は普段Intellij IDEAという年間2万円近くする高級品を使ってソースコードを書いています。お金をかけたらと言って素晴らしいコードがかけるわけでもないですが、拙い私を助けてくれる偉大な道具です。
しかしIDEAはC言語むきではないので今回はNeoVimを使います。

coc.nvim

https://github.com/neoclide/coc.nvim
NeoVimにはとくにIDEのようなすぐ使えるようなコード補完などの便利なツールがあるわけではないので、coc.nvimというプラグインをNeoVimに入れて、コードの補完やエラーが出るようにします。

ccls

https://github.com/MaskRay/ccls
C言語のランゲージサーバーです。
これを入れて、coc.nvimに設定を行うことでCを書く準備はある程度できたと言っていいでしょう。

RISC V

https://operating-system-in-1000-lines.vercel.app/ja/assembly
RISC V今回のCPUアーキテクチャに選ばれたのは、ひとえに教育的だからでしょう。
x86-64のような現実広く長く使われているアーキテクチャはきっと複雑なのだろうなと予想ができます。

カーネル

最小限のカーネル
最初に書いたのはこのようなコードです。

typedef unsigned char uint8_t;
typedef unsigned int uint32_t;
typedef uint32_t size_t;

extern char __bss[], __bss_end[], __stack_top[];

void *memset(void *buf, char c, size_t n) {
    uint8_t *p = (uint8_t *) buf;
    while (n--)
        *p++ = c;
    return buf;
}

void kernel_main(void) {
    memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);

    for (;;);
}

__attribute__((section(".text.boot")))
__attribute__((naked))
void boot(void) {
    __asm__ __volatile__(
        "mv sp, %[stack_top]\n"
        "j kernel_main\n"
        :
        : [stack_top] "r" (__stack_top)
    );
}

意味わかんない。
のでChatGPTさんに聞いてみましょう

以下のように説明してくれました

あなたの提供したコードは、OS開発の文脈でよく見られる低レベルのC言語のコードです。Go言語と比べて、C言語はよりハードウェアに近い操作が可能ですが、それには詳細な理解が必要です。それぞれの部分について詳しく説明します。

typedef宣言

typedef unsigned char uint8_t;
typedef unsigned int uint32_t;
typedef uint32_t size_t;

typedefは新しい型名を定義します。
unsigned charは符号なしの8ビット整数型、uint8_tとして再定義されます。
unsigned intは符号なしの整数型(サイズはプラットフォーム依存)、uint32_tとして再定義されます。
size_tは一般的にメモリサイズを表すために使われる型で、uint32_t(符号なしの32ビット整数型)に再定義されています。


Goの

type Foo uint8

みたいな感じかな?
と理解してました。


extern宣言

extern char __bss[], __bss_end[], __stack_top[];

これらは他のファイル(おそらくアセンブリ言語で書かれた部分)で定義されるグローバル変数を宣言しています。
__bssはBSSセグメント(未初期化データ)の開始を、__bss_endはその終わりを指します。
__stack_topはスタックのトップ(最初の位置)を指します。
memset関数

void *memset(void *buf, char c, size_t n) {
    uint8_t *p = (uint8_t *) buf;
    while (n--)
        *p++ = c;
    return buf;
}

memsetはメモリの一部を特定の値(c)で埋める標準的な関数です。
void *bufは埋めるメモリの先頭アドレス、char cは埋める値、size_t nは埋めるバイト数です。
uint8_t *p = (uint8_t *) buf;でbufをuint8_t型のポインタにキャストし、バイト単位で操作できるようにします。
whileループでnバイト分のメモリにcを代入します。
kernel_main関数

void kernel_main(void) {
    memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);

    for (;;);
}

kernel_mainはカーネルのメイン関数です。
memsetを使ってBSSセグメント(未初期化データ領域)をゼロで初期化します。これはグローバル変数が初期値なしで宣言された場合、デフォルトでゼロ値であることを保証するために行われます。
for (;;);は無限ループで、プログラムがここで停止します。
boot関数

__attribute__((section(".text.boot")))
__attribute__((naked))
void boot(void) {
    __asm__ __volatile__(
        "mv sp, %[stack_top]\n"
        "j kernel_main\n"
        :
        : [stack_top] "r" (__stack_top)
    );
}

__attribute__((section(".text.boot")))はこの関数が特定のセクション(ここでは.text.boot)に配置されることを指示します。
__attribute__((naked))は関数がプロローグやエピローグを持たないことを示します


Cがわからなくても、なんとなくわかるような気がしてきました。

これを実行してみたところ、ThinkPadがウンウンとうなり始めました。
今回出来たのはここまでです。次回以降文字を表示するところをやっていきます。

宣伝

この記事はVoicyアドベントカレンダーに参加しています。
他の記事も読んでいただけると幸いです。
また、採用も積極的に行っておりますので興味があればお話だけでも。
https://qiita.com/advent-calendar/2023/voicy

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0