eyasliu/blog

CGO 交叉静态编译

eyasliu opened this issue · 8 comments

在新的项目中,由于必须要使用sqlite数据库,所以引入了 go-sqlite3 库,而且目标还是安卓开发板,板子是 aarch64 架构,也就是说,目的是要把启用了CGO的项目交叉编译到 aarch64 安卓系统。

本篇文章不局限于 aarch64 架构,其他架构同理,只是到时候下载的工具包不太一样

本文章使用的操作系统为 ubuntu

名词解释:

  • 交叉编译:是在一个平台上生成另一个平台上的可执行文件
  • 静态编译:在编译可执行文件的时候,将可执行文件需要调用的对应库都集成到可执行文件内部,使得可执行文件不需要其他任何依赖就能运行

无CGO项目的交叉静态编译

在不启用CGO的情况下,交叉编译是非常简单的,因为 golang 本身的交叉编译做的非常好

CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags '-s -w --extldflags "-static -fpic"' main.go
  • CGO_ENABLED=0 这个值默认是1,也就是开启的,需要手动指定为关闭,因为CGO是不支持交叉编译的,使用 go env CGO_ENABLED 查看默认值
  • GOOS, GOARCH 构建的平台,GOOS=linux是因为安卓底层就是linux,aarch64 架构直接使用 arm64,如果GOARCH=arm,则要使用 GOARM=7,指定arm版本可选5,6,7,在这里看完整支持列表
  • -ldflags 编译选项,-s -w 去掉调试信息,可以减小构建后文件体积,--extldflags "-static -fpic" 完全静态编译,要交叉编译放到其他系统和架构中运行,静态编译是最好的,不然启动的时候会提示找不到依赖的so文件

这样编译生成的文件就可以任意放到指定平台下运行不会有问题。如果没有CGO,一切都很完美。

CGO 项目的交叉静态编译

记录一下在 CGO 交叉编译过程

交叉编译

刚开始,静态编译是不敢想的了,也只有交叉编译可以试一试,安装两个包,如果是其他架构和系统,安装其他对应的包就行了,本文章都以 aarch64 的 linux 为例子

sudo apt install -y cpp-aarch64-linux-gnu g++-aarch64-linux-gnu

安装好以后,指定gcc和g++编译器为上面两个

CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ GOOS=linux GOARCH=arm64 go build -o server -ldflags '-s -w' main.go

命令解释

  • CGO_ENABLED=1 开启CGO,因为项目用到了C语言的代码
  • CC=aarch64-linux-gnu-gcc 指定gcc的编译器为 aarch64-linux-gnu-gcc,这个默认值是gcc,也就是当前操作系统和架构使用的gcc,使用命令 $(go env CC) --target-help 可以看看默认gcc支持什么平台
  • CXX=aarch64-linux-gnu-g++ 指定g++的编译器为 aarch64-linux-gnu-g++,规则和 CC 一样,只是用来编C++代码的,如果还用到了C++代码,必须指定该项
  • -ldflags '-s -w' go编译选项,-s -w 去掉调试信息,可以减小构建后文件体积

这样编出来是成功的,整个过程应该会很顺畅,但是把文件放到对应的平台运行,如果运气好,那肯定是运行起来了。如果运气一般会发现提示

./server: No such file or directory

这时候可以用 ldd 看一下

$ ldd server
libdl.so.2
libpthread.so.0
libc.so.6
ld-linux-aarch64.so.1

会发现依赖的so文件好像都有,可以去依赖目录/system/lib(这是安卓的)看看 ,但是运行的时候就是不成功,这里原因我不是太确定。我猜测是这样的,编译器依赖的这些 so 其实是编译器带的那些so,并不是系统的那些,所以在执行的时候这些依赖虽然有,但是并不是执行文件需要的。

这时候,把编译器下面的这几个依赖复制出来,然后执行放到和执行文件同一个目录,这样去执行,应该是可以成功执行的

cd /usr/aarch64-linux-gnu/lib/
cp libdl.so.2 libpthread.so.0 libc.so.6 ld-linux-aarch64.so.1 ~ # 去编译器目录把这几个文件复制出来
chmod +x libdl.so.2 libpthread.so.0 libc.so.6 ld-linux-aarch64.so.1 # 给这几个文件加上可执行权限
scp ...... # 想办法把上面这几个文件复制到对应平台和执行文件一个目录

# 对应平台执行
./libc.so.6 ./libdl.so.2 ./libpthread.so.0 ld-linux-aarch64.so.1 ./server 
# 也可以指定依赖目录
LD_LIBRARY_PATH=. ./server

这样子手动的执行依赖库,就能成功的启动起来了,如果还不行的话,继续往下看吧,下面有把所有so静态编译进去的方法。

静态编译

回看刚刚交叉编译时候的命令,我们是不是在 -ldflags 那里漏掉了刚开始的那个 --extldflags "-static -fpic",现在把它加上去

$ CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ GOOS=linux GOARCH=arm64 go build -o server -ldflags '-s -w --extldflags "-static -fpic"' main.go
# command-line-arguments
/tmp/go-link-972598095/000015.o: In function `unixDlOpen':
/home/eyas/.GOPATH/pkg/mod/github.com/mattn/go-sqlite3@v1.10.0/sqlite3-binding.c:38461: warning: Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-972598095/000031.o: In function `mygetgrouplist':
/opt/golang/src/os/user/getgrouplist_unix.go:16: warning: Using 'getgrouplist' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-972598095/000030.o: In function `mygetgrgid_r':
/opt/golang/src/os/user/cgo_lookup_unix.go:38: warning: Using 'getgrgid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-972598095/000030.o: In function `mygetgrnam_r':
/opt/golang/src/os/user/cgo_lookup_unix.go:43: warning: Using 'getgrnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-972598095/000030.o: In function `mygetpwnam_r':
/opt/golang/src/os/user/cgo_lookup_unix.go:33: warning: Using 'getpwnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-972598095/000030.o: In function `mygetpwuid_r':
/opt/golang/src/os/user/cgo_lookup_unix.go:28: warning: Using 'getpwuid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-972598095/000004.o: In function `_cgo_26061493d47f_C2func_getaddrinfo':
/tmp/go-build/cgo-gcc-prolog:58: warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking

好吧,报错了。

我不太懂C语言,但是根据错误信息能得知,有些库文件是不能被静态链接的,gcc 编译器不支持。也就是说 cpp-aarch64-linux-gnu 这个编译器不支持静态编译。

MUSL GCC

事实上这卡住了我很长一段时间,直到后面在玩 RUST 的时候发现了个好东西,就是支持gcc静态编译的 musl gcc,才终于解决 CGO 静态编译

先下载 musl 对应的平台的编译器,进入 这里 能发现 musl 其实只能在 x86_64 架构下的 linux 系统才能运行,也就是说只能在 linux 系统下去交叉编译其他平台的执行文件(也不知道我说的对不对)

下载 aarch64 linux 的编译器: https://musl.cc/aarch64-linux-musl-cross.tgz

解压,然后把解压好的目录下 bin 文件路径放到 PATH 环境变量中,再把编译器换成这个

$ CGO_ENABLED=1 CC=aarch64-linux-musl-gcc CXX=aarch64-linux-musl-g++ GOOS=linux GOARCH=arm64 go build -o server -ldflags '-s -w --extldflags "-static -fpic"' main.go

没有报错,成功编译了,不用做什么其他操作,把可执行文件放到对应平台上运行,正常。

完成了,折腾到此结束

可以用这个地址:https://musl.cc/aarch64-linux-musl-cross.tgz

感谢指正

我想问一下,如果我想在windows下交叉编译cgo到linux上,我该怎么安装或者寻找对应的linux上需要的gcc和g++编译器呢,我甚至都不知道搜索关键词该怎么查找。。。。
我试过搜索“windows交叉编译cgo到linux”,结果确实牛马不相及。。。。🥺

@yingyi666 你需要的应该是运行在 windows 系统下能编译到 linux 的 gcc 和 g++ ,这个你可能在这里能找到 https://musl.cc/ ,具体是哪一个我也不确定,也可能没有,不过如果你换个方式,在 linux 下能交叉编译到其他平台的 gcc 应该还是很好找的

感谢,我目前的代替方案是使用docker自行构建一个开启了cgo的镜像,然后创建容器编译

如果CGO程序中需要dlopen依赖库,是不是没法静态编译。我尝试在mac上交叉编译包含CGO的代码到linux平台的时候,使用musl-cross虽然能编译成功,但是在linux上执行的时候,会报错undefined symbol

如果CGO程序中需要dlopen依赖库,是不是没法静态编译。我尝试在mac上交叉编译包含CGO的代码到linux平台的时候,使用musl-cross虽然能编译成功,但是在linux上执行的时候,会报错undefined symbol

目前cgo是无法静态编译的,除非你可以配置好对应平台的工具链,然后在编译的期间指定工具链