オペレーティング システムを実行せずに、プログラムだけを実行するにはどうすればよいでしょうか。コンピューターが起動時に読み込んで実行できるアセンブリ プログラムを作成できますか。たとえば、コンピューターをフラッシュ ドライブから起動すると、CPU 上のプログラムが実行されます。
ベストアンサー1
実行可能な例
OS なしで実行される、小さなベアメタルの Hello World プログラムをいくつか作成して実行してみましょう。
- x86のレノボ シンクパッド T430UEFI BIOS 1.16ファームウェアを搭載したラップトップ
- ARMベースのラズベリーパイ3
また、開発にはより安全で便利なため、可能な限り QEMU エミュレーターでテストします。QEMU テストは、QEMU 2.11.1 があらかじめパッケージ化された Ubuntu 18.04 ホストで実施されています。
以下のすべてのx86サンプルのコードとその他のコードは、このGitHubリポジトリ。
x86実ハードウェア上でサンプルを実行する方法
実際のハードウェアで例を実行すると危険であることに注意してください。たとえば、誤ってディスクを消去したり、ハードウェアを壊したりする可能性があります。重要なデータが含まれていない古いマシンでのみこれを実行してください。または、さらに良い方法として、Raspberry Pi などの安価な半使い捨ての開発ボードを使用します。以下の ARM の例を参照してください。
一般的な x86 ラップトップの場合は、次のようにする必要があります。
イメージを USB スティックに書き込みます (データは破壊されます)。
sudo dd if=main.img of=/dev/sdX
USBをコンピュータに差し込む
それをオン
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
以下が含まれます:
\364
8 進数 == 16 進数: CPU に動作を停止するように指示する命令0xf4
のエンコード。hlt
したがって、プログラムは何も実行せず、開始と停止のみを実行します。
\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
。これらは 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
20
ASCII ではスペースです。
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"
リンク
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 ベアメタル プログラム
私が実現した、より複雑なベアメタル セットアップをいくつか紹介します。
- マルチコア:マルチコアアセンブリ言語とはどのようなものですか?
- ページング:x86 ページングはどのように機能しますか?
アセンブリの代わりにCを使用する
要約: GRUB マルチブートを使用すると、これまで考えたこともなかった多くの厄介な問題が解決されます。以下のセクションを参照してください。
x86 での主な難しさは、BIOS がディスクからメモリに 512 バイトしかロードしないことであり、C を使用するとその 512 バイトが使い果たされる可能性があります。
これを解決するには、2段階ブートローダーこれにより、さらにBIOS呼び出しが行われ、ディスクからメモリにさらに多くのバイトがロードされます。以下は、int 0x13 BIOS呼び出し:
あるいは:
- QEMU でのみ動作させる必要があり、実際のハードウェアでは動作させたくない場合には、
-kernel
ELF ファイル全体をメモリにロードするオプションを使用します。この方法で作成した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
memcmp
Newlib は、、、などmemcpy
、退屈な OS 固有ではないものをすべて実装します。次に、必要なシステムコールを自分で実装するためのスタブがいくつか提供されます。
たとえば、
exit()
セミホスティングを通じて ARM に実装できます。void _exit(int status) { __asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456"); }
に示すようにこの例では。
たとえば、
printf
UARTやARMシステムにリダイレクトしたりexit()
、セミホスティング。 組み込みオペレーティングシステムフリーRTOSそしてゼファー。
このようなオペレーティング システムでは通常、プリエンプティブ スケジューリングをオフにできるため、プログラムの実行時間を完全に制御できます。
これらは、一種の事前に実装された Newlib として考えることができます。
GNU GRUB マルチブート
ブートセクターはシンプルですが、あまり便利ではありません。
- ディスクごとに1つのOSしか持てない
- ロードコードは非常に小さく、512バイトに収まる必要があります。
- 保護モードへの移行など、多くの起動作業を自分で行う必要がある
そういった理由からGNU GRUBマルチブートと呼ばれるより便利なファイル形式を作成しました。
私も使っています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.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 が完全にマップされていることを意味するのではなく、通常はそうではありません。ただし、ブート プロセスを開始するのに十分なだけマップされています。その他のデバイスは忘れてください。
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を点滅させる
アップロードしました:
いくつかの簡単な QEMU C + Newlib と生のアセンブリの例GitHubはこちら。
のprompt.c の例たとえば、ホスト端末から入力を受け取り、シミュレートされた UART を通じて出力を返します。
enter a character got: a new alloc of 1 bytes at address 0x0x4000a1c0 enter a character got: b new alloc of 2 bytes at address 0x0x4000a1c0 enter a character
完全に自動化された Raspberry Pi ブリンカーのセットアップ:https://github.com/cirosantilli/raspberry-pi-bare-metal-blinker
参照:Raspberry Pi で OS なしで C プログラムを実行するにはどうすればよいでしょうか?
QEMU の LED を「見る」には、デバッグ フラグを使用してソースから QEMU をコンパイルする必要があります。https://raspberrypi.stackexchange.com/questions/56373/is-it-possible-to-get-the-state-of-the-leds-and-gpios-in-a-qemu-emulation-like-t
次に、UART hello world を試してみましょう。blinker の例から始めて、カーネルを次のように置き換えます。https://github.com/dwelch67/raspberrypi/tree/bce377230c2cdd8ff1e40919fdedbc2533ef5a00/uart01
まず、私が説明したように、Raspbian で UART を動作させます。https://raspberrypi.stackexchange.com/questions/38/prepare-for-ssh-without-a-screen/54394#54394次のようになります:
必ず正しいピンを使用してください。そうしないと、UART から USB へのコンバーターが焼損する可能性があります。私はすでに 2 回、グランドと 5V を短絡させて焼損したことがあります...
最後に、次のコマンドを使用してホストからシリアルに接続します。
screen /dev/ttyUSB0 115200
Raspberry Pi の場合、実行可能ファイルを格納するために USB スティックではなく Micro SD カードを使用します。通常、実行可能ファイルをコンピューターに接続するためにアダプターが必要です。
以下に示すように、SD アダプターのロックを解除することを忘れないでください。https://askubuntu.com/questions/213889/microsd-card-is-set-to-read-only-state-how-can-i-write-data-on-it/814585#814585
https://github.com/dwelch67/raspberrypi現在入手可能な最も人気のあるベアメタル Raspberry Pi チュートリアルのようです。
x86 との違いは次のとおりです。
IO はマジック アドレスに直接書き込むことによって行われ、命令はありませ
in
んout
。これはメモリマップIO。
Raspberry Pi などの実際のハードウェアの場合は、ファームウェア (BIOS) を自分でディスク イメージに追加できます。
これはファームウェアの更新がより透明になるため、良いことです。
リソース
- http://wiki.osdev.orgそれらの問題に関する素晴らしい情報源です。
- https://github.com/scanlime/metalkitより自動化された一般的なベアメタルコンパイルシステムであり、小さなカスタムAPIを提供します。