TL;DR
我认为是线性地址。
继续阅读测试方法和测试代码。
它不是有效地址(也就是偏移量)
要对此进行测试,只需使用具有未对齐基础的段即可。
在我的测试中,我使用了一个基数为 1 的 32 位数据段。
该测试是一个“简单”的传统(即非 UEFI)引导加载程序,它将创建所述描述符并测试访问具有 DWORD 宽度的偏移量 0x7000 和 0x7003。
前者会生成#AC,后者不会。
这表明检查的不是单独的偏移量,因为 0x7000 是一个对齐的偏移量,它仍然以 1 为基数出错。
这是意料之中的。
我有使用最小输出进行测试的传统,所以解释是强制性的。
首先,六个蓝色 As 被写入 VGA 缓冲区中的六个连续行中。
然后在执行加载之前,为这些 As 中的每一个设置一个指针。
#AC 处理程序将递增指向的字节。
因此,如果一行包含 B,则访问会生成 #AC。
前四行用于:
- 使用基数为 0 且偏移量为 0x7000h 的段进行访问。不出所料,没有#AC
- 使用基数为 0 且偏移量为 0x7003h 的段进行访问。不出所料,#AC
- 使用基数为 1 且偏移量为 0x7000h 的段进行访问。这确实会生成一个#AC,从而证明它是所检查的物理地址的线性地址。
- 使用基数为 1 且偏移量为 0x7003h 的段进行访问。这不会生成 #AC,确认第 3 点。
接下来的两行用于检查线性地址与物理地址。
不是物理地址:#AC 而不是#PF
#AC 测试仅对齐最多 16 个字节,但线性地址和物理地址共享相同的对齐,至少最多 4KiB。
我们需要一个内存访问,它需要一个至少 8KiB 对齐的数据结构来测试它是用于检查的物理地址还是线性地址。
很遗憾,(目前)还没有这样的访问权限。
我想我仍然可以通过检查未对齐的加载针对未映射的页面时生成的异常来收集一些见解。
如果生成#PF,CPU 将首先转换线性地址,然后进行检查。反之,如果生成了#AC,CPU 会在翻译前进行检查(记住页面没有被映射)。
我修改了测试以启用页面,映射最小数量的页面并通过将指针下的字节增加 2 来处理#PF。
执行加载时,如果生成#AC,则对应的A将变为B,如果生成#PF,则对应的A将变为C。
请注意,两者都是错误(堆栈上的eip 指向有问题的指令),但两个处理程序都从 next 指令恢复(因此每个加载只执行一次)。
这些是最后两行的含义:
- 使用基数为 1 且偏移量为 0x7003h 的段访问未映射的页面。这会按预期生成 #PF(访问是对齐的,因此这里唯一可能的例外是 #PF)。
- 使用基数为 1 且偏移量为 0x7000h 的段访问未映射的页面。这会生成 #AC,因此 CPU 在尝试转换地址之前会检查对齐情况。
第 6 点似乎表明 CPU 将检查 线性地址,因为没有完成对页表的访问。
在第 6 点中,可能会生成两个异常,没有生成 #PF 的事实意味着 CPU 在执行对齐检查时没有尝试翻译地址。 (或者说 #AC 在逻辑上优先。但很可能硬件在发生 #AC 异常之前不会执行页面遍历,即使它在执行 base+offset 计算之后确实探测了 TLB。)
测试代码
代码比人们想象的要混乱和繁琐。
主要障碍是#AC 仅在 CPL=3 下工作。
所以我们需要创建 CPL=3 描述符,加上一个 TSS 段和一个 TSS 描述符。
要处理异常,我们需要 IDT,还需要分页。
BITS 16
ORG 7c00h
;Skip the BPB (My BIOS actively overwrite it)
jmp SHORT __SKIP_BPB__
;I eyeballed the BPB size (at least the part that may be overwritten)
TIMES 40h db 0
__SKIP_BPB__:
;Set up the segments (including CS)
xor ax, ax
mov ds, ax
mov ss, ax
xor sp, sp
jmp 0:__START__
__START__:
;Clear and set the video mode (before we switch to PM)
mov ax, 03h
int 10h
;Disable the interrupts and load the GDT and IDT
cli
lgdt [GDT]
lidt [IDT]
;Enable PM
mov eax, cr0
or al, 1
mov cr0, eax
;Write a TSS segment, we zeros 104h DWORDs and only set the SS0:ESP0 fields
mov di, 7000h
mov cx, 104h
xor ax, ax
rep stosd
mov DWORD [7004h], 7c00h ;ESP0
mov WORD [7008h], 10h ;SS0
;Set AC in EFLAGS
pushfd
or DWORD [esp], 1 << 18
popfd
;Set AM in CR0
mov eax, cr0
or eax, 1<<18
mov cr0, eax
;OK, let's go in PM for real
jmp 08h:__32__
__32__:
BITS 32
;Set the stack and DS
mov ax, 10h
mov ss, ax
mov esp, 7c00h
mov ds, ax
;Set the #AC handler
mov DWORD [IDT+8+17*8], ((AC_handler-$$+7c00h) & 0ffffh) | 00080000h
mov DWORD [IDT+8+17*8+4], 8e00h | (((AC_handler-$$+7c00h) >> 16) << 16)
;Set the #PF handler
mov DWORD [IDT+8+14*8], ((PF_handler-$$+7c00h) & 0ffffh) | 00080000h
mov DWORD [IDT+8+14*8+4], 8e00h | (((PF_handler-$$+7c00h) >> 16) << 16)
;Set the TSS
mov ax, 30h
ltr ax
;Paging is:
;7xxx -> Identity mapped (contains code and all the stacks and system structures)
;8xxx -> Not present
;9xxx -> Mapped to the VGA text buffer (0b8xxxh)
;Note that the paging structures are at 6000h and 5000h, this is OK as these are physical addresses
;Set the Page Directory at 6000h
mov eax, 6000h
mov cr3, eax
;Set the Page Directory Entry 0 (for 00000000h-00300000h) to point to a Page Table at 5000h
mov DWORD [eax], 5007h
;Set the Page Table Entry 7 (for 00007xxxh) to identity map and Page Table Entry 8 (for 000008xxxh) to be not present
mov eax, 5000h + 7*4
mov DWORD [eax], 7007h
mov DWORD [eax+4], 8006h
;Map page 9000h to 0b8000h
mov DWORD [eax+8], 0b801fh
;Enable paging
mov eax, cr0
or eax, 80000000h
mov cr0, eax
;Change privilege (goto CPL=3)
push DWORD 23h ;SS3
push DWORD 07a00h ;ESP3
push DWORD 1bh ;CS3
push DWORD __32user__ ;EIP3
retf
__32user__:
;
;Here we are at CPL=3
;
;Set DS to segment with base 0 and ES to one with base 1
mov ax, 23h
mov ds, ax
mov ax, 2bh
mov es, ax
;Write six As in six consecutive row (starting from the 4th)
xor ecx, ecx
mov ecx, 6
mov ebx, 9000h + 80*2*3 ;Points to 4th row in the VGA text framebuffer
.init_markers:
mov WORD [ebx], 0941h
add bx, 80*2
dec ecx
jnz .init_markers
;ebx points to the first A
sub ebx, 80*2 * 6
;Base 0 + Offset 0 = 0, Should not fault (marker stays A)
mov eax, DWORD [ds:7000h]
;Base 0 + Offset 1 = 1, Should fault (marker becomes B)
add bx, 80*2
mov eax, DWORD [ds:7001h]
;Base 1 + Offset 0 = 1, Should fault (marker becomes B)
add bx, 80*2
mov eax, DWORD [es:7000h]
;Base 1 + Offset 3 = 4, Should not fault (marker stays A)
add bx, 80*2
mov eax, DWORD [es:7003h]
;Base 1 + Offset 3 = 4 but page not mapped, Should #PF (markers becomes C)
add bx, 80*2
mov eax, DWORD [es:8003h]
;Base 1 + Offset 0 = 1 but page not mapped, if #PF the markers becomes C, if #AC the markers becomes B
add bx, 80*2
mov eax, DWORD [es:8000h]
;Loop foever (cannot use HLT at CPL=3)
jmp $
;#PF handler
;Increment the byte pointed by ebx by two
PF_handler:
add esp, 04h ;Remove the error code
add DWORD [esp], 6 ;Skip the current instruction
add BYTE [ebx], 2 ;Increment
iret
;#AC handler
;Same as the #PF handler but increment by one
AC_handler:
add esp, 04h
add DWORD [esp], 6
inc BYTE [ebx]
iret
;The GDT (entry 0 is used as the content for GDTR)
GDT dw GDT.end-GDT - 1
dd GDT
dw 0
dd 0000ffffh, 00cf9a00h ;08 Code, 32, DPL 0
dd 0000ffffh, 00cf9200h ;10 Data, 32, DPL 0
dd 0000ffffh, 00cffa00h ;18 Code, 32, DPL 3
dd 0000ffffh, 00cff200h ;20 Data, 32, DPL 3
dd 0001ffffh, 00cff200h ;28 Data, 32, DPL 3, Base = 1
dd 7000ffffh, 00cf8900h ;30 Data, 32, 0 (TSS)
.end:
;The IDT, to save space the entries are set dynamically
IDT dw 18*8-1
dd IDT+8
dw 0
;Signature
TIMES 510-($-$$) db 0
dw 0aa55h
检查线性地址有意义吗?
我认为这不是特别相关。
如上所述,线性地址和物理地址共享相同的对齐方式,最高可达 4KiB。
所以,就目前而言,这根本不重要。
目前,超过 64 字节的访问仍需要以块的形式执行,并且此限制在 x86 CPU 的微架构中设置得很深。