Intel x86 contoh baremetal runnable minimal
Runnable bare metal contoh dengan semua boilerplate yang dibutuhkan . Semua bagian utama dibahas di bawah ini.
Diuji pada Ubuntu 15.10 QEMU 2.3.0 dan tamu perangkat keras Lenovo ThinkPad T400 nyata .
The Intel Pedoman Volume 3 System Programming Guide - 325384-056US September 2015 meliputi SMP di bab 8, 9 dan 10.
Tabel 8-1. "Siaran INIT-SIPI-SIPI Urutan dan Pilihan Timeout" berisi contoh yang pada dasarnya hanya berfungsi:
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
Pada kode itu:
Sebagian besar sistem operasi akan membuat sebagian besar operasi tersebut menjadi mustahil dari dering 3 (program pengguna).
Jadi Anda perlu menulis kernel Anda sendiri untuk bermain bebas dengannya: program userland Linux tidak akan berfungsi.
Pada awalnya, sebuah prosesor berjalan, yang disebut prosesor bootstrap (BSP).
Itu harus membangunkan yang lain (disebut Application Processors (AP)) melalui interupsi khusus yang disebut Inter Processor Interrupts (IPI) .
Interupsi tersebut dapat dilakukan dengan memprogram Advanced Programmable Interrupt Controller (APIC) melalui Interrupt command register (ICR)
Format ICR didokumentasikan di: 10.6 "MENERBITKAN INTERROCESSOR INTERRUPTS"
IPI terjadi segera setelah kami menulis ke ICR.
ICR_LOW didefinisikan pada 8.4.4 "Contoh Inisialisasi MP" sebagai:
ICR_LOW EQU 0FEE00300H
Nilai ajaib 0FEE00300
adalah alamat memori ICR, seperti yang didokumentasikan pada Tabel 10-1 "Peta Alamat Pendaftaran APIC Lokal"
Metode paling sederhana yang mungkin digunakan dalam contoh: ini mengatur ICR untuk mengirim siaran IPI yang dikirim ke semua prosesor lain kecuali yang saat ini.
Tetapi juga mungkin, dan direkomendasikan oleh beberapa orang , untuk mendapatkan informasi tentang prosesor melalui pengaturan struktur data khusus oleh BIOS seperti tabel ACPI atau tabel konfigurasi MP Intel dan hanya membangunkan yang Anda perlukan satu per satu.
XX
di 000C46XXH
menyandikan alamat instruksi pertama yang akan dijalankan prosesor sebagai:
CS = XX * 0x100
IP = 0
Ingatlah bahwa CS melipatgandakan alamat dengan0x10
, jadi alamat memori sebenarnya dari instruksi pertama adalah:
XX * 0x1000
Jadi jika misalnya XX == 1
, prosesor akan mulai 0x1000
.
Kami kemudian harus memastikan bahwa ada kode mode nyata 16-bit untuk dijalankan di lokasi memori itu, misalnya dengan:
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
Menggunakan skrip linker adalah kemungkinan lain.
Loop penundaan adalah bagian yang mengganggu untuk mulai bekerja: tidak ada cara super sederhana untuk melakukan tidur seperti itu secara tepat.
Metode yang mungkin termasuk:
- PIT (digunakan dalam contoh saya)
- HPET
- mengkalibrasi waktu loop sibuk dengan yang di atas, dan gunakan itu sebagai gantinya
Terkait: Bagaimana menampilkan nomor di layar dan dan tidur selama satu detik dengan rakitan DOS x86?
Saya pikir prosesor awal harus berada dalam mode terproteksi agar ini berfungsi saat kami menulis ke alamat 0FEE00300H
yang terlalu tinggi untuk 16-bit
Untuk berkomunikasi antara prosesor, kita dapat menggunakan spinlock pada proses utama, dan memodifikasi kunci dari inti kedua.
Kita harus memastikan bahwa memori write back dilakukan, misalnya melalui wbinvd
.
Keadaan bersama antara prosesor
8.7.1 "Keadaan Prosesor Logis" mengatakan:
Fitur-fitur berikut adalah bagian dari keadaan arsitektur prosesor logis dalam prosesor Intel 64 atau IA-32 yang mendukung Teknologi Intel Hyper-Threading. Fitur-fitur dapat dibagi menjadi tiga kelompok:
- Digandakan untuk setiap prosesor logis
- Dibagikan oleh prosesor logis dalam prosesor fisik
- Dibagi atau digandakan, tergantung pada implementasinya
Fitur-fitur berikut diduplikasi untuk setiap prosesor logis:
- Register tujuan umum (EAX, EBX, ECX, EDX, ESI, EDI, ESP, dan EBP)
- Register segmen (CS, DS, SS, ES, FS, dan GS)
- Register EFLAGS dan EIP. Perhatikan bahwa CS dan EIP / RIP mendaftar untuk setiap prosesor logis mengarah ke aliran instruksi untuk utas yang dijalankan oleh prosesor logis.
- x87 register FPU (ST0 hingga ST7, kata status, kata kontrol, kata tag, penunjuk operan data, dan penunjuk instruksi)
- Register MMX (MM0 hingga MM7)
- Register XMM (XMM0 hingga XMM7) dan register MXCSR
- Register kontrol dan register penunjuk tabel sistem (GDTR, LDTR, IDTR, register tugas)
- Register debug (DR0, DR1, DR2, DR3, DR6, DR7) dan kontrol debug MSR
- Mesin memeriksa status global (IA32_MCG_STATUS) dan kemampuan pemeriksaan mesin (IA32_MCG_CAP) MSR
- Modulasi jam termal dan ACPI MSRs kontrol manajemen daya
- Penghitung cap waktu MSR
- Sebagian besar register MSR lainnya, termasuk tabel atribut halaman (PAT). Lihat pengecualian di bawah ini.
- Register APIC lokal.
- Register tujuan umum tambahan (R8-R15), register XMM (XMM8-XMM15), register kontrol, IA32_EFER pada prosesor Intel 64.
Fitur-fitur berikut dibagikan oleh prosesor logis:
- Register rentang jenis memori (MTRR)
Apakah fitur berikut ini dibagikan atau digandakan khusus untuk implementasi:
- IA32_MISC_ENABLE MSR (alamat MSR 1A0H)
- MSR arsitektur periksa mesin (MCA) (kecuali untuk MSR IA32_MCG_STATUS dan IA32_MCG_CAP)
- Kontrol pemantauan kinerja dan counter MSR
Pembagian cache dibahas di:
Intel hyperthreads memiliki cache dan pembagian pipa yang lebih besar daripada core yang terpisah: /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858
Kernel Linux 4.2
Tindakan inisialisasi utama tampaknya di arch/x86/kernel/smpboot.c
.
Contoh ARM minimal runnable runemable
Di sini saya memberikan contoh ARMv8 aarch64 runnable minimal untuk QEMU:
.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 hulu .
Merakit dan menjalankan:
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 \
;
Dalam contoh ini, kami menempatkan CPU 0 dalam putaran spinlock, dan hanya keluar dengan CPU 1 melepaskan spinlock.
Setelah spinlock, CPU 0 kemudian melakukan panggilan keluar semihost yang membuat QEMU berhenti.
Jika Anda memulai QEMU hanya dengan satu CPU -smp 1
, maka simulasi hanya hang selamanya di spinlock.
CPU 1 dibangunkan dengan antarmuka PSCI, lebih detail di: ARM: Start / Wakeup / Bringup core / AP CPU lainnya dan berikan alamat mulai eksekusi?
Versi upstream juga memiliki beberapa penyesuaian untuk membuatnya bekerja pada gem5, sehingga Anda dapat bereksperimen dengan karakteristik kinerja juga.
Saya belum mengujinya pada perangkat keras asli, jadi dan saya tidak yakin seberapa portabel ini. Daftar pustaka Raspberry Pi berikut mungkin menarik:
Dokumen ini memberikan beberapa panduan tentang cara menggunakan primitif sinkronisasi ARM yang kemudian dapat Anda gunakan untuk melakukan hal-hal menyenangkan dengan banyak inti: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitive.pdf
Diuji pada Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.
Langkah selanjutnya untuk programabilitas yang lebih nyaman
Contoh sebelumnya membangunkan CPU sekunder dan melakukan sinkronisasi memori dasar dengan instruksi khusus, yang merupakan awal yang baik.
Tetapi untuk membuat sistem multicore mudah diprogram, misalnya seperti POSIX pthreads
, Anda juga perlu masuk ke topik yang lebih terlibat berikut:
setup menyela dan menjalankan timer yang secara berkala memutuskan thread mana yang akan berjalan sekarang. Ini dikenal sebagai multithreading preemptive .
Sistem seperti itu juga perlu menyimpan dan mengembalikan register utas saat diaktifkan dan dihentikan.
Dimungkinkan juga untuk memiliki sistem multitasking non-preemptive, tetapi yang mungkin mengharuskan Anda untuk memodifikasi kode Anda sehingga setiap utas menghasilkan (misalnya dengan pthread_yield
implementasi), dan itu menjadi lebih sulit untuk menyeimbangkan beban kerja.
Berikut adalah beberapa contoh timer logam sederhana:
menangani konflik memori. Khususnya, setiap utas akan membutuhkan tumpukan unik jika Anda ingin kode dalam C atau bahasa tingkat tinggi lainnya.
Anda bisa membatasi utas untuk memiliki ukuran tumpukan maksimum tetap, tetapi cara yang lebih baik untuk mengatasinya adalah dengan paging yang memungkinkan tumpukan "ukuran tak terbatas" yang efisien.
Berikut adalah contoh baremetal aarch64 naif yang akan meledak jika tumpukan tumbuh terlalu dalam
Itulah beberapa alasan bagus untuk menggunakan kernel Linux atau sistem operasi lain :-)
Sinkronisasi memori Userland primitif
Meskipun thread start / stop / management umumnya di luar lingkup userland, namun Anda dapat menggunakan instruksi perakitan dari thread userland untuk menyinkronkan akses memori tanpa potensi panggilan sistem yang lebih mahal.
Anda tentu saja harus lebih suka menggunakan perpustakaan yang dapat membungkus primitif tingkat rendah ini. C ++ standar itu sendiri telah membuat kemajuan besar pada <mutex>
dan <atomic>
header, dan khususnya denganstd::memory_order
. Saya tidak yakin apakah itu mencakup semua semantik memori yang mungkin dapat dicapai, tetapi mungkin saja.
Semantik yang lebih halus sangat relevan dalam konteks mengunci struktur data gratis , yang dapat menawarkan manfaat kinerja dalam kasus-kasus tertentu. Untuk mengimplementasikannya, Anda mungkin harus belajar sedikit tentang berbagai jenis hambatan memori: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/
Boost misalnya memiliki beberapa implementasi wadah bebas kunci di: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html
Instruksi userland semacam itu juga tampaknya digunakan untuk mengimplementasikan futex
panggilan sistem Linux , yang merupakan salah satu primitif sinkronisasi utama di Linux. man futex
4.15 berbunyi:
Panggilan sistem futex () menyediakan metode untuk menunggu hingga kondisi tertentu menjadi benar. Ini biasanya digunakan sebagai konstruksi pemblokiran dalam konteks sinkronisasi memori bersama. Saat menggunakan futex, sebagian besar operasi sinkronisasi dilakukan di ruang pengguna. Program ruang pengguna menggunakan panggilan sistem futex () hanya bila ada kemungkinan program harus diblokir untuk waktu yang lebih lama hingga kondisinya menjadi benar. Operasi futex () lainnya dapat digunakan untuk membangunkan setiap proses atau utas yang menunggu kondisi tertentu.
Nama syscall itu sendiri berarti "Fast Userspace XXX".
Berikut adalah contoh C ++ x86_64 / aarch64 minimal yang tidak berguna dengan inline assembly yang menggambarkan penggunaan dasar dari instruksi semacam itu sebagian besar untuk bersenang-senang:
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 hulu .
Output yang mungkin:
my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267
Dari sini kita melihat bahwa instruksi awalan x86 LOCK / aarch64 LDADD
membuat atom tambahan: tanpanya kita memiliki kondisi balapan pada banyak add , dan jumlah total pada akhirnya kurang dari 20000 yang disinkronkan.
Lihat juga:
Diuji dalam Ubuntu 19.04 amd64 dan dengan mode pengguna QEMU aarch64.