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 (사용자 프로그램)에서 이러한 작업의 대부분을 불가능하게합니다.
따라서 자유롭게 커널을 사용하려면 커널을 작성해야합니다. userland Linux 프로그램이 작동하지 않습니다.
처음에는 부트 스트랩 프로세서 (BSP)라고하는 단일 프로세서가 실행됩니다.
IPI (Inter Processor Interrupts) 라는 특수한 인터럽트를 통해 다른 것 (애플리케이션 프로세서 (AP))을 깨워 야합니다 .
인터럽트는 IRC (Interrupt Command Register)를 통해 APIC (Advanced Programmable Interrupt Controller)를 프로그래밍하여 수행 할 수 있습니다.
ICR의 형식은 다음과 같습니다. 10.6 "ISSUING INTERPROCESSOR INTERRUPTS"
IPI는 ICR에 쓰는 즉시 발생합니다.
ICR_LOW는 8.4.4 "MP 초기화 예"에서 다음과 같이 정의됩니다.
ICR_LOW EQU 0FEE00300H
매직 값 0FEE00300
은 표 10-1 "로컬 APIC 레지스터 주소 맵"에 설명 된대로 ICR의 메모리 주소입니다.
예제에서 가장 간단한 방법이 사용됩니다. 현재 IPC를 제외한 다른 모든 프로세서로 전달되는 브로드 캐스트 IPI를 보내도록 ICR을 설정합니다.
그러나 그것은 또한 가능하며, 일부에서 권장하는 등의 BIOS에서 특별한 데이터 구조 설정을 통해 프로세서에 대한 정보를 얻기 위해, ACPI 테이블이나 인텔의 MP 구성 테이블 만이 하나씩 필요로하는 사람을 깨워.
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 비트에 비해 너무 높은 주소에 쓸 때 초기 프로세서가 보호 모드에 있어야 작동합니다.
프로세서 간 통신을 위해 기본 프로세스에서 스핀 락을 사용하고 두 번째 코어에서 잠금을 수정할 수 있습니다.
메모리 쓰기가 예를 들어 통해 이루어 지도록해야합니다 wbinvd
.
프로세서 간 공유 상태
8.7.1 "논리 프로세서의 상태"는 다음과 같이 말합니다.
다음 기능은 Intel 하이퍼 스레딩 기술을 지원하는 Intel 64 또는 IA-32 프로세서 내 논리 프로세서의 아키텍처 상태의 일부입니다. 기능은 세 그룹으로 세분 될 수 있습니다.
- 각 논리 프로세서마다 중복
- 실제 프로세서에서 논리 프로세서가 공유
- 구현에 따라 공유 또는 복제
각 논리 프로세서에 대해 다음 기능이 복제됩니다.
- 범용 레지스터 (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 (Machine Check Architecture) MSR (IA32_MCG_STATUS 및 IA32_MCG_CAP MSR 제외)
- 성능 모니터링 제어 및 카운터 MSR
캐시 공유는 다음에서 논의됩니다.
인텔 하이퍼 스레드가 별도의 코어보다 캐시와 파이프 라인을 공유 할 수 있습니다 /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858
리눅스 커널 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를 종료 시키는 세미 호스트 종료 호출 을 수행합니다.
를 사용하여 하나의 CPU로 QEMU를 시작하면 -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
하려면 다음과 같은 관련 주제로 넘어 가야합니다.
setup은 인터럽트를 수행하고 타이머를 실행하여 현재 실행할 스레드를 주기적으로 결정합니다. 이것을 선점 형 멀티 스레딩이라고 합니다.
또한 이러한 시스템은 스레드 레지스터가 시작 및 중지 될 때 저장 및 복원해야합니다.
비선 점형 멀티 태스킹 시스템을 사용하는 것도 가능하지만 모든 스레드가 (예 : pthread_yield
구현으로) 생성되도록 워크로드의 균형을 맞추기가 더 어려워 지도록 코드를 수정해야 할 수도 있습니다 .
간단한 베어 메탈 타이머 예제는 다음과 같습니다.
메모리 충돌을 다룹니다. 특히 C 또는 다른 고급 언어로 코딩하려면 각 스레드마다 고유 스택 이 필요합니다 .
스레드가 고정 된 최대 스택 크기를 갖도록 제한 할 수 있지만이를 처리하는 더 좋은 방법 은 효율적인 "무제한 크기"스택을 허용 하는 페이징 입니다.
다음은 스택이 너무 깊이 커질 경우 날려 버릴 것이라고 순진 aarch64의 baremetal 예제
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
의 주요 동기화 기본 요소 중 하나 인 Linux 시스템 호출 을 구현하는 데에도 사용됩니다 . man futex
4.15는 다음과 같이 읽습니다.
futex () 시스템 호출은 특정 조건이 충족 될 때까지 기다리는 메소드를 제공합니다. 일반적으로 공유 메모리 동기화 컨텍스트에서 블로킹 구성으로 사용됩니다. futex를 사용할 때 대부분의 동기화 작업은 사용자 공간에서 수행됩니다. 사용자 공간 프로그램은 futex () 시스템 호출을 사용합니다. 조건이 true가 될 때까지 프로그램이 더 오랜 시간 동안 차단해야 할 경우입니다. 다른 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 prefix / aarch64 LDADD
명령이 추가를 원자화했다는 것을 알았 습니다.이를 사용하지 않으면 많은 추가에 대한 경쟁 조건이 있으며 끝의 총 개수는 동기화 된 20000보다 적습니다.
또한보십시오:
Ubuntu 19.04 amd64 및 QEMU aarch64 사용자 모드에서 테스트되었습니다.