tangptr@126.com 发表于 4 天前

【处理器】eip跨界溢出行为在不同CPU上的表现


# 前言
属于是弹幕流以前好奇过的一个问题了:如果访问的内存跨越地址边界,CPU会怎么样。实验一下之后发现:这个答案取决于CPU自己怎么实现的。Intel和AMD的行为是不一样的。

# 实验理论
为了防止弹幕流能看懂,这里直接用纯汇编实现,因此选择了传统BIOS环境。
代码的基本逻辑如下:

1. 进入保护模式
2. 构造对等页表:即虚拟地址=物理地址。
3. 将0x000-0xFFF和0xFFFFF000-0xFFFFFFFF范围映射到已知有效的一个物理页上。这样保证跨界的地址总是指向有效的物理内存。
4. 写一个跨界的指令。这里以`cpuid`(字节码是`0F A2`)为例,把`0F`写在0xFFFFFFFF上,把`A2`写在0x0上。别忘了构造一个返回指令。
5. 构造IDT接收异常。我推测是CPU会认为这个指令可能会引起`cs`段出界,那么应该是`#GP`异常。那么IDT只需要接住这个异常即可。
6. 执行0xFFFFFFFF处的`cpuid`指令。
7. 如果IDT接收到异常,则向串口和屏幕输出接到异常的指令。
8. 如果顺利返回,则向串口和屏幕输出`cpuid`的执行结果。这个结果包含的是当前CPU的型号。

选择同时向屏幕和串口输出应该可以保证绝大部分机器都能看到输出了。

# 实验代码
```Asm
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
        ; 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 ,0x9007
        mov eax,0xFFC00007
        mov edx,0x1000
        call init_pxe
        ; Setup PTE - 0x000 to 0xFFF
        mov dword ,0xA007
        mov eax,0x7
        call init_pxe
        ; Map 0xFFFFF000 to 0xB000
        mov dword ,0xB007
        ; Map 0x0 to 0x000
        mov dword ,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 ,gp_fault_handler
        mov word ,0x08
        mov dword ,0x8E00
        lidt
        ; Create CPUID instruction at 0xFFFFFFFF
        mov byte ,0x0F
        mov byte ,0xA2
        ; Create return instruction
        mov byte ,0xC3
        mov esi,0x7E00
        mov edi,0xB8000
        ; Jump
        mov eax,0x80000002
        push 0xFFFFFFFF
        call
        mov ,eax
        mov ,ebx
        mov ,ecx
        mov ,edx
        mov eax,0x80000003
        push 0xFFFFFFFF
        call
        mov ,eax
        mov ,ebx
        mov ,ecx
        mov ,edx
        mov eax,0x80000004
        push 0xFFFFFFFF
        call
        mov ,eax
        mov ,ebx
        mov ,ecx
        mov ,edx
        mov word ,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
        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个字节了,有点极限了。。。

# 编译与运行
用(https://nasm.us)编译本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的兆芯和海光)对此的规则则未知。

dehby1024 发表于 4 天前

前排

0xAA55 发表于 3 天前

这下子,弹幕流是真的看不懂了哈哈哈
页: [1]
查看完整版本: 【处理器】eip跨界溢出行为在不同CPU上的表现