为了账号安全,请及时绑定邮箱和手机立即绑定

多核汇编语言是什么样的?

/ 猿问

多核汇编语言是什么样的?

多核汇编语言是什么样的?

曾几何时,为了编写x86汇编程序,你会得到一条说明“加载EDX寄存器的值为5”,“递增EDX”寄存器等的指令。

对于具有4个核心(甚至更多)的现代CPU,在机器代码级别上它看起来就像有4个独立的CPU(即只有4个不同的“EDX”寄存器)?如果是这样,当你说“递增EDX寄存器”时,是什么决定了哪个CPU的EDX寄存器递增?现在x86汇编程序中是否存在“CPU上下文”或“线程”概念?

核心之间的通信/同步如何工作?

如果您正在编写操作系统,那么通过硬件公开哪种机制可以让您在不同的内核上安排执行?这是一些特殊的特权指示吗?

如果您正在为多核CPU编写优化编译器/字节码VM,那么您需要具体了解x86,以使其生成能够在所有内核中高效运行的代码?

对x86机器代码进行了哪些更改以支持多核功能?


查看完整描述

3 回答

?
隔江千里

这不是问题的直接答案,但它是对评论中出现的问题的答案。从本质上讲,问题是硬件对多线程操作的支持。

Nicholas Flynt说得对,至少对于x86而言。在多线程环境(超线程,多核或多处理器)中,Bootstrap线程(通常在处理器0中的核0中的线程0)开始从地址获取代码0xfffffff0。所有其他线程都在一个名为Wait-for-SIPI的特殊睡眠状态下启动。作为初始化的一部分,主线程通过APIC向WFS中的每个线程发送称为SIPI(启动IPI)的特殊处理器间中断(IPI)。SIPI包含该线程应从其开始获取代码的地址。

此机制允许每个线程从不同的地址执行代码。所需要的只是每个线程的软件支持,以建立自己的表和消息队列。操作系统使用它们来进行实际的多线程调度。

就实际组装而言,正如Nicholas所写,单线程或多线程应用程序的程序集之间没有区别。每个逻辑线程都有自己的寄存器集,所以写:

mov edx, 0

将只更新EDX当前运行的线程EDX使用单个汇编指令无法在另一个处理器上进行修改。您需要某种系统调用来要求操作系统告诉另一个线程运行将更新自己的代码EDX


查看完整回答
反对 回复 2019-08-09
?
莫回无

英特尔x86最小可运行裸机示例

可运行的裸机示例,包含所有必需的样板。所有主要部分均包含在下面。


在Ubuntu 15.10 QEMU 2.3.0和联想ThinkPad T400 真实硬件客户机上测试过。


“ 英特尔手册第3卷系统编程指南 - 325384-056US 2015年9月”在第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

在那段代码上:

  1. 大多数操作系统将使第3环(用户程序)无法完成大部分操作。

    因此,您需要编写自己的内核以便随意使用它:用户域Linux程序将无法运行。

  2. 首先,运行一个处理器,称为自举处理器(BSP)。

    它必须通过称为处理器间中断(IPI)的特殊中断唤醒其他(称为应用程序处理器(AP)

    可以通过中断命令寄存器(ICR)编程高级可编程中断控制器(APIC)来完成这些中断

    ICR的格式记录在:10.6“发布INTERPROCESSOR INTERRUPTS”

    IPI会在我们写入ICR后立即发生。

  3. ICR_LOW在8.4.4“MP初始化示例”中定义为:

    ICR_LOW EQU 0FEE00300H

    神奇值0FEE00300是ICR的存储器地址,如表10-1“本地APIC寄存器地址映射”中所述。

  4. 在示例中使用了最简单的方法:它设置ICR以发送广播IPI,这些IPI被传送到除当前处理器之外的所有其他处理器。

    但是,有些人也可以通过BIOS设置的特殊数据结构(如ACPI表或英特尔MP配置表)获取有关处理器的信息,并且只能逐个唤醒您需要的信息。

  5. XXin 000C46XXH将处理器将执行的第一条指令的地址编码为:

    CS = XX * 0x100IP = 0

    请记住,CS将地址乘以0x10,因此第一条指令的实际内存地址为:

    XX * 0x1000

    因此,例如XX == 1,如果处理器将从0x1000

    然后我们必须确保在该存储器位置运行16位实模式代码,例如:

    cldmov $init_len, %ecxmov $init, %esimov 0x1000, %edirep movsb.code16init:
        xor %ax, %ax
        mov %ax, %ds
        /* Do stuff. */
        hlt.equ init_len, . - init

    使用链接描述文件是另一种可能性。

  6. 延迟循环是一个令人讨厌的工作部分:没有超级简单的方法来精确地进行这样的睡眠。

    可能的方法包括:

    相关:如何在屏幕上显示一个数字,并使用DOS x86程序集睡眠一秒钟?

    • PIT(在我的例子中使用)

    • HPET

    • 用上面的方法校准繁忙循环的时间,然后使用它

  7. 我认为初始处理器需要处于保护模式,因为我们写入的地址0FEE00300H对于16位来说太高了

  8. 要在处理器之间进行通信,我们可以在主进程上使用自旋锁,并从第二个核心修改锁。

    我们应该确保记忆回写完成,例如通过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

  • 大多数其他MSR寄存器,包括页面属性表(PAT)。请参阅以下例外情况。

  • 本地APIC注册。

  • 英特尔64处理器上的附加通用寄存器(R8-R15),XMM寄存器(XMM8-XMM15),控制寄存器,IA32_EFER。

逻辑处理器共享以下功能:

  • 存储器类型范围寄存器(MTRR)

以下功能是共享还是重复是特定于实现的:

  • IA32_MISC_ENABLE MSR(MSR地址1A0H)

  • 机器检查架构(MCA)MSR(IA32_MCG_STATUS和IA32_MCG_CAP MSR除外)

  • 性能监控控制和计数器MSR

Linux内核4.2

主要的初始化动作似乎是在arch/x86/kernel/smpboot.c

ARM最小可运行裸机示例

在这里,我为QEMU提供了一个最小的可运行ARMv8 aarch64示例:

.global mystartmystart:
    /* 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_onlycpu1_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.
     */
    sevcpu1_sleep_forever:
    /* Hint CPU 1 to enter low power mode.
     * Optional, but could save power on a real system.
     */
    wfe
    b cpu1_sleep_forevercpu0_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 0spinlock_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 0xf000spinlock:
    .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,您还需要进入以下更多涉及的主题:

  • 设置中断并运行一个定时器,定期决定现在运行哪个线程。这被称为抢先式多线程

    这样的系统还需要在启动和停止时保存和恢复线程寄存器。

    也可以使用非抢占式多任务系统,但这些系统可能需要您修改代码以使每个线程产生(例如,使用pthread_yield实现),并且平衡工作负载变得更加困难。

    以下是一些简单的裸机定时器示例:

  • 处理内存冲突。值得注意的是,每个线程都需要一个唯一的堆栈。

    您可以将线程限制为具有固定的最大堆栈大小,但处理此问题的更好方法是使用分页,这允许有效的“无限大小”堆栈。

这些是使用Linux内核或其他操作系统的一些很好的理由:-)

Userland内存同步原语

虽然线程启动/停止/管理通常超出用户范围,但您可以使用来自用户态线程的汇编指令来同步内存访问,而无需更昂贵的系统调用。

您当然应该更喜欢使用可以包含这些低级基元的库。C ++标准本身在<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

这是一个最小的无用的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;#endifsize_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 15264my_arch_non_atomic_ulong 15267

从这里我们看到x86 LOCK前缀/ aarch64 LDADD指令使得加法原子:没有它我们在许多添加上有竞争条件,并且最后的总计数小于同步的20000。


查看完整回答
反对 回复 2019-08-09
?
智慧大石

据我了解,每个“核心”都是一个完整的处理器,有自己的寄存器集。基本上,BIOS会在一个核心运行时启动,然后操作系统可以通过初始化它们并将它们指向要运行的代码等来“启动”其他核心。

同步由OS完成。通常,每个处理器为OS运行不同的进程,因此操作系统的多线程功能负责决定哪个进程触摸哪个内存,以及在内存冲突的情况下该怎么做。


查看完整回答
反对 回复 2019-08-09

添加回答

回复

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信