Intel x86 minimum çalıştırılabilir baremetal örneği
Gerekli tüm kazan plakası ile çalıştırılabilir çıplak metal örneği . Tüm büyük parçalar aşağıda ele alınmıştır.
Ubuntu 15.10 QEMU 2.3.0 ve Lenovo ThinkPad T400 üzerinde test edildi gerçek donanım konuğu .
325384-056US Eylül 2015 - Intel Manuel Cilt 3 Sistem Programlama Kılavuzu bölümlerde 8, 9 ve 10'da kapakları SMP.
Tablo 8-1. "Yayın INIT-SIPI-SIPI Sırası ve Zaman Aşımlarının Seçimi" temel olarak sadece çalışan bir örnek içerir:
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
Bu kodda:
Çoğu işletim sistemi bu işlemlerin çoğunu halka 3'ten (kullanıcı programları) imkansız hale getirecektir.
Bu nedenle, özgürce oynamak için kendi çekirdeğinizi yazmanız gerekir: bir kullanıcı ülkesi Linux programı çalışmaz.
İlk başta, bootstrap işlemci (BSP) adı verilen tek bir işlemci çalışır.
Diğerlerini (Uygulama İşlemcileri (AP) olarak adlandırılır) denilen özel kesintilerle uyandırmalıdır. İşlemciler Arası Kesmeler (IPI) .
Bu kesintiler, Kesinti komut kaydı (ICR) aracılığıyla Gelişmiş Programlanabilir Kesme Denetleyicisi (APIC) programlanarak yapılabilir
ICR'nin biçimi şu adreste belgelenmiştir: 10.6 "ENTERPROCESSOR KESİNTİLERİN SORUNU"
IPI, ICR'ye yazdığımız anda olur.
ICR_LOW, 8.4.4 "MP Başlatma Örneği" de şu şekilde tanımlanır:
ICR_LOW EQU 0FEE00300H
Sihirli değer 0FEE00300
, Tablo 10-1 "Yerel APIC Kayıt Adres Haritası" nda belgelendiği gibi ICR'nin bellek adresidir.
Örnekte mümkün olan en basit yöntem kullanılır: ICR'yi, geçerli olan dışındaki tüm diğer işlemcilere gönderilen yayın IPI'lerini gönderecek şekilde ayarlar.
Ancak, bazıları tarafından, ACPI tabloları veya Intel'in MP yapılandırma tablosu gibi BIOS tarafından ayarlanan özel veri yapıları aracılığıyla işlemciler hakkında bilgi almak ve yalnızca tek tek ihtiyacınız olanları uyandırmak da mümkündür.
XX
in 000C46XXH
, işlemcinin yürüteceği ilk komutun adresini kodlar:
CS = XX * 0x100
IP = 0
CS'nin adresleri ile çarptığını0x10
unutmayın , bu nedenle ilk talimatın gerçek bellek adresi:
XX * 0x1000
Yani, örneğin XX == 1
, işlemci0x1000
.
Ardından, o bellek konumunda çalıştırılacak 16 bit gerçek mod kodunun olduğundan emin olmalıyız, örneğin:
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
Bir bağlayıcı komut dosyası kullanmak başka bir olasılıktır.
Gecikme döngüleri çalışmaya başlamak için can sıkıcı bir parçadır: bu tür uykuları tam olarak yapmanın süper basit bir yolu yoktur.
Olası yöntemler şunları içerir:
- PIT (örneğimde kullanılır)
- HPET
- meşgul döngüsünün süresini yukarıdakilerle kalibre edin ve bunun yerine kullanın
İlgili: Nasıl ekranda bir sayı görüntülemek ve DOS x86 derleme ile bir saniye uyku?
0FEE00300H
16 bit için çok yüksek olan adrese yazarken bunun için ilk işlemcinin korumalı modda olması gerektiğini düşünüyorum
İşlemciler arasında iletişim kurmak için, ana işlemde bir spinlock kullanabilir ve kilidi ikinci çekirdekten değiştirebiliriz.
Belleğe geri yazma işleminin yapılmasını sağlamalıyız, örn wbinvd
.
İşlemciler arasında paylaşılan durum
8.7.1 "Mantıksal İşlemcilerin Durumu" diyor:
Aşağıdaki özellikler, Intel Hyper-Threading Teknolojisini destekleyen Intel 64 veya IA-32 işlemcilerdeki mantıksal işlemcilerin mimari durumunun bir parçasıdır. Özellikler üç gruba ayrılabilir:
- Her mantıksal işlemci için çoğaltılmış
- Fiziksel bir işlemcideki mantıksal işlemciler tarafından paylaşılıyor
- Uygulamaya bağlı olarak paylaşılan veya çoğaltılan
Her mantıksal işlemci için aşağıdaki özellikler çoğaltılır:
- Genel amaçlı kayıtlar (EAX, EBX, ECX, EDX, ESI, EDI, ESP ve EBP)
- Segment kayıtları (CS, DS, SS, ES, FS ve GS)
- EFLAGS ve EIP kayıtları. Her mantıksal işlemci için CS ve EIP / RIP kayıtlarının, mantıksal işlemci tarafından yürütülen iş parçacığının talimat akışını gösterdiğini unutmayın.
- x87 FPU kayıtları (ST0 - ST7, durum word'ü, kontrol word'ü, etiket word'ü, veri işlenen işaretçisi ve talimat işaretçisi)
- MMX kayıtları (MM0 - MM7)
- XMM kayıtları (XMM0 - XMM7) ve MXCSR kaydı
- Kontrol kayıtları ve sistem tablosu işaretçi kayıtları (GDTR, LDTR, IDTR, görev kaydı)
- Hata ayıklama kayıtları (DR0, DR1, DR2, DR3, DR6, DR7) ve hata ayıklama denetimi MSR'leri
- Makine kontrolü genel durumu (IA32_MCG_STATUS) ve makine kontrolü kapasitesi (IA32_MCG_CAP) MSR'ler
- Termal saat modülasyonu ve ACPI Güç yönetimi kontrolü MSR'ler
- Zaman damgası sayacı MSR'leri
- Sayfa özellik tablosu (PAT) dahil olmak üzere diğer MSR kayıtlarının çoğu. Aşağıdaki istisnalara bakın.
- Yerel APIC kayıtları.
- Ek genel amaçlı kayıtlar (R8-R15), XMM kayıtları (XMM8-XMM15), kontrol kaydı, Intel 64 işlemcilerde IA32_EFER.
Aşağıdaki özellikler mantıksal işlemciler tarafından paylaşılır:
- Bellek tipi aralık kayıtları (MTRR'ler)
Aşağıdaki özelliklerin paylaşılması veya çoğaltılması, uygulamaya özgüdür:
- IA32_MISC_ENABLE MSR (MSR adresi 1A0H)
- Makine kontrol mimarisi (MCA) MSR'leri (IA32_MCG_STATUS ve IA32_MCG_CAP MSR'ler hariç)
- Performans izleme kontrolü ve karşı MSR'ler
Önbellek paylaşımı:
Intel hyperthreads, ayrı çekirdeklerden daha fazla önbellek ve boru hattı paylaşımına sahiptir: /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858
Linux çekirdeği 4.2
Ana başlatma eylemi gerçekleşiyor gibi görünüyor arch/x86/kernel/smpboot.c
.
ARM minimal çalıştırılabilir baremetal örneği
Burada QEMU için minimal runnable ARMv8 aarch64 örneği sağlıyorum:
.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 akış yukarı .
Birleştirin ve çalıştırın:
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 \
;
Bu örnekte, CPU 0'ı bir spinlock döngüsüne koyduk ve sadece CPU 1 ile çıkar, spinlock'u serbest bırakır.
Döndürme kilidinden sonra, CPU 0, QEMU'dan çıkmayı sağlayan bir yarı ana bilgisayar çıkış çağrısı yapar.
QEMU'yu yalnızca bir CPU ile başlatırsanız -smp 1
, simülasyon sadece sonsuza dek kilitlenir.
CPU 1, PSCI arabirimi ile uyandırılır, daha fazla bilgi için: ARM: Başlat / Uyandır / Diğer CPU çekirdeklerini / AP'lerini getir ve yürütme başlangıç adresini geç?
memba versiyon siz de performans özellikleri ile deneme yapabilecekleri de, bu gem5 üzerinde çalışmasını sağlamak için birkaç ince ayar vardır.
Gerçek donanımda test etmedim, bu yüzden ne kadar taşınabilir olduğundan emin değilim. Aşağıdaki Raspberry Pi bibliyografyası ilgi çekici olabilir:
Bu belge, birden çok çekirdekli eğlenceli şeyler yapmak için kullanabileceğiniz ARM senkronizasyon ilkelerini kullanma konusunda bazı rehberlik sağlar: 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'da test edildi.
Daha rahat programlanabilirlik için sonraki adımlar
Önceki örnekler ikincil CPU'yu uyandırır ve özel talimatlarla temel bellek senkronizasyonu yapar, bu da iyi bir başlangıçtır.
Ancak, POSIX gibi çok çekirdekli sistemleri programlamayı kolaylaştırmak için pthreads
, aşağıdaki daha ilgili konulara da girmeniz gerekir:
kurulum kesintiye uğrar ve şimdi hangi iş parçacığının çalışacağına periyodik olarak karar veren bir zamanlayıcı çalıştırır. Bu, önleyici çoklu kullanım olarak bilinir .
Bu sistem ayrıca başlatılıp durduğunda iş parçacığı kayıtlarını kaydetmeli ve geri yüklemelidir.
Önleyici olmayan çoklu görev sistemlerine sahip olmak da mümkündür, ancak bunlar her bir iş parçacığının vermesi için kodunuzu değiştirmenizi gerektirebilir (örneğin bir pthread_yield
uygulama ile) ve iş yüklerini dengelemek zorlaşır.
İşte bazı basit çıplak metal zamanlayıcı örnekleri:
hafıza çatışmalarıyla başa çıkmak. Özellikle, C veya diğer üst düzey dillerde kodlamak istiyorsanız , her bir iş parçacığının benzersiz bir yığına ihtiyacı olacaktır .
Sadece iş parçacıklarını sabit bir maksimum yığın boyutuna sahip olacak şekilde sınırlayabilirsiniz, ancak bununla başa çıkmanın en güzel yolu verimli "sınırsız boyut" yığınlarına izin veren sayfalamadır .
İşte yığın çok derinleşirse patlayacak saf bir aarch64 baremetal örneği.
Bunlar Linux çekirdeğini veya başka bir işletim sistemini kullanmak için iyi nedenlerdir :-)
Kullanıcı alanı bellek senkronizasyonu ilkelleri
İş parçacığı başlatma / durdurma / yönetimi genellikle kullanıcı alanı kapsamının dışında olsa da, potansiyel olarak daha pahalı sistem çağrıları yapmadan bellek erişimlerini senkronize etmek için kullanıcı alanı iş parçacıklarından gelen montaj talimatlarını kullanabilirsiniz.
Elbette bu düşük seviyeli ilkelleri portatif olarak saran kütüphaneleri kullanmayı tercih etmelisiniz. C ++ standardının kendisi <mutex>
ve <atomic>
üstbilgilerinde ve özelliklestd::memory_order
. Ulaşılabilen tüm olası bellek anlambilimini kapsayıp kapsamadığından emin değilim, ama olabilir.
Daha ince semantikler, belirli durumlarda performans avantajları sunabilen kilitsiz veri yapıları bağlamında özellikle önemlidir . Bunları uygulamak için, farklı bellek engelleri hakkında biraz bilgi edinmeniz gerekecektir: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/
Örneğin Boost'un kilitsiz konteyner uygulamaları var: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html
Bu tür kullanıcı alanı talimatları, Linux'taki futex
ana senkronizasyon ilkelerinden biri olan Linux sistem çağrısını uygulamak için de kullanılıyor gibi görünmektedir . man futex
4.15 okur:
Futex () sistem çağrısı, belirli bir koşul gerçekleşene kadar beklemek için bir yöntem sağlar. Genellikle paylaşılan bellek senkronizasyonu bağlamında bir engelleme yapısı olarak kullanılır. Futexleri kullanırken, senkronizasyon işlemlerinin çoğu kullanıcı alanında gerçekleştirilir. Bir kullanıcı-alanı programı futex () sistem çağrısını yalnızca, koşul gerçekleşene kadar programın daha uzun bir süre engellenmesi gerektiğinde kullanır. Belirli bir koşulu bekleyen işlemleri veya iş parçacıklarını uyandırmak için diğer futex () işlemleri kullanılabilir.
Sistem çağrısı adının kendisi "Hızlı Kullanıcı Alanı XXX" anlamına gelir.
İşte bu tür talimatların çoğunlukla eğlence için temel kullanımını gösteren satır içi montajlı minimal işe yaramaz C ++ x86_64 / aarch64 örneği:
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 akış yukarı .
Olası çıktı:
my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267
Bundan, x86 LOCK öneki / aarch64 LDADD
komutunun ekleme atomunu yaptığını görüyoruz : onsuz birçok eklemde yarış koşulları var ve sondaki toplam sayı senkronize 20000'den daha az.
Ayrıca bakınız:
Ubuntu 19.04 amd64 ve QEMU aarch64 kullanıcı modunda test edilmiştir.