需要保存在x86-64体系结构中,使用C函数返回时的上下文有哪些?

huangapple go评论87阅读模式
英文:

Which contexts need to be saved in x86-64 with a c function return?

问题

我正在编写一个玩具操作系统,我知道在切换线程时,需要保存线程的上下文,以便应用程序感觉不到线程切换的发生。这包括所有xmm、ymm、zmm寄存器,这是非常重要的。

但是,在某种情况下,线程切换恰好是c函数的返回,例如thrd_yield(),根据x86调用约定,被调用的C函数不需要保存xmm、ymm、zmm。如果thrd_yield()的实现也不使用xmm、ymm、zmm,那么我们就不需要保存这些寄存器。

我想要问的是,在这种情况下,我需要保存哪些寄存器?我能想到的只有:

  1. thrd_yield()使用的寄存器
  2. callee-save寄存器,thrd_yield()没有使用
  3. rflags寄存器(其他线程可能不是C程序,可能不遵循C语言的调用约定,比如更改直接标志。)
  4. MXCSR寄存器

假设我正在使用System V ABI,是否还需要保存其他寄存器,以及如何保存?能否给我一些建议?非常感谢!

英文:

I'm writing a toy os, I know that when switching a thread, we need to save the thread's context so that the application can not feel the occurrence of thread switching. This includes all the xmm, ymm, zmm registers, it is so heavy.

But in one case, the thread switch happens to be the return of a c function, for example the thrd_yield(), according to x86 calling conventions, the callee C function don't need to save xmm, ymm, zmm. if the implementation of thrd_yield() doesn't use xmm, ymm, zmm either, we don't need to save these.

What I want to ask is, under the circumstances, which regs do I need to save? All I can think of is:

  1. regs that the thrd_yield() used
  2. callee-save regs thrd_yield() not used
  3. rflags register(other threads maybe not a c program, maybe it doesn't follow the C language call convention, like changing direct flag.)
  4. MXCSR register

Assume that I'm using the System V ABI, are there any other registers needed to save and how to save? Can you give me some advice? Thanks very much!

答案1

得分: 4

以下是您要翻译的代码部分:

  1. If your context-switch function looks like a normal function call to its callers (which just eventually returns much later), then C compiler generated code that call it will Just Work as long as the function itself saves the call-preserved registers from its caller's context, and restores those regs from the new context. So it looks to the caller like a function-call that follows the standard calling convention.

    如果您的上下文切换函数在其调用者看起来像普通的函数调用(只是最终会在很久以后返回),那么调用它的C编译器生成的代码将正常工作,只要该函数本身保存了来自其调用者上下文的保存寄存器,并从新上下文中恢复这些寄存器。因此,对调用者而言,它看起来就像遵循标准调用约定的函数调用。

  2. For x86-64 System V, that's only RSP, RBP, RBX, and R12-R15.
    Everything else is call clobbered, like RFLAGS, all the vector regs, AVX-512 mask regs, and x87 st0..7.

    对于x86-64 System V来说,只有RSP、RBP、RBX以及R12-R15是被保存的。
    其他所有寄存器都会被破坏,比如RFLAGS,所有的矢量寄存器,AVX-512掩码寄存器以及x87的st0..7。

  3. The status bits in MXCSR are also basically call-clobbered, but if you want different threads to have different FP environments (e.g. rounding mode and FTZ/DAZ), then you do need to save/restore that. Same for the x87 control register, maybe not the status register.

    MXCSR寄存器中的状态位也基本上会被破坏,但如果您希望不同线程拥有不同的浮点环境(例如舍入模式和FTZ/DAZ),那么您需要保存/恢复它。x87控制寄存器也是如此,或许不是状态寄存器。

  4. MPX is deprecated now so you probably don't need to worry about bnd0-3. If you want to have per-task performance-counter stuff, you could save/restore the PMU performance counters like Linux does of PAPI / perf.

    MPX现在已经被弃用,所以您可能不需要担心bnd0-3。如果您想拥有每个任务的性能计数器,您可以像Linux一样保存/恢复PMU性能计数器,例如PAPI/perf。

  5. Thread-local storage using fsbase or gsbase should be saved/restored if your OS or user-space uses it. There are MSRs for the segment bases (so you can leave the actual segment register values as 0, the null selector). Or if you enable it (for use in user-space or kernel) on a CPU that supports it, rdfsbase / wrfsbase can copy the segment base to/from an integer register even more easily and efficiently than rdmsr / wrmsr. (x86-64 SysV uses FS for thread-local storage.)

    如果您的操作系统或用户空间使用fsbasegsbase来进行线程本地存储,那么应该保存/恢复这些寄存器。段基址寄存器有相应的模型特权寄存器(MSRs),因此您可以将实际段寄存器的值保留为0,即null选择器。或者,如果您在支持它的CPU上启用了这个功能(用于用户空间或内核),rdfsbase / wrfsbase可以更轻松、高效地将段基址复制到/从整数寄存器中,甚至比rdmsr / wrmsr更容易。 (x86-64 SysV使用FS进行线程本地存储。)

  6. An asm caller should treat call thrd_yield exactly like a call to a compiler-generated function, assuming it clobbers all call-clobbered registers, leaving others unmodified.

    汇编调用者应该将call thrd_yield视为对编译器生成的函数的调用,假设它破坏了所有被调用的寄存器,而不改变其他寄存器。

  7. For RFLAGS specifically, the x86-64 SysV ABI also requires DF=0 before a call, and guarantees DF=0 after it returns. You could make thrd_yield run a cld instruction to support sloppy callers.

    特别是对于RFLAGS,x86-64 SysV ABI还要求在调用前DF=0,并保证在返回后DF=0。您可以让thrd_yield执行一个cld指令来支持不严格的调用者。

  8. It's normal for everything to leave AC=0, don't fault on misaligned loads/stores. If you want some tasks to be able to set it without corrupting each other, then you'll have to save/restore the AC bit in RFLAGS. You might as well save/restore the whole RFLAGS since there's no harm in saving/restoring other stuff along with it.

    一切都正常,不会因不对齐的加载/存储而出现故障,AC寄存器会被设置为0。如果您希望某些任务能够在不相互干扰的情况下设置AC寄存器,那么您将不得不保存/恢复RFLAGS中的AC位。最好保存/恢复整个RFLAGS,因为除此之外没有保存/恢复其他内容会有害。

英文:

If your context-switch function looks like a normal function call to its callers (which just eventually returns much later), then C compiler generated code that call it will Just Work as long as the function itself saves the call-preserved registers from its caller's context, and restores those regs from the new context. So it looks to the caller like a function-call that follows the standard calling convention.

For x86-64 System V, that's only RSP, RBP, RBX, and R12-R15.
Everything else is call clobbered, like RFLAGS, all the vector regs, AVX-512 mask regs, and x87 st0..7.

The status bits in MXCSR are also basically call-clobbered, but if you want different threads to have different FP environments (e.g. rounding mode and FTZ/DAZ), then you do need to save/restore that. Same for the x87 control register, maybe not the status register.

MPX is deprecated now so you probably don't need to worry about bnd0-3. If you want to have per-task performance-counter stuff, you could save/restore the PMU performance counters like Linux does of PAPI / perf.

Thread-local storage using fsbase or gsbase should be saved/restored if your OS or user-space uses it. There are MSRs for the segment bases (so you can leave the actual segment register values as 0, the null selector). Or if you enable it (for use in user-space or kernel) on a CPU that supports it, rdfsbase / wrfsbase can copy the segment base to/from an integer register even more easily and efficiently than rdmsr / wrmsr. (x86-64 SysV uses FS for thread-local storage.)


An asm caller should treat call thrd_yield exactly like a call to a compiler-generated function, assuming it clobbers all call-clobbered registers, leaving others unmodified.

For RFLAGS specifically, the x86-64 SysV ABI also requires DF=0 before a call, and guarantees DF=0 after it returns. You could make thrd_yield run a cld instruction to support sloppy callers.

It's normal for everything to leave AC=0, don't fault on misaligned loads/stores. If you want some tasks to be able to set it without corrupting each other, then you'll have to save/restore the AC bit in RFLAGS. You might as well save/restore the whole RFLAGS since there's no harm in saving/restoring other stuff along with it.


Of course, if this thrd_yield() function is ever called from an interrupt handler that might run when user-space has valuable state everywhere (pre-emptive multi-tasking), that's a whole different ball-game.

The way Linux manages it is roughly:

  • Entering the kernel in the first place saves state of integer registers, on a per-thread kernel stack. This will be restored later when returning to user-space for this task, potentially between any two instructions so it's safe for async interrupts.

  • The vector regs aren't used by kernel code (unless it calls kernel_fpu_begin() first). So the interrupt and system call entry points don't have to run xsave; that can be deferred until switching to a new user-space task. At which point you do xsave (or xsaveopt or whatever) for the old context, then xrstor to load the new after switching to the new task's kernel stack.

  • Calling switch_to (see https://stackoverflow.com/questions/6525905/how-does-scheduleswitch-to-functions-from-linux-kernel-actually-work) just switches call-preserved integer registers (of the kernel state of the caller), and saves/restores the user-space FP/SIMD state from the vector regs. (Older kernels used to try to defer this, but modern user-space uses movaps all the time for memcpy and stuff.)

  • When that new kernel state eventually returns back to the syscall or interrupt entry point that got that task into the kernel, the user-space state will be restored. The call-preserved registers will already have been restored by the kernel C functions, but Linux saves/restores all the registers anyway so debuggers (the ptrace system call) can modify that state all in one place.

huangapple
  • 本文由 发表于 2023年3月9日 13:50:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/75680863.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定