Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
winlinvip opened this issue Sep 30, 2021 · 0 comments
Closed
Assignees
Labels

Comments

@winlinvip
Copy link
Member

winlinvip commented Sep 30, 2021

移植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!

大功告成。

@winlinvip winlinvip self-assigned this Sep 30, 2021
@winlinvip winlinvip changed the title How to porting ST to other OS and CPU arch? 如何移植ST到其他系统或CPU体系? How to porting ST to other OS/CPU? 如何移植ST到其他系统或CPU? Sep 30, 2021
@winlinvip winlinvip pinned this issue Oct 1, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant