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

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

こちらのページの内容をやっていきます。
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の実装
ここからが本題です。
## 環境
https://operating-system-in-1000-lines.vercel.app/ja/setting-up-development-environment

私は私物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](https://www.jetbrains.com/ja-jp/idea/)という年間2万円近くする高級品を使ってソースコードを書いています。お金をかけたらと言って素晴らしいコードがかけるわけでもないですが、拙い私を助けてくれる偉大な道具です。
しかしIDEAはC言語むきではないので今回は[NeoVim](https://neovim.io/)を使います。

### 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のような現実広く長く使われているアーキテクチャはきっと複雑なのだろうなと予想ができます。


## カーネル
[最小限のカーネル](https://operating-system-in-1000-lines.vercel.app/ja/boot#%E6%9C%80%E5%B0%8F%E9%99%90%E3%81%AE%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB)
最初に書いたのはこのようなコードです。
```c
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宣言**
```c
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の
```go
type Foo uint8
```
みたいな感じかな？
と理解してました。

--------
**extern宣言**

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

```c
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関数**
```c
void kernel_main(void) {
    memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);

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

``` c
__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

# 参考
https://operating-system-in-1000-lines.vercel.app/ja/welcome