【问题标题】:x86/x64: modifying TSS fieldsx86/x64:修改 TSS 字段
【发布时间】:2017-03-31 21:52:36
【问题描述】:

当激活(即 TR 寄存器指向的那个)TSS 的字段改变时会发生什么?特别是,对 ESP0/RSP0 字段的更改是否会立即生效?或者处理器是否像使用段选择器一样保留 TSS 缓存,因此需要 LTR 指令来强制处理器重新加载 TSS 字段?

【问题讨论】:

    标签: assembly x86 x86-64


    【解决方案1】:

    处理器使用 TSS 存储当前上下文并加载下一个要调度的上下文在任务切换期间
    在 CPU 切换到此类 TSS 之前,更改 TSS 结构不会影响任何上下文。

    CPU 执行任务切换时

    软件或处理器可以通过以下方式之一分派任务以执行:

    • 使用 CALL 指令显式调用任务。
    • 使用 JMP 指令显式跳转到任务。
    • (由处理器)对中断处理程序任务的隐式调用。
    • 对异常处理程序任务的隐式调用。
    • 当EFLAGS 寄存器中的NT 标志被设置时返回(以IRET 指令开始)。

    您可以在 Intel 手册 3 的第 7 章阅读 TSS。


    ltr 不执行切换,来自英特尔手册 2:

    段选择器加载到任务寄存器后,处理器使用段选择器定位 全局描述符表 (GDT) 中 TSS 的段描述符。
    然后它加载段限制和基数 从段描述符到任务寄存器的 TSS 地址。
    任务寄存器指向的任务是 标记为忙,但不会切换到任务


    编辑:我实际测试过 CPU 是否缓存了 TSS 中的静态值。
    测试包含一个引导程序(附件),

    • 创建一个 GDT,其中包含两个具有 DPL 0 和 3 的代码段、两个具有 DPL 0 和 3 的数据段、一个 TSS 和一个具有 DPL 3 的调用门到具有 DPL 0 的代码段。
    • 切换到保护模式,将TSS中ESP0的值设置为v1并加载tr
    • 回到DPL 3的代码段,将ESP0的值改为v2并调用Call gate。
    • 检查 ESP 是 v1-10h 还是 v2-10h,分别打印 1 或 2(如果由于某种原因不匹配,则打印 0)。

    在我的 Haswell 和 Bochs 上,结果为 2,这意味着 CPU 在需要时从内存(层次结构)中读取 TSS。

    虽然对模型的测试不能推广到 ISA,但情况并非如此。


    BITS 16
    
    xor ax, ax          ;Most EFI CPS need the first instruction to be this
    
    ;But I like to have my offset to be close to 0, not 7c00h
    
    jmp 7c0h : WORD __START__
    
    __START__:
    
      cli
    
      ;Set up the segments to 7c0h
    
      mov ax, cs
      mov ss, ax
      xor sp, sp
      mov ds, ax
    
    
      ;Switch to PM
    
      lgdt [GDT]
    
      mov eax, cr0
      or ax, 1
      mov cr0, eax
    
      ;Set CS
    
      jmp CS_DPL0 : WORD __PM__ + 7c00h
    
    __PM__:
    
      BITS 32
    
      ;Set segments
    
      mov ax, DS_DPL0
      mov ss, ax
      mov ds, ax
      mov es, ax
    
      mov esp, ESP_VALUE0
    
      ;Make a minimal TSS BEFORE loading TR
    
      mov eax, DS_DPL0
      mov DWORD [TSS_BASE + TSS_SS0], eax
      mov DWORD [TSS_BASE + TSS_ESP0], ESP_VALUE1
    
    
      ;Load TSS in TR
    
      mov ax, TSS_SEL
      ltr ax
    
      ;Go to CPL = 3
    
      push DWORD DS_DPL3 | RPL_3
      push DWORD ESP_VALUE0
      push DWORD CS_DPL3 | RPL_3
      push DWORD __PMCPL3__ + 7c00h
      retf
    
    __PMCPL3__:
    
      ;UPDATE ESP IN TSS
    
      mov ax, DS_DPL3 | RPL_3
      mov ds, ax
    
      mov DWORD [TSS_BASE + TSS_ESP0], ESP_VALUE2
    
    
      ;SWITCH STACK
    
      call CALL_GATE : 0
    
      jmp $
    
    
    __PMCG__:
    
      mov eax, esp
    
    
      mov bx, 0900h | '1'
      cmp eax, ESP_VALUE1 - 10h
      je __write
    
      mov bl, '2'
      cmp eax, ESP_VALUE2 - 10h
      je __write
    
      mov bl, '0'
    
    __write:
    
      mov WORD [0b8000h + 80*5*2], bx
    
      cli
      hlt
    
    
    GDT dw 37h
        dd GDT + 7c00h      ;GDT symbol is relative to 0 for the assembler
                    ;We translate it to linear
    
        dw 0
    
    
        ;Index 1 (Selector 08h)
        ;TSS starting at 8000h and with length = 64KiB
    
        dw 0ffffh
        dw TSS_BASE
        dd 0000e900h
    
    
        ;Index 2 (Selector 10h)
        ;Code segment with DPL=3
    
        dd 0000ffffh, 00cffa00h
    
        ;Index 3 (Selector 18h)
        ;Data segment with DPL=0
    
        dd 0000ffffh, 00cff200h
    
    
        ;Index 4 (Selector 20h)
        ;Code segment with DPL=0
    
        dd 0000ffffh, 00cf9a00h
    
        ;Index 5 (Selector 28h)
        ;Data segment with DPL=0
    
        dd 0000ffffh, 00cf9200h
    
        ;Index 6 (Selector 30h)
        ;Call gate with DPL = 3 for SEL=20
    
        dw __PMCG__ + 7c00h
        dw CS_DPL0
        dd 0000ec00h
    
    
      ;Fake partition table entry
    
      TIMES 446-($-$$) db 0
    
      db 80h, 0,0,0, 07h
    
    
      TIMES 510-($-$$) db 0
      dw 0aa55h
    
      TSS_BASE  EQU     8000h
      TSS_ESP0  EQU     4
      TSS_SS0   EQU     8
    
      ESP_VALUE0    EQU 7c00h
      ESP_VALUE1    EQU 6000h
      ESP_VALUE2    EQU 7000h
    
      CS_DPL0   EQU 20h
      CS_DPL3   EQU 10h
      DS_DPL0   EQU 28h
      DS_DPL3   EQU 18h
      TSS_SEL   EQU 08h
      CALL_GATE EQU 30h
    
    
      RPL_3     EQU 03h
    

    【讨论】:

    • 正确,但还有一个细节:TSS 还包含用于权限级别更改的堆栈指针。它们不会在上下文切换时“立即”使用。处理器是否在“影子副本”中的上下文切换中加载它们,然后在需要时使用这些副本?
    • @GiuseppeGuerrini 我相信他们会在需要时阅读,call 指令描述和手册中的调用门描述似乎暗示了这一点。不过,这很容易测试,也许我可以调查一下。
    • @GiuseppeGuerrini 我实际上测试了 CPU 的行为,在我的模型中,值是在需要时从内存中读取的。我相信这可以推广到任何模型。
    • 好实验!我的猜测是,您观察到的行为现在可以被认为是架构性的。如果像 Haswell 这样的最新(和流行)芯片组以这种方式工作,那么英特尔在不破坏兼容性的情况下进行更改可能为时已晚。 (但是,关于 Bochs:我不确定它在模拟中的准确性是否达到如此详细的程度)。
    【解决方案2】:

    仅在必要时读取 TSS,并且没有特殊的 TSS 缓存。 (GDT 中的 TSS 描述符与段描述符一样被缓存,但不是 TSS 本身的内容。TSS 可以像任何其他内存区域一样缓存在普通的 L1/L2/L3 内存缓存中。)

    在不同情况下可以读取 TSS 的三个不同区域。在适当的情况发生之前,更改 TSS 中的任何值均无效。它们是:

    1. 在虚拟 8086 模式下或当 CPL > IOPL 时执行 I/O 指令(IN、INS、OUT、OUTS)。这会导致读取 I/O 映射基地址字段和它指向的 I/O 映射。
    2. 当 CR4.VME 为 1 时,在虚拟 8086 模式下执行软件中断 (INT)。这会导致读取 I/O 映射基字段及其指向的中断重定向位图。
    3. 从较低特权级别更改为较高特权级别(从较高编号的环到较低编号的环)会导致堆栈切换。这会导致读取 SS0/ESP0、SS1/ESP1、SS2/ESP2、RSP0、RSP1 或 RSP2 字段,具体取决于新的权限级别。
    4. 发生任务切换,导致读取新 TSS 中的所有已定义字段,但 I/O 映射基地址、SS0/ESP0、SS1/ESP1、SS2/ESP2 和先前任务链接字段除外。当 IRET 指令导致任务切换回嵌套任务时,将读取旧 TSS 的上一个任务链接字段。
    5. 在 64 位模式下发生任何类型的中断或异常,并且相应 IDT 条目的 IST 字段不为 0。这会导致读取 TSS 的相应 ISTn 字段。

    请注意,在 64 位模式下,仅会出现第 1、3 和 5 种情况,因为 64 位模式不支持虚拟 8086 模式,也不支持任务切换。

    除了 GDT 中与给定选择器对应的条目外,LTR 指令不会读取任何内存区域,也没有任何内部 TSS 缓存可供它刷新。

    【讨论】:

    • 我的问题特别是关于第 (3) 点。我想知道 RSPx 的更改是否足以影响后续的权限更改,或者需要某种“刷新/重新加载”操作。您的回答暗示 RSPx 的变化就足够了,而 Margaret Bloom 的回答则相反(见我上面的评论)
    • @GiuseppeGuerrini 我相信罗斯和我确实在说同样的话,TSS 在需要时从内存层次结构中读取。
    猜你喜欢
    • 1970-01-01
    • 2021-10-26
    • 2017-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-11
    • 1970-01-01
    相关资源
    最近更新 更多