龙芯开源社区

 找回密码
 注册新用户(newuser)
查看: 4813|回复: 5

龙芯上的GO语言支持

[复制链接]
发表于 2015-9-1 22:48:16 | 显示全部楼层 |阅读模式
Go语言是一个google开发的新型系统级编程语言,它在设计时吸取了C和C++语言等语言的一些教训,并对高性能并发等需求有很好的设计支持,受到了广泛的关注。如日中天的容器计算平台docker就是用go语言开发的。

---背景---
产业界对使用龙芯搭建云计算平台的需求与日俱增,因此龙梦这两年开始着手研究龙芯的云计算平台技术。2014年我们在龙芯上搭建了openstack原型系统,由于龙芯目前支持kvm等虚拟机类的技术还有所欠缺(3A2000/3B2000之前的芯片缺乏对虚拟化的硬件支持),底层用的是lxc。这是一个非主流的路线,所以当时碰到了比较多的问题,虽然最后可以正常运行了,感觉路线有些问题就没有继续深入。2015年,随着docker的流行和发展,我们感觉机会来了。docker这样的容器计算路线不需要特殊的硬件支持(本质上像一个增强版的chroot),又有很多实用的优点,理论上在龙芯上跑起来效果会不错。但是在龙芯上跑docker一样有不小的障碍:它是用go语言开发的,龙芯没有直接支持。于是我们先来研究怎么支持go语言。


go语言现在主要由两个工具,一个是原生的go编译器,是一个从早期plan9系统演化而来的带汇编器、链接器和编译器的完整工具链;另一个是gccgo编译器,是在gcc的基础上添加go语言的前端和runtime得到的,汇编器和链接器可以用现有的版本。前者要增加新的架构支持是一个比较浩大的工程,涉及汇编器、链接器等很多文件都要针对新的架构增加代码支持,不是短期能够完成的任务。后者则好得多,gccgo中涉及后端架构相关部分的代码很少。事实上,下载gcc-5.2.0的源代码,排除少量编译环境上的困难之后,可以直接得到一个能运行的gccgo程序(./configure --enable-languages=c,c++,go)。当然了,事情没那么简单。还有两大地雷等着排除。

一,缺乏libffi中go closure支持。运行go命令,它就会报如下错误:
            libgo built without FFI does not support reflect.Call or runtime.SetFinalizer
在gcc源代码中找到出处:
   libgo/runtime/go-reflect-call.c有
  1. #if defined(USE_LIBFFI) && FFI_GO_CLOSURES
  2. 。。。
  3. #else /* !defined(USE_LIBFFI) */
  4. void
  5. reflect_call (const struct __go_func_type *func_type __attribute__ ((unused)),
  6.               FuncVal *func_val __attribute__ ((unused)),
  7.               _Bool is_interface __attribute__ ((unused)),
  8.               _Bool is_method __attribute__ ((unused)),
  9.               void **params __attribute__ ((unused)),
  10.               void **results __attribute__ ((unused)))
  11. {
  12.   /* Without FFI there is nothing we can do.  */
  13.   runtime_throw("libgo built without FFI does not support "
  14.                 "reflect.Call or runtime.SetFinalizer");
  15. }

  16. #endif /* !defined(USE_LIBFFI) */
复制代码


所以原因很清楚,libffi没有必要的支持。再看一下,USE_LIBFFI是定义了的,libffi对mips还是有基本的支持,但是FFI_GO_CLOSURES没有。看libffi的源代码x86/alpha/sparc/powerpc相关的目录下都有定义FFI_GO_CLOSURES,mips目录下没有。顾名思义,是libffi在mips上缺乏对go闭包特性的支持。开始有些疑惑,为什么支持闭包会要求libffi增加代码(它是一个协助实现跨语言调用的函数包,本身就支持闭包)。后来google找到一些帖子,大致明白了缘由:
              https://gcc.gnu.org/ml/gcc-patches/2014-10/msg00098.html
              https://gcc.gnu.org/ml/gcc-patches/2014-10/msg01009.html
简单总结:曾经有一段时间,gccgo实现反射功能时,x86架构用的是直接实现,其他结构用的是libffi的closure支持。原因是go的闭包可以比通用的C闭包实现简单些,但是维护者觉得为此每个结构都重新实现一遍closure不合理,相反应该在libffi里对go的闭包专门做一个支持。

很不幸mips的支持他没有加。所以我们只好化了一些时间去理解这个支持的意义,再针对mips增加相应支持代码。具体的代码可以参考:
       https://github.com/foxsen/libffi mips_go_closure分支
go closure和libffi closure主要的差别是,go的闭包函数值相关的结构自己有闭包信息,libffi的函数指针只是一个指针不带其他信息,实现闭包需要再用一个内部数据结构,实现时需要用trampoline,考虑到有些架构上可执行和可读写代码的限制,还可能要mmap来专门处理;go语言的函数值自带了closure信息,在间接调用时closure的值会用static chain register(mips上是t7,$15)传递。具体实现上主要由三个难点:
  1) 理解上述差别所在,找出mips的statis chain register
  2) ffi_call_go增加传递closure参数,go_closure_*中从t7获得closure参数,需要修改相应的参数处理相关的汇编代码
  3) 理解dwarf格式,在汇编文件手工写的unwind信息中根据汇编程序的变化调整相关参数(主要是栈偏移代码位置等),增加新增函数的unwind信息。在已有的代码基础上只要大体理解就很容易依样画葫芦写出代码了。这个unwind信息的作用是在发生异常的时候让代码有能力回溯栈,如果没有这个信息,回溯到这里的汇编函数时可能会走不下去。特别是C++代码,在closure调用的函数里如果throw一个异常,没有这个信息在closure外部就catch不了这个异常了。

二、解决了第一个问题后,go命令开始能跑了,可是很快发现go命令有些异常,在编译一些调用C代码的go包时老是会出错。经过一些研究明白了go调用C的方式:通过cgo程序自动处理。把问题缩小到如下测试程序:
  1. package main

  2. import (
  3.        "C"
  4. )

  5. func main() {
  6.   var i C.int
  7. }
复制代码

命名为t.go,用cgo -godefs t.go (一般不会直接调用cgo,而是从go tool cgo间接调用,这是通过strace go的运行来得知它调用这个工具),死活无法正确生成结果。没有办法只好去研究cgo的原理。对于类型定义,它大致是把C.xxx抽出来生成一个C程序去用XXX,然后-gdwarf2编译,查看生成的dwarf信息得到XXX的类型信息,再根据具体类型翻译为go的类型。例如上述例子正常翻译结果是:
  1. // Created by cgo -godefs - DO NOT EDIT
  2. // ./cgo -godefs t.go

  3. package main

  4. func main() {
  5.         var i int32  //这里C.int变成了go的类型
  6. }
复制代码

但是在龙芯上死活不对。无奈把整个cgo程序拷贝到龙芯上,编译加调试语句运行(这时候纯go程序已经可以正常编译了),发现它解释dwarf信息有问题,正常应该(和readelf对照):
  1. &{11 CompileUnit true [{Producer GNU C11 5.2.0 20150716 (Red Hat 5.2.0-5) -mel -mabi=64 -mllsc -mno-shared -gdwarf-2} {Language 12} {CompDir /root/mygo/src/cgo} {StmtList 0}]}
  2. &{25 BaseType false [{ByteSize 8} {Encoding 5} {Name long int}]}
  3. &{32 BaseType false [{ByteSize 8} {Encoding 7} {Name long unsigned int}]}
  4. &{39 BaseType false [{ByteSize 4} {Encoding 5} {Name int}]}
  5. &{46 BaseType false [{ByteSize 8} {Encoding 5} {Name long long int}]}
  6. &{53 BaseType false [{ByteSize 16} {Encoding 4} {Name long double}]}
  7. &{60 BaseType false [{ByteSize 1} {Encoding 6} {Name char}]}
  8. &{67 Variable false [{Name __cgo__0} {DeclFile 1} {DeclLine 14} {Type 89} {External true} {Location [3 0 0 0 0 0 0 0 0]}]}
  9. &{67 Variable false [{Name __cgo__0} {DeclFile 1} {DeclLine 14} {Type 89} {External true} {Location [3 0 0 0 0 0 0 0 0]}]}
  10. __cgo__0 haha 89
  11. hehe *int
  12. *int
  13. __cgo__0 0 0
  14. &{89 PointerType false [{ByteSize 8} {Type 39}]}
  15. &{95 Variable false [{Name __cgo__1} {DeclFile 1} {DeclLine 15} {Type 89} {External true} {Location [3 0 0 0 0 0 0 0 0]}]}
  16. &{95 Variable false [{Name __cgo__1} {DeclFile 1} {DeclLine 15} {Type 89} {External true} {Location [3 0 0 0 0 0 0 0 0]}]}
  17. __cgo__1 haha 89
  18. hehe *int
  19. *int
  20. __cgo__1 1 0
  21. &{117 ArrayType true [{Type 46} {Sibling 133}]}
  22. &{133 BaseType false [{ByteSize 8} {Encoding 7} {Name sizetype}]}
  23. &{140 Variable false [{Name __cgodebug_data} {DeclFile 1} {DeclLine 16} {Type 117} {External true} {Location [3 0 0 0 0 0 0 0 0]}]}
  24. &{140 Variable false [{Name __cgodebug_data} {DeclFile 1} {DeclLine 16} {Type 117} {External true} {Location [3 0 0 0 0 0 0 0 0]}]}
  25. __cgodebug_data haha 117
  26. &{0 0 false []}
  27. <nil>
复制代码


它这里是:
  1. &{11 CompileUnit true [{Producer GNU C11 5.2.0 20150716 (Red Hat 5.2.0-5) -mel -mabi=64 -mllsc -mno-shared -gdwarf-2} {Language 12} {CompDir /root/mygo/src/cgo} {StmtList 0}]}
  2. &{25 BaseType false [{ByteSize 8} {Encoding 5} {Name long int}]}
  3. &{32 BaseType false [{ByteSize 8} {Encoding 7} {Name long unsigned int}]}
  4. &{39 BaseType false [{ByteSize 4} {Encoding 5} {Name int}]}
  5. &{46 BaseType false [{ByteSize 8} {Encoding 5} {Name long long int}]}
  6. &{53 BaseType false [{ByteSize 16} {Encoding 4} {Name long double}]}
  7. &{60 BaseType false [{ByteSize 1} {Encoding 6} {Name char}]}
  8. &{67 Variable false [{Name __cgo__0} {DeclFile 1} {DeclLine 14} {Type 89} {External true} {Location [3 0 0 0 0 0 0 0 0]}]}
  9. &{67 Variable false [{Name __cgo__0} {DeclFile 1} {DeclLine 14} {Type 89} {External true} {Location [3 0 0 0 0 0 0 0 0]}]}
  10. __cgo__0 haha 89
  11. hehe *int
  12. *int
  13. __cgo__0 0 0
  14. &{89 PointerType false [{ByteSize 8} {Type 39}]}
  15. &{95 Variable false [{Name __cgo__0} {DeclFile 1} {DeclLine 15} {Type 89} {External true} {Location [3 0 0 0 0 0 0 0 0]}]}
  16. &{95 Variable false [{Name __cgo__0} {DeclFile 1} {DeclLine 15} {Type 89} {External true} {Location [3 0 0 0 0 0 0 0 0]}]}
  17. __cgo__1 haha 89
  18. hehe *int
  19. *int
  20. __cgo__1 1 0
  21. &{117 ArrayType true [{Type 46} {Sibling 133}]}
  22. &{133 BaseType false [{ByteSize 8} {Encoding 7} {Name sizetype}]}
  23. &{140 Variable false [{Name __cgo__0} {DeclFile 1} {DeclLine 16} {Type 117} {External true} {Location [3 0 0 0 0 0 0 0 0]}]}
  24. &{140 Variable false [{Name __cgo__0} {DeclFile 1} {DeclLine 16} {Type 117} {External true} {Location [3 0 0 0 0 0 0 0 0]}]}
  25. __cgodebug_data haha 117
  26. &{0 0 false []}
  27. <nil>
复制代码


从cgo的源代码一直追溯,最后发现libgo里边根本不支持mips架构的elf,libgo/go/debug/elf/{elf.go,file.go}里边压根没有mips的relocation和相关支持,导致dwarf里边的relocation无法实施,所有的符号都变成指向第一个符号了。

从binutils拷贝出来相关定义,仿照其他结构加上实现,信心满满地跑,结果还是失败。经过一整天的痛苦比较,cgo的输出和readelf输出二进制数据逐一比较,终于发现了一个令人吐血的事实:mips64下,relocation存储和其他所有结构不同,binutils里有如下信息:
/* In little-endian objects, r_info isn't really a 64-bit little-endian value: it has a 32-bit
     little-endian symbol index followed by four individual byte fields.  Reorder INFO  accordingly. */
简单说就是r_info里边的信息不能简单当64位值用,而是要经过类似下面的变换才能是最终需要的信息:
   
  1.    rela.Info = (((rela.Info & 0xffffffff) << 32) | ((rela.Info >> 56) & 0xff) | ((rela.Info >> 40) & 0xff00) | ((rela.Info >> 24) & 0xff0000) | ((rela.Info >> 8) & 0xff000000));
复制代码

解决了这个问题,cgo终于正常了,至此gccgo在龙芯的mips64 fedora系统上开始正常运行。后来用它编译的docker也能够正常,经受了初步的考验。


 楼主| 发表于 2015-9-2 08:26:40 | 显示全部楼层
更正(自己的帖子都不能修改了):cgo的错误调试输出里(未修改前,cgo是完全没有输出,而不是生成处理掉C.xxx的go程序)还有几个地方没有改到,后边的__cgo_1和__cgo_debugdata应该都是__cgo_0,因为没有relocation符号都找到第一个去了。由于过去一段时间了没有存着真正的错误输出和当时的环境(带调试语句的cgo等),为了讲清楚手工编辑的。
发表于 2015-9-2 15:59:58 | 显示全部楼层
老大,花点心思好好把处理器手册搞好,比解决这些技术的细枝末节重要太多了。别小看处理器手册这种“小事”。一个好的处理器手册相当于给龙芯增加了成千上万的潜在开发者,比龙芯自己的那点开发人员多多了。手册做好了,像 go 语言之类的事情,龙芯做不做都无所谓,需要的时候自然有别人来做;龙芯自己手册做不好,那么别人谁也做不了帮不了,就只能自己闭门造车了。
 楼主| 发表于 2015-9-2 22:57:38 | 显示全部楼层
本帖最后由 foxsen 于 2015-9-2 23:46 编辑
quene 发表于 2015-9-2 15:59
老大,花点心思好好把处理器手册搞好,比解决这些技术的细枝末节重要太多了。别小看处理器手册这种“小事” ...


我很同意啊。只是我现在也没直接参与芯片开发,没法直接做这个事情,只能设法推动。具体的开发文档,我也很头疼... 还好现在总算比以前好些了。我们这里可以设法加强些主板和系统软件级的开发文档
发表于 2015-9-4 23:55:21 | 显示全部楼层
gcc 不是有 go 语言前段吗?这东西可以输出 mips 的二进制代码吧?

点评

搞的就是gccgo,只要两个补丁就可以用了  发表于 2015-9-6 11:03

本版积分规则

小黑屋|手机版|Archiver|Lemote Inc.  

GMT+8, 2018-12-15 03:36 , Processed in 0.192536 second(s), 18 queries .

快速回复 返回顶部 返回列表