Description
移植ST比想象的要简单很多,最关键的就是实现setjmp/longjmp
,也就是保存寄存器和恢复寄存器。
目前已经实现的OS和CPU如下:
OS | CPU | Status | Command | Description |
---|---|---|---|---|
Linux | x86-64 | Stable | make linux-debug | For CentOS,Ubuntu server, etc. |
Linux | arm | Stable | make linux-debug | For ARM(v7) device, #1 |
Linux | aarch64 | Stable | make linux-debug | For ARM(v8) server, #9 |
Linux | mips | Dev | make linux-debug | For OpenWRT device, #21 |
Linux | mips64 | Dev | make linux-debug | For Loongson 3A4000/3B3000, #21 |
Linux | loongarch64 | Dev | make linux-debug | For Loongson CPU, #24 |
Linux | riscv | Dev | make linux-debug | For RISCV CPU, #28 |
OSX | x86-64 | Stable | make darwin-debug | For OSX(MacPro, etc.) #11 |
OSX | m1(aarch64) | Dev | make darwin-debug | For OSX(MacPro M1, etc.) #30 |
Windows | x86-64 | Dev | make cygwin64-debug | For Windows(x64) desktop, #20 |
Note: 早期ST直接使用setjmp,然后修改jmpbuf的SP寄存器内容,这依赖于知道glibc如何使用jmpbuf的布局,而后来glibc改变了(加密了)布局所以就出现很多平台无法使用。其实全部使用汇编实现,移植性会更好,因为要支持的系统和CPU有限,寄存器的布局是确定的,资料也很好找。
OS
编译ST需要明确指定OS,比如:
- Linux:
make linux-debug
- OSX:
make darwin-debug
- Windows:
make cygwin64-debug
不同的OS的依赖的文件可能不同,如果需要支持其他OS则需要修改Makefile。
Note: 如果你的系统的规范和现有的一样,就可以尝试用现有的OS,比如Unix一般可以指定为Linux或OSX。
CPU
不同CPU的寄存器布局不同,比如Linux下支持多种CPU,一般可以通过宏定义检测到,所以一般都使用如下命令编译:
make linux-debug
如果发现报错Unknown CPU architecture
,那么可以明确指定你的CPU体系:
- x86-64:
make linux-debug EXTRA_CFLAGS="-D__x86_64__"
- arm:
make linux-debug EXTRA_CFLAGS="-D__arm__"
- aarch64:
make linux-debug EXTRA_CFLAGS="-D__aarch64__"
- mips:
make linux-debug EXTRA_CFLAGS="-D__mips__"
- mips64:
make linux-debug EXTRA_CFLAGS="-D__mips64"
- loonarch64:
make linux-debug EXTRA_CFLAGS="-D__loongarch64"
- riscv:
make linux-debug EXTRA_CFLAGS="-D__riscv"
使用命令检测你的CPU,比如检测armv8/aarch64/arm64
:
g++ -dM -E - </dev/null |grep -i aarch64
如果你的CPU不属于已经适配的CPU,就需要适配,也并不难。下面介绍一些适配的工具。
Tools
适配新CPU的工具如下:
- 分析你的平台的寄存器使用,也就是函数调用规范。一般是由系统(Linux/OSX/Windows)和CPU(x86/ARM/MIPS)决定的。有个小工具打印这些信息,参考porting.c。
- 有个小工具验证ST是否正常工作,会启动一个ST的协程,不断打印消息,调用st_sleep切换协程和等待,参考helloworld.c。
- 覆盖常用的ST的函数的调用,比如thread、cond、sleep、mutex、cond等相关API和数据结构,参考verify.c。
- 由于不同的平台的
jmpbuf
的定义可能会有所不同,我们自己定义了这个数据结构,参考 Define and use a new jmpbuf, because the structure is different. #29 ,有个小工具可以打印这个结构体的定义,是通过gcc -E
预处理指令可以看到头文件中关于jmpbuf
的定义,参考jmpbuf.c。 - 有时候需要关注函数调用PCS(Procedure Call Standard),参考pcs.c
- 有时候需要关注栈的情况,参考stack.c
了解这些工具后,可以很方便的适配新的CPU,参考下面的步骤。
Porting
以MIPS为例,我们找下MIPS Calling Conventions,可以看到Callee主要保存以下寄存器:
$gp
global pointer$fp
frame pointer$sp
stack pointer$s0–$s7
saved temporaries
我们修改porting.c,增加MIPS下的print_jmpbuf
,并在OpenWRT上执行,可以看到setjmp
还是明文并没有混淆:
root@OpenWrt:~# ./porting OS specs:
__linux__: 1
CPU specs:
__mips__: 1, __mips:32, __mips_isa_rev:2, _MIPSEL:1
Compiler specs:
sizeof(long)=4
sizeof(long long int)=8
sizeof(void*)=4
sizeof(__ptr_t)=4
Calling conventions:
ra=0x400818, sp=0x7f968898, s0=0x7f968a8c, s1=0x1, s2=0x7f968a84, s3=0x4006b0, s4=0x77e029d0,
s5=0x77e01660, s6=0x77e14c38, s7=0, fp=0x7f968898, gp=0x419000
sizeof(jmp_buf)=104 (unsigned long long [13])
0x18 0x08 0x40 0x00 # ra, the return address
0x98 0x88 0x96 0x7f # sp
0x8c 0x8a 0x96 0x7f # s0
0x01 0x00 0x00 0x00 # s1
0x84 0x8a 0x96 0x7f # s2
0xb0 0x06 0x40 0x00 # s3
0xd0 0x29 0xe0 0x77 # s4
0x60 0x16 0xe0 0x77 # s5
0x38 0x4c 0xe1 0x77 # s6
0x00 0x00 0x00 0x00 # s7
0x98 0x88 0x96 0x7f # fp/s8
0x00 0x90 0x41 0x00 # gp
0x00 0x00 0x00 0x00
............
0x00 0x00 0x00 0x00
Note: 最简单的办法,就是将jmpbuf[1],直接设置为
_sp
也就是协程从堆上开辟的堆栈地址,但这样依赖于glibc的布局,我们还是选择使用汇编实现,自己定义jmpbuf如何使用,不给以后挖坑了。
可以调试下setjmp,在gdb执行disassemble
,就可以看到它保存的寄存器:
sw ra,0(a0)
sw sp,4(a0)
sw s0,8(a0)
sw s1,12(a0)
sw s2,16(a0)
sw s3,20(a0)
sw s4,24(a0)
sw s5,28(a0)
sw s6,32(a0)
sw s7,36(a0)
sw s8,40(a0)
sw gp,44(a0)
jr ra
同样的,可以看下longmp,可以发现恢复寄存器后,就是直接跳转到ra的地址:
lw ra,0(a0)
lw sp,4(a0)
......
jr ra
Note: 只是用这种方式确认下使用的寄存器,我们并不需要严格按照glibc的方式布局jmpbuf,因为各种版本的glibc实现都不相同,我们使用汇编实现所有平台的setjmp时,可以让布局尽量一致。
ASM
接下来就是关键的用汇编实现寄存器保存,根据OS的不同,分成了不同的汇编文件:
md_linux.S
,所有Linux平台的汇编,根据CPU架构(宏)实现不同平台的函数。md_darwin.S
,针对OSX/Mac的汇编,目前实现了x86_64
架构,M1(aarch64)的支持情况请看最开始的表格。md_cygwin64.S
,针对Cygwin64/Windows的汇编,目前实现了x86_64
架构,还没有支持32位Windows。
显然OpenWRT/MIPS
是Linux平台,所以我们先实现两个空函数:
#elif defined(__mips__)
#define JB_SP 0
.text
.globl _st_md_cxt_save
_st_md_cxt_save:
.size _st_md_cxt_save, .-_st_md_cxt_save
.globl _st_md_cxt_restore
_st_md_cxt_restore:
.size _st_md_cxt_restore, .-_st_md_cxt_restore
#endif
Note: 实际上,
_st_md_cxt_save
就是setjmp
,而_st_md_cxt_restore
就是longjmp
。
然后我们编译ST,用verify.c验证这两个函数是否正常工作。
cd tools/verify && make && ./verify
root@OpenWrt:~# ./verify
gp=0x419000, fp=0x7fe3af20, sp=0x7fe3af20, s0=0x7fe3b10c, s1=0x1, s2=0x7fe3b104, s3=0x400670,
s4=0x77e759d0, s5=0x77e74660, s6=0x77e87c38, s7=0
0x00 0x00 0x00 0x00
............
0x00 0x00 0x00 0x00
Note: 由于没有实现,所以jmpbuf都是空的。
最后,就是用汇编实现函数,需要找下平台相关的资料。也可以直接通过调试setjmp和longjmp的实现,来学习如何将寄存器保存到jmpbuf,以及如何从jmpbuf恢复):
root@OpenWrt:~# gdb porting
(gdb) b main
(gdb) r
(gdb) layout next
(gdb) layout next
Note: 按
CTRL+X A
退出GDB的文本图形模式,进入普通的GDB模式。
Note: 如果想知道汇编怎么实现,可以看下C语言被翻译成什么汇编,调试下就能知道个大概齐,再配合搜索引擎找找资料,很快就能知道怎么实现了。
Build
实现汇编后,有些地方需要修改,比如MIPS的jmpbuf定义不太一样。
一般的jmpbuf定义如下,字段名是__jmpbuf
:
typedef struct __jmp_buf_tag jmp_buf[1];
struct __jmp_buf_tag {
__jmp_buf __jmpbuf;
int __mask_was_saved;
__sigset_t __saved_mask;
};
而在MIPS中定义的字段不同,它的字段名是__jb
:
typedef struct __jmp_buf_tag {
__jmp_buf __jb;
unsigned long __fl;
unsigned long __ss[128/sizeof(long)];
} jmp_buf[1];
因此,需要我们在md.h
中定义如何使用jmpbuf,SP是在__jb[0]
的位置:
#elif defined(__mips__)
/* https://github.com/ossrs/state-threads/issues/21 */
#define MD_USE_BUILTIN_SETJMP
#define MD_GET_SP(_t) *((long *)&((_t)->context[0].__jb[0]))
Note: 在MIPS中,指针是4字节的,而
__jb
是long long
类型8字节的,所以需要转换类型。
其中,宏定义MD_GET_SP
,就是如何将jmpbuf的SP,更新为协程的栈地址。这是在MD_INIT_CONTEXT
,也就是创建协程时调用的。
Note: 创建协程时,当时的SP可能是在另外一个协程,所以创建的协程并不能直接使用当前的SP,而需要从堆上重新申请虚拟的stack,所以在setjmp后需要更新jmpbuf中的SP地址。
HelloWorld
编译成功后,我们使用一个小工具验证,会初始化ST后,不断打印日志,参考helloworld.c。
root@OpenWrt:~# ./helloworld
#000, Hello, state-threads world!
#001, Hello, state-threads world!
#002, Hello, state-threads world!
#003, Hello, state-threads world!
大功告成。