web-dev-qa-db-ja.com

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

オペレーティングシステムを実行せずにプログラムを単独で実行する方法を教えてください。起動時にコンピュータがロードして実行できるアセンブリプログラムを作成できますか。フラッシュドライブからコンピュータを起動し、それはCPU上にあるプログラムを実行しますか?

214
user2320609

オペレーティングシステムを実行せずにプログラムを単独で実行する方法を教えてください。

再起動後にプロセッサが探す場所にバイナリコードを配置します(例:ARMのアドレス0)。

起動時にコンピュータがロードして実行できるアセンブリプログラムを作成できますか(たとえば、フラッシュドライブからコンピュータを起動し、ドライブ上にあるプログラムを実行するなど)。

質問に対する一般的な回答:それは可能です。それはしばしば「ベアメタルプログラミング」と呼ばれます。フラッシュドライブから読み取るためには、USBが何であるかを知りたい、そしてあなたはこのUSBで動作するようにいくつかのドライバを持っていたいです。このドライブ上のプログラムは、特定のファイルシステム上の特定のフォーマットである必要があります。これは、ブートローダが通常行うことですが、ファームウェアに必要な場合に限り、プログラムに独自のブートローダを含めることができる小さなコードブロックをロードします。

ARMボードの多くは、それらを可能にします。基本的なセットアップを手助けするブートローダーを持っている人もいます。

ここで Raspberry Piで基本的なオペレーティングシステムを実行する方法に関する素晴らしいチュートリアルを見つけることができます。

編集:この記事、そして全体のwiki.osdev.orgはあなたの質問の大部分を助長します http://wiki.osdev.org/はじめに

また、ハードウェアで直接試したくない場合は、qemuのようなハイパーバイザーを使用して仮想マシンとして実行することができます。仮想化されたARMハードウェア で直接「hello world」を実行する方法はこちら を参照してください。

138
Kissiel

実行可能な例

OSなしで実行される、ごくわずかなベアメタルHello Worldプログラムを作成して実行しましょう。

また、QEMUエミュレーターでできる限り安全に試してみてください。開発の方がより安全で便利です。 QEMUテストは、QEMU 2.11.1が事前にパッケージ化されたUbuntu 18.04ホストで行われました。

以下のすべてのx86サンプルのコードは this GitHub repo にあります。

x86の実際のハードウェアでサンプルを実行する方法

実際のハードウェアでサンプルを実行するのは危険です。誤ってディスクを消去したり、ハードウェアを破壊したりする可能性があります。これは、重要なデータが含まれていない古いマシンでのみ実行してください。あるいは、Raspberry Piなどの安価な半使い捨ての開発ボードを使用することもできます。以下のARMの例を参照してください。

典型的なx86ラップトップの場合、次のようなことをする必要があります。

  1. 画像をUSBスティックに書き込みます(データが破壊されます!):

    Sudo dd if=main.img of=/dev/sdX
    
  2. uSBをコンピューターに接続します

  3. それをオン

  4. uSBから起動するように指示します。

    これは、ファームウェアがハードディスクの前にUSBを選択することを意味します。

    それがお使いのマシンのデフォルトの動作ではない場合、電源投入後、USBから起動することを選択できる起動メニューが表示されるまで、Enter、F12、ESCなどの奇妙なキーを押し続けます。

    多くの場合、これらのメニューで検索順序を構成できます。

たとえば、T430では次のように表示されます。

電源を入れた後、Enterキーを押してブートメニューに入る必要があります。

enter image description here

次に、ここでF12を押して、ブートデバイスとしてUSBを選択する必要があります。

enter image description here

そこから、次のようにUSBを起動デバイスとして選択できます。

enter image description here

または、起動順序を変更し、USBの優先順位を高くするために毎回手動で選択する必要がないようにするには、[スタートアップ割り込みメニュー]画面でF1を押してから、次の場所に移動します。

enter image description here

ブートセクタ

X86でできる最も簡単で最も低いレベルのことは、 マスターブートセクター(MBR) を作成することです。これは ブートセクター のタイプであり、次にインストールしますディスク。

ここでは、1つのprintf呼び出しで1つ作成します。

printf '\364%509s\125\252' > main.img
Sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img

結果:

enter image description here

何もしなくても、いくつかの文字がすでに画面に印刷されていることに注意してください。これらはファームウェアによって印刷され、システムを識別するのに役立ちます。

T430では、カーソルが点滅する空白の画面が表示されます。

enter image description here

main.imgには次が含まれます。

  • 8進数の\364 == 16進数の0xf4hlt命令のエンコード。CPUに動作を停止するよう指示します。

    したがって、プログラムは何も行いません。開始と停止のみです。

    \x 16進数はPOSIXで指定されていないため、8進数を使用します。

    このエンコードは次の方法で簡単に取得できます。

    echo hlt > a.S
    as -o a.o a.S
    objdump -S a.o
    

    どの出力:

    a.o:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    0000000000000000 <.text>:
       0:   f4                      hlt
    

    もちろん、Intelのマニュアルにも記載されています。

  • %509sは509個のスペースを生成します。バイト510までファイルに入力する必要がありました。

  • \125\252 8進数== 0x55の後に0xaaが続きます。

    これらは、511バイトと512バイトの2つの必須マジックバイトです。

    BIOSは、ブート可能なディスクを探すためにすべてのディスクを通過し、これらの2つのマジックバイトを持つブート可能なディスクのみを考慮します。

    存在しない場合、ハードウェアはこれを起動可能なディスクとして扱いません。

printfマスターでない場合は、main.imgの内容を次の方法で確認できます。

hd main.img

予想されるものを示します:

00000000  f4 20 20 20 20 20 20 20  20 20 20 20 20 20 20 20  |.               |
00000010  20 20 20 20 20 20 20 20  20 20 20 20 20 20 20 20  |                |
*
000001f0  20 20 20 20 20 20 20 20  20 20 20 20 20 20 55 aa  |              U.|
00000200

ここで、20はASCIIのスペースです。

BIOSファームウェアはそれらの512バイトをディスクから読み取り、メモリに入れ、PCを最初のバイトに設定して実行を開始します。

Hello worldブートセクター

最小限のプログラムを作成したので、こんにちは世界に移りましょう。

明らかな質問は次のとおりです。IOの実行方法いくつかのオプション:

  • ファームウェアを尋ねます、例えばBIOSまたはUEFI、私たちのためにそれを行う
  • VGA:書き込まれた場合に画面に印刷される特別なメモリ領域。保護モードで使用できます。
  • ドライバーを作成し、ディスプレイハードウェアと直接話します。これはそれを行うための「適切な」方法です。より強力ですが、より複雑です。
  • シリアルポート 。これは、ホスト端末と文字を送受信する非常に単純な標準化されたプロトコルです。

    デスクトップでは、次のようになります。

    enter image description here

    ソース

    残念ながら、ほとんどの最新のラップトップでは公開されていませんが、開発ボードを使用する一般的な方法です。以下のARM例を参照してください。

    このようなインターフェースは本当に便利なので、これは本当に残念です たとえばLinuxカーネルをデバッグするために

  • チップのデバッグ機能を使用します。 ARMは、それらを呼び出します セミホスティング など。実際のハードウェアでは、追加のハードウェアとソフトウェアのサポートが必要ですが、エミュレーターでは無料の便利なオプションになります。

ここでは、x86でより簡単なBIOSの例を実行します。ただし、これは最も堅牢な方法ではないことに注意してください。

main.S

.code16
    mov $msg, %si
    mov $0x0e, %ah
loop:
    lodsb
    or %al, %al
    jz halt
    int $0x10
    jmp loop
halt:
    hlt
msg:
    .asciz "hello world"

GitHubアップストリーム

link.ld

SECTIONS
{
    /* The BIOS loads the code from the disk to this location.
     * We must tell that to the linker so that it can properly
     * calculate the addresses of symbols we might jump to.
     */
    . = 0x7c00;
    .text :
    {
        __start = .;
        *(.text)
        /* Place the magic boot bytes at the end of the first 512 sector. */
        . = 0x1FE;
        SHORT(0xAA55)
    }
}

アセンブルとリンク:

as -g -o main.o main.S
ld --oformat binary -o main.img -T link.ld main.o
qemu-system-x86_64 -hda main.img

結果:

enter image description here

T430の場合:

enter image description here

テスト済み:Lenovo Thinkpad T430、UEFI BIOS 1.16。 Ubuntu 18.04ホストで生成されたディスク。

標準のユーザーランドアセンブリ手順の他に、次のものがあります。

  • .code16:GASに16ビットコードを出力するよう指示します

  • cli:ソフトウェア割り込みを無効にします。これらは、hltの後にプロセッサの実行を再開させる可能性があります

  • int $0x10:BIOS呼び出しを行います。これは、文字を1つずつ印刷するものです。

重要なリンクフラグは次のとおりです。

  • --oformat binary:生のバイナリアセンブリコードを出力します。通常のユーザーランド実行可能ファイルの場合のように、ELFファイル内にラップしないでください。

リンカスクリプトの部分をよりよく理解するには、リンクの再配置手順に精通してください。 リンカは何をしますか?

Cooler x86ベアメタルプログラム

以下は、私が達成したいくつかのより複雑なベアメタル設定です。

アセンブリの代わりにCを使用

要約:GRUBマルチブートを使用します。これにより、考えもしなかった多くの迷惑な問題が解決されます。以下のセクションを参照してください。

X86の主な難点は、BIOSがディスクからメモリに512バイトしかロードしないことであり、Cを使用するとこれらの512バイトが爆発する可能性があります。

それを解決するために、 two-stage bootloader を使用できます。これにより、さらにBIOS呼び出しが行われ、ディスクからメモリにより多くのバイトがロードされます。 int 0x13 BIOS calls を使用したゼロからの最小限のステージ2アセンブリの例を次に示します。

代わりに:

  • qEMUでのみ動作し、実際のハードウェアでは動作しない場合は、-kernelオプションを使用します。これにより、ELFファイル全体がメモリにロードされます。 このメソッドで作成したARMの例
  • raspberry Piの場合、デフォルトのファームウェアが、QEMU kernel7.imgと同様に、-kernelという名前のELFファイルからのイメージの読み込みを処理します。

教育目的のみのために、 1段階の最小Cの例 を示します。

main.c

void main(void) {
    int i;
    char s[] = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
    for (i = 0; i < sizeof(s); ++i) {
        __asm__ (
            "int $0x10" : : "a" ((0x0e << 8) | s[i])
        );
    }
    while (1) {
        __asm__ ("hlt");
    };
}

entry.S

.code16
.text
.global mystart
mystart:
    ljmp $0, $.setcs
.setcs:
    xor %ax, %ax
    mov %ax, %ds
    mov %ax, %es
    mov %ax, %ss
    mov $__stack_top, %esp
    cld
    call main

リンカー.ld

ENTRY(mystart)
SECTIONS
{
  . = 0x7c00;
  .text : {
    entry.o(.text)
    *(.text)
    *(.data)
    *(.rodata)
    __bss_start = .;
    /* COMMON vs BSS: https://stackoverflow.com/questions/16835716/bss-vs-common-what-goes-where */
    *(.bss)
    *(COMMON)
    __bss_end = .;
  }
  /* https://stackoverflow.com/questions/53584666/why-does-gnu-ld-include-a-section-that-does-not-appear-in-the-linker-script */
  .sig : AT(ADDR(.text) + 512 - 2)
  {
      SHORT(0xaa55);
  }
  /DISCARD/ : {
    *(.eh_frame)
  }
  __stack_bottom = .;
  . = . + 0x1000;
  __stack_top = .;
}

走る

set -eux
as -ggdb3 --32 -o entry.o entry.S
gcc -c -ggdb3 -m16 -ffreestanding -fno-PIE -nostartfiles -nostdlib -o main.o -std=c99 main.c
ld -m elf_i386 -o main.elf -T linker.ld entry.o main.o
objcopy -O binary main.elf main.img
qemu-system-x86_64 -drive file=main.img,format=raw

C標準ライブラリ

しかし、C標準ライブラリの機能の多くを実装するLinuxカーネルがないため、C標準ライブラリも使用したい場合は、もっと楽しくなります POSIXを介して

Linuxのような本格的なOSに移行することなく、いくつかの可能性があります。

  • 自分で書いてください。最終的には単なるヘッダーとCファイルの束に過ぎませんか?右??

  • Newlib

    詳細な例: https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931

    Newlibはすべての退屈な非OS固有のものを実装します。 memcmpmemcpyなど.

    次に、必要なsyscallを実装するためのスタブをいくつか提供します。

    たとえば、ARMにexit()を実装するには、セミホスティングを使用します。

    void _exit(int status) {
        __asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456");
    }
    

    この例では で示されています。

    たとえば、printfをUARTまたはARMシステムにリダイレクトしたり、 セミホスティングexit()を実装したりできます。

  • FreeRTOSZephyr などの組み込みオペレーティングシステム。

    このようなオペレーティングシステムでは、通常、プリエンプティブスケジューリングをオフにできるため、プログラムのランタイムを完全に制御できます。

    これらは、事前実装されたNewlibの一種と見ることができます。

GNU GRUBマルチブート

ブートセクターは単純ですが、あまり便利ではありません。

  • ディスクごとに1つのOSのみを使用できます
  • ロードコードは本当に小さく、512バイトに収まらなければなりません
  • 保護モードに移行するなど、多くのスタートアップを自分で行う必要があります

GNU GRUB がマルチブートと呼ばれるより便利なファイル形式を作成したのは、こうした理由からです。

最小限の作業例: https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world

GitHub examples repo でも使用して、USBを100万回書き込むことなく、実際のハードウェアですべての例を簡単に実行できるようにします。

QEMUの結果:

enter image description here

T430:

enter image description here

OSをマルチブートファイルとして準備すると、GRUBは通常のファイルシステム内でそれを見つけることができます。

これは、ほとんどのディストリビューションがOSイメージを/bootの下に置くことです。

マルチブートファイルは基本的に、特別なヘッダーを持つELFファイルです。それらは次の場所でGRUBで指定されます: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html

grub-mkrescueを使用して、マルチブートファイルをブート可能なディスクに変換できます。

ファームウェア

実際、ブートセクタは、システムのCPUで実行される最初のソフトウェアではありません。

実際に最初に実行されるのは、いわゆるfirmwareです。これはソフトウェアです。

  • ハードウェアメーカーによって作られた
  • 通常はクローズドソースですが、Cベースの可能性があります
  • 読み取り専用メモリに保存されるため、ベンダーの同意なしに変更することは難しく/不可能です。

よく知られているファームウェアは次のとおりです。

  • BIOS :古いall-present x86ファームウェア。 SeaBIOSは、QEMUで使用されるデフォルトのオープンソース実装です。
  • UEFI :BIOSの後継、より標準化されているが、より能力があり、信じられないほど肥大化している。
  • Coreboot :高貴なクロスArchオープンソースの試み

ファームウェアは次のようなことを行います:

  • 起動可能なものが見つかるまで、各ハードディスク、USB、ネットワークなどをループします。

    QEMUを実行すると、-hdaは、main.imgがハードウェアに接続されたハードディスクであり、hdaが最初に試行され、使用されることを示します。

  • 最初の512バイトをRAMメモリアドレス0x7c00にロードし、CPUのRIPをそこに置いて実行させます

  • ディスプレイにブートメニューやBIOS印刷呼び出しなどを表示する

ファームウェアは、ほとんどのOSが依存するOSのような機能を提供します。例えば。 PythonサブセットがBIOS/UEFIで実行するように移植されました: https://www.youtube.com/watch?v=bYQ_lq5dcvM

ファームウェアはOSと見分けがつかず、ファームウェアは唯一可能な「真の」ベアメタルプログラミングであると主張できます。

このように CoreOS開発者はそれを置く

難しい部分

PCの電源を入れたとき、チップセットを構成するチップ(ノースブリッジ、サウスブリッジ、SuperIO)はまだ適切に初期化されていません。 BIOS ROMはCPUから可能な限り削除されていますが、CPUからアクセス可能である必要があります。そうでない場合、CPUには実行する命令がありません。これは、BIOS ROMが完全にマップされることを意味するものではなく、通常はマップされません。しかし、ブートプロセスを開始するのに十分なだけがマップされています。他のデバイスは、忘れてください。

QEMUでCorebootを実行すると、Corebootの上位層とペイロードを試すことができますが、QEMUは低レベルのスタートアップコードを試す機会がほとんどありません。一つには、RAMは最初から動作するだけです。

BIOS初期状態のポスト

ハードウェアの多くのものと同様に、標準化は弱く、notに依存すべきでないものの1つは、BIOSの後にコードが実行を開始するときのレジスタの初期状態です。

だから、あなた自身に賛成して、次のようないくつかの初期化コードを使用してください: https://stackoverflow.com/a/32509555/895245

%ds%esのようなレジスターには重要な副作用があるため、明示的に使用していなくてもゼロにする必要があります。

一部のエミュレーターは実際のハードウェアよりも優れており、初期状態が良いことに注意してください。その後、実際のハードウェアで実行すると、すべてが壊れます。

エルトリト

CDに書き込むことができる形式: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29

ISOまたはUSBで動作するハイブリッドイメージを作成することもできます。これは、grub-mkrescue )で実行できます。また、isohybridを使用して、make isoimage上のLinuxカーネルでも実行できます。

ARM

ARMでは、一般的な考え方は同じです。

IOに使用するBIOSのような広く利用可能な準標準化されたプリインストールファームウェアはないため、できる2つの最も簡単なIOタイプは次のとおりです。

  • シリアル、開発ボードで広く利用可能
  • lEDを点滅させる

アップロードしました:

X86との違いは次のとおりです。

  • IOは、マジックアドレスに直接書き込むことによって行われます。inおよびout命令はありません。

    これは memory mapped IO と呼ばれます。

  • raspberry Piなどの一部の実際のハードウェアでは、ファームウェア(BIOS)を自分でディスクイメージに追加できます。

    ファームウェアの更新がより透過的になるため、これは良いことです。

リソース