x86/x64: 修改 TSS 字段

x86/x64: modifying TSS fields

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

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

CPU执行任务切换时

Software or the processor can dispatch a task for execution in one of the following ways:

• A explicit call to a task with the CALL instruction.
• A explicit jump to a task with the JMP instruction.
• An implicit call (by the processor) to an interrupt-handler task.
• An implicit call to an exception-handler task.
• A return (initiated with an IRET instruction) when the NT flag in the EFLAGS register is set.

关于TSS你可以在Intel手册3的第7章读到


ltr不执行切换,来自Intel手册2:

After the segment selector is loaded in the task register, the processor uses the segment selector to locate the segment descriptor for the TSS in the global descriptor table (GDT).
It then loads the segment limit and base address for the TSS from the segment descriptor into the task register.
The task pointed to by the task register is marked busy, but a switch to the task does not occur.


编辑:我实际上已经测试过 CPU 是否缓存了来自 TSS 的静态值。
测试包含一个启动程序(附后)

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

在我的 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只在必要时读取,没有专门的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 Map Base 字段和它指向的中断重定向位图被读取。
  3. 从较低特权级别到较高特权级别(从较高编号环到较低编号环)的更改会导致堆栈切换。这会导致根据新权限级别读取 SS0/ESP0、SS1/ESP1、SS2/ESP2、RSP0、RSP1 或 RSP2 字段。
  4. 发生任务切换,导致读取新 TSS 中的所有定义字段,除了 I/O 映射基地址、SS0/ESP0、SS1/ESP1、SS2/ESP2,以及上一个任务 Link 字段。当 IRET 指令导致任务切换回嵌套任务时,读取旧 TSS 的上一个任务 Link 字段。
  5. 在 64 位模式下发生任何类型的中断或异常,并且相应 IDT 条目的 IST 字段不为 0。这会导致读取 TSS 的相应 ISTn 字段。

请注意,在 64 位模式下,只有情况 1、3 和 5 会发生,因为 64 位模式不支持虚拟 8086 模式,也不支持任务切换。

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