Intel x86 最小限の実行可能なベアメタルの例
必要なすべてのボイラープレートを備えた実行可能なベアメタルの例。主要な部分はすべて以下で説明します。
Ubuntu 15.10 QEMU 2.3.0 および Lenovo ThinkPad T400実ハードウェア ゲストでテスト済み。
インテル マニュアル ボリューム 3 システム プログラミング ガイド - 325384-056US 2015 年 9 月では、 8、9、10章で SMP について説明しています。
表 8-1. 「Broadcast INIT-SIPI-SIPI Sequence and Choice of Timeouts」には、基本的に機能する例が含まれています。
MOV ESI, ICR_LOW ; Load address of ICR low dword into ESI.
MOV EAX, 000C4500H ; Load ICR encoding for broadcast INIT IPI
; to all APs into EAX.
MOV [ESI], EAX ; Broadcast INIT IPI to all APs
; 10-millisecond delay loop.
MOV EAX, 000C46XXH ; Load ICR encoding for broadcast SIPI IP
; to all APs into EAX, where xx is the vector computed in step 10.
MOV [ESI], EAX ; Broadcast SIPI IPI to all APs
; 200-microsecond delay loop
MOV [ESI], EAX ; Broadcast second SIPI IPI to all APs
; Waits for the timer interrupt until the timer expires
そのコードで:
ほとんどのオペレーティング システムでは、リング 3 (ユーザー プログラム) からこれらの操作のほとんどが不可能になります。
したがって、カーネルを自由に操作するには、独自のカーネルを作成する必要があります。ユーザーランドの Linux プログラムは機能しません。
最初は、ブートストラップ プロセッサ (BSP) と呼ばれる単一のプロセッサが実行されます。
プロセッサ間割り込み (IPI)と呼ばれる特別な割り込みを介して、他のもの (アプリケーション プロセッサ (AP) と呼ばれる) をウェイクアップする必要があります。
これらの割り込みは、割り込みコマンド レジスタ (ICR) を介して Advanced Programmable Interrupt Controller (APIC) をプログラミングすることで実行できます。
ICR の形式は、10.6「ISSUING INTERPROCESSOR INTERRUPTS」に記載されています。
IPI は、ICR に書き込むとすぐに発生します。
ICR_LOW は、8.4.4「MP 初期化の例」で次のように定義されています。
ICR_LOW EQU 0FEE00300H
表 10-1「ローカル APIC レジスタ アドレス マップ」に記載されているように、マジック値0FEE00300
は ICR のメモリ アドレスです。
この例では、最も単純な方法が使用されています。これは、現在のプロセッサを除く他のすべてのプロセッサに配信されるブロードキャスト IPI を送信するように ICR をセットアップします。
しかし、 ACPI テーブルや Intel の MP 構成テーブルなどの BIOS による特別なデータ構造のセットアップを通じてプロセッサに関する情報を取得し、必要なものだけを 1 つずつ起動することも可能であり、推奨されています。
XX
in000C46XXH
は、プロセッサが実行する最初の命令のアドレスを次のようにエンコードします。
CS = XX * 0x100
IP = 0
CSはアドレスを 倍する0x10
ので、最初の命令の実際のメモリ アドレスは次のようになります。
XX * 0x1000
したがって、たとえばXX == 1
の場合、プロセッサは で開始され0x1000
ます。
次に、そのメモリ位置で実行される 16 ビット リアル モード コードがあることを確認する必要があります。
cld
mov $init_len, %ecx
mov $init, %esi
mov 0x1000, %edi
rep movsb
.code16
init:
xor %ax, %ax
mov %ax, %ds
/* Do stuff. */
hlt
.equ init_len, . - init
別の方法として、リンカー スクリプトを使用することもできます。
遅延ループは、作業を開始するのに厄介な部分です。このようなスリープを正確に実行するための非常に簡単な方法はありません。
可能な方法は次のとおりです。
- PIT (私の例で使用)
- HPET
- 上記でビジーループの時間を調整し、代わりに使用します
関連: DOS x86 アセンブリで画面に数字を表示して 1 秒間スリープする方法は?
0FEE00300H
16ビットには高すぎるアドレスに書き込むため、これが機能するには、最初のプロセッサを保護モードにする必要があると思います
プロセッサ間で通信するには、メイン プロセスでスピンロックを使用し、2 番目のコアからロックを変更します。
などを介して、メモリの書き戻しが確実に行われるようにする必要がありますwbinvd
。
プロセッサ間の共有状態
8.7.1「論理プロセッサの状態」は次のように述べています。
次の機能は、Intel ハイパースレッディング テクノロジをサポートする Intel 64 または IA-32 プロセッサ内の論理プロセッサのアーキテクチャ状態の一部です。機能は、次の 3 つのグループに分類できます。
- 論理プロセッサごとに複製
- 物理プロセッサ内の論理プロセッサで共有
- 実装に応じて共有または複製
次の機能は、論理プロセッサごとに複製されます。
- 汎用レジスター (EAX、EBX、ECX、EDX、ESI、EDI、ESP、および EBP)
- セグメントレジスタ (CS、DS、SS、ES、FS、および GS)
- EFLAGS および EIP レジスタ。各論理プロセッサの CS および EIP/RIP レジスタは、論理プロセッサによって実行されているスレッドの命令ストリームを指していることに注意してください。
- x87 FPU レジスタ (ST0 ~ ST7、ステータス ワード、コントロール ワード、タグ ワード、データ オペランド ポインタ、命令ポインタ)
- MMX レジスタ (MM0 ~ MM7)
- XMM レジスター (XMM0 ~ XMM7) および MXCSR レジスター
- 制御レジスタとシステム テーブル ポインタ レジスタ (GDTR、LDTR、IDTR、タスク レジスタ)
- デバッグ レジスタ (DR0、DR1、DR2、DR3、DR6、DR7) およびデバッグ制御 MSR
- マシン チェック グローバル ステータス (IA32_MCG_STATUS) およびマシン チェック機能 (IA32_MCG_CAP) MSR
- サーマル クロック変調および ACPI 電源管理制御 MSR
- タイム スタンプ カウンター MSR
- ページ属性テーブル (PAT) を含む、他のほとんどの MSR レジスタ。以下の例外を参照してください。
- ローカル APIC レジスタ。
- 追加の汎用レジスタ (R8 ~ R15)、XMM レジスタ (XMM8 ~ XMM15)、制御レジスタ、Intel 64 プロセッサの IA32_EFER。
次の機能は、論理プロセッサによって共有されます。
次の機能が共有されるか複製されるかは、実装によって異なります。
- IA32_MISC_ENABLE MSR (MSR アドレス 1A0H)
- マシン チェック アーキテクチャ (MCA) MSR (IA32_MCG_STATUS および IA32_MCG_CAP MSR を除く)
- パフォーマンス監視制御およびカウンター MSR
キャッシュ共有については、次の場所で説明されています。
Intelハイパースレッドは、個別のコアよりもキャッシュとパイプラインの共有が優れています: https://superuser.com/questions/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858
Linux カーネル 4.2
主な初期化アクションはarch/x86/kernel/smpboot.c
.
ARM の最小限の実行可能なベアメタルの例
ここでは、QEMU 用の最小限の実行可能な ARMv8 aarch64 の例を示します。
.global mystart
mystart:
/* Reset spinlock. */
mov x0, #0
ldr x1, =spinlock
str x0, [x1]
/* Read cpu id into x1.
* TODO: cores beyond 4th?
* Mnemonic: Main Processor ID Register
*/
mrs x1, mpidr_el1
ands x1, x1, 3
beq cpu0_only
cpu1_only:
/* Only CPU 1 reaches this point and sets the spinlock. */
mov x0, 1
ldr x1, =spinlock
str x0, [x1]
/* Ensure that CPU 0 sees the write right now.
* Optional, but could save some useless CPU 1 loops.
*/
dmb sy
/* Wake up CPU 0 if it is sleeping on wfe.
* Optional, but could save power on a real system.
*/
sev
cpu1_sleep_forever:
/* Hint CPU 1 to enter low power mode.
* Optional, but could save power on a real system.
*/
wfe
b cpu1_sleep_forever
cpu0_only:
/* Only CPU 0 reaches this point. */
/* Wake up CPU 1 from initial sleep!
* See:https://github.com/cirosantilli/linux-kernel-module-cheat#psci
*/
/* PCSI function identifier: CPU_ON. */
ldr w0, =0xc4000003
/* Argument 1: target_cpu */
mov x1, 1
/* Argument 2: entry_point_address */
ldr x2, =cpu1_only
/* Argument 3: context_id */
mov x3, 0
/* Unused hvc args: the Linux kernel zeroes them,
* but I don't think it is required.
*/
hvc 0
spinlock_start:
ldr x0, spinlock
/* Hint CPU 0 to enter low power mode. */
wfe
cbz x0, spinlock_start
/* Semihost exit. */
mov x1, 0x26
movk x1, 2, lsl 16
str x1, [sp, 0]
mov x0, 0
str x0, [sp, 8]
mov x1, sp
mov w0, 0x18
hlt 0xf000
spinlock:
.skip 8
GitHub アップストリーム.
組み立てて実行します。
aarch64-linux-gnu-gcc \
-mcpu=cortex-a57 \
-nostdlib \
-nostartfiles \
-Wl,--section-start=.text=0x40000000 \
-Wl,-N \
-o aarch64.elf \
-T link.ld \
aarch64.S \
;
qemu-system-aarch64 \
-machine virt \
-cpu cortex-a57 \
-d in_asm \
-kernel aarch64.elf \
-nographic \
-semihosting \
-smp 2 \
;
この例では、CPU 0 をスピンロック ループに入れ、CPU 1 がスピンロックを解放したときにのみ終了します。
スピンロックの後、CPU 0は QEMU を終了させるセミホスト終了呼び出しを行います。
を使用して 1 つの CPU だけで QEMU を開始すると-smp 1
、シミュレーションはスピンロックで永久にハングアップします。
CPU 1 は PSCI インターフェイスでウェイクアップされます。詳細については、ARM: 他の CPU コア/AP を開始/ウェイクアップ/起動し、実行開始アドレスを渡しますか?
アップストリーム バージョンには、gem5 で動作するようにいくつかの調整が加えられているため、パフォーマンス特性も試すことができます。
実際のハードウェアでテストしていないため、これがどれほど移植性があるかはわかりません。次の Raspberry Pi 参考文献が参考になるかもしれません。
このドキュメントでは、複数のコアで楽しいことを行うために使用できる ARM 同期プリミティブの使用に関するガイダンスを提供します。
Ubuntu 18.10、GCC 8.2.0、Binutils 2.31.1、QEMU 2.12.0 でテスト済み。
より便利なプログラミングのための次のステップ
前の例では、セカンダリ CPU をウェイクアップし、専用の命令で基本的なメモリ同期を行っています。これは良い出発点です。
しかし、たとえばPOSIX のようなマルチコア システムをプログラムしやすくするpthreads
には、以下のより複雑なトピックにも入る必要があります。
割り込みをセットアップし、現在どのスレッドを実行するかを定期的に決定するタイマーを実行します。これは、プリエンプティブ マルチスレッドと呼ばれます。
このようなシステムでは、スレッド レジスタを開始および停止するときに、スレッド レジスタを保存および復元する必要もあります。
プリエンプティブでないマルチタスク システムを使用することもできますが、その場合、コードを変更してすべてのスレッドが (たとえば、pthread_yield
実装で) 生成されるようにする必要があり、ワークロードのバランスを取るのが難しくなります。
単純化したベア メタル タイマーの例を次に示します。
メモリの競合に対処します。特に、 C やその他の高級言語でコーディングする場合は、各スレッドに固有のスタックが必要になります。
スレッドの最大スタックサイズを固定するように制限することもできますが、これに対処するより適切な方法は、効率的な「無制限のサイズ」スタックを可能にするページングを使用することです。
これは単純な aarch64 ベアメタルの例で、スタックが深くなりすぎると爆発します。
これらは、Linuxカーネルまたは他のオペレーティングシステムを使用するいくつかの正当な理由です:-)
ユーザーランドのメモリ同期プリミティブ
スレッドの開始/停止/管理は通常、ユーザーランドの範囲を超えていますが、ユーザーランド スレッドからのアセンブリ命令を使用して、潜在的に高価なシステム コールなしでメモリ アクセスを同期することができます。
もちろん、これらの低レベルのプリミティブを移植可能にラップするライブラリを使用することをお勧めします。C++ 標準自体は、<mutex>
および<atomic>
ヘッダー、特にstd::memory_order
. 達成可能なすべての可能なメモリセマンティクスをカバーしているかどうかはわかりませんが、そうかもしれません。
より微妙なセマンティクスは、特定のケースでパフォーマンス上の利点を提供できるロック フリー データ構造のコンテキストに特に関連しています。それらを実装するには、さまざまなタイプのメモリバリアについて少し学ぶ必要があるでしょう: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/
たとえば、ブーストには、https ://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html にいくつかのロック フリー コンテナーの実装があります。
futex
このようなユーザーランド命令は、Linuxの主要な同期プリミティブの 1 つであるLinux システム コールの実装にも使用されているようです。man futex
4.15 読み取り:
futex() システムコールは、特定の条件が真になるまで待機するメソッドを提供します。これは通常、共有メモリ同期のコンテキストでブロッキング コンストラクトとして使用されます。futex を使用する場合、同期操作の大部分はユーザー空間で実行されます。ユーザー空間プログラムが futex() システム コールを使用するのは、条件が true になるまでプログラムを長時間ブロックする必要がある可能性が高い場合だけです。他の futex() 操作を使用して、特定の条件を待機しているプロセスまたはスレッドをウェイクアップできます。
システムコール名自体は「Fast Userspace XXX」を意味します。
以下は、インライン アセンブリを使用した最小限の役に立たない C++ x86_64 / aarch64 の例です。
main.cpp
#include <atomic>
#include <cassert>
#include <iostream>
#include <thread>
#include <vector>
std::atomic_ulong my_atomic_ulong(0);
unsigned long my_non_atomic_ulong = 0;
#if defined(__x86_64__) || defined(__aarch64__)
unsigned long my_arch_atomic_ulong = 0;
unsigned long my_arch_non_atomic_ulong = 0;
#endif
size_t niters;
void threadMain() {
for (size_t i = 0; i < niters; ++i) {
my_atomic_ulong++;
my_non_atomic_ulong++;
#if defined(__x86_64__)
__asm__ __volatile__ (
"incq %0;"
: "+m" (my_arch_non_atomic_ulong)
:
:
);
// https://github.com/cirosantilli/linux-kernel-module-cheat#x86-lock-prefix
__asm__ __volatile__ (
"lock;"
"incq %0;"
: "+m" (my_arch_atomic_ulong)
:
:
);
#elif defined(__aarch64__)
__asm__ __volatile__ (
"add %0, %0, 1;"
: "+r" (my_arch_non_atomic_ulong)
:
:
);
// https://github.com/cirosantilli/linux-kernel-module-cheat#arm-lse
__asm__ __volatile__ (
"ldadd %[inc], xzr, [%[addr]];"
: "=m" (my_arch_atomic_ulong)
: [inc] "r" (1),
[addr] "r" (&my_arch_atomic_ulong)
:
);
#endif
}
}
int main(int argc, char **argv) {
size_t nthreads;
if (argc > 1) {
nthreads = std::stoull(argv[1], NULL, 0);
} else {
nthreads = 2;
}
if (argc > 2) {
niters = std::stoull(argv[2], NULL, 0);
} else {
niters = 10000;
}
std::vector<std::thread> threads(nthreads);
for (size_t i = 0; i < nthreads; ++i)
threads[i] = std::thread(threadMain);
for (size_t i = 0; i < nthreads; ++i)
threads[i].join();
assert(my_atomic_ulong.load() == nthreads * niters);
// We can also use the atomics direclty through `operator T` conversion.
assert(my_atomic_ulong == my_atomic_ulong.load());
std::cout << "my_non_atomic_ulong " << my_non_atomic_ulong << std::endl;
#if defined(__x86_64__) || defined(__aarch64__)
assert(my_arch_atomic_ulong == nthreads * niters);
std::cout << "my_arch_non_atomic_ulong " << my_arch_non_atomic_ulong << std::endl;
#endif
}
GitHub アップストリーム.
可能な出力:
my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267
このことから、x86 LOCK プレフィックス / aarch64LDADD
命令が追加をアトミックにしたことがわかります。それがないと、多くの追加で競合状態が発生し、最後の合計カウントは同期された 20000 未満になります。
以下も参照してください。
Ubuntu 19.04 amd64 および QEMU aarch64 ユーザー モードでテスト済み。