Skip to content

How to porting ST to other OS/CPU? 如何移植ST到其他系统或CPU? #22

Closed
@winlinvip

Description

@winlinvip

移植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的工具如下:

  1. 分析你的平台的寄存器使用,也就是函数调用规范。一般是由系统(Linux/OSX/Windows)和CPU(x86/ARM/MIPS)决定的。有个小工具打印这些信息,参考porting.c
  2. 有个小工具验证ST是否正常工作,会启动一个ST的协程,不断打印消息,调用st_sleep切换协程和等待,参考helloworld.c
  3. 覆盖常用的ST的函数的调用,比如thread、cond、sleep、mutex、cond等相关API和数据结构,参考verify.c
  4. 由于不同的平台的jmpbuf的定义可能会有所不同,我们自己定义了这个数据结构,参考 Define and use a new jmpbuf, because the structure is different. #29 ,有个小工具可以打印这个结构体的定义,是通过gcc -E预处理指令可以看到头文件中关于jmpbuf的定义,参考jmpbuf.c
  5. 有时候需要关注函数调用PCS(Procedure Call Standard),参考pcs.c
  6. 有时候需要关注栈的情况,参考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字节的,而__jblong 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!

大功告成。

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions