65.9K
CodeProject 正在变化。 阅读更多。
Home

底层 M3ss:DOS 多核模式接口

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (22投票s)

2015年5月16日

CPOL

8分钟阅读

viewsIcon

40592

downloadIcon

284

一篇关于原始CPU技术、在仍可访问DOS中断的情况下,从DOS访问多个核心和保护模式或长模式的综合性文章。

准备就绪

任何已经读过我的“臭名昭著的三部曲”的人

都希望将所有内容整合到一个精美的应用程序中。这里就是这样的一个结合体,还包含了一些在前几篇文章中未讨论过的新技巧/技术。它实现为一个TSR,其他应用程序可以调用它来实现真正意义上的多线程,支持原始DOS下的实模式、保护模式或长模式。

使用此代码,您可以创建一个DOS应用程序,它可以

  • 共同使用您的所有CPU
  • 锁定/解锁互斥锁
  • 在实模式、保护模式、长模式和虚拟化模式下启动线程

您需要Flat Assembler,以及一个可以在虚拟化环境中拥有多个核心的FreeDOS安装。VMware在虚拟化方面有效,DOSBox无效,因为它不暴露ACPI。Bochs在特殊的SMP版本中对实模式、保护模式和长模式的虚拟化有效。VirtualBox支持尚未完成。我的github项目为您提供了所有这些设置,非常方便。

背景

  • 1024本汇编书籍
  • 4.023 x 10^23行C++代码
  • 您脑海中还有 1 << 62 的可用空间。高位比特保留给内核。
  • 大量的耐心和幽默感 :)

锁定互斥锁

是的,在Win32中,您有方便的Mutex函数。但在原始DOS中呢?

首先,关于自旋锁。当一个Win32线程调用WaitForSingleObject时,内核会检查对象是否被信号化,如果没有,它就不会安排线程恢复执行。如果没有线程可供安排,内核就会用HLT指令暂停CPU代码,直到稍后。在我们的这个小程序中,我们拥有系统,没有调度器。所以代码只会一直循环,直到互斥锁可用。

因此,可以预期代码如下

    ; BL is the index of this CPU
    .Loop1:
    CMP [shared_var],0xFF ; shared_var would be 0xFF if mutex is released
    JZ .MutexIsFree
    JMP .Loop1
    
    .MutexIsFree:
    MOV [shared_var],BL ; Lock it

事实并非如此。问题在于,当互斥锁释放时,另一个CPU可能在我们的代码之前锁定该变量。也就是说,JZ命令之后、MOV命令之前可能会执行一些操作。

因此,我们必须使用某些原子操作来实现锁定

    ; BL is the index of this CPU
    
    CMP [shared_val],BL ; Perhaps it is locked to us anyway
    JZ .OutLoop2
    .Loop1:
    CMP [shared_val],0xFF ; Free
    JZ .OutLoop1 ; Yes
    pause ; equal to rep nop.
    JMP .Loop1 ; Else, retry
    
    .OutLoop1:

    ; Lock is free, grab it
    MOV AL,0xFF
    LOCK CMPXCHG [shared_val],BL
    JNZ .Loop1 ; Write failed
    
    .OutLoop2: ; Lock Acquired

这里的诀窍很简单。我们使用CMPXCHG指令,并结合LOCK前缀,它可以原子地测试共享变量val是否仍为0xFFAL中的值),如果是,则将其写入BL并设置ZF标志。如果另一个CPU已获取了互斥锁,则ZF标志将被清除,并且BL不会被移动到shared_var。非常方便。

另一个有趣的是pause操作码,它向CPU提示我们正在一个自旋循环中。这极大地提高了性能,因为CPU知道我们处于自旋循环中,因此不会预取代码。

唤醒CPU

正如我们在三部曲中看到的,我们发送INITSIPI。CPU必须从一个4096字节对齐的地址开始,所以我填充了一个包含NOP的数组,并相应地调整了启动地址。CPU从实模式开始。

因此,“SipiStart”例程将是这样的

SipiStart:
    
db 4096 dup (144) ; // fill NOPs

CLI
mov di,DATA16
mov ds,di
lidt fword [ds:RealIDT]; Load real mode interrupts in case they are not loaded
STI

call FAR CODE16:EnterUnreal; Far call because CS is not CODE16 at this point

; Enable APIC
MOV EDI,[DS:LocalApic]
ADD EDI,0x0F0
MOV EDX,[FS:EDI]; unreal mode, FS:EDI works.
OR EDX,0x1FF
MOV [FS:EDI],EDX

mov di,StartSipiAddrOfs ; a dd that contains pre-configured jump to the actual routine for this CPU
jmp far [ds:di]

无论如何,要访问APIC,我必须进入非真实模式,所以我调用EnterUnreal。请注意FAR调用;EnterUnreal开始时的段值与SIPI加载时的CS不同。新唤醒的CPU还必须启用伪向量和软件APIC,正如我们之前看到的。最后,代码将远跳到CPU的“startup”地址,具体取决于CPU索引。

处理器间中断

APIC为我们提供了一种向另一个CPU发送消息的方式。除了我们之前看到的INITSIPI之外,局部APIC还可以用于发送“normal”中断,即在目标CPU的上下文中执行INT XX。我们必须考虑以下几点:

  • 如果CPU处于HLT状态,中断会唤醒它,当中断返回时,CPU将从HLT操作码之后的指令继续执行。如果还有CLI,那么我们必须发送NMI中断(APIC中断寄存器中的A标志)来唤醒CPU。
  • 如果CPU处于HLT状态,并且我们再次发送INITSIPI,CPU将从实模式重新开始。
  • 中断必须存在于目标处理器中。例如,在保护模式下,中断必须已在IDT中定义。
  • 局部APIC对所有CPU都是通用的(在内存方面),因此,在我们发出中断之前,必须对其写访问进行锁定(互斥锁)。
  • 由于寄存器无法在CPU之间传递,我们必须将所有(如果需要)将用于中断的寄存器写入一个单独的内存区域。
  • 中断可能会失败。我不知道为什么,但他们是这么说的。因此,您必须依赖某种CPU间通信(通过共享内存和互斥锁)来验证传递。我在代码中通过一个简单的标志来实现这一点。
  • 最后,中断的处理程序必须告诉其自身的局部APIC“中断结束”。过去是out 020h,al?现在我们向EOI寄存器(LocalApic + 0xB0)写入值0

CPU实模式

如果CPU将在实模式下运行,您可能想调用DOS。这会有效,前提是没有其他CPU同时调用DOS,这在我们这个简单的应用程序中当然无法保证。因此,您必须使用int 0xF0函数5来管理互斥锁。线程自动在非真实模式下启动,并保存了堆栈和FS。线程通过retf终止。如果您通过中断0xF0函数4调用DOS,则会自动提供锁定。

这是dmmic.asm实模式线程中的代码

rt1:

sti
push cs
pop ds
mov dx,m1
mov ax,0x0900
int 0x21

; unlock mut
push cs
pop es
mov di,mut1
mov ax,0x0503
int 0xF0

retf

CPU保护模式

此线程在32位全4GB保护模式下运行。GS指向基址0的32位数据。它使用int 0xF0调用DOS,然后退出。

; ---- Protected Mode Thread
SEGMENT T32 USE32

rt2:

; Int 0xF0 works also in protected mode
mov ax,0
int 0xF0

; DOS call
mov ax,0x0421 ; al = interrupt number
mov bp,0x0900 ; bp = new AX when DOS will be called
xor esi,esi
mov si,MAIN16 ; uppser ESI = new DS
shl esi,16
mov dx,m2
int 0xF0

; Unlock mutex
mov ax,0x0503
linear edi,mut1,MAIN16
int 0xF0

retf

CPU长模式

正如我在三部曲中提到的,因为RDMSRWRMSR指令可用,所以可以直接从实模式进入长模式。这也被分为两部分。一部分是通过以下方式准备长模式:

  • 加载GDT。
  • 准备一个用于前1GB的“透明”页表,并将局部APIC映射到一个固定位置(1GB - 2MB)的内存区域,因为局部APIC通常位于0xFEE00000,这意味着它在我们的1GB透明区域中不可见,*或者*,准备一个4GB的页表,使用1GB页面,如果您的系统支持1GB页面。大多数都支持。
  • 启用PAEPSE和长模式。

另一部分是通过启用分页、启用中断(可通过int 0xf0访问)然后跳转到代码来进入长模式。请记住,长模式是平坦的64位,CSDSESSS没有意义。至少他们是这么说的,我仍然不得不在Bochs中将SS设置为page64_idx。可能是Bochs的bug?

; ---- Long Mode Thread
SEGMENT T64 USE64

rt3:

nop
nop
nop
nop
nop

; Int 0xF0 works also in long mode
mov ax,0
int 0xF0

; DOS call
mov rax,0x0421
mov rbp,0x0900
xor rsi,rsi
mov si,MAIN16
shl rsi,16
mov rdx,m2
;int 0xF0; Whops, DOS still buggy here

; Unlock mutex
mov ax,0x0503
linear rdi,mut1,MAIN16
int 0xF0

ret

CPU虚拟化保护模式

此线程在32位全4GB虚拟化保护模式下运行。它仍然可以调用DOS。这种模式非常有用,因为无论您的线程做什么,它都永远不会导致整个PC崩溃,只会通过VMEXIT过程退出。

v1:

; Int 0xF0 works also in protected mode
mov ax,0
int 0xF0

; DOS call
mov ax,0x0421 ; al = interrupt number
mov bp,0x0900 ; bp = new AX when DOS will be called
xor esi,esi
mov si,MAIN16 ; uppser ESI = new DS
shl esi,16
mov dx,m2
int 0xF0

; Unlock mutex
mov ax,0x0503
linear edi,mut1,MAIN16
int 0xF0

retf; or even VMCALL

DMMI

我称之为DOS多核模式接口。它是一个驱动程序,可帮助您使用int 0xF0为DOS开发32位和64位应用程序。这个中断可以从实模式、保护模式和长模式访问。将功能号放入AH

要检查其是否存在,请检查INT 0xF0的向量。它不应该指向0IRETES:BX+2应该指向一个dword“dmmi”。

Int 0xF0为所有模式(realprotectedlong)提供以下功能:

  • AH = 0,验证是否存在。返回值,AX = 0xFACE表示驱动程序存在,DL = 总CPU数。此功能可从realprotectedlong模式访问。
  • AH = 1,开始线程。BL是CPU索引(1max-1)。该函数创建一个线程,具体取决于AL的值:
    • 0,启动(非)真实模式线程。ES:DX = 新线程的段:偏移。线程以支持非真实模式寻址的FS运行,必须使用RETF返回。
    • 1,启动32位保护模式线程。EDX是线程的线性地址。线程必须使用RETF返回。
    • 2,启动64位长模式线程。EDX包含代码的线性地址,用于在64位长模式下启动。线程必须使用RET终止。
    • 3,启动虚拟化线程。BH包含虚拟化模式(目前仅支持模式2 = 保护模式虚拟化),EDX包含虚拟化堆栈的线性地址。线程必须使用RETFVMCALL返回。
  • AH = 5,互斥锁函数。
    • AL = 0 => 初始化互斥锁到ES:DI(实模式),EDI线性(保护模式),RDI线性(长模式)。
    • AL = 1 => 锁定互斥锁
    • AL = 2 => 解锁互斥锁
    • AL = 3 => 等待互斥锁
  • AH = 4,执行实模式中断。AL是中断号,BP保存AX值,BXCXDXSIDI会传递给中断。DSESESIEDI的高16位加载。

现在,如果您有多个CPU,您的DOS游戏现在可以直接访问所有2^64内存和您所有的CPU,同时仍然可以直接调用DOS。这难道不有趣吗?

INT 0x21重定向

为了避免直接从汇编调用int 0xF0,并使驱动程序与更高级的语言兼容,安装了一个INT 0x21重定向处理程序。如果您从主线程调用INT 0x21,则直接执行INT 0x21。如果您从protectedlong模式线程调用INT 0x21,则会自动执行INT 0xF0函数AX = 0x0421

因此,只要运气好,您就可以直接从另一个线程中的C函数使用您喜欢的stdio函数!

代码

一旦您使用/r运行entry.exe,库就会安装为TSR,并且int 0xf0可用。DMMIC.asm显示了示例调用。

待办事项

  • 添加更多虚拟化模式

历史

  • 2018年1月8日:添加了虚拟化功能
  • 2018年1月7日:修复了长模式下的int 0xF0调用
  • 2018年1月6日:将DMMI更新到我的新github项目
  • 2015年5月22日:感谢Brendan提供的同步技巧
  • 2015年5月18日:修复了中断结束写入的多个调用错误
  • 2015年5月17日:首次发布
© . All rights reserved.