Intel x86最小実行可能ベアメタルの例
必要なすべてのボイラープレートを備えた実行可能なベアメタルの例。主要な部分はすべて以下でカバーされています。
Ubuntu 15.10 QEMU 2.3.0およびLenovo ThinkPad T400の実際のハードウェアゲストでテスト済み。
325384-056US 2015年9月-インテル・マニュアル第3巻のシステム・プログラミング・ガイドの章8、9および10でカバーSMP。
表8-1。「ブロードキャストINIT-SIPI-SIPIシーケンスとタイムアウトの選択」には、基本的に機能する例が含まれています。
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"
ICRに書き込むとすぐにIPIが発生します。
ICR_LOWは、8.4.4「MP初期化の例」で次のように定義されています。
ICR_LOW EQU 0FEE00300H
0FEE00300
表10-1「ローカルAPICレジスタアドレスマップ」に記載されているように、マジック値はICRのメモリアドレスです。
この例では、最も簡単な方法が使用されています。これは、現在のIPプロセッサを除く他のすべてのプロセッサに配信されるブロードキャストIPIを送信するようにICRを設定します。
ただし、ACPIテーブルやIntelのMP構成テーブルなどの BIOSによってセットアップされた特別なデータ構造を介してプロセッサに関する情報を取得し、必要なものだけをウェイクアップすることも可能であり、一部では推奨されています。
XX
in 000C46XXH
は、プロセッサが実行する最初の命令のアドレスを次のようにエンコードします。
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 "論理プロセッサの状態"は次のように述べています。
以下の機能は、インテルハイパースレッディングテクノロジーをサポートするインテル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を終了します。
QEMUを1つのCPUだけで起動すると-smp 1
、シミュレーションはスピンロックで永久にハングします。
CPU 1はPSCIインターフェースでウェイクアップされます。詳細は、ARM:他のCPUコア/ APを起動/ウェイクアップ/起動して、実行開始アドレスを渡しますか?
上流のバージョンでは、あなたが同様の性能特性を試すことができるようにも、それはgem5上で動作させるためにいくつかの調整があります。
実際のハードウェアではテストしていないので、これがどれほど移植性があるかはわかりません。次のRaspberry Piの参考文献が興味深いかもしれません。
このドキュメントでは、ARM同期プリミティブを使用して、複数のコアで楽しいことを行うためのガイダンスを提供します。http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf
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/
たとえばBoostには、https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.htmlにロックフリーのコンテナー実装があります。
このようなユーザーランド命令は、Linux futex
の主要な同期プリミティブの1つであるLinux システムコールの実装にも使用されているようです。man futex
4.15読み取り:
futex()システムコールは、特定の条件がtrueになるまで待機するメソッドを提供します。これは通常、共有メモリ同期のコンテキストでブロッキング構造として使用されます。futexを使用する場合、同期操作の大部分はユーザー空間で実行されます。ユーザー空間プログラムは、条件がtrueになるまでプログラムがより長い時間ブロックしなければならない可能性が高い場合にのみfutex()システムコールを使用します。他のfutex()操作を使用して、特定の条件を待機しているプロセスまたはスレッドを起こすことができます。
syscall名自体は「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プレフィックス/ aarch64 LDADD
命令が加算をアトミックにしたことがわかります。これがないと、多くの加算で競合状態が発生し、最後の合計カウントは同期された20000未満になります。
以下も参照してください。
Ubuntu 19.04 amd64およびQEMU aarch64ユーザーモードでテスト済み。