オペレーティングシステムなしでプログラムを実行するにはどうすればいいですか? 質問する

オペレーティングシステムなしでプログラムを実行するにはどうすればいいですか? 質問する

オペレーティング システムを実行せずに、プログラムだけを実行するにはどうすればよいでしょうか。コンピューターが起動時に読み込んで実行できるアセンブリ プログラムを作成できますか。たとえば、コンピューターをフラッシュ ドライブから起動すると、CPU 上のプログラムが実行されます。

ベストアンサー1

実行可能な例

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

また、開発にはより安全で便利なため、可能な限り QEMU エミュレーターでテストします。QEMU テストは、QEMU 2.11.1 があらかじめパッケージ化された Ubuntu 18.04 ホストで実施されています。

以下のすべてのx86サンプルのコードとその他のコードは、このGitHubリポジトリ

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

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

一般的な x86 ラップトップの場合は、次のようにする必要があります。

  1. イメージを USB スティックに書き込みます (データは破壊されます)。

    sudo dd if=main.img of=/dev/sdX
    
  2. USBをコンピュータに差し込む

  3. それをオン

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

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

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

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

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

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

あるいは、起動順序を変更して USB を優先するように選択し、毎回手動で選択しなくても済むようにするには、「スタートアップ割り込みメニュー」画面で F1 キーを押して、次の場所に移動します。

ここに画像の説明を入力してください

ブートセクタ

x86では、最もシンプルで低レベルのことは、マスターブートセクター (MBR)、これはブートセクター、ディスクにインストールします。

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

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

結果:

ここに画像の説明を入力してください

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

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

ここに画像の説明を入力してください

main.img以下が含まれます:

  • \3648 進数 == 16 進数: CPU に動作を停止するように指示する命令0xf4のエンコード。hlt

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

    \x16 進数は 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 のマニュアルにも記載されています。

  • %509s509 個のスペースを生成します。バイト 510 までファイルを埋めるのに必要です。

  • \125\2528進数では ==0x55の後に . が続きます0xaa

    これらは 2 つの必須マジック バイトであり、バイト 511 と 512 である必要があります。

    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

20ASCII ではスペースです。

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

Hello World ブートセクター

最小限のプログラムを作成したので、Hello World に移りましょう。

当然の疑問は、IO をどうやって行うかということです。いくつかのオプションがあります。

  • BIOSやUEFIなどのファームウェアにそれを実行するよう依頼する

  • VGA: 書き込まれると画面に表示される特別なメモリ領域。保護モードで使用できます。

  • ドライバーを書いて、ディスプレイ ハードウェアと直接通信します。これは「適切な」方法です。より強力ですが、より複雑です。

  • シリアルポートこれは、ホスト端末から文字を送受信する非常に単純な標準化されたプロトコルです。

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

    ここに画像の説明を入力してください

    ソース

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

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

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

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

メイン.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 アップストリーム

リンク

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

結果:

ここに画像の説明を入力してください

T430の場合:

ここに画像の説明を入力してください

テスト環境: Lenovo Thinkpad T430、UEFI BIOS 1.16。ディスクは Ubuntu 18.04 ホストで生成されました。

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

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

  • cli: ソフトウェア割り込みを無効にします。これにより、プロセッサが再起動してhlt

  • int $0x10: BIOS 呼び出しを実行します。これにより、文字が 1 つずつ印刷されます。

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

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

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

よりクールな x86 ベアメタル プログラム

私が実現した、より複雑なベアメタル セットアップをいくつか紹介します。

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

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

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

これを解決するには、2段階ブートローダーこれにより、さらにBIOS呼び出しが行われ、ディスクからメモリにさらに多くのバイトがロードされます。以下は、int 0x13 BIOS呼び出し:

あるいは:

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

教育目的のみで、ここに1 段階の最小限の C の例:

メイン.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");
    };
}

エントリー.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 ファイルの集まりに過ぎませんよね? そうですよね??

  • ニューリブ

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

    memcmpNewlib は、、、などmemcpy、退屈な OS 固有ではないものをすべて実装します。

    次に、必要なシステムコールを自分で実装するためのスタブがいくつか提供されます。

    たとえば、exit()セミホスティングを通じて ARM に実装できます。

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

    に示すようにこの例では

    たとえば、printfUARTやARMシステムにリダイレクトしたりexit()セミホスティング

  • 組み込みオペレーティングシステムフリーRTOSそしてゼファー

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

    これらは、一種の事前に実装された Newlib として考えることができます。

GNU GRUB マルチブート

ブートセクターはシンプルですが、あまり便利ではありません。

  • ディスクごとに1つのOSしか持てない
  • ロードコードは非常に小さく、512バイトに収まる必要があります。
  • 保護モードへの移行など、多くの起動作業を自分で行う必要がある

そういった理由からGNU GRUBマルチブートと呼ばれるより便利なファイル形式を作成しました。

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

私も使っていますGitHub サンプル リポジトリUSB を何百万回も焼き付けることなく、すべての例を実際のハードウェア上で簡単に実行できるようにします。

QEMU の結果:

ここに画像の説明を入力してください

T430:

ここに画像の説明を入力してください

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

ほとんどのディストリビューションでは、OS イメージを の下に置いています/boot

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

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

ファームウェア

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

実際に最初に実行されるのは、いわゆるファームウェア、つまりソフトウェアです。

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

よく知られているファームウェアには以下のものがあります。

  • BIOS: 古くて一般的な x86 ファームウェア。SeaBIOS は、QEMU で使用されるデフォルトのオープン ソース実装です。
  • UEFI: BIOS の後継。標準化は進んでいるが、機能が強化され、信じられないほど肥大化している。
  • コアブート: ノーブルクロスアーチオープンソースの試み

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

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

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

  • 最初の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 が完全にマップされていることを意味するのではなく、通常はそうではありません。ただし、ブート プロセスを開始するのに十分なだけマップされています。その他のデバイスは忘れてください。

Coreboot を QEMU で実行すると、Coreboot の上位レイヤーとペイロードを試すことができますが、QEMU では低レベルの起動コードを試す機会がほとんどありません。まず、RAM は最初から正常に動作します。

BIOS初期状態後

ハードウェアの多くの部分と同様に、標準化は弱く、依存すべきではないものの 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() で実行され、make isoimageを使用する場合には Linux カーネルによっても実行されますisohybrid

ARM でも、基本的な考え方は同じです。

IO に使用できる BIOS のような、広く入手可能な半標準化されたプリインストールされたファームウェアは存在しないため、実行できる最も単純な 2 種類の IO は次のとおりです。

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

アップロードしました:

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

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

    これはメモリマップIO

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

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

リソース

おすすめ記事