オペレーティングシステムを実行せずにプログラムを単独で実行する方法を教えてください。起動時にコンピュータがロードして実行できるアセンブリプログラムを作成できますか。フラッシュドライブからコンピュータを起動し、それはCPU上にあるプログラムを実行しますか?
オペレーティングシステムを実行せずにプログラムを単独で実行する方法を教えてください。
再起動後にプロセッサが探す場所にバイナリコードを配置します(例:ARMのアドレス0)。
起動時にコンピュータがロードして実行できるアセンブリプログラムを作成できますか(たとえば、フラッシュドライブからコンピュータを起動し、ドライブ上にあるプログラムを実行するなど)。
質問に対する一般的な回答:それは可能です。それはしばしば「ベアメタルプログラミング」と呼ばれます。フラッシュドライブから読み取るためには、USBが何であるかを知りたい、そしてあなたはこのUSBで動作するようにいくつかのドライバを持っていたいです。このドライブ上のプログラムは、特定のファイルシステム上の特定のフォーマットである必要があります。これは、ブートローダが通常行うことですが、ファームウェアに必要な場合に限り、プログラムに独自のブートローダを含めることができる小さなコードブロックをロードします。
ARMボードの多くは、それらを可能にします。基本的なセットアップを手助けするブートローダーを持っている人もいます。
ここで Raspberry Piで基本的なオペレーティングシステムを実行する方法に関する素晴らしいチュートリアルを見つけることができます。
編集:この記事、そして全体のwiki.osdev.orgはあなたの質問の大部分を助長します http://wiki.osdev.org/はじめに
また、ハードウェアで直接試したくない場合は、qemuのようなハイパーバイザーを使用して仮想マシンとして実行することができます。仮想化されたARMハードウェア で直接「hello world」を実行する方法はこちら を参照してください。
実行可能な例
OSなしで実行される、ごくわずかなベアメタルHello Worldプログラムを作成して実行しましょう。
また、QEMUエミュレーターでできる限り安全に試してみてください。開発の方がより安全で便利です。 QEMUテストは、QEMU 2.11.1が事前にパッケージ化されたUbuntu 18.04ホストで行われました。
以下のすべてのx86サンプルのコードは this GitHub repo にあります。
x86の実際のハードウェアでサンプルを実行する方法
実際のハードウェアでサンプルを実行するのは危険です。誤ってディスクを消去したり、ハードウェアを破壊したりする可能性があります。これは、重要なデータが含まれていない古いマシンでのみ実行してください。あるいは、Raspberry Piなどの安価な半使い捨ての開発ボードを使用することもできます。以下のARMの例を参照してください。
典型的なx86ラップトップの場合、次のようなことをする必要があります。
画像をUSBスティックに書き込みます(データが破壊されます!):
Sudo dd if=main.img of=/dev/sdX
uSBをコンピューターに接続します
それをオン
uSBから起動するように指示します。
これは、ファームウェアがハードディスクの前にUSBを選択することを意味します。
それがお使いのマシンのデフォルトの動作ではない場合、電源投入後、USBから起動することを選択できる起動メニューが表示されるまで、Enter、F12、ESCなどの奇妙なキーを押し続けます。
多くの場合、これらのメニューで検索順序を構成できます。
たとえば、T430では次のように表示されます。
電源を入れた後、Enterキーを押してブートメニューに入る必要があります。
次に、ここでF12を押して、ブートデバイスとしてUSBを選択する必要があります。
そこから、次のようにUSBを起動デバイスとして選択できます。
または、起動順序を変更し、USBの優先順位を高くするために毎回手動で選択する必要がないようにするには、[スタートアップ割り込みメニュー]画面でF1を押してから、次の場所に移動します。
ブートセクタ
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
結果:
何もしなくても、いくつかの文字がすでに画面に印刷されていることに注意してください。これらはファームウェアによって印刷され、システムを識別するのに役立ちます。
T430では、カーソルが点滅する空白の画面が表示されます。
main.img
には次が含まれます。
8進数の\364
== 16進数の0xf4
:hlt
命令のエンコード。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の実行方法いくつかのオプション:
シリアルポート 。これは、ホスト端末と文字を送受信する非常に単純な標準化されたプロトコルです。
デスクトップでは、次のようになります。
ソース 。
残念ながら、ほとんどの最新のラップトップでは公開されていませんが、開発ボードを使用する一般的な方法です。以下の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"
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
結果:
T430の場合:
テスト済み: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アセンブリの例を次に示します。
代わりに:
-kernel
オプションを使用します。これにより、ELFファイル全体がメモリにロードされます。 このメソッドで作成したARMの例 。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ファイルの束に過ぎませんか?右??
詳細な例: https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931
Newlibはすべての退屈な非OS固有のものを実装します。 memcmp
、memcpy
など.
次に、必要なsyscallを実装するためのスタブをいくつか提供します。
たとえば、ARMにexit()
を実装するには、セミホスティングを使用します。
void _exit(int status) {
__asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456");
}
この例では で示されています。
たとえば、printf
をUARTまたはARMシステムにリダイレクトしたり、 セミホスティング でexit()
を実装したりできます。
FreeRTOS や Zephyr などの組み込みオペレーティングシステム。
このようなオペレーティングシステムでは、通常、プリエンプティブスケジューリングをオフにできるため、プログラムのランタイムを完全に制御できます。
これらは、事前実装されたNewlibの一種と見ることができます。
GNU GRUBマルチブート
ブートセクターは単純ですが、あまり便利ではありません。
GNU GRUB がマルチブートと呼ばれるより便利なファイル形式を作成したのは、こうした理由からです。
GitHub examples repo でも使用して、USBを100万回書き込むことなく、実際のハードウェアですべての例を簡単に実行できるようにします。
QEMUの結果:
T430:
OSをマルチブートファイルとして準備すると、GRUBは通常のファイルシステム内でそれを見つけることができます。
これは、ほとんどのディストリビューションがOSイメージを/boot
の下に置くことです。
マルチブートファイルは基本的に、特別なヘッダーを持つELFファイルです。それらは次の場所でGRUBで指定されます: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
grub-mkrescue
を使用して、マルチブートファイルをブート可能なディスクに変換できます。
ファームウェア
実際、ブートセクタは、システムのCPUで実行される最初のソフトウェアではありません。
実際に最初に実行されるのは、いわゆるfirmwareです。これはソフトウェアです。
よく知られているファームウェアは次のとおりです。
ファームウェアは次のようなことを行います:
起動可能なものが見つかるまで、各ハードディスク、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タイプは次のとおりです。
アップロードしました:
いくつかの簡単な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- led-and-gpios-in-a-qemu-emulation-like-t の状態
次に、UART hello worldを試してください。ブリンカーの例から始めて、カーネルを次の例に置き換えることができます。 https://github.com/dwelch67/raspberrypi/tree/bce377230c2cdd8ff1e40919fdedbc2533ef5a00/uart01
最初にUARTをRaspbianで動作するように説明します: https://raspberrypi.stackexchange.com/questions/38/prepare-for-ssh-without-a-screen/ 54394#54394 次のようになります。
必ず正しいピンを使用してください。そうでない場合は、UARTをUSBコンバーターに焼き付けることができます。接地と5Vを短絡することにより、すでに2回実行しています。
最後に、ホストからシリアルに接続します:
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
命令はありません。
これは memory mapped IO と呼ばれます。
raspberry Piなどの一部の実際のハードウェアでは、ファームウェア(BIOS)を自分でディスクイメージに追加できます。
ファームウェアの更新がより透過的になるため、これは良いことです。
リソース