内存对齐校验机制校验的地址是有效地址、线性地址还是物理地址?
Is the address checked by the memory alignment check mechanism a effective address, a linear address or a physical address?
正在研究对齐检查的问题。但是我不知道处理器是检查有效地址,线性地址还是物理地址,还是全部检查。
比如某个数据的有效地址已经对齐,但是段描述符基地址相加形成的线性地址不再对齐,此时处理器抛出#AC异常。
TL;DR
我认为是线性地址。
继续阅读测试方法和测试代码。
它不是有效地址(又名偏移量)
要对此进行测试,使用基部未对齐的片段就足够了。
在我的测试中,我使用了一个基数为 1 的 32 位数据段。
该测试是一个“简单的”遗留(即非 UEFI)引导加载程序,它将创建所述描述符并测试访问具有 DWORD 宽度的偏移量 0x7000 和 0x7003。
前者会生成#AC,后者不会。
这表明检查的不仅仅是偏移量,因为 0x7000 是一个对齐的偏移量,它仍然以 1 为基数出错。
这是预期的。
我有使用最小输出进行测试的传统,因此必须进行解释。
首先,在VGA缓冲区中连续六行写入六个蓝色As。
然后在执行加载之前,为这些 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 将在翻译前进行检查(记住页面未映射)。
我修改了测试以启用页面,映射最少数量的页面并通过将指针下的字节 增加两个 来处理 #PF。
执行加载时,如果生成#AC,则对应的A将变为B;如果生成#PF,则对应的A将变为C。
请注意,两者都是错误(堆栈上的 eip
指向有问题的指令)但是两个处理程序都从 next 指令恢复(因此每个加载只执行一次)。
这些是最后两行的含义:
- 使用基数为 1 且偏移量为 0x7003h 的段访问未映射的页面。这会按预期生成 #PF(访问已对齐,因此此处唯一可能的例外是 #PF)。
- 使用基数为 1 且偏移量为 0x7000h 的段访问未映射的页面。这会生成一个 #AC,因此 CPU 在尝试转换地址之前检查对齐。
第 6 点似乎暗示 CPU 将对 线性地址 执行检查,因为没有完成对页面 table 的访问。
在第 6 点中,可能会生成两个异常,未生成 #PF 的事实意味着 CPU 在执行对齐检查时未尝试转换地址。 (或者 #AC 在逻辑上优先。但是硬件可能不会在出现 #AC 异常之前进行页面遍历,即使它在进行基数 + 偏移量计算之后确实探测了 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 CPUs.
的微体系结构中设置得很深。
正在研究对齐检查的问题。但是我不知道处理器是检查有效地址,线性地址还是物理地址,还是全部检查。
比如某个数据的有效地址已经对齐,但是段描述符基地址相加形成的线性地址不再对齐,此时处理器抛出#AC异常。
TL;DR
我认为是线性地址。
继续阅读测试方法和测试代码。
它不是有效地址(又名偏移量)
要对此进行测试,使用基部未对齐的片段就足够了。
在我的测试中,我使用了一个基数为 1 的 32 位数据段。
该测试是一个“简单的”遗留(即非 UEFI)引导加载程序,它将创建所述描述符并测试访问具有 DWORD 宽度的偏移量 0x7000 和 0x7003。
前者会生成#AC,后者不会。
这表明检查的不仅仅是偏移量,因为 0x7000 是一个对齐的偏移量,它仍然以 1 为基数出错。
这是预期的。
我有使用最小输出进行测试的传统,因此必须进行解释。
首先,在VGA缓冲区中连续六行写入六个蓝色As。
然后在执行加载之前,为这些 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 将在翻译前进行检查(记住页面未映射)。
我修改了测试以启用页面,映射最少数量的页面并通过将指针下的字节 增加两个 来处理 #PF。
执行加载时,如果生成#AC,则对应的A将变为B;如果生成#PF,则对应的A将变为C。
请注意,两者都是错误(堆栈上的 eip
指向有问题的指令)但是两个处理程序都从 next 指令恢复(因此每个加载只执行一次)。
这些是最后两行的含义:
- 使用基数为 1 且偏移量为 0x7003h 的段访问未映射的页面。这会按预期生成 #PF(访问已对齐,因此此处唯一可能的例外是 #PF)。
- 使用基数为 1 且偏移量为 0x7000h 的段访问未映射的页面。这会生成一个 #AC,因此 CPU 在尝试转换地址之前检查对齐。
第 6 点似乎暗示 CPU 将对 线性地址 执行检查,因为没有完成对页面 table 的访问。
在第 6 点中,可能会生成两个异常,未生成 #PF 的事实意味着 CPU 在执行对齐检查时未尝试转换地址。 (或者 #AC 在逻辑上优先。但是硬件可能不会在出现 #AC 异常之前进行页面遍历,即使它在进行基数 + 偏移量计算之后确实探测了 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 CPUs.