底层 M3ss:DOS 多核模式接口






4.83/5 (22投票s)
一篇关于原始CPU技术、在仍可访问DOS中断的情况下,从DOS访问多个核心和保护模式或长模式的综合性文章。
- Github链接:https://github.com/WindowsNT/asm
准备就绪
任何已经读过我的“臭名昭著的三部曲”的人
都希望将所有内容整合到一个精美的应用程序中。这里就是这样的一个结合体,还包含了一些在前几篇文章中未讨论过的新技巧/技术。它实现为一个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
是否仍为0xFF
(AL
中的值),如果是,则将其写入BL
并设置ZF
标志。如果另一个CPU已获取了互斥锁,则ZF
标志将被清除,并且BL
不会被移动到shared_var
。非常方便。
另一个有趣的是pause
操作码,它向CPU提示我们正在一个自旋循环中。这极大地提高了性能,因为CPU知道我们处于自旋循环中,因此不会预取代码。
唤醒CPU
正如我们在三部曲中看到的,我们发送INIT
和SIPI
。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发送消息的方式。除了我们之前看到的INIT
和SIPI
之外,局部APIC还可以用于发送“normal
”中断,即在目标CPU的上下文中执行INT XX
。我们必须考虑以下几点:
- 如果CPU处于
HLT
状态,中断会唤醒它,当中断返回时,CPU将从HLT
操作码之后的指令继续执行。如果还有CLI
,那么我们必须发送NMI中断(APIC中断寄存器中的A标志)来唤醒CPU。 - 如果CPU处于
HLT
状态,并且我们再次发送INIT
和SIPI
,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长模式
正如我在三部曲中提到的,因为RDMSR
和WRMSR
指令可用,所以可以直接从实模式进入长模式。这也被分为两部分。一部分是通过以下方式准备长模式:
- 加载GDT。
- 准备一个用于前1GB的“透明”页表,并将局部APIC映射到一个固定位置(1GB - 2MB)的内存区域,因为局部APIC通常位于
0xFEE00000
,这意味着它在我们的1GB透明区域中不可见,*或者*,准备一个4GB的页表,使用1GB页面,如果您的系统支持1GB页面。大多数都支持。 - 启用
PAE
、PSE
和长模式。
另一部分是通过启用分页、启用中断(可通过int 0xf0
访问)然后跳转到代码来进入长模式。请记住,长模式是平坦的64位,CS
、DS
、ES
、SS
没有意义。至少他们是这么说的,我仍然不得不在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
的向量。它不应该指向0
或IRET
,ES:BX+2
应该指向一个dword“dmmi”。
Int 0xF0
为所有模式(real
、protected
、long
)提供以下功能:
AH = 0
,验证是否存在。返回值,AX = 0xFACE
表示驱动程序存在,DL
= 总CPU数。此功能可从real
、protected
和long
模式访问。AH = 1
,开始线程。BL是CPU索引(1
到max-1
)。该函数创建一个线程,具体取决于AL
的值:0
,启动(非)真实模式线程。ES:DX
= 新线程的段:偏移。线程以支持非真实模式寻址的FS运行,必须使用RETF
返回。1
,启动32位保护模式线程。EDX
是线程的线性地址。线程必须使用RETF
返回。2
,启动64位长模式线程。EDX
包含代码的线性地址,用于在64位长模式下启动。线程必须使用RET
终止。3
,启动虚拟化线程。BH包含虚拟化模式(目前仅支持模式2 = 保护模式虚拟化),EDX包含虚拟化堆栈的线性地址。线程必须使用RETF
或VMCALL
返回。
AH = 5
,互斥锁函数。AL = 0
=> 初始化互斥锁到ES:DI
(实模式),EDI
线性(保护模式),RDI
线性(长模式)。AL = 1
=> 锁定互斥锁AL = 2
=> 解锁互斥锁AL = 3
=> 等待互斥锁
AH = 4
,执行实模式中断。AL
是中断号,BP
保存AX
值,BX
、CX
、DX
、SI
、DI
会传递给中断。DS
和ES
从ESI
和EDI
的高16位加载。
现在,如果您有多个CPU,您的DOS游戏现在可以直接访问所有2^64内存和您所有的CPU,同时仍然可以直接调用DOS。这难道不有趣吗?
INT 0x21重定向
为了避免直接从汇编调用int 0xF0
,并使驱动程序与更高级的语言兼容,安装了一个INT 0x21
重定向处理程序。如果您从主线程调用INT 0x21
,则直接执行INT 0x21
。如果您从protected
或long
模式线程调用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日:首次发布