我正在使用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'。
答案 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)或中断描述符表寄存器(IDTR)中。源操作数指定一个6字节的内存位置,其中包含基地址(线性地址)和全局描述符表的限制(表的大小(以字节为单位))( GDT)
您的代码将GDT记录定义为:
gdt_pointer:
dw gdt_end - gdt_start
dd gdt_start
在使用org 0x7c00
dd gdt_start
的引导程序中,将使用gdt_start
的偏移量进行填充。这将是一个地址0x7cxx,其中xx
与gdt_start
所在的引导程序起始点的起点有些距离。可以很好地证明gdt_start
的值也与线性地址相同!
在下面的信息中假定您已修改程序,因此它不再具有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那样固定的地址。考虑进入保护模式:
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之后会发生什么:
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]
,因为它们不再需要。
进行上述更改后,您应该可以使用APIC代码。该代码有很多缺点。请参阅Brendan关于APIC特定问题的答案,但除此之外,还有几个问题需要解决: