是否可以通过INIT-SIPI-SIPI序列唤醒所有内核处于实模式的intel内核?

时间:2019-05-27 03:10:24

标签: assembly x86 dos multicore fasm

我正在使用DOS来启动并启动我的应用程序 test.exe 。该程序以实模式启动BSP(引导处理器),并访问FEE0:0000处的APIC表以启用偏移量为0x0F0的SVI(虚假向量中断)并使用两者发送一个INIT-SIPI-SIPI序列ICR_low(偏移量0x300)和ICR_high(偏移量0x310)。 BSP进入循环 jmp $ 以停止执行,并让AP(应用处理器)在地址0000:8000执行代码并打印字符。

似乎没有将消息发送到AP,因为我看不到它们中的任何内容都会在显示器上打印任何内容。

我在实模式下使用FreeDos。要编译,我使用的是 FASM (平面汇编程序)

我使用了 OsDev 手册,其中包括我用来测试(经过一些修改)的代码,该代码尽可能地简单,以查看是否可以使它工作。我还参考了 Intel程序员手册和其他规范以及Code Project的教程。

我只是想唤醒AP并执行一些简单的代码。我发现的所有示例都进入了虚幻模式,保护模式,长模式或专注于多核处理。我只是在编写此代码以了解其工作原理。

我的代码是:

    format MZ  

    USE16 

    start:
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    xor sp, sp
    cld
    ;Clear screen                
       mov ax, 03h
       int 10h
    ;Move payload to the desired address
       mov si, payload
       mov cx, payload_end-payload + 1
       mov bx,es
       mov ax,7c0h
       mov es,ax
       mov di,400h                 ;07c0:400 = 8000h
       rep movsb
       mov es,bx
    ;Enable APIC table
       call enable_lapic
    ; Wakeup the other APs
      ;INIT
       call lapic_send_init
       mov cx, WAIT_10_ms
       call us_wait
      ;SIPI
       call lapic_send_sipi
       mov cx, WAIT_200_us
       call us_wait
      ;SIPI
       call lapic_send_sipi

      ;Jump to the payload
      ;Para teste de acordar nucleos
      jmp 0000h:8000h ;voltar esse depois


    ;Payload é o código que será movido para o endereço físico 0x08000
    payload:
      mov ax, cs
      mov ds, ax
      xor sp, sp
      cld
    ;Only print letter 'A' directly to video memory
      mov cx,0b800h
      mov es,cx
      mov di,00h
      mov al,41h
      stosb
      cli    
      hlt    
    payload_end:

    enable_lapic:
      mov ecx, IA32_APIC_BASE_MSR
      rdmsr
      or ah, 08h ;Enable global APIC flag
      wrmsr
      and ah, 0f0h ; Mask to obtain APIC_Base address
      mov DWORD [APIC_BASE], eax ;Save it
      shr eax,16
      mov bx,fs
      mov fs,ax
      mov ecx, DWORD [fs:APIC_REG_SIV] ;Load value from SIV (FEE0:00F0) to ecx
      or ch, 01h    ;bit8: APIC SOFTWARE enable/disable
      mov DWORD [fs:APIC_REG_SIV], ecx ;Save it
      mov fs,bx
      ret

    IA32_APIC_BASE_MSR = 1bh
    APIC_REG_SIV       = 0f0h
    APIC_REG_ICR_LOW   = 300h
    APIC_REG_ICR_HIGH  = 310h
    APIC_REG_ID        = 20h

    APIC_BASE         dd 00h

    ;CX = Wait (in ms) Max 65536 us (=0 on input)
    us_wait:
      mov dx, 80h               ;POST Diagnose port, 1us per IO
      xor si, si
      rep outsb
      ret
      WAIT_10_ms     = 10000
      WAIT_200_us    = 200

    lapic_send_init:
      mov eax, DWORD [APIC_BASE]
      xor ebx, ebx
      shr eax,16
      mov cx,fs
      mov fs,ax
      mov DWORD [fs:APIC_REG_ICR_HIGH], ebx
      mov ebx, 0c4500h
      mov DWORD [fs:APIC_REG_ICR_LOW], ebx  ;Writing the low DWORD sent the IPI
      mov fs,cx
      ret

    lapic_send_sipi:
      mov eax, DWORD [APIC_BASE]
      xor ebx, ebx
      shr eax,16
      mov cx,fs
      mov fs,ax
      mov DWORD [fs:APIC_REG_ICR_HIGH], ebx
      mov ebx, 0c4608h
      mov DWORD [fs:APIC_REG_ICR_LOW], ebx  ;Writing the low DWORD sent the IPI
      mov fs,cx
      ret

我希望BSP进入无限循环,并且AP在0000:8000处执行代码并在视频内存上打印'A'。

2 个答案:

答案 0 :(得分:2)

  

是否可以通过INIT-SIPI-SIPI序列唤醒所有内核处于实模式的英特尔内核?

是(也许)。有2个选项:

a)如果CPU支持x2APIC,则可以启用它并使用MSR发送INIT-SIPI-SIPI序列(无需访问您无法在实模式下访问的地址的内存映射寄存器)

b)对于xAPIC;可能可以更改本地APIC使用的地址(通过写入APIC_BASE MSR),以便可以在实模式下对其进行访问。但是,这需要格外小心,因为不应将本地APIC放在已经使用的任何位置,并且您可以在实模式下访问的所有空间都可能已经在使用中。为了解决这个问题,您可能需要“特定于芯片组”的代码来修改访问的路由位置(到RAM,到PCI总线等),然后再执行代码以重新配置相应的MTRR。 APIC_BASE MSR还是有点“特定于CPU”的(在80486上不存在,在其他供应商的CPU上可能不存在)。注意:我不会认为此选项理智或实用(特别是对于需要在多台计算机上运行的代码而言)。

注意:您应该仅启动固件中指出存在的CPU(并且不应将INIT-SIPI-SIPI序列广播给有故障和禁用的CPU);并且很可能您将无法以实模式访问ACPI表(需要找出存在哪些CPU)。出于这个原因(因为在不使用保护模式的情况下启动其他CPU没有意义),我的回答应被视为“仅出于学术目的”。

答案 1 :(得分:0)

当我第一次遇到这个问题时,我知道部分问题是DOS如何给需要线性地址的代码增加一定程度的复杂性。我建议将其作为引导程序来测试删除DOS环境的复杂性的位置。旧版BIOS引导加载程序将始终将代码置于物理地址0x07c00。在实模式下,物理和线性地址是同一回事。只要您的引导程序在启动时将段设置为0x0000并使用org 0x7c00伪指令-所有内存引用都将相对于内存的开头。 segment:offset pair为0x0000:0x7c00 =物理地址(0x0000 << 4)+ 0x07c00。

准确知道程序在物理内存中的位置非常重要,因为LGDT指令是少数加载需要线性地址的信息的指令之一:

  

将源操作数中的值加载到全局描述符表寄存器(GDTR)或中断描述符表寄存器(ID​​TR)中。源操作数指定一个6字节的内存位置,其中包含基地址线性地址)和全局描述符表的限制(表的大小(以字节为单位))( GDT)

您的代码将GDT记录定义为:

gdt_pointer:
    dw gdt_end - gdt_start
    dd gdt_start

在使用org 0x7c00 dd gdt_start的引导程序中,将使用gdt_start的偏移量进行填充。这将是一个地址0x7cxx,其中xxgdt_start所在的引导程序起始点的起点有些距离。可以很好地证明gdt_start的值也与线性地址相同!


使用DOS时有何不同?

在下面的信息中假定您已修改程序,因此它不再具有org 0x7c00,不再填充512字节(和引导签名),并且文件的顶行现在是format MZ用于DOS可执行文件。

DOS的问题是生成的程序具有相对于DOS加载代码和数据的段的开头的偏移量。每次运行程序时,这些段可能会有所不同,具体取决于内存中的内容。在组装时,我们不知道代码在内存中的加载位置,因此,在DOS加载程序并运行之前,我们可能无法知道物理(线性)地址。这不同于始终在相同物理地址加载的引导加载程序。

为什么这一切都重要?当FASM为MZ(DOS)EXE程序生成代码时,生成的偏移量将相对于DOS将我们加载到的段的开头。如果gdt_start与段的开头之间的偏移量为0x60(例如),则GDT指针dd gdt_start将被填充值0x60。由于LGDT指令会将其视为线性地址,该指令告诉LGDT GDT本身位于线性(物理地址)0x00000060。那是中断表中间的地址,而不是我们程序中的地址!进入保护模式后,第一次重新加载段时,处理器将在错误的内存位置查找GDT,读取伪造的描述符表,最有可能崩溃(三重故障/重新启动)。实际上,当您执行jmp CODE_SEG:exit并将 CS 选择器加载到伪造的GDT中时,该选择器会崩溃。

如果DOS从段0x1230的开头开始加载程序(例如),并且GDT在程序中的偏移量为0x60,则内存中GDT的线性地址(物理)实际上是(0x1234 << 4) + 0x60 = 0x123a0。程序开始运行时,您需要做的是确定DOS将程序加载到了哪个段并进行此计算并更新gdt_pointer结构中的GDT地址。使用FASM创建不带segment指令的DOS程序会将所有代码和数据放在同一段中。您可以通过获取 CS 的值来获取段,然后将该值左移4位,然后将其添加到由汇编器存储在gdt_pointer中的偏移量中。将 CS 加载到其他寄存器中时,可以在代码的开头执行此操作。必须在设置 DS 之后完成:

mov eax, cs
mov ds, ax
mov es, ax

mov ebx, eax
shl ebx, 4
add [gdt_pointer+2], ebx

; mov ss, ax
; xor sp, sp

我已经删除了设置SS:SP的步骤,因为在DOS EXE加载程序加载程序时,DOS已经为我们设置了它们。我将 CS 移至 EAX ,以使 EAX 的高16位为零,从而简化了计算代码。我们将 EAX 复制到 EBX ,将值左移4位(与乘以16十进制相同),然后将其直接添加到gdt_pointer的GDT偏移量部分(gdt_pointer + 2是存储GDT偏移的位置)。汇编器将gdt_start的偏移量存储在gdt_pointer+2处,我们正在将其调整为线性地址。

如果要汇编代码并运行它,它将崩溃!


我使用线性地址设置GDT-现在怎么样?

GDT并不是代码中唯一需要像GDT那样固定的地址。考虑进入保护模式:

jmp CODE_SEG:exit ;long jump to the code segment
exit:

标签exit相对于我们要加载的段的开头。CODE_SEG选择器指向基数为0x00000000的4GiB平面代码描述符。 Exit的偏移量较小,为了便于讨论,我们说它是0xf5。 FAR JMP将转到CODE_SEG:0xf5,它将是内存地址0x000000f5,而不是我们加载的地址。有许多方法可以解决此问题,但大多数方法涉及将FAR JMPing到我们必须在运行时计算的固定地址。一种机制是在GDT代码描述符中使用非零基数,但是该选项不在此答案的范围内。最容易理解的是在内存中创建6字节指针(32位偏移量和16位段),然后执行间接 FAR JMP。我们可以像执行exit一样修正gdt_start的偏移量。此时,我将exit重命名为pmode或其他有意义的名称。

要进行修复,我们可以像gdt_pointer修复一样一开始就进行修复。起始代码现在看起来像:

mov eax, cs
mov ds, ax
mov es, ax

mov ebx, eax
shl ebx, 4
add [gdt_pointer+2], ebx
add [pmode_farptr], ebx

; mov ss, ax
; xor sp, sp

在引导加载程序的同一区域,您拥有gdt_pointer结构,您将添加一个新的pmode_farptr结构,如下所示:

gdt_pointer:
    dw gdt_end - gdt_start
    dd gdt_start
CODE_SEG = gdt_code - gdt_start
DATA_SEG = gdt_data - gdt_start

pmode_farptr:
    dd pmode      ; Offset of pmode label
    dw CODE_SEG   ; Segment to use

现在可以通过以下方式完成间接FAR JMP:

    jmp fword [pmode_farptr];long jump to the code segment
                            ;indirectly through 6 byte (fword)
                            ;pointer at pmode_farptr
pmode:
    ret

我现在修复了我的FAR JMP,但它仍然崩溃!

问题是现在的FAR JMP之后会发生什么:

    jmp fword [pmode_farptr];long jump to the code segment
                            ;indirectly through 6 byte (fword)
                            ;pointer at pmode_farptr
pmode:
    ret

pmode标签上,您现在处于32位保护模式。有ret,但是您没有设置 SS 指向有效的数据描述符,也没有设置 ESP 堆栈指针,并且您还没有设置其他段寄存器!即使您将pmode之后的堆栈设置为指向实模式堆栈所指向的位置,堆栈上的返回地址也将是一个问题。 call enterProtectedMode完成后,将2字节的NEAR返回地址压入堆栈。我们现在处于32位保护模式,其中NEAR地址为4个字节。最简单的方法是抛开ret并将pmode标签移到32位模式下已有的代码。这段代码:

    call enterProtectedMode

use32

    ;Enable the APIC
     call enable_lapic

现在可以成为:

    call enterProtectedMode

use32
pmode:
    movzx esp, sp           ; Extend SP to ESP zero extending upper bits
    mov eax, ss
    shl eax, 4
    add esp, eax            ; ESP is now the linear address of original SS:SP pointer
    mov ax, DATA_SEG        ; Reload segment register with 32-bit flat
                            ; flat data selector
    mov ss, ax
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    ...

注意:删除pmode之后的ret标签和jmp fword [pmode_farptr],因为它们不再需要。


形势大好

进行上述更改后,您应该可以使用API​​C代码。该代码有很多缺点。请参阅Brendan关于APIC特定问题的答案,但除此之外,还有几个问题需要解决:

  • 您的代码将AP(应用处理器)有效负载盲目复制到0x8000。如果DOS堆栈位于该区域会怎样?尽管极不可能,但是如果堆栈确实与代码冲突,程序可能会崩溃。
  • 有效负载中的代码已复制到0x8000。尽管这适用于简单的有效负载代码,但是如果您在有效负载内添加数据,则所有生成的数据偏移都将是错误的。您的有效负载原本是整个程序的一部分,并且与段的开头之间没有偏移0x0000。
  • 在有效负载中,您将 CS 复制到 DS ,但是尚未设置 CS 。将 CS 复制到 DS 不会达到您的期望,但不会损害您编写的不依赖 DS < / em>是一个特定值-没关系。
  • AP代码无法与Bootstrap处理器(BSP)进行数据通信,因为AP不知道DOS在内存中将原始程序加载到的位置。这也会严重限制您的有效负载代码可以执行的操作。使用引导加载程序,您知道您始终可以访问引导加载程序和BSP数据,因为您知道数据相对于0x0000:0x7c00位于内存中。
  • 这些观察结果有效地概括为:有效负载代码可能无法执行任何复杂的操作,而尝试这样做会给您带来意想不到的行为。您可以修改代码以解决所有这些缺陷,但这超出了此答案的范围。