风和日丽的某一天,我思考着能否用C\C++自己手动实现线程上下文的保存和恢复,从而实现类似C#中await\async的协程(Coroutine)。在协程的执行过程中,在线程可以在当前执行的子程序下中断,然后转而执行别的子程序,再在适当的时候再返回来接着执行。不同于操作系统对线程的调度,协程可以由程序来手动控制何时中断、何时恢复这一个过程的。要实现协程的话,其中最核心的要素当属保存和恢复线程上下文(Context)。
C\C++的线程上下文(Thread Context)
所谓的线程上下文,简单来说指的是线程在执行过程中的“工作状态”,例如:程序执行到了什么位置,各个相关变量的值,等等。在操作系统的角度而言,线程上下文可以是线程当前的堆栈(Stack)、程序计数器以及所有寄存器的值的一个“快照”。通过线程上下文,操作系统可以在任意一个中断允许的时刻暂停线程的执行,再在未来地某一时刻无缝地恢复。在程序的角度而言,线程上下文可以有着不同的含义。
例如,对于C#而言,它基于执行IL字节码的堆叠式虚拟机器(Stack-Based VM),它的线程上下文只需要包含堆栈和程序计数器即可。
而对于C\C++这种更接近机器的语言,它的线程上下文总体来说依然是堆栈、程序计数器和寄存器,但具体是哪些寄存器与CPU架构、调用约定息息相关。在C\C++的函数调用过程中,寄存器可以分为两类,一类是易变(Volatile)的,另一类是非易变(Non-volatile)的:
- 易变寄存器被认为会在函数调用过程中发生改变。如果程序在调用函数时有感兴趣的数据存放在易变寄存器中,则需要在调用函数之前保存到栈上、调用返回后再从栈上恢复这些寄存器的值;
- 非易变寄存器被认为不会再函数调用过程中发生改变。如果程序想改变这些寄存器的值,必须将原有的值先保存在栈中,在自身返回前从栈上恢复这些寄存器的值。
那么,假定我们现在要在一个特定的函数中去实现线程上下文的保存与恢复,除了线程堆栈和程序计数器外,必须要保存和恢复的是非易变寄存器。对于易变寄存器则可以忽略,因为调用函数会自己将值保存在堆栈上。至于哪些寄存器是非易变寄存器,则与CPU架构及调用约定(Calling conventions)有关。
值得一提的时,最重要的程序计数器在不同的体系架构上可以和寄存器和堆栈有所交集。例如,在X86下,函数返回地址会通过CALL指令直接压入栈顶、通过RET指令从栈顶取出,这样保存、恢复程序计数器实际上就和保存、恢复堆栈整合在了一起。
C\C++的调用约定(Calling Conventions)
承前文,我们需要根据调用约定来区分哪些寄存器是非易变寄存器。调用约定还规定了函数参数传递的方式。囿于体系结构、编译器等方面的多样性,C\C++的调用约定没有一个统一。下面列举几个比较有代表性的。
X86:cdecl
X86下C语言的默认调用约定,使用栈进行参数传递,对应的非易变寄存器为:EBX, EBP, ESP, EDI, ESI
X86-64:MICROSOFT ABI
X64下Windows和MSVC、Cygwin、Mingw编译器下的C\C++的调用约定,除了栈外使用RCX、RDX、R8、R9等寄存器进行参数传递,对应的非易变寄存器为:RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15(参考自MSDN)
X86-64:SYSTEM-V ABI
X64下其它操作系统和编译器下C\C++的调用约定,除了栈外使用RDI、RSI、RCX、RDX、R8、R9等寄存器进行参数传递,对应的非易变寄存器为:RBX, RBP, RSP, R12, R13, R14, R15(参考Intel文档第16页)
未完待续