【问题标题】:Is the address checked by the memory alignment check mechanism a effective address, a linear address or a physical address?内存对齐检查机制检查的地址是有效地址、线性地址还是物理地址?
【发布时间】:2021-09-01 04:18:05
【问题描述】:

我正在研究对齐检查的问题。但我不知道处理器是在检查有效地址、线性地址还是物理地址,还是全部检查。

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

【问题讨论】:

  • 好问题。页面是对齐的,因此线性和物理之间没有区别,但段基数是字节粒度的,尽管建议对齐。说明书没说。也许做一个测试:)
  • @Jester 谢谢你的回答。我目前猜测处理器不会检查有效地址的对齐,因为编译器可以很好地解决它的对齐问题。正如你所说,对于今天的操作系统,线性地址和物理地址没有区别。虚拟内存和物理内存之间也存在页级映射关系。因此,如果线性(虚拟)地址对齐,那么物理地址必须对齐。综上所述,我认为对齐检查机制是用来保持线性地址的对齐的。
  • @Jester 不能用一些标志设置字节大小的页面吗?我有点忘记了所有这些细节。
  • @fuz 好像没看到可以控制页面大小的flag~~
  • @fuz: 页面大小没有;可能您正在考虑分段限制,可以按 4k 或 1 缩放。wiki.osdev.org/Global_Descriptor_Table

标签: assembly x86 memory-alignment osdev memory-segmentation


【解决方案1】:

TL;DR

我认为是线性地址。

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


不是有效地址(也就是偏移量)

要对此进行测试,只需使用具有未对齐基础的段即可。
在我的测试中,我使用了一个基数为 1 的 32 位数据段。

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

这表明检查的不是单独的偏移量,因为 0x7000 是一个对齐的偏移量,它仍然以 1 为基数出错。

这是意料之中的。

我有使用最小输出进行测试的传统,所以解释是强制性的。

首先,六个蓝色 As 被写入 VGA 缓冲区中的六个连续行中。
然后在执行加载之前,为这些 As 中的每一个设置一个指针。
#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 会在翻译前进行检查(记住页面没有被映射)。

我修改了测试以启用页面,映射最小数量的页面并通过将指针下的字节增加 2 来处理#PF。

执行加载时,如果生成#AC,则对应的A将变为B,如果生成#PF,则对应的A将变为C。
请注意,两者都是错误(堆栈上的eip 指向有问题的指令),但两个处理程序都从 next 指令恢复(因此每个加载只执行一次)。

这些是最后两行的含义:

  1. 使用基数为 1 且偏移量为 0x7003h 的段访问未映射的页面。这会按预期生成 #PF(访问是对齐的,因此这里唯一可能的例外是 #PF)。
  2. 使用基数为 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 的微架构中设置得很深。

【讨论】:

  • 在这种情况下,CPU 只翻译第一个字节的地址,然后假设结构在 物理 内存中是连续的。 - 那个描述如果您的虚拟页面映射到连续的物理页面,或者您正在使用大/超大页面,则(错误地)暗示您只能使用 FXSAVE。我认为 MS-ROM 更有可能执行一系列正常的 32 字节或 64 字节存储 uop,类似于 rep stos 所做的(但当然是从 SIMD/FPU 状态而不是 AL 获取数据)。因此,每个商店 uop 都会进行自己的 TLB 访问。或者当您说“假设”时,您只是为了#AC 目的吗?
  • @PeterCordes 我想我是。我不记得确切的结构(可能不是 FXSAVE 区域),但有一种情况是 CPU 会转换起始地址,然后将其用作基地址。稍后我会看看我是否能找到这个案例到底是什么(或者我是否记错了)。或者可能是关于跨越两页的结构?我认为是后一种情况。
  • @PeterCordes 我找不到我要找的东西。我删除那段以避免写废话:)
  • 对于某些事情(尤其是涉及分页或虚拟化的事情)听起来是合理的,但对于 fxsave m512byte(或更高版本的 xsave / xsaveopt)则不然。它们是非特权指令,连续的物理内存可能会让用户空间损坏/覆盖未映射到它的内存,方法是在页面末尾附近启动一个。例如您从 mmap 获得的单个页面。所以这几乎排除了这一点。
  • 非常感谢您的回答,我完全明白了!
猜你喜欢
  • 1970-01-01
  • 2011-05-11
  • 1970-01-01
  • 2019-09-28
  • 1970-01-01
  • 1970-01-01
  • 2018-10-22
  • 2021-12-15
  • 2014-09-06
相关资源
最近更新 更多