简介
Swift_Boost_Context is a foundational library that provides a sort of cooperative multitasking on a single thread. By providing an abstraction of the current execution state in the current thread, including the stack (with local variables) and stack pointer, all registers and CPU flags, and the instruction pointer, a execution context represents a specific point in the application's execution path. This is useful for building higher-level abstractions, like coroutines, cooperative threads (userland threads) or an equivalent to C# keyword yield in C++.
翻译一下:
Swift_Boost_Context 是一个基本库,提供了在单个线程上的某种协作多任务处理。通过提供当前线程中当前执行状态的抽象,包括堆栈(带有本地变量)和堆栈指针、所有寄存器和CPU标志以及指令指针,执行上下文表示应用程序执行路径中的一个特定点。这对于构建更高层次的抽象很有用,比如协程、协作线程(用户线程)或者等价于c++中的c#关键字yield。
另外,
此库是借助 Swift 语言重新实现 boostorg/context 的版本.
编写此库的初衷是 由于 Swift 从 5.3 版本开始官方已支持目前世界上大多数流行的操作系统和平台, 包括但不限于 Windows10, Android, Linux相关发行版本(Ubuntu, centOS), 最新一代前端技术 WASM 等等, 为此想借助 Swift 这门系统级别语言的跨平台特性将 协程(Corountine) 引入到上面的操作系统和平台当中去.
这里其实隐含了多个信息:
- 可以在 Android 的软件开发中使用
Swift 版本的 Corountine而不是Jvm 生态中 Kotlin 版本的 Corountine. - 可以在 Windows10 的软件开发中使用
Swift 版本的 Corountine. - 可以在 Linux 的软件开发中使用
Swift 版本的 Corountine. - 在 Apple 旗下的操作系统做开发的就更不用说了.
讲解
通过提供当前线程中当前执行状态的抽象,
BoostContext 这个协议(protocol, 别的语言叫 interface)就是代表着当前线程中当前执行状态的抽象.
当前线程中当前执行状态的抽象包括堆栈(带有本地变量)和堆栈指针、所有寄存器和CPU标志以及指令指针,执行上下文表示应用程序执行路径中的一个特定点。
也就是说 BoostContext 是包括堆栈(带有本地变量)和堆栈指针、所有寄存器和CPU标志以及指令指针,执行上下文表示应用程序执行路径中的一个特定点.
想象一下, 每个单独的 BoostContext 实例都包含着不同的 当前线程中当前执行状态, 然后在程序运行的过程中有目的性地去动态切换这新些不同 BoostContext 实例, 就能实现程序的流程跳转 (类似于 goto 语句).
下面是一段代码, 用于演示如何借助 BoostContext 实现程序流程跳转.
import Swift_Boost_Context
func f1(data: Int, yield: FN_YIELD<String, Int>) -> String {
defer {
print("f1 finish")
}
print("main ----> f1 data = \(data)")
let data1: Int = yield("1234567")
print("main ----> f1 data = \(data1)")
let data2: Int = yield("7654321")
print("main ----> f1 data = \(data2)")
return "9876543"
}
func main() throws {
let yield: FN_YIELD<Int, String> = makeBoostContext(f1)
let data1: String = yield(123)
print("main <---- f1 data = \(data1)")
let data2: String = yield(765)
print("main <---- f1 data = \(data2)")
let data3: String = yield(987)
print("main <---- f1 data = \(data3)")
}
do {
try main()
} catch {
print("main : \(error)")
}
Output: (Run on macOS x86_64)
main ----> f1 data = 123
main <---- f1 data = 1234567
main ----> f1 data = 765
main <---- f1 data = 7654321
main ----> f1 data = 987
f1 finish
main <---- f1 data = 9876543
Process finished with exit code 0
上面的输出非常直观地显示出程序流程:
main(start) --> f1(start) --> f2(start) ---
↑
| 来回反复跳转
↓
main(end) <-- f1(end) <--
原理
BoostContext 是如何实现的呢?
BoostContext 是
包括堆栈(带有本地变量)和堆栈指针、所有寄存器和CPU标志以及指令指针,执行上下文表示应用程序执行路径中的一个特定点.
由于需要将所有寄存器和CPU标志以及指令指针(PC, Process Count)存储到一个 BoostContext 的实例当中去, 从而方便后续的切换.
因此我们需要借助 汇编 技术来操作这些 寄存器和CPU标志以及指令指针(PC, Process Count), 这里通过展示 macOS x86_64( 应该算是最简单的😓 , 跟 Linux 的差不多 ) 和 Android aarch64 的方案来进行一个比较粗略的认知:
- macOS x86_64
本文上个小节中 BoostContext 是通过 makeBoostContext 创建实例的:
// ...
let yield: FN_YIELD<Int, String> = makeBoostContext(f1)
// ...
使用 Swift 编写的函数 makeBoostContext 最终会调用 汇编 实现的函数 make_fcontext.
现在就来看看 make_fcontext 的汇编部分的代码实现:
函数原型:
fcontext_t make_fcontext(void *sp, size_t size, void (*fn)(transfer_t));
具体实现:
/*
Copyright Oliver Kowalke 2009.
Distributed under the Boost Software License, Version 1.0.
(See accompanying file LICENSE_1_0.txt or copy at
http://www.boost.org/LICENSE_1_0.txt)
*/
/****************************************************************************************
* *
* ---------------------------------------------------------------------------------- *
* | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | *
* ---------------------------------------------------------------------------------- *
* | 0x0 | 0x4 | 0x8 | 0xc | 0x10 | 0x14 | 0x18 | 0x1c | *
* ---------------------------------------------------------------------------------- *
* | fc_mxcsr|fc_x87_cw| R12 | R13 | R14 | *
* ---------------------------------------------------------------------------------- *
* ---------------------------------------------------------------------------------- *
* | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | *
* ---------------------------------------------------------------------------------- *
* | 0x20 | 0x24 | 0x28 | 0x2c | 0x30 | 0x34 | 0x38 | 0x3c | *
* ---------------------------------------------------------------------------------- *
* | R15 | RBX | RBP | RIP | *
* ---------------------------------------------------------------------------------- *
* *
****************************************************************************************/
.text
.globl _make_fcontext
.align 8
_make_fcontext:
/* first arg of make_fcontext() == top of context-stack */
movq %rdi, %rax
/* shift address in RAX to lower 16 byte boundary */
andq $-16, %rax
/* reserve space for context-data on context-stack */
/* on context-function entry: (RSP -0x8) % 16 == 0 */
leaq -0x40(%rax), %rax
/* third arg of make_fcontext() == address of context-function */
/* stored in RBX */
movq %rdx, 0x28(%rax)
/* save MMX control- and status-word */
stmxcsr (%rax)
/* save x87 control-word */
fnstcw 0x4(%rax)
/* compute abs address of label trampoline */
leaq trampoline(%rip), %rcx
/* save address of trampoline as return-address for context-function */
/* will be entered after calling jump_fcontext() first time */
movq %rcx, 0x38(%rax)
/* compute abs address of label finish */
leaq finish(%rip), %rcx
/* save address of finish as return-address for context-function */
/* will be entered after context-function returns */
movq %rcx, 0x30(%rax)
ret /* return pointer to context-data */
trampoline:
/* store return address on stack */
/* fix stack alignment */
push %rbp
/* jump to context-function */
jmp *%rbx
finish:
/* exit code is zero */
xorq %rdi, %rdi
/* exit application */
call __exit
hlt
下面需要一点基础知识, 希望能尽量简单和说明白吧😓:
- 基础知识一
先来看看这个表:
/****************************************************************************************
* *
* ---------------------------------------------------------------------------------- *
* | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | *
* ---------------------------------------------------------------------------------- *
* | 0x0 | 0x4 | 0x8 | 0xc | 0x10 | 0x14 | 0x18 | 0x1c | *
* ---------------------------------------------------------------------------------- *
* | fc_mxcsr|fc_x87_cw| R12 | R13 | R14 | *
* ---------------------------------------------------------------------------------- *
* ---------------------------------------------------------------------------------- *
* | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | *
* ---------------------------------------------------------------------------------- *
* | 0x20 | 0x24 | 0x28 | 0x2c | 0x30 | 0x34 | 0x38 | 0x3c | *
* ---------------------------------------------------------------------------------- *
* | R15 | RBX | RBP | RIP | *
* ---------------------------------------------------------------------------------- *
* *
****************************************************************************************/
此表对应的数据(大多数情况是真实数据的内存地址)其实只是作为其中一部分存储在一块在堆上分配的内存中.
为什么是其中一部分? 因为函数(如上一节代码里的 f1 函数)有 本地变量, 并且 f1 函数里可能再调用别的函数, 别的函数再调用别的函数, 别的函数总得也有自己的 本地变量. 这里的 本地变量 可是包括 函数返回值的哦.
这块在堆上分配的内存可以通过 C 语言的 malloc 创建出来,
在 Swift 上是这样创建和销毁:
class BoostContextImpl<_IN>: BoostContext {
/// 析构方法
deinit {
// 销毁
_sp.deallocate()
}
/// 构造方法
init(_ fn: @escaping FN) {
// 创建
let spSize: Int = .pageSize * 16
let sp: FContextStack = .allocate(byteCount: spSize, alignment: .pageSize)
}
}
重点:
这个堆的内存大小是 let spSize: Int = .pageSize * 16, 随具体平台的系统配置变化, 不过肯定大于 64 byte .
PS:
pageSize一般是 4092 btye
- 基础知识二
| ... | ... | R12 | R13 | R14 |
| R15 | RBX | RBP | RIP |
这些 RXX 代表着一些寄存器.
R is short for Register(寄存器).
* | 0x20 | 0x24 | 0x28 | 0x2c | 0x30 | 0x34 | 0x38 | 0x3c | *
* ---------------------------------------------------------------------------------- *
* | R15 | RBX | RBP | RIP | *
例如 R15 寄存器里面的数据要存存储在地址区间为 [ 0x20 0x28 ) 的这个字节中.
PS: 地址
0x20对应前面那条竖杠|, 别理解错了.
重点:
将这个表对应的数据存满需要占用的内存是 64 btye.
- 基础知识三
大小为 64byte 的此表对应的数据存储在大小为 .pageSize * 16的堆内存的 bottom 的位置, 俗称 栈底 (stack bottom). 栈底的地址是高地址.
这块大小为 .pageSize * 16 的堆内存, 俗称 栈 (stack), 当然是相对于这个库的作用的上下文而言.
为什么明明堆上分配的内存会被俗称 栈?
这是因为这个库的本质就是通过在堆上分配一块内存来模拟通常发生在栈上的函数调用过程.
- 基础知识四
| fc_mxcsr|fc_x87_cw|
指的是CPU标志.(我也不知道拿来干什么用的呢)
- 借助效果图整理一下上面的基础知识
函数原型(这里 Copy 一份,方便对比):
fcontext_t make_fcontext(void *sp, size_t size, void (*fn)(transfer_t));
(sp - size) sp 指针
⭣ ⭣
-------------------------- 堆大小: .pageSize * 16 ------------------------------------------------ |
| |
| -------------------------- -------------------------- ---------------------- |
| ..... | 被 f1 调用的函数及其本地变量 | | f1 栈底 (8 byte) | | 16 byte 地址对齐 | |
| -------------------------- -------------------------- ---------------------- |
| (直到函数调用栈 overflow) |
------------------------------------------------------------------------------------------------- |
| |
(低地址 base addr) (高地址 base addr + .pageSize * 16)
- 终于到解释上面那段汇编代码的时间 😓 :
/* first arg of make_fcontext() == top of context-stack */
movq %rdi, %rax
/* shift address in RAX to lower 16 byte boundary */
andq $-16, %rax
首先, 地址对齐, 作用是 将地址取16的整数倍.
将 sp指针 的值(一个地址) 复制在 rax 寄存器.
-16 的补码: 0xfffffff0 (二进制: 1...1 0000),
andq $-16, %rax 将高地址的 rax 的低4位置 0.
效果示意图:
0x000000010015e000
0x000000010014e000 ⭣ 0x000000010015e000
⭣ ⭣ ⭣
(sp - size) rax sp
⭣ ⭣ ⭣
-------------------------- 堆大小: .pageSize * 16 ------------------------------------------------ |
| |
| -------------------------- -------------------------- ---------------------- |
| ..... | 被 f1 调用的函数及其本地变量 | | f1 栈底 (8 byte) | | 16 byte 地址对齐 | |
| -------------------------- -------------------------- ---------------------- |
| (直到函数调用栈 overflow) |
------------------------------------------------------------------------------------------------- |
| |
(低地址 base addr) (高地址 base addr + .pageSize * 16)
/* reserve space for context-data on context-stack */
/* on context-function entry: (RSP -0x8) % 16 == 0 */
leaq -0x40(%rax), %rax
向低地址的方向偏移 0x40 (相当于分配了 64 byte的存储空间)
效果示意图:
0x000000010015dfc0
⭣
(sp - size) rax 0x000000010015e000 sp
⭣ ⭣ ⭣ ⭣
-------------------------- 堆大小: .pageSize * 16 ------------------------------------------------ |
| |
| -------------------------- -------------------------- ---------------------- |
| ..... | 被 f1 调用的函数及其本地变量 | | f1 栈底 (64 byte) | | 16 byte 地址对齐 | |
| -------------------------- -------------------------- ---------------------- |
| (直到函数调用栈 overflow) |
------------------------------------------------------------------------------------------------- |
| |
(低地址 base addr) (高地址 base addr + .pageSize * 16)
/* third arg of make_fcontext() == address of context-function */
/* stored in RBX */
movq %rdx, 0x28(%rax)
往高地址方向位移, 将 f1的地址(放在rdx里)存储在 RBX 寄存器对应的内存位置.
/* compute abs address of label trampoline */
leaq trampoline(%rip), %rcx
/* save address of trampoline as return-address for context-function */
/* will be entered after calling jump_fcontext() first time */
movq %rcx, 0x38(%rax) // <---- (2)
// ...
ret /* return pointer to context-data */ // <------ (3)
trampoline:
/* store return address on stack */
/* fix stack alignment */
push %rbp
/* jump to context-function */
jmp *%rbx // <---- (1)
trampoline 是另外一个汇编实现的函数(汇编里通常叫label), rbx 存的是 f1 的地址(还记得吧), 当 trampoline 被调用的时候就 jmp *%rbx (其实就是调用 f1).
另外, trampoline 函数的地址存储在 RIP 对应的内存位置.
那 trampoline 什么时候被调用? 直接上代码:
func main() throws {
let bc1: BoostContext = makeBoostContext(f1)
let resultF1ToMain: BoostTransfer<String> = try bc1.jump(data: 123) // <--- trampoline 被调用的地方
print("main <---- f1 resultF1ToMain = \(resultF1ToMain.data)")
}
答案是调用 jump_fcontext 的时候.
这里面的原理将会在下一部分 如何切换到通过 make_fcontext 创建的 BoostContext? 中有所解释.
重点:
代码位置 (3) 返回的是此时 rax 里的值 0x000000010015dfc0, 用代码来解释也就是说:
typedef void *fcontext_t;
fcontext_t fc1 = make_fcontext(sp1 + FCONTEXT_SIZE, FCONTEXT_SIZE, f1);
fc1这个返回值是个指针, 里面的地址是 0x000000010015dfc0
- 如何切换 BoostContext?
需要借助 汇编 实现的 jump_fcontext.
函数原型:
transfer_t jump_fcontext(fcontext_t const to, void *vp);
PS: 这里的第一个参数
fcontext_t const to就是上一 part 通过调用make_fcontext的返回值0x000000010015dfc0
汇编实现:
/*
Copyright Oliver Kowalke 2009.
Distributed under the Boost Software License, Version 1.0.
(See accompanying file LICENSE_1_0.txt or copy at
http://www.boost.org/LICENSE_1_0.txt)
*/
/****************************************************************************************
* *
* ---------------------------------------------------------------------------------- *
* | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | *
* ---------------------------------------------------------------------------------- *
* | 0x0 | 0x4 | 0x8 | 0xc | 0x10 | 0x14 | 0x18 | 0x1c | *
* ---------------------------------------------------------------------------------- *
* | fc_mxcsr|fc_x87_cw| R12 | R13 | R14 | *
* ---------------------------------------------------------------------------------- *
* ---------------------------------------------------------------------------------- *
* | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | *
* ---------------------------------------------------------------------------------- *
* | 0x20 | 0x24 | 0x28 | 0x2c | 0x30 | 0x34 | 0x38 | 0x3c | *
* ---------------------------------------------------------------------------------- *
* | R15 | RBX | RBP | RIP | *
* ---------------------------------------------------------------------------------- *
* *
****************************************************************************************/
.text
.globl _jump_fcontext
.align 8
_jump_fcontext:
leaq -0x38(%rsp), %rsp /* prepare stack */
#if !defined(BOOST_USE_TSX)
stmxcsr (%rsp) /* save MMX control- and status-word */
fnstcw 0x4(%rsp) /* save x87 control-word */
#endif
movq %r12, 0x8(%rsp) /* save R12 */
movq %r13, 0x10(%rsp) /* save R13 */
movq %r14, 0x18(%rsp) /* save R14 */
movq %r15, 0x20(%rsp) /* save R15 */
movq %rbx, 0x28(%rsp) /* save RBX */
movq %rbp, 0x30(%rsp) /* save RBP */
/* store RSP (pointing to context-data) in RAX */
movq %rsp, %rax
/* restore RSP (pointing to context-data) from RDI */
movq %rdi, %rsp
movq 0x38(%rsp), %r8 /* restore return-address */ // <----- (1)
#if !defined(BOOST_USE_TSX)
ldmxcsr (%rsp) /* restore MMX control- and status-word */
fldcw 0x4(%rsp) /* restore x87 control-word */
#endif
movq 0x8(%rsp), %r12 /* restore R12 */
movq 0x10(%rsp), %r13 /* restore R13 */
movq 0x18(%rsp), %r14 /* restore R14 */
movq 0x20(%rsp), %r15 /* restore R15 */
movq 0x28(%rsp), %rbx /* restore RBX */
movq 0x30(%rsp), %rbp /* restore RBP */
leaq 0x40(%rsp), %rsp /* prepare stack */
/* return transfer_t from jump */
/* RAX == fctx, RDX == data */
movq %rsi, %rdx
/* pass transfer_t as first arg in context function */
/* RDI == fctx, RSI == data */
movq %rax, %rdi
/* indirect jump to context */
jmp *%r8 // <----- (2)
首先看下面这两句重点代码:
// ...
movq 0x38(%rsp), %r8 /* restore return-address */ // <----- (1)
// ...
/* indirect jump to context */
jmp *%r8 // <----- (2)
应该还记得 trampoline 函数的地址存储在 0x38(%rsp), 而 trampoline 调用又调用 f1,
也就是说 jmp *%r8 本质上就是间接通过调用 trampoline 去调用 f1.
另外的代码也是很简单:
jmp *%r8 之前需要恢复创建 BoostContext 时的环境 和 设置 jump_fcontext 的返回值,
/* restore RSP (pointing to context-data) from RDI */
movq %rdi, %rsp // -----> start: 恢复`创建 BoostContext 时的环境`
movq 0x38(%rsp), %r8 /* restore return-address */
#if !defined(BOOST_USE_TSX)
ldmxcsr (%rsp) /* restore MMX control- and status-word */
fldcw 0x4(%rsp) /* restore x87 control-word */
#endif
movq 0x8(%rsp), %r12 /* restore R12 */
movq 0x10(%rsp), %r13 /* restore R13 */
movq 0x18(%rsp), %r14 /* restore R14 */
movq 0x20(%rsp), %r15 /* restore R15 */
movq 0x28(%rsp), %rbx /* restore RBX */
movq 0x30(%rsp), %rbp /* restore RBP */
leaq 0x40(%rsp), %rsp /* prepare stack */ // <----- end: 恢复`创建 BoostContext 时的环境`
/* return transfer_t from jump */
/* RAX == fctx, RDX == data */
movq %rsi, %rdx // -----> start: 设置 `jump_fcontext` 的返回值
/* pass transfer_t as first arg in context function */
/* RDI == fctx, RSI == data */
movq %rax, %rdi // -----> end: 设置 `jump_fcontext` 的返回值
/* indirect jump to context */
jmp *%r8 //
而恢复 创建 BoostContext 时的环境 前又需要保存当前的环境
#if !defined(BOOST_USE_TSX)
stmxcsr (%rsp) /* save MMX control- and status-word */
fnstcw 0x4(%rsp) /* save x87 control-word */
#endif
movq %r12, 0x8(%rsp) /* save R12 */
movq %r13, 0x10(%rsp) /* save R13 */
movq %r14, 0x18(%rsp) /* save R14 */
movq %r15, 0x20(%rsp) /* save R15 */
movq %rbx, 0x28(%rsp) /* save RBX */
movq %rbp, 0x30(%rsp) /* save RBP */
至此, 汇编部分的代码已经简略地解释完了. 需要更深入的了解还得学汇编.
Android 演示
这一 part 特意用来演示下在 Android 上的运行效果.
在 Android 平台上展示一下可以运行 Swift-Coroutine 的事实.
另外, Android 真机(aarch64架构) 和 模拟器(x86_64架构)均可运行.
再次强调: 是 Swift-Coroutine 而不是 Kotlin-Coroutine, Swift-Coroutine 也能运行于 Apple 旗下的一些操作系统.
首先编译 Swift_Boost_Context 项目 :
目前只编译 x86_64 的可执行文件.
# (1)
mkdir -P ~/dev_kit/sdk/swift_source
cd ~/dev_kit/sdk/swift_source
git clone https://github.com/Guang1234567/Swift_Boost_Context.git Swift_Boost_Context
cd Swift_Boost_Context
# (2) 下载最新的 swift_android-toolchain 交叉编译工具(只能运行 macOS)
git clone https://github.com/Guang1234567/swift-android-toolchain_5_3_1_release_ndk_20.git swift-android-toolchain_5_3_1_release_ndk_20
#export SWIFT_ANDROID_ARCH=aarch64
export SWIFT_ANDROID_ARCH=x86_64
export SWIFT_ANDROID_HOME=$HOME/dev_kit/sdk/swift_source/swift-android-5.3.1-release
# 编译
${HOME}/dev_kit/sdk/swift_source/swift-android-5.3.1-release/build-tools/1.9.6-swift5/swift-build --configuration debug -Xswiftc -DDEBUG -Xswiftc -g
# 编译成功完成后会有下面的产物
ls -al .build/x86_64-none-linux-android/debug/Example
# (3) 将之 copy 到 Android 虚拟机中去
adb push .build/x86_64-none-linux-android/debug/Example /data/local/tmp
adb push ${HOME}/dev_kit/sdk/swift_source/swift-android-toolchain_5_3_1_release_ndk_20/usr/lib/swift/android/x86_64/*.so /data/local/tmp
# (4) 运行
adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/Example
输出
$ adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/Example
bc1 = BoostContextImpl(_spSize: 65536, _sp: 0x00007c733dc4e000, _fctx: 0x00007c733dc5dfc0)
main ----> f1 fromCtx = BoostContextProxy(_fctx: 0x00007ffca9cacf80) data = 123
resultF1ToMain = BoostTransfer(fromContext: BoostContextProxy(_fctx: 0x00007c733dc5d9d0))
main <---- f1 resultF1ToMain = 7654321
....
截图