内存对齐检查机制所检查的地址是有效地址、线性地址还是物理地址?

7

我正在研究对齐检查的问题。但我不知道处理器是在有效地址、线性地址还是物理地址上进行检查,或者是否都要进行检查。

例如,数据的有效地址已经对齐,但通过添加段描述符的基地址形成的线性地址不再对齐,此时处理器会抛出 #AC 异常。


3
好问题。页面对齐,因此线性和物理之间没有区别,但是分段基于字节粒度,尽管建议它们进行对齐。手册中没有说明。也许可以做一个测试 :) (译文):很好的问题。页面是对齐的,因此在线性和物理之间没有区别,但是分段是按字节为单位的,尽管建议对其进行对齐。手册中没有明确说明,也许可以进行一次测试 :) - Jester
@Jester 谢谢你的回答。我目前猜测处理器不会检查有效地址的对齐,因为编译器可以很好地解决其对齐问题。正如你所说,对于今天的操作系统,线性地址和物理地址之间没有区别。虚拟内存和物理内存之间也存在页面级映射关系。因此,如果线性(虚拟)地址对齐,则物理地址必须对齐。总之,我认为对齐检查机制用于维护线性地址的对齐。 - ShelterRin
@fuz 我好像没有看到可以控制页面大小的标志~~ - ShelterRin
2
@fuz:页面大小不是问题;你可能在考虑段限制,可以通过4k或1进行缩放。https://wiki.osdev.org/Global_Descriptor_Table - Peter Cordes
似乎 #AC 的目的是为了捕捉不必要的缓慢内存访问,以便程序员可以修复他们的软件,使其更加总线友好。除了物理地址之外,对于该检查来说没有其他相关性是有意义的。 - Michael Karcher
显示剩余3条评论
1个回答

6

简而言之

我认为这是线性地址。

The test result is A B B A C B (by row)

请继续阅读测试方法和测试代码。


这不是有效地址(也称为偏移量)

要测试它,只需使用一个未对齐的基础段。
在我的测试中,我使用了一个32位数据段,其基础为1。

该测试是一个“简单”的传统(即非UEFI)引导程序,将创建上述描述符并测试使用DWORD宽度访问偏移量0x7000和0x7003。
前者将生成#AC,后者则不会。

这表明仅检查偏移量是不够的,因为当基础为1时,即使0x7000是对齐的偏移量,仍然会出现故障。

这是预期的。

我有一个使用最小输出进行测试的传统,因此需要解释。

首先,在VGA缓冲区的六个连续行中写入六个蓝色的A。
然后在执行加载操作之前,将指针设置到这些A中的每一个。
#AC处理程序将递增指向的字节。
因此,如果一行包含B,则访问将生成#AC。

前四行用于:

  1. 使用基址为0、偏移量为0x7000h的段进行访问,预期结果无#AC。
  2. 使用基址为0、偏移量为0x7003h的段进行访问,预期结果有#AC。
  3. 使用基址为1、偏移量为0x7000h的段进行访问,这会生成#AC,从而证明被检查的是线性地址或物理地址之一。
  4. 使用基址为1、偏移量为0x7003h的段进行访问,这不会生成#AC,确认了第3点。

接下来的两行用于检查线性地址与物理地址之间的关系。

这不是物理地址:#AC而不是#PF

#AC测试只对齐到16个字节,但线性地址和物理地址至少在4KiB上共享相同的对齐方式。
我们需要进行一次需要数据结构对齐至少8KiB的内存访问,以测试是使用了物理地址还是线性地址进行检查。

不幸的是,目前没有这样的访问方式。

我认为通过检查当未对齐的加载指向未映射页面时生成的异常可以收集一些洞见。
如果生成#PF,则CPU将首先翻译线性地址,然后再进行检查。另一方面,如果生成#AC,则CPU将在翻译之前进行检查(请记住该页未映射)。
我修改了测试以启用页面,映射最少量的页面,并通过将指针下的字节增加二来处理#PF。
执行负载时,相应的A将成为B(如果生成#AC)或C(如果生成#PF)。
请注意,两者都是故障(堆栈上的eip指向有问题的指令),但两个处理程序都从下一个指令恢复(因此每个加载仅执行一次)。
这是最后两行的含义:
  1. 使用基址为1,偏移量为0x7003h的段访问未映射页面。这将生成预期的#PF异常(访问已对齐,因此唯一可能的异常是#PF)。
  2. 使用基址为1,偏移量为0x7000h的段访问未映射页面。这将生成#AC异常,因此CPU在尝试翻译地址之前会检查其对齐方式。

第6点似乎表明CPU将对线性地址执行检查,因为没有访问页表。
在第6点中,两种异常都可能被生成,没有生成#PF意味着当执行对齐检查时,CPU尚未尝试翻译地址。(或者#AC逻辑上优先。但是,即使在执行基址+偏移量计算后探测TLB,硬件也不太可能在引发#AC异常之前进行页面遍历。)

测试代码

代码混乱,比人们预料的更加繁琐。
主要障碍是#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的微架构中。


在这种情况下,CPU仅翻译第一个字节的地址,然后假定结构在物理内存中是连续的。该描述会(错误地)暗示您只能在虚拟页面映射到连续的物理页面或使用大型/巨大页面时才能使用FXSAVE。我认为更可能的是MS-ROM执行一系列正常的32字节或64字节存储uop,类似于rep stos(但当然从SIMD / FPU状态获取数据,而不是AL)。因此,每个存储uop都将进行自己的TLB访问。或者当您说“假设”时,您是否只是指#AC目的? - Peter Cordes
1
@PeterCordes 我想我是。我不记得确切的结构(也许不是FXSAVE区域),但有一种情况是CPU会翻译起始地址,然后将其用作基础。稍后我会看看能否找到这个确切的情况(或者我是否记错了)。或者可能是关于跨越两个页面的结构的问题?我想这是后一种情况。 - Margaret Bloom
@PeterCordes 我找不到我要找的东西。我删掉了那段话,以避免写无意义的内容 :) - Margaret Bloom
对于某些东西(特别是与分页或虚拟化有关的东西)来说,这听起来很合理,但对于 fxsave m512byte(或后来的 xsave / xsaveopt)来说则不然。它们是非特权指令,连续的物理内存可能会让用户空间破坏/覆盖未映射到其上的内存,例如从mmap获得的单个页面的末尾开始。因此,这几乎可以排除掉。 - Peter Cordes
非常感谢您的回答,我完全理解了! - ShelterRin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接