オペレーティングシステムなしでプログラムを実行する方法



PCIバスをポーリングするメカニズムを説明している記事では、最も重要なことは詳細に説明されていませんでした。実際のハードウェアでこのコードを実行する方法ですか? 独自のブートディスクを作成する方法は? この記事では、これらすべての質問に詳細に回答します(部分的には、これらの問題については前の記事で説明しましたが、読みやすくするために、資料の重複を少し許可します)。



インターネットには、何百もの既製の小さな趣味OSが存在する場合でも、独自のミニOSを作成する方法に関する多くの説明とチュートリアルがあります。 このテーマに関する最も価値のあるリソースの1つは、osdev.orgポータルです。 PCIに関する以前の記事(および最新のOSに存在するさまざまな機能に関する後続の記事を作成する機能)を補足するために、Cの通常のプログラムを使用してブートディスクを作成する手順をステップごとに説明します。自分で整理してください。



そのため、目標は、できるだけ少ない労力で、独自の起動可能なUSBフラッシュドライブを作成します。これにより、コンピューター画面に古典的な「Hello World」が印刷されます。



より正確には、ページのアドレス指定と割り込みを無効にして保護モードに「入る」必要があります。これは、単純なコンソールプログラムの通常の動作を伴う最も単純なプロセッサモードです。 この目標を達成する最も合理的な方法は、マルチブート形式をサポートするカーネルを構築し、一般的なGrubブートローダーを使用してロードすることです。 このソリューションに代わる方法は、独自のボリュームブートレコード(VBR)を記述することです。これにより、独自のローダー(ローダー)がロードされます。 少なくともまともなブートローダーは、ディスク、ファイルシステム、およびelfイメージを解析できる必要があります。 つまり、大量のアセンブラコードと大量のCコードを記述する必要があります。要するに、必要なすべての方法を既に知っているGrubを使用する方が簡単です。



最初に、特定のコンパイラとユーティリティのセットがさらなるアクションに必要です。 最も簡単な方法は、何らかの種類のLinux(Ubuntuなど)を使用することです。これには、ブート可能なフラッシュドライブを作成するために必要なものがすべて含まれているためです。 Windowsでの作業に慣れている場合は、Linuxで仮想マシンを構成できます(Virtual BoxまたはVMware Workstationを使用)。



Linux Ubuntuを使用している場合、最初にいくつかのユーティリティをインストールする必要があります。

1.グラブ。 これを行うには、次のコマンドを使用します。

sudo apt-get install grub
      
      







2. Qemu。 すべてをすばやくテストおよびデバッグする必要があります。このため、コマンドは似ています。

 sudo apt-get install qemu
      
      







これで、計画は次のようになります。

1.画面に行を印刷するCプログラムを作成します。

2.そこからミニブート形式のイメージ(kernel.bin)をコンパイルし、GRUBを使用してダウンロードできるようにします。

3.ブートディスクイメージファイルを作成し、フォーマットします。

4.このイメージにGrubをインストールします。

5.作成したプログラム(kernel.bin)をディスクにコピーします。

6.イメージを物理メディアに書き込むか、qemuで実行します。



およびシステムの起動プロセス:





動作させるには、いくつかのファイルとディレクトリを作成する必要があります。

kernel.c





Cで記述されたプログラムコード。プログラムは画面にメッセージを出力します。





メイクファイル





Makefile。プログラムのアセンブリ全体を実行し、ブートイメージを作成するスクリプト。





リンカー.ld





カーネルのリンカースクリプト。





loader.s





Grubによって呼び出され、Cプログラムからメイン関数に制御を転送するアセンブラーコード。





含む/





ヘッダーファイルのあるフォルダー。





グラブ/





GRUBファイルフォルダー。





共通/





汎用機能を備えたフォルダー。 printfの実装を含みます。









ステップ1.ターゲットプログラム(カーネル)のコードの作成:





画面にメッセージを出力する次のコードを含むkernel.cファイルを作成します。



 #include "printf.h" #include "screen.h" #include "types.h" void main(void) { clear_screen(); printf("\n>>> Hello World!\n"); }
      
      







ここではすべてがおなじみでシンプルです。 printfおよびclear_screen関数の追加については、後で説明します。 それまでの間、Grubでロードできるように、このコードを必要なもので補完する必要があります。

カーネルをマルチブート形式にするには、カーネルイメージの最初の8キロバイトに次の構造が必要です。



0x1BADB002 =マジック





マルチブート署名





0x0 =フラグ





カーネルとローダー(カーネル)に渡されるパラメーターをロードするための追加要件を含むフラグ。 この場合、すべてのフラグがリセットされます。





0xE4524FFE =-(マジック+フラグ)





チェックサム。









これらすべての条件が満たされている場合、Grubは%eaxおよび%ebxを通過し、それぞれマルチブート情報構造および値0x1BADB002へのポインターを登録します。 マルチブート情報構造には、ロードされたモジュールとその場所のリストを含むさまざまな情報が含まれています。これらの情報は、システムをさらにロードするために必要になる場合があります。

プログラムファイルに必要な署名を含めるには、次の内容のloader.sファイルを作成します。



 .text .global loader # making entry point visible to linker # setting up the Multiboot header - see GRUB docs for details .set FLAGS, 0x0 # this is the Multiboot 'flag' field .set MAGIC, 0x1BADB002 # 'magic number' lets bootloader find the header .set CHECKSUM, -(MAGIC + FLAGS) # checksum required .align 4 .long MAGIC .long FLAGS .long CHECKSUM # reserve initial kernel stack space .set STACKSIZE, 0x4000 # that is, 16k. .lcomm stack, STACKSIZE # reserve 16k stack .comm mbd, 4 # we will use this in kmain .comm magic, 4 # we will use this in kmain loader: movl $(stack + STACKSIZE), %esp # set up the stack movl %eax, magic # Multiboot magic number movl %ebx, mbd # Multiboot data structure call main # call C code cli hang: hlt # halt machine should kernel return jmp hang
      
      







コードをより詳細に検討してください。 このコードはwiki.osdev.org/Bare_Bonesからほとんど変更されていません 。 gccはコンパイルに使用されるため、GAS構文が使用されます。 このコードの機能を詳しく見てみましょう。

 .text
      
      





後続のコードはすべて、実行可能セクション.textに分類されます。

 .global loader
      
      





リンカから見えるローダーシンボルを宣言します。 これは、リンカーがローダーをエントリポイントとして使用するために必要です。

  .set FLAGS, 0x0 #  FLAGS = 0x0 .set MAGIC, 0x1BADB002 #  MAGIC = 0x1BADB002 .set CHECKSUM, -(MAGIC + FLAGS) #  CHECKSUM = -(MAGIC + FLAGS) .align 4 #     4  .long MAGIC #      MAGIC .long FLAGS #      FLAGS .long CHECKSUM #      CHECKSUM
      
      





このコードは、マルチブート形式の署名を形成します。 .setディレクティブは、文字の値をコンマの右側の式に設定します。 .align 4ディレクティブは、後続のコンテンツを4バイトに揃えます。 .longディレクティブは、次の4バイトに値を格納します。

  .set STACKSIZE, 0x4000 #  STACKSIZE = 0x4000 .lcomm stack, STACKSIZE #  STACKSIZE . stack    .comm mbd, 4 #  4    mdb   COMMON .comm magic, 4 #  4    magic   COMMON
      
      





ブートプロセス中、grubはスタックを構成しません。カーネルが最初に行うべきことは、スタックを構成することです。このために、0x4000(16Kb)バイトを予約します。 .lcommディレクティブは、.bssセクションで、小数点の後に指定されたバイト数を予約します。 名前スタックは、コンパイルされたファイルでのみ表示されます。 .commディレクティブは.lcommと同じですが、シンボル名はグローバルに宣言されます。 これは、Cコードで次の行を記述することで使用できることを意味します。

extern int magic



そして今、最後の部分:

 loader: movl $(stack + STACKSIZE), %esp #   movl %eax, magic #  %eax   magic movl %ebx, mbd #  %ebx   mbd call main #   main cli #     hang: hlt #       jmp hang #    hang
      
      







最初の命令は、スタックのトップの値を%espレジスタに保存します。 スタックが大きくなると、スタックに割り当てられた範囲の終わりのアドレスが%espに書き込まれます。 後続の2つの命令は、Grubが%eax、%ebxレジスタに渡す値を4バイトの以前に予約された範囲に保存します。 次に、すでにCで記述されているメイン関数が呼び出されます。 この手順から戻ると、プロセッサはループします。



ステップ2.プログラム(システムライブラリ)の追加コードの準備:





プログラム全体がゼロから作成されるため、printf関数はゼロから作成する必要があります。 これを行うには、いくつかのファイルを準備します。

共通およびインクルードフォルダーを作成します。



 mkdir common mkdir include
      
      







一般的なprintf関数の実装を含むファイルcommon \ printf.cを作成します。 このファイル全体は、 www.bitvisor.orgプロジェクトから取得できます。 bitvisorのソース内のファイルへのパス:core / printf.c。 ターゲットプログラムで使用するために、bitvisorからコピーされたprintf.cファイルで、次の行を置き換える必要があります。



 #include "initfunc.h" #include "printf.h" #include "putchar.h" #include "spinlock.h"
      
      





行ごと:

 #include "types.h" #include "stdarg.h" #include "screen.h"
      
      







次に、このファイルからprintf_init_global関数とそのすべての参照を削除します。



 static void printf_init_global (void) { spinlock_init (&printf_lock); } INITFUNC ("global0", printf_init_global);
      
      







次に、このファイル内のprintf_lock変数とそのすべての参照を削除します。

 static spinlock_t printf_lock; … spinlock_lock (&printf_lock); … spinlock_unlock (&printf_lock);
      
      







printf関数はputchar関数を使用しますが、これも記述する必要があります。 これを行うには、次の内容のファイルcommon \ screen.cを作成します。

 #include "types.h" #define GREEN 0x2 #define MAX_COL 80 // Maximum number of columns #define MAX_ROW 25 // Maximum number of rows #define VRAM_SIZE (MAX_COL*MAX_ROW) // Size of screen, in short's #define DEF_VRAM_BASE 0xb8000 // Default base for video memory static unsigned char curr_col = 0; static unsigned char curr_row = 0; // Write character at current screen location #define PUT(c) ( ((unsigned short *) (DEF_VRAM_BASE)) \ [(curr_row * MAX_COL) + curr_col] = (GREEN << 8) | (c)) // Place a character on next screen position static void cons_putc(int c) { switch (c) { case '\t': do { cons_putc(' '); } while ((curr_col % 8) != 0); break; case '\r': curr_col = 0; break; case '\n': curr_row += 1; if (curr_row >= MAX_ROW) { curr_row = 0; } break; case '\b': if (curr_col > 0) { curr_col -= 1; PUT(' '); } break; default: PUT(c); curr_col += 1; if (curr_col >= MAX_COL) { curr_col = 0; curr_row += 1; if (curr_row >= MAX_ROW) { curr_row = 0; } } }; } void putchar( int c ) { if (c == '\n') cons_putc('\r'); cons_putc(c); } void clear_screen( void ) { curr_col = 0; curr_row = 0; int i; for (i = 0; i < VRAM_SIZE; i++) cons_putc(' '); curr_col = 0; curr_row = 0; }
      
      







指定されたコードには、テキストモードで画面に文字を印刷するための簡単なロジックが含まれています。 このモードでは、2バイトを使用して文字(1つは文字コード、もう1つはその属性)を書き込み、画面にすぐに表示されるアドレス0xB8000で始まるビデオメモリに直接書き込みます。 画面解像度は80x25文字です。 文字は、PUTマクロを使用して直接印刷されます。

いくつかのヘッダーファイルのみが欠落しています。

1.ファイルには\ screen.hが含まれます。 printf関数で使用されるputchar関数を宣言します。 ファイルの内容:

 #ifndef _SCREEN_H #define _SCREEN_H void clear_screen( void ); void putchar( int c ); #endif
      
      







2.ファイルには\ printf.hが含まれます。 mainで使用されるprintf関数を宣言します。 ファイルの内容:

 #ifndef _PRINTF_H #define _PRINTF_H int printf (const char *format, ...); #endif
      
      







3.ファイルには\ stdarg.hが含まれます。 事前に数がわからない引数を反復処理する関数を宣言します。 ファイル全体はwww.bitvisor.orgプロジェクトから取得されます。 bitvisorプロジェクトコード内のファイルへのパス:include \ core \ stdarg.h。

4.ファイルには\ types.hが含まれます。 NULLおよびsize_tを宣言します。 ファイルの内容:

 #ifndef _TYPES_H #define _TYPES_H #define NULL 0 typedef unsigned int size_t; #endif
      
      





したがって、インクルードフォルダーと共通フォルダーには、プログラムに必要な最小限のシステムライブラリコードが含まれています。



ステップ3.リンカー用のスクリプトの作成:





リンカがターゲットプログラムファイル(kernel.bin)を生成するために使用する、linker.ldファイルを作成します。 ファイルには次のものが含まれている必要があります。



 ENTRY (loader) LMA = 0x00100000; SECTIONS { . = LMA; .multiboot ALIGN (0x1000) : { loader.o( .text ) } .text ALIGN (0x1000) : { *(.text) } .rodata ALIGN (0x1000) : { *(.rodata*) } .data ALIGN (0x1000) : { *(.data) } .bss : { *(COMMON) *(.bss) } /DISCARD/ : { *(.comment) } }
      
      







組み込み関数ENTRY()を使用すると、カーネルのエントリポイントを設定できます。 カーネルの起動後にgrubが制御を渡すのはこのアドレスです。 このスクリプトを使用して、リンカーはELF形式のバイナリファイルを作成します。 ELFファイルは、一連のセグメントとセクションで構成されています。 セグメントのリストは、プログラムヘッダーテーブル、セクションヘッダーテーブルのセクションのリストに含まれています。 リンカはセクション、イメージローダー(この場合はGRUB)、セグメントを使用して動作します。





図からわかるように、セグメントはセクションで構成されています。 セクションを記述するフィールドの1つは、セクションが実行時に存在する仮想アドレスです。 実際、セグメントには、その場所を記述する2つのフィールドがあります。セグメントの仮想アドレスとセグメントの物理アドレスです。 セグメントの仮想アドレスは、コード実行時のセグメントの最初のバイトの仮想アドレスです。セグメントの物理アドレスは、セグメントをロードする物理アドレスです。 アプリケーションの場合、これらのアドレスは常に一致します。 Grubは、物理アドレスでイメージセグメントをダウンロードします。 Grubはページのアドレス指定を構成しないため、プログラムでは仮想メモリも構成されていないため、セグメントの仮想アドレスは物理アドレスと一致する必要があります。



 SECTIONS
      
      





セクションについてさらに説明します。

 . = LMA;
      
      





この式は、後続のすべてのセクションがLMAアドレスの後にあることをリンカーに示します。

  ALIGN (0x1000)
      
      





上記のディレクティブは、セクションが0x1000バイトに揃えられることを意味します。

 .multiboot ALIGN (0x1000) : { loader.o( .text ) }
      
      





loader.oファイルの.textセクションを含む別のマルチブートセクションは、マルチブート形式の署名がカーネルイメージの最初の8kbに入るようにするために設計されています。

 .bss : { *(COMMON) *(.bss) }
      
      





*(COMMON)は、命令.commおよび.lcommによってメモリが予約される領域です。 .bssセクションに配置します。

 /DISCARD/ : { *(.comment) }
      
      





DISCARDとマークされたすべてのセクションが画像から削除されます。 この場合、リンカーのバージョンに関する情報を含む.commentセクションを削除します。



次のコマンドを使用して、コードをバイナリファイルにコンパイルします。

 as -o loader.o loader.s gcc -Iinclude -Wall -fno-builtin -nostdinc -nostdlib -o kernel.o -c kernel.c gcc -Iinclude -Wall -fno-builtin -nostdinc -nostdlib -o printf.o -c common/printf.c gcc -Iinclude -Wall -fno-builtin -nostdinc -nostdlib -o screen.o -c common/screen.c ld -T linker.ld -o kernel.bin kernel.o screen.o printf.o loader.o
      
      





objdumpを使用して、リンク後のカーネルイメージがどのようになるかを見ていきます。

 objdump -ph ./kernel.bin
      
      











ご覧のとおり、イメージ内のセクションは、リンカスクリプトで説明したセクションと一致しています。 リンカーは、説明されたセクションから3つのセグメントを形成しました。 最初のセグメントには、セクション.multiboot、.text、.rodataが含まれ、仮想および物理アドレス0x00100000があります。 2番目のセグメントには、.dataセクションと.bssセクションが含まれ、0x00104000にあります。 これで、Grubを使用してこのファイルをダウンロードする準備がすべて整いました。



ステップ4. Grubブートローダーの準備:

grubフォルダーを作成します。

 mkdir grub
      
      







イメージにインストールするために必要ないくつかのGrubファイルをこのフォルダーにコピーします(Grubがシステムにインストールされている場合、以下のファイルが存在します)。 これを行うには、次のコマンドを実行します。

 cp /usr/lib/grub/i386-pc/stage1 ./grub/ cp /usr/lib/grub/i386-pc/stage2 ./grub/ cp /usr/lib/grub/i386-pc/fat_stage1_5 ./grub/
      
      







次の内容でgrub / menu.lstファイルを作成します。

 timeout 3 default 0 title mini_os root (hd0,0) kernel /kernel.bin
      
      







手順5.ブートイメージを自動化して作成します。



ビルドプロセスを自動化するには、makeユーティリティを使用します。 これを行うには、コンパイルしてソースコードをコンパイルし、カーネルをコンパイルし、ブートイメージを作成するメイクファイルを作成します。 メイクファイルには次の内容が含まれている必要があります。



 CC = gcc CFLAGS = -Wall -fno-builtin -nostdinc -nostdlib LD = ld OBJFILES = \ loader.o \ common/printf.o \ common/screen.o \ kernel.o image: @echo "Creating hdd.img..." @dd if=/dev/zero of=./hdd.img bs=512 count=16065 1>/dev/null 2>&1 @echo "Creating bootable first FAT32 partition..." @losetup /dev/loop1 ./hdd.img @(echo c; echo u; echo n; echo p; echo 1; echo ; echo ; echo a; echo 1; echo t; echo c; echo w;) | fdisk /dev/loop1 1>/dev/null 2>&1 || true @echo "Mounting partition to /dev/loop2..." @losetup /dev/loop2 ./hdd.img \ --offset `echo \`fdisk -lu /dev/loop1 | sed -n 10p | awk '{print $$3}'\`*512 | bc` \ --sizelimit `echo \`fdisk -lu /dev/loop1 | sed -n 10p | awk '{print $$4}'\`*512 | bc` @losetup -d /dev/loop1 @echo "Format partition..." @mkdosfs /dev/loop2 @echo "Copy kernel and grub files on partition..." @mkdir -p tempdir @mount /dev/loop2 tempdir @mkdir tempdir/boot @cp -r grub tempdir/boot/ @cp kernel.bin tempdir/ @sleep 1 @umount /dev/loop2 @rm -r tempdir @losetup -d /dev/loop2 @echo "Installing GRUB..." @echo "device (hd0) hdd.img \n \ root (hd0,0) \n \ setup (hd0) \n \ quit\n" | grub --batch 1>/dev/null @echo "Done!" all: kernel.bin rebuild: clean all .so: as -o $@ $< .co: $(CC) -Iinclude $(CFLAGS) -o $@ -c $< kernel.bin: $(OBJFILES) $(LD) -T linker.ld -o $@ $^ clean: rm -f $(OBJFILES) hdd.img kernel.bin
      
      







ファイルでは、2つの主な目標が宣言されています。all-カーネルをコンパイルし、image-ブートディスクを作成します。 通常のメイクファイルと同様に、すべてのターゲットには、* .sおよび* .cファイルをオブジェクトファイル(* .o)にコンパイルする.soおよび.coのサブゴールと、以前に作成されたスクリプトでリンカーを呼び出すkernel.binを生成するためのターゲットが含まれます。 これらの目標は、ステップ3にリストされているコマンドとまったく同じコマンドを実行します。

ここで最も興味深いのは、ブートイメージhdd.img(ターゲットイメージ)の作成です。 これがどのように起こるかを段階的に考えてみましょう。

 dd if=/dev/zero of=./hdd.img bs=512 count=16065 1>/dev/null 2>&1
      
      





このコマンドは、さらに作業が行われるイメージを作成します。 セクターの数は偶然選択されませんでした:16065 = 255 *63。デフォルトでは、fdsikはディスクでCHSジオメトリを持っているかのように動作します。ヘッダー(H)= 255、セクター(S)= 63、シリンダー(C)はディスクサイズ。 したがって、fdsikユーティリティがデフォルトのジオメトリを変更せずに使用できる最小ディスクサイズは、512 * 255 * 63 * 1 = 8225280バイトです。512はセクターサイズで、1はシリンダー数です。

次に、パーティションテーブルが作成されます。

 losetup /dev/loop1 ./hdd.img (echo c; echo u; echo n; echo p; echo 1; echo ; echo ; echo a; echo 1; echo t; echo c; echo w;) | fdisk /dev/loop1 1>/dev/null 2>&1 || true
      
      





最初のコマンドは、hdd.imgファイルをブロックデバイス/ dev / loop1にマウントし、ファイルをデバイスとして操作できるようにします。 2番目のコマンドは、デバイス/ dev / loop1にパーティションテーブルを作成します。このテーブルには、ディスクのプライマリブートパーティションが1つあり、ディスク全体を占有し、FAT32ファイルシステムラベルが付いています。

次に、作成したセクションをフォーマットします。 これを行うには、ブロックデバイスとしてマウントし、フォーマットを実行します。

 losetup /dev/loop2 ./hdd.img \ --offset `echo \`fdisk -lu /dev/loop1 | sed -n 10p | awk '{print $$3}'\`*512 | bc` \ --sizelimit `echo \`fdisk -lu /dev/loop1 | sed -n 10p | awk '{print $$4}'\`*512 | bc` losetup -d /dev/loop1
      
      





最初のコマンドは、以前に作成されたパーティションをデバイス/ dev / loop2にマウントします。 -offsetオプションはセクションの先頭のアドレスを指定し、-sizelimitはセクションの末尾のアドレスを指定します。 両方のパラメーターは、fdiskコマンドを使用して取得されます。

 mkdosfs /dev/loop2
      
      





mkdosfsユーティリティは、パーティションをFAT32ファイルシステムにフォーマットします。

カーネルを直接組み立てるために、以前に説明したコマンドが古典的なmakefile構文で使用されます。

次に、パーティションにGRUBをインストールする方法を検討します。

 mkdir -p tempdir #    mount /dev/loop2 tempdir #     mkdir tempdir/boot #   /boot   cp -r grub tempdir/boot/ #   grub  /boot cp kernel.bin tempdir/ #      sleep 1 #  Ubuntu umount /dev/loop2 #    rm -r tempdir #    losetup -d /dev/loop2 #  
      
      





上記のコマンドを実行すると、イメージはGRUBをインストールする準備が整います。 次のコマンドは、hdd.imgディスクイメージのMBRにGRUBをインストールします。

 echo "device (hd0) hdd.img \n \ root (hd0,0) \n \ setup (hd0) \n \ quit\n" | grub --batch 1>/dev/null
      
      







すべてをテストする準備ができました!



ステップ6.起動:





コンパイルするには、次のコマンドを使用します。

 make all
      
      





その後、kernel.binファイルが表示されます。

ブートディスクイメージを作成するには、次のコマンドを使用します。

 sudo make image
      
      





その結果、hdd.imgファイルが表示されます。

これで、hdd.imgディスクイメージから起動できます。 次のコマンドでこれを確認できます。

 qemu -hda hdd.img -m 32
      
      





または:

 qemu-system-i386 -hda hdd.img
      
      











実際のマシンで確認するには、フラッシュドライブでこのイメージをddにして、そこから起動する必要があります。 たとえば、次のコマンドでは:

 sudo dd if=./hdd.img of=/dev/sdb
      
      







要約すると、実行されたアクションの結果として、システムプログラミングの分野でさまざまな実験を行うことができるソースとスクリプトのセットを取得できたと言えます。 最初のステップは、ハイパーバイザーやオペレーティングシステムなどのシステムソフトウェアの作成に向けて行われました。



シリーズの次の記事へのリンク:

" オペレーティングシステムなしでプログラムを実行する方法: パート2 "

オペレーティングシステムなしでプログラムを実行する方法: パート3:グラフィックス

オペレーティングシステムなしでプログラムを実行する方法: パート4.並列計算

オペレーティングシステムなしでプログラムを実行する方法: パート5. OSからBIOSにアクセスする

オペレーティングシステムなしでプログラムを実行する方法: パート6. FATファイルシステムでディスクを操作するためのサポート



All Articles