首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >掀起你的汇编来:如何移植ST协程到其他系统或CPU?

掀起你的汇编来:如何移植ST协程到其他系统或CPU?

作者头像
Winlin
发布2022-03-18 17:14:00
发布2022-03-18 17:14:00
9250
举报
文章被收录于专栏:SRS开源服务器SRS开源服务器

SRS是一个单进程多协程的服务器,保持高并发同时还能利用ST协程避免异步回调的问题,这也导致新的平台需要移植ST,而且是汇编代码。

其实,移植ST比想象的要简单很多,最关键的就是实现setjmp/longjmp,也就是保存寄存器和恢复寄存器,所以步骤如下:

1.分析你的平台的寄存器使用,也就是函数调用规范。一般是由系统(Linux/OSX/Windows)和CPU(x86/ARM/MIPS)决定的。有个小工具打印这些信息,参考porting.c[1]。2.使用汇编实现寄存器的保存和恢复,不同系统的汇编语法有差异,目前几个系统都已经实现,可以参考现有的汇编实现。有个小工具调用汇编的函数,显示jmpbuf信息,参考verify.c[2]。3.实现ST的Makefile的适配,编译并在新的平台验证ST。有个小工具验证ST是否正常工作,参考helloworld.c[3]。

目前已经实现的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[4]

Linux

aarch64

Stable

make linux-debug

For ARM(v8) server, #9[5]

Linux

mips

Dev

make linux-debug

For OpenWRT device, #21[6]

OSX

x86-64

Stable

make darwin-debug

For OSX(MacPro, etc.) #11[7]

Windows

x86-64

Dev

make cygwin64-debug

For Windows(x64) desktop, #20[8]

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[9]。

Note: 如果你的系统的规范和现有的一样,就可以尝试用现有的OS,比如Unix一般可以指定为Linux或OSX。

CPU

不同CPU的寄存器布局不同,比如Linux下支持多种CPU,一般可以通过宏定义检测到,所以一般都使用如下命令编译:

代码语言:javascript
复制
make linux-debug

如果发现报错Unknown CPU architecture,那么可以明确指定你的CPU体系:

•x86-64: make linux-debug EXTRA_CFLAGS="-D__x86_64__"•arm: make linux-debug EXTRA_CFLAGS="-D__x86_64__"•aarch64: make linux-debug EXTRA_CFLAGS="-D__aarch64__"•mips: make linux-debug EXTRA_CFLAGS="-D__mips__"

如果你的CPU不属于已经适配的CPU,就需要适配,也并不难。参考下面的步骤。

Porting

以MIPS为例,我们找下MIPS Calling Conventions[10],可以看到Callee主要保存以下寄存器:

$gp global pointer•$fp frame pointer•$sp stack pointer•$s0–$s7 saved temporaries

我们修改porting.c[11],增加MIPS下的print_jmpbuf,并在OpenWRT上执行,可以看到setjmp还是明文并没有混淆:

代码语言:javascript
复制
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,可以看到它保存的寄存器:

代码语言:javascript
复制
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的地址:

代码语言:javascript
复制
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(ARM)。•md_cygwin64.S,针对Cygwin64/Windows的汇编,目前实现了x86_64架构,还没有支持32位Windows。

显然OpenWRT/MIPS是Linux平台,所以我们先实现两个空函数:

代码语言:javascript
复制
#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[12]验证这两个函数是否正常工作。

代码语言:javascript
复制
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恢复),详细参考 #21[13]。

Build

实现汇编后,有些地方需要修改,比如MIPS的jmpbuf定义不太一样。

一般的jmpbuf定义如下,字段名是__jmpbuf

代码语言:javascript
复制
     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

代码语言:javascript
复制
    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]的位置:

代码语言:javascript
复制
        #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[14]。

代码语言:javascript
复制
root@OpenWrt:~# ./helloworld 
#000, Hello, state-threads world!
#001, Hello, state-threads world!
#002, Hello, state-threads world!
#003, Hello, state-threads world!

大功告成。

References

[1] porting.c: https://github.com/ossrs/state-threads/blob/srs/tools/porting/porting.c [2] verify.c: https://github.com/ossrs/state-threads/blob/srs/tools/verify/verify.c [3] helloworld.c: https://github.com/ossrs/state-threads/blob/srs/tools/helloworld/helloworld.c [4] #1: https://github.com/ossrs/state-threads/issues/1 [5] #9: https://github.com/ossrs/state-threads/issues/9 [6] #21: https://github.com/ossrs/state-threads/issues/21 [7] #11: https://github.com/ossrs/state-threads/issues/11 [8] #20: https://github.com/ossrs/state-threads/issues/20 [9] Makefile: https://github.com/ossrs/state-threads/blob/srs/Makefile [10] MIPS Calling Conventions: https://github.com/ossrs/state-threads/issues/21#issue-1012719185 [11] porting.c: https://github.com/ossrs/state-threads/blob/ed6c73030881daea8f4f25f8a55120d2d4c9c7c1/porting/porting.c#L46 [12] verify.c: https://github.com/ossrs/state-threads/blob/srs/tools/verify/verify.c [13] #21: https://github.com/ossrs/state-threads/issues/21 [14] helloworld.c: https://github.com/ossrs/state-threads/blob/srs/tools/helloworld/helloworld.c

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 SRS开源服务器 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • OS
  • CPU
  • Porting
  • ASM
  • Build
  • HelloWorld
    • References
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档