LoginSignup
11
8

More than 3 years have passed since last update.

自作OS勉強プロジェクト - 実際に自作OSを勉強する上で気をつけたこと -

Posted at

はじめに

なお、ここに執筆した事は組み込み系のことに関して未経験の者が書いているので、実際な技術的な話というよりは勉強の進め方としてどうすればいいのかという所を重点に置いて執筆してみます。

 組み込みOSの勉強をする前に気をつけたこと

12ステップで作る 組込みOS自作入門や様々なサイトを参考にして勉強中ですが、その内実際に組み込みOSを勉強・実装する上で以下のことを守ることを徹底していました。

  • 大事なのはハードウェアに合わせて組み込み系OSを作るのではなく、組み込み系OSに合わせて適したハードウェアを選ぶこと

つまりは、Raspberry PiやPINE 64と言ったマイコンボードを実際に購入して試すのではなく、先に組み込みOS系の実装を行い、それをエミュレータで動かしてみて、自分が予測した動きをするのかと言ったことを検証することとしました。
何故、このコンテクストになったかと言うと、昔からRaspberry PiやPINE 64と言ったハードウェアの購入はしていたのですが、ハードウェアを購入することが目的という状況になっており、実際ハードウェアを購入してなにか作るといった経験がないままとなっていたからです。
そのため、今回参考にした書籍・サイトの対象となっているマイコンボードも実際買っていません。
進め方として

  1. 書籍・サイトを参考にして、組み込みOSを自作する上で考えなくてはならないことを整理すること
  2. 1の目的から実際にコードを書いてみること
  3. エミュレーションによる挙動から大方自分が予測した挙動になっているか確認すること

この3つを気をつけて進めることとしました。

クロスコンパイラの環境準備(H8用)

OS自作のためにまず用意しなければならないのが、プログラミング環境を用意することです。この時、一般的なPCでマイコン用の環境を用意するためのクロスコンパイラの環境を用意します。
通常、PC上でプログラムを作成する場合には、その上でコンパイルしプログラムを実行する訳ですが、新規で購入したマイコンボードにはそもそも動作するOSが入っていない事が主です。
そのため、開発に関してはマイコンボードで行うのではなく、一般的なPC上で行い、またマイコンボードのプラットフォームに合わせた環境を作成するために、クロスコンパイラの環境準備が必要となります。

以下の、いくつかの内容から実際にH8/3069F マイコンボード用のコンパイラの準備をします。

% sudo pkg install gcc
% sudo pkg install bison
% sudo pkg install make
% sudo pkg install flex
% sudo pkg install isl
% sudo pkg install mpc
% sudo pkg install mpfr
% sudo pkg install mpc
% sudo pkg install texinfo
% sudo pkg install gmake
% mkdir cross_compiler
% cd cross_compiler
% wget http://ftp.tsukuba.wide.ad.jp/software/gcc/releases/gcc-9.3.0/gcc-9.3.0.tar.gz
% wget http://ftp.gnu.org/gnu/binutils/binutils-2.34.tar.gz

まずは、binutilsをビルドします。なお、binutils,gcc共にビルド用のディレクトリを別途設けることが推奨されている模様なので、
ビルド用のディレクトリを作成することとしました。

% tar zxvf binutils-2.34.tar.gz
% cd binutils-2.34
% mkdir build
% cd build
% ../configure --target=h8300-elf --disable-nls --prefix=/home/himrock922/h8300
% gmake
% gmake install

インストールしたbinutilsが扱えるように、PATHの追加をしてあげます。

続いて、gccのビルドも行います。

% export PATH=/home/himrock922/h8300/bin:$PATH
% tar zxvf gcc-9.3.0.tar.gz
% cd gcc-9.3.0
% mkdir build
% cd build
% ../configure --target=h8300-elf --enable-languages=c --prefix=/home/himrock922/h8300 --with-gmp=/usr/local --with-mpfr=/usr/local --with-mpc=/usr/local --disable-nls --disable-threads --disable-shared --disable-libssp
% gmake
% gmake install

基本的には、書籍の通りですが、このような環境準備のために、/usr/localディレクトリに入れるよりも、/homeディレクトリで環境ごとに(プロジェクトごとに)コンパイラのインストール先を別にしたいために、prefixでインストール先を変更しています。
また、参考にした書籍の当時のbinutils,gccのversionが結構古いこともあり、この記事では最新のbinutils,gccのversionで対応することとしました。そのため、書籍とはまた違った部分が出てきていますが、一先ずこれで対応したいと思います。

ビルドに失敗した場合はその都度config.logを見るようにしましょう。

「Hello World」のソースコード

書籍参照

http://kozos.jp/kozos/osbook/osbook_03/01/

さて、一通りの練習は終わったので、ここまでで一旦環境を準備するためにしなければならないことを整理します。

  1. 基本的には一般的なPCで組み込み系で動くコンパイラの環境を構築する
  2. ターゲットとなるマイコンボードやCPUのアーキテクチャに合わせてターゲットを変える
  3. ビルドに失敗した場合はconfig.logに書いてあることを確認する

ざっくりとした整理ですが、この進め方・考え方で進めていきます。

自作OSを搭載するハードウェアをRasberry Piと想定して進めていく

ハードウェアに関しては実際に購入に至っていないこと、及び最初からエミュレーション頼りでOSを自作していくにはどうすれば良いのかということもあり、ここからはRaspberry Piを想定して進めていくことにします。

下記の購入率のこともあり、また多くのエビデンスがあるRaspberry Piを選ぶことにしました(汎用目的として適しているのかはちょっと分かりかねますが)

https://news.mynavi.jp/article/20170322-a079/

当初、Rapberry Pi用の自作OSを作っていくために参考にしたサイトがhttps://wiki.osdev.org/Raspberry_Pi_Bare_Bonesになりますが、OS起動までに必要なサンプルコードが記載されていることに限っているため、ここからハードウェアも一緒に勉強することとなると、最初の取っ掛かりとして参考にする程度で後々違うサイトを参考することにしました。

上記の練習以降に参考にしたサイトは以下の2つです。

  1. https://github.com/bztsrc/raspi3-tutorial

  2. https://github.com/s-matyukevich/raspberry-pi-os

参考にしながらと言いつつ、ほとんどの内容をまとめてしまっているので、参考にした2つのリポジトリのライセンスを明記します。

この2つのサイトを同時に読みながら進めていきます。

Clangコンパイラでクロスコンパイルの環境を整える

FreeBSD 10.0-RELEASEから組み込まれたclangコンパイラ、及びld.lldリンカを使えば、実はgccの様に、ターゲットに応じてコンパイルする必要がなくなっていることを知りました。

% clang --target=aarch64-elf
% ld.lld -m aarch64elf

あいにく、コンパイル後に必要なllvm-objcopyコマンドがFreeBSD 12.1-RELEASEで標準搭載されたLLVMでもまだサポートされていないversionのLLVMなため、諦めてgccでやっていくことにします。

% tar zxvf binutils-2.34.tar.gz
% cd binutils-2.34
% mkdir build
% cd build
% ../configure --target=aarch64-elf --disable-nls --prefix=/home/himrock922/rpb
% gmake
% gmake install
% export PATH=/home/himrock922/rpb/bin:$PATH
% tar zxvf gcc-9.3.0.tar.gz
% cd gcc-9.3.0
% mkdir build
% cd build
% ../configure --target=aarch64-elf --enable-languages=c --prefix=/home/himrock922/rpb --with-gmp=/usr/local --with-mpfr=/usr/local --with-mpc=/usr/local --disable-nls --disable-threads --disable-shared --disable-libssp
% gmake
% gmake install

Makefileについて

今回のようなコードを直したら、再度コンパイルが必要な自作OS系の開発において、プログラムのどの部分を再コンパイルするべきなのかを自動的に判断するために、Makefileの書き方をここで改めて調べてみます。

ARMELF ?= aarch64-elf

COPS = -Wall -nostdlib -nostartfiles -ffreestanding -Iinclude -mgeneral-regs-only
ASMOPS = -Iinclude 

BUILD_DIR = build
SRC_DIR = src

all : kernel8.img

clean :
    rm -rf $(BUILD_DIR) *.img 

$(BUILD_DIR)/%_c.o: $(SRC_DIR)/%.c
    mkdir -p $(@D)
    $(ARMELF)-gcc $(COPS) -MMD -c $< -o $@

$(BUILD_DIR)/%_s.o: $(SRC_DIR)/%.S
    $(ARMELF)-gcc $(ASMOPS) -MMD -c $< -o $@

C_FILES = $(wildcard $(SRC_DIR)/*.c)
ASM_FILES = $(wildcard $(SRC_DIR)/*.S)
OBJ_FILES = $(C_FILES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%_c.o)
OBJ_FILES += $(ASM_FILES:$(SRC_DIR)/%.S=$(BUILD_DIR)/%_s.o)

DEP_FILES = $(OBJ_FILES:%.o=%.d)
-include $(DEP_FILES)

kernel8.img: $(SRC_DIR)/linker.ld $(OBJ_FILES)
    $(ARMELF)-ld -T $(SRC_DIR)/linker.ld -o $(BUILD_DIR)/kernel8.elf  $(OBJ_FILES)
    $(ARMELF)-objcopy $(BUILD_DIR)/kernel8.elf -O binary kernel8.img

上記のMakefileに対する定数に関しては以下のとおりです。

  • ARMELF : クロスコンパイラ環境を構築した際に用意された実行ファイルのプレフィックスを指定しています。
  • COPS, ASMOPS : Cおよびアセンブラコードをコンパイルするときに通すオプションを指定しています。
  • -Wall : 警告をすべて表示します。
  • -nostdlib : C言語の標準ライブラリを使用しないオプションです。自作OSの場合、標準ライブラリを使用するための呼び出し元OSがないため、このオプションを指定しています。
  • -nostartfiles : 標準のスタートアップスクリプトを使用せずに、スタックポインタの設定、静的データの初期化、メインエントリへのジャンプ等、自分で用意します。
  • -ffreestanding : プログラムの起動が必ずしもmainになるとは限らず、標準ライブラリが存在しない可能性があることを示します。
  • -Iinclude: includeディレクトリ内のヘッダファイルを検索します。
  • -mgeneral-regs-only : 汎用レジスタのみを使用するようにします。

後は、ビルド用のディレクトリを指定したり、Cのソースファイルやオブジェクトファイルを見つけられるように指定しています。

基本的には、ELF実行ファイル(コンパイラが生成するオブジェクト、およびライブラリとリンクされた実行ファイルのファイルフォーマット)からすべての実行可能セクションとデータセクションを抽出し、それらをkernel8.imgに配置する必要があります。

リンカスクリプト

リンカスクリプトは、入力されたオブジェクトファイルをELFへ出力する際にどうマッピングすれば良いのかを指定します。

linker.ld
SECTIONS
{
    .text.boot : { *(.text.boot) }
    .text :  { *(.text) }
    .rodata : { *(.rodata) }
    .data : { *(.data) }
    . = ALIGN(0x8);
    bss_begin = .;
    .bss : { *(.bss*) } 
    bss_end = .;
}

Raspberry Pi起動時にRaspberry Piはkernel8.imgをメモリ内に読み込み、ファイルの先頭から実行を開始します。そのため、text.bootを最初に起動するようにします。このセクション内にOSスタートアップコードを配置します。
.text,.rodata,.dataセクションには、カーネルでコンパイルされた命令、読み込み専用データ、そして通常のデータが含まれます。
.bssセクションは0に初期化すべきデータを含みます。0に初期化すべきデータを別のセクションに置くことにより、コンパイラはELFバイナリの一部の領域を節約できます。セクションサイズはELFヘッダに載りますが、セクション自体は省略されます。メモリにimageをロード後、.bssセクションを0に初期化する必要があり、セクションの開始と終了を記録し、セクションを8の倍数のアドレスで開始するように配置する必要があります。セクションが整列されていない場合、str命令は8バイトで整列されたアドレスでのみ使用できるため、bssセクションの先頭に0を格納するために、str命令を利用することが難しくなります。

カーネルの起動

boot.Sにはカーネルの起動コードが含まれています。

boot.S
#include "mm.h"

.section ".text.boot"

.globl _start
_start:
    mrs x0, mpidr_el1       
    and x0, x0,#0xFF        // Check processor id
    cbz x0, master      // Hang for all non-primary CPU
    b   proc_hang

proc_hang: 
    b   proc_hang

master:
    adr x0, bss_begin
    adr x1, bss_end
    sub x1, x1, x0
    bl  memzero

    mov sp, #LOW_MEMORY 
    bl  kernel_main
    b   proc_hang       // should never come here
.section ".text.boot"

最初に、boot.Sで定義されているすべてのものが、.text.bootセクションに入るように指定します。
以前は、リンカスクリプトによって、セクションがカーネルイメージの先頭に配置されていました。
したがって、カーネルが起動すると、start関数から実行されます。

_start:
    mrs x0, mpidr_el1       
    and x0, x0,#0xFF        // Check processor id
    cbz x0, master      // Hang for all non-primary CPU
    b   proc_hang

最初にstart関数はプロセッサIDをチェックします。Raspberry Pi3は4つのコアプロセッサを持っており、デバイスの電源が入ると、各コアで同じコードが実行されます。ですが、本当に最初の最初の自作OSなため、シングルコアのみ実行させて他のコアは実行待ち(これは無限ループで解決できる?)にしたいと思います。mpidr_el1システムレジスタからプロセッサIDを取得し、現在のプロセッサIDが0の場合、master関数に転送されます。

master:
    adr    x0, bss_begin
    adr    x1, bss_end
    sub    x1, x1, x0
    bl     memzero

master関数では、memzeroを呼び出して、.bssセクションをクリーンアップします。memzero関数は開始アドレス(bss_begin)と、クリーンアップするべきサイズ(bss_end - bss_begin)の2つの引数を渡します。

    mov    sp, #LOW_MEMORY
    bl    kernel_main

.bssセクションでクリーンアップ後、スタックポインタを初期化し、kernel_main関数実行のために渡します。
LOW_MEMORYがmm.hで定義されており、4MBと等しいものとなっています。
カーネルイメージが大きくなり、スタックがカーネルイメージをオーバーライドしないように、初期スタックポインタを充分に大きなものにしています。

以下はこのboot.Sで利用している命令一覧です。

  • mrs: システムレジスタからの値、汎用レジスタ(x0-x30)の一つに値をロードします。
  • and: AND論理演算を実行します。このコマンドを利用して、mpidr_ellレジスタから取得した値から、最後のバイトを取り除きます。
  • cbz: 以前に、実行された操作の結果を0と比較し、比較結果がtrueの場合は、指定されたラベルにジャンプします。
  • b: 一部のラベルに無条件分岐を実行します。
  • adr: ラベルの相対アドレスをターゲットレジスタにロードします。
  • sub: 2つのレジスタの値から減算します。
  • bl: リンク付き分岐。無条件分岐を実行し、戻りアドレスを0x30(リンクレジスタ)に格納します。サブルーチンが終了したら、ret命令を使用して戻りアドレスにジャンプバックします。
  • mov: レジスタ間または定数からレジスタに値を移動します。

因みに、ARMの命令セットについて詳しく知りたい場合は以下のサイトを読むのが一番良さそうです。
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.den0024a/index.html

kernel_main関数

kernel.c
#include "mini_uart.h"

void kernel_main(void)
{
    uart_init();
    uart_send_string("Hello, world!\r\n");

    while (1) {
        uart_send(uart_recv());
    }
}

ブートローダは、最終的にkernel_main関数に制御を渡します。
kernel_main関数がやっている事は、ユーザの入力を読み込み、スクリーンに出力するためにMini UARTデバイスと連携します。
カーネルはHello, world!を読み込み出力し、後はユーザ入力を読み込み画面に送り返す無限ループに入ります。

Raspberry Piのハードウェア構成

Raspberry Pi3のボードでは、Broadcom BCM2837をSoCとして搭載しています。SoCとはSystem-on-a-Chipの略語であり、一般的なコンピュータではCPU以外に必要なグラフィックボードやディスク等の様々なパーツを接続して、システムを動かすことができますが、Raspberru Piのようなミニマムなサイズのコンピュータに様々なパーツを詰め込むわけにもいかないので、システムを動かすためにあらかじめ必要なパーツを一つの半導体チップにまとめたもののことを言います。

SoCでは、メモリマップされたレジスタを介して、全てのデバイスへアクセスされます。Raspberry Pi3では、0x3F000000より上のアドレスがデバイス用のアドレスとして予約されています。特定のデバイスをアクティビティ化、設定するためには、デバイスレジスタの1つにデータを書き込む必要があります。デバイスレジスタは、メモリの32bit領域です。

kernel_main関数から、Mini UARTデバイスにアクセスしていることが分かります。
UARTは、Universal Asynchronous Receiver/Transmitterの略称で、シリアル通信の一つであり、1バイト8ビットのデータを1本の電線で送るために、時系列に分解して、1ビットずつ出力または入力します。また、このデバイスはメモリマップレジスタの1つに格納されている一連の値を高電圧と低電圧に返還できます。この一連の値は、TTLシリアルケーブルを経由して渡され、ターミナルエミュレータが解釈します。

もう一つ重要なデバイスとして、GPIO(集積回路やコンピュータボード上の一般的なピン)があります。GPIOでは、様々なGPIOピンの動作を構成でき、Mini UARTを使用するには、ピン14と15をアクティブにする必要がある。

Mini UART初期化

mini_uart.c
#include "utils.h"
#include "peripherals/mini_uart.h"
#include "peripherals/gpio.h"

void uart_send ( char c )
{
    while(1) {
        if(get32(AUX_MU_LSR_REG)&0x20) 
            break;
    }
    put32(AUX_MU_IO_REG,c);
}

char uart_recv ( void )
{
    while(1) {
        if(get32(AUX_MU_LSR_REG)&0x01) 
            break;
    }
    return(get32(AUX_MU_IO_REG)&0xFF);
}

void uart_send_string(char* str)
{
    for (int i = 0; str[i] != '\0'; i ++) {
        uart_send((char)str[i]);
    }
}

void uart_init ( void )
{
    unsigned int selector;

    selector = get32(GPFSEL1);
    selector &= ~(7<<12);                   // clean gpio14
    selector |= 2<<12;                      // set alt5 for gpio14
    selector &= ~(7<<15);                   // clean gpio15
    selector |= 2<<15;                      // set alt5 for gpio15
    put32(GPFSEL1,selector);

    put32(GPPUD,0);
    delay(150);
    put32(GPPUDCLK0,(1<<14)|(1<<15));
    delay(150);
    put32(GPPUDCLK0,0);

    put32(AUX_ENABLES,1);                   //Enable mini uart (this also enables access to it registers)
    put32(AUX_MU_CNTL_REG,0);               //Disable auto flow control and disable receiver and transmitter (for now)
    put32(AUX_MU_IER_REG,0);                //Disable receive and transmit interrupts
    put32(AUX_MU_LCR_REG,3);                //Enable 8 bit mode
    put32(AUX_MU_MCR_REG,0);                //Set RTS line to be always high
    put32(AUX_MU_BAUD_REG,270);             //Set baud rate to 115200

    put32(AUX_MU_CNTL_REG,3);               //Finally, enable transmitter and receiver
}

Mini UARTの初期化を行うコードです。ここでは、put32,get32の関数を使うことで、32ビットレジスタからデータの読み書きを行うことができます。
これらの関数は、utils.Sで実装されています。

utils.S
.globl put32
put32:
    str w1,[x0]
    ret

.globl get32
get32:
    ldr w0,[x0]
    ret

.globl delay
delay:
    subs x0, x0, #1
    bne delay
    ret

GPIO代替機能

まず、最初にGPIOピンをアクティベートする必要があります。ほとんどのピンは様々なデバイスで使用できるため、特定のピンを使用する前にピンの代替機能を選択する必要があります。代替機能は、各ピンに設定できる0から5までの数字で、ピンに接続するデバイスを構成します。
ここでは、ピン14と15がTXD1とRXD1の代替機能として利用することができます。
ピン14とピン15に代替機能番号5を選択した場合、Mini UARTによるデータ送受信として使用されます。
GPFSEL1レジスタは、ピン10-19の代替機能を制御するために使用されます。

GPIO プルアップ/ダウン

特定のピンを入力として使用し、ピンに接続しない場合、ピンの値が0か1かを識別することができません。
実際、デバイスはランダムな値を出力します。
プルアップ/プルダウンメカニズムにより、この問題を解決できます。ピンをプルアップ状態に設定し、何も接続されていない場合、値は常に1を出力します(プルダウンの場合は0を出力)。
ピンの状態はリブート後も維持されるため、ピンを使用する前に常に状態を初期化する必要があります。使用可能な状態はプルアップ、プルダウン、両方(現在のプルアップ/プルダウン状態を削除するため)、3番目の状態が必要です。

電気回路のスイッチを物理的に切り替える必要があるため、ピンの状態を切り替えるのは簡単な手順ではありません。
https://cs140e.sergio.bz/docs/BCM2837-ARM-Peripherals.pdf 101ページにプルアップ/プルダウンの設定方法より、プルアップ/プルダウンの両方を削除するコードにしてあります。

    put32(GPPUD,0);
    delay(150);
    put32(GPPUDCLK0,(1<<14)|(1<<15));
    delay(150);
    put32(GPPUDCLK0,0);

Mini UART初期化

    put32(AUX_ENABLES,1);                   //Enable mini uart (this also enables access to it registers)

この行で、Mini UARTを有効にします。

    put32(AUX_MU_CNTL_REG,0);               //Disable auto flow control and disable receiver and transmitter (for now)

ここでは、設定が完了する前にレシーバとトランスミッターを無効にします。また、自動フロー制御も無効にしています。
自動フロー制御についてはhttp://www.deater.net/weave/vmwprod/hardware/pi-rts/この記事が参考になる模様です。

    put32(AUX_MU_IER_REG,0);                //Disable receive and transmit interrupts

新しいデータが利用可能になるたびに、プロセッサを割り込みを生成するように、Mini UARTを構成することが可能です。

    put32(AUX_MU_LCR_REG,3);                //Enable 8 bit mode

Mini UARTは7ビット/8ビット動作をサポートできるため、8ビットモードで動作するようにします。

    put32(AUX_MU_MCR_REG,0);                //Set RTS line to be always high

RTSの設定を常に高く設定します。

    put32(AUX_MU_BAUD_REG,270);             //Set baud rate to 115200

ボーレートをの速度設定を行います。ボーレートは通信チャネルで情報が転送される速度です。

115200ボーはシリアルポートが毎秒最大115200ビットを転送できることを意味します。
Raspberry Pi Mini UARTデバイスのボーレートは、ターミナルエミュレータのボーレートと同じにする必要があります。

Mini UARTの場合、この速度設定をどうやって導き出すかですが、

baudrate = system_clock_freq / (8 * ( baudrate_reg + 1 ))

system_clock_freqが250MHzなため、baudrate_regは270と導き出すことができます(どうやら、115200はMini UARTの転送速度の標準設定なため、この設定に合わせて導き出している模様です)。

https://github.com/s-matyukevich/raspberry-pi-os/issues/76

    put32(AUX_MU_CNTL_REG,3);               //Finally, enable transmitter and receiver

最後にトランスミッターとレシーバを有効にします。

Mini UARTを使用したデータ送信

mini_uart.c
void uart_send ( char c )
{
    while(1) {
        if(get32(AUX_MU_LSR_REG)&0x20) 
            break;
    }
    put32(AUX_MU_IO_REG,c);
}

char uart_recv ( void )
{
    while(1) {
        if(get32(AUX_MU_LSR_REG)&0x01) 
            break;
    }
    return(get32(AUX_MU_IO_REG)&0xFF);
}

void uart_send_string(char* str)
{
    for (int i = 0; str[i] != '\0'; i ++) {
        uart_send((char)str[i]);
    }
}

Mini UARTでデータを送受信するために、AUX_MU_LSR_REGレジスタを使用しています。
ビット0が1に設定されている場合、データの準備ができていることを示します。
ビット5は、1に設定されている場合、トランスミッターが空であることを示し、UARTに書き込むことができます。
次に、AUX_MU_IO_REGを使用し、送信した文字を格納するか、受信した文字の値を読み取ります。

Raspberry Piの設定

Raspberry Piはデバイスの電源が入った後、GPUが起動し、ブートパーティションから、config.txtファイルを読み込みます。
config.txtはスタートアップシーケンスをさらに調整するために、GPUが使用するいくつかの構成パラメータが含まれています。

シンプルなOSを実行するために、config.txtには以下の設定を行っています。

config.txt
kernel_old=1
disable_commandline_tags=1

kernel_old: カーネルイメージがアドレス0にロードされることを指定します。
disable_commandline_tags: ブートされたイメージにコマンドライン引数を渡さないようにします。

QEMUによるブート

ここまでのMarkDownの内容そのまま読んでいましたが、一先ずこれでHello, world!が出力されるようになります。
因みに、このプロジェクトではLinux用カーネルをコンパイルできるように、gcc,binutilsをクロスコンパイルしていますが、今回はlinuxではなく、arm 64ビットELFバイナリファイル形式でコンパイルするようにしています。

この辺り、後々苦労するかもしれませんが、まだLinux OSの専用のAPIを使うようなコードも書いていないため、汎用的なコンパイル環境にしました。

QEMUでも一応、起動するようなので、テストしてみます。

https://github.com/s-matyukevich/raspberry-pi-os/blob/master/exercises/lesson01/4/stefanji/run_on_qemu.sh

% qemu-system-aarch64 -m 1024 -M raspi3 -cpu cortex-a53 -kernel ~/Downloads/kernel8.img -nographic -serial null -chardev stdio,id=uart1 -serial chardev:uart1 -monitor none
Hello, world!
qemu-system-aarch64: terminating on signal 2 from pid 582 (<unknown process>)

無事、Hello, world!が出力されました。

まとめ

自作OSの本当に最初の一歩を踏み込めた(ようやく)気がします。本当はハードウェアのポンチ絵と照らし合わせながら、原文を読むのが一番良いのですが、中々、ハードウェアのポンチ絵書くのにまだ慣れていないので、原文にあるポンチ絵を読みながら、文章だけ読み込んでいました。
この辺りのポンチ絵も卒なく書けるようになれば、ハードウェアの勉強も拒否感起こさずにやっていけるような気がします。
本当にチュートリアルを読み込んでいるだけなので、ARMの命令セットの詳細やレジスタのことも勉強していきたいと思います。

それでは。

参考文献

11
8
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
11
8