前言
属于是弹幕流以前好奇过的一个问题了:如果访问的内存跨越地址边界,CPU会怎么样。实验一下之后发现:这个答案取决于CPU自己怎么实现的。Intel和AMD的行为是不一样的。
实验理论
为了防止弹幕流能看懂,这里直接用纯汇编实现,因此选择了传统BIOS环境。
代码的基本逻辑如下:
- 进入保护模式
- 构造对等页表:即虚拟地址=物理地址。
- 将0x000-0xFFF和0xFFFFF000-0xFFFFFFFF范围映射到已知有效的一个物理页上。这样保证跨界的地址总是指向有效的物理内存。
- 写一个跨界的指令。这里以
cpuid
(字节码是0F A2
)为例,把0F
写在0xFFFFFFFF上,把A2
写在0x0上。别忘了构造一个返回指令。
- 构造IDT接收异常。我推测是CPU会认为这个指令可能会引起
cs
段出界,那么应该是#GP
异常。那么IDT只需要接住这个异常即可。
- 执行0xFFFFFFFF处的
cpuid
指令。
- 如果IDT接收到异常,则向串口和屏幕输出接到异常的指令。
- 如果顺利返回,则向串口和屏幕输出
cpuid
的执行结果。这个结果包含的是当前CPU的型号。
选择同时向屏幕和串口输出应该可以保证绝大部分机器都能看到输出了。
实验代码
bits 16
org 0x7C00
%macro write_serial 2
mov dx,0x3f8+%1
mov al,%2
out dx,al
%endmacro
start:
cli
; Reset DS segment
xor ax,ax
mov ds,ax
; Load new GDT
lgdt [GDTR]
; Turn on Protection Mode
mov eax,CR0
or al,1
mov cr0,eax
; Refresh code segment
jmp dword 0x08: PEStart
GDTR:
.limit:
dw GDT.End-GDT-1
dd GDT
GDT:
; Null selector
dq 0
; CS.Limit
dw 0xFFFF
; CS.BaseLow
dw 0
; CS.BaseMid
db 0
; CS.Attrib
dw 0xCF9A
; CS.BaseHigh
db 0
; DS.Limit
dw 0xFFFF
; DS.BaseLow
dw 0
; DS.BaseMid
db 0
; DS.Attrib
dw 0xCF92
; DS.BaseHigh
db 0
; CS.Limit
dw 0xFFFF
; CS.BaseLow
dw 0x800
; CS.BaseMid
db 0
; CS.Attrib
dw 0xCF9A
; CS.BaseHigh
db 0
; DS.Limit
dw 0xFFFF
; DS.BaseLow
dw 0x800
; DS.BaseMid
db 0
; DS.Attrib
dw 0xCF92
; DS.BaseHigh
db 0
.End:
bits 32
PEStart:
; Initialize segment registers
mov ax,0x10
mov es,ax
mov ss,ax
mov ds,ax
mov gs,ax
mov ax,0x20
mov fs,ax
; Setup stack
mov esp,0xFFFC
; Initialize Serial Port
write_serial 1,0x00
write_serial 3,0x80
write_serial 0,0x03
write_serial 1,0x00
write_serial 3,0x03
write_serial 2,0xC7
write_serial 4,0x0B
; Setup PDEs
mov edi,0x8000
mov eax,0x87
mov edx,0x200000
call init_pxe
; Setup PTE - 0xFFC000000 to 0xFFFFFFFF
mov dword [0x8FFC],0x9007
mov eax,0xFFC00007
mov edx,0x1000
call init_pxe
; Setup PTE - 0x000 to 0xFFF
mov dword [0x8000],0xA007
mov eax,0x7
call init_pxe
; Map 0xFFFFF000 to 0xB000
mov dword [0x9FFC],0xB007
; Map 0x0 to 0x000
mov dword [0xA000],0xB007
; Setup CR4
mov eax,0x18
mov cr4,eax
; Setup CR3
mov eax,0x8000
mov cr3,eax
; Enable Paging
mov eax,cr0
bts eax,31
mov cr0,eax
; Setup IDT
mov word [0xC068],gp_fault_handler
mov word [0xC06A],0x08
mov dword [0xC06C],0x8E00
lidt [IDT]
; Create CPUID instruction at 0xFFFFFFFF
mov byte [0xFFFFFFFF],0x0F
mov byte [0x0],0xA2
; Create return instruction
mov byte [0x1],0xC3
mov esi,0x7E00
mov edi,0xB8000
; Jump
mov eax,0x80000002
push 0xFFFFFFFF
call [esp]
mov [esi],eax
mov [esi+0x04],ebx
mov [esi+0x08],ecx
mov [esi+0x0C],edx
mov eax,0x80000003
push 0xFFFFFFFF
call [esp]
mov [esi+0x10],eax
mov [esi+0x14],ebx
mov [esi+0x18],ecx
mov [esi+0x1C],edx
mov eax,0x80000004
push 0xFFFFFFFF
call [esp]
mov [esi+0x20],eax
mov [esi+0x24],ebx
mov [esi+0x28],ecx
mov [esi+0x2C],edx
mov word [esi+0x32],0x0D0A
mov ecx,0x32
call dprint
; End
hlt
; Setup PTE
; Input:
; edi: PTE Base
; eax: Page Base /w Permission
; edx: Page Size
; ecx will be cleared to zero
init_pxe:
mov ecx,1024
.setup_pxe_loop:
stosd
add eax,edx
loop .setup_pxe_loop
ret
; Input:
; esi: source text
; edi: VGA Buffer
; ecx: text length
dprint:
push edx
mov ax,0x0700
cld
.transmit_loop:
.check_loop:
mov dx,0x3f8+5
in al,dx
test al,0x20
jz .check_loop
lodsb
mov dx,0x3F8
out dx,al
stosw
loop .transmit_loop
pop edx
ret
gp_fault_handler:
mov esi,gp_text
mov ecx,dword [gp_text.len]
call dprint
pop eax
hlt
IDT:
dw 0x7FF
dd 0xC000
gp_text:
db "#GP is intercepted!",13,10
.len:
dd .len-gp_text
times 510-($-$$) db 0
dw 0xAA55
times 1440*1024-($-$$) db 0
编译出来发现,512个字节空间里只剩20个字节了,有点极限了。。。
编译与运行
用NASM编译本demo:
nasm test.asm -o test.img
QEMU
qemu-system-x86_64 -drive format=raw,file=test.img -serial stdio
由于是模拟执行,不论什么CPU,你能同时在屏幕上和串口上看到QEMU Virtual CPU version 2.5+
。
你可以让QEMU通过硬件虚拟化技术运行demo,这样就能体现出CPU之间的差异了。在Linux上追加-accel kvm -cpu host
,在Windows上追加-accel whpx
,在Mac上追加-accel hvf
。
在Intel的CPU上,你应该能看到CPU的型号,而在AMD的CPU上,你应该会看到#GP is intercepted!
。
VMware
要用VMware运行这个demo,需要将这个demo的映像添加为虚拟机软盘,并设置使用BIOS作为固件,启动虚拟机。
在Intel的CPU上,你应该能看到CPU的型号,而在AMD的CPU上,你应该会看到#GP is intercepted!
。
如果想看串口输出,需要添加一个串口设备,令其通过命名管道进行通信。选择“This end is the server”,“The other end is an application”,然后勾选“Yield CPU on poll”。
用PuTTY通过命名管道与虚拟机串口连接即可。
Hyper-V
与VMware同理,注意这里必须使用一代的Hyper-V虚拟机。否则无法使用传统BIOS。
实机
要用实机运行这个demo,可以将这个映像写进一个U盘里,然后通过U盘运行。
结论
Intel和AMD对于跨界的指令有不同的规则。至于其他x86厂商(如国产x86的兆芯和海光)对此的规则则未知。