浅谈go语言交叉编译

浅谈go语言交叉编译

  • 基础
    • cgo
      • cgo设置编译和链接参数
    • 静态库和动态库
      • 静态库
      • 动态库
      • 静态编译
    • cgo的内部连接和外部连接
      • internal linking
      • external linking
  • 交叉编译
    • 没有C代码,禁用CGO
    • 有C代码,启用CGO -XGO
      • karalabe/xgo
      • techknowlogick/xgo
      • crazy-max/xgo
    • 手动编译
      • musl-cross
      • xgo
  • todo
  • 参考

基础

cgo

go语言自带的一个工具/特性,用来支持C语言函数调用,同时可以把go语言导出C动态库给其他语言使用。

默认情况下cgo是启用的,可以通过go env命令查看CGO_ENABLED变量确认启用

通过import "C"来启用cgo特性,同时包含C语言头文件,来使用C.puts,C.CString

import "C" 后的注释是C语言代码

把要调用的C和go写到一个文件,代码及执行效果:
浅谈go语言交叉编译_第1张图片
把C代码和go代码分开,此时建立单独的hello.c文件,同时在项目目录运行go mod init gotest,新版本go mod默认启用,需要初始化后才可以编译包。
浅谈go语言交叉编译_第2张图片

因为关注交叉编译,在此不再叙述如何用go导出C语言函数。

cgo设置编译和链接参数

通过import "C"前的注释语句设置

// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include 
import "C"
  • 编译阶段的参数:定义相关宏和指定头文件检索路径
  • 链接阶段的参数:指定库文件检索路径和要链接的库文件
  • -D:定义宏,PNG_DEBUG=1
  • -I:定义头文件包含的检索目录
  • -L:指定链接库文件的检索目录
  • -l:连接时需要连接的png库

静态库和动态库

CGO在使用C/C++资源的时候三种形式:源码;静态链接库,动态链接库。使用源码就是第一部分讲的直接在源码中写C代码或者包含C源文件。

静态库

优势:生成的程序不会产生额外的运行时依赖
缺点:静态库包含了全部代码和符号信息,不同静态库可能出现符号冲突导致连接失败
目录结构:

(base) # susu @ susu-tools in ~/gotest [8:55:55] 
$ tree
.
├── go.mod
├── main.go
└── number
    ├── libnumber.a
    ├── number.c
    ├── number.h
    └── number.o

1 directory, 6 files

有一个库number,定义如下:

//number.h
int number_add_mod(int a, int b, int mod);

//number.c
#include "number.h"

int number_add_mod(int a, int b, int mod) {
    return (a+b)%mod;
}

CGO使用gcc来编译连接C和Go桥接的代码,所以静态库需要用gcc来编译:

gcc -c -o number.o number.c
ar rcs libnumber.a number.o

浅谈go语言交叉编译_第3张图片
生成的libnumber.a就是静态链接库。
main.go代码:

package main

//#cgo CFLAGS: -I./number
//#cgo LDFLAGS: -L${SRCDIR}/number -lnumber
//
//#include "number.h"
import "C"
import "fmt"

func main() {
	fmt.Println(C.number_add_mod(10, 5, 12))
}

参数解释:

  • -I./number,把number库对应的头文件所在目录添加到检索路径
  • -L${SRCDIR}/number -lnumber:添加静态库的检索路径,连接静态库
    build的可执行文件信息及运行结果:
    浅谈go语言交叉编译_第4张图片

动态库

优点:节省内存和磁盘,隔离不同动态库减少符号冲突
缺点:运行时依赖对应的动态库

创建上述示例的动态库:
gcc -shared -o libnumber.so number.c

编译运行结果:
浅谈go语言交叉编译_第5张图片
原因是没有把动态库文件放到默认的搜索路径去,所以执行的时候找不到。

执行sudo cp ./number/libnumber.so /usr/lib拷贝到/usr/lib/目录下,再运行就可以找到了
在这里插入图片描述

静态编译

默认情况下有动态库有静态库,会选择动态连接
可以通过命令强制静态连接:go build --ldflags '-extldflags "-static"' ~/gotest
编译出的文件信息及运行结果:
浅谈go语言交叉编译_第6张图片

cgo的内部连接和外部连接

internal linking

internal linking的大致意思是若用户代码中仅仅使用了net、os/user等几个标准库中的依赖cgo的包时,cmd/link默认使用internal linking,而无需启动外部external linker(如:gcc、clang等),不过由于cmd/link功能有限,仅仅是将.o和pre-compiled的标准库的.a写到最终二进制文件中。因此如果标准库中是在CGO_ENABLED=1情况下编译的,那么编译出来的最终二进制文件依旧是动态链接的,即便在go build时传入 -ldflags '-extldflags "-static"'亦无用,因为根本没有使用external linker

这样就会出现下文中命令行带参数-ldflags '-extldflags "-static"',编译出来的还是会显示为动态连接。

external linking

而external linking机制则是cmd/link将所有生成的.o都打到一个.o文件中,再将其交给外部的链接器,比如gcc或clang去做最终链接处理。如果此时,我们在cmd/link的参数中传入 -ldflags '-linkmode "external" -extldflags "-static"',那么gcc/clang将会去做静态链接,将.o中undefined的符号都替换为真正的代码。我们可以通过-linkmode=external来强制cmd/link采用external linker

交叉编译

没有C代码,禁用CGO

不用启用CGO的交叉编译,最简单
基础命令:
env CGO_ENABLED=0 GOOS=${os} GOARCH=${arch} GOMIPS=${gomips} go build -trimpath -ldflags "-s -w" -o ${bin_name} ${package_name}

其中编译参数-ldflags部分按需即可
${os},${arch},${gomips}的可选组合,可以参考https://golang.org/doc/install/source#environment
$GOARM(默认为6),$GOMIPS,$GOPPC64 指定特定的架构版本
如何查看Go支持的系统和架构:go tool dist list

$ go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
illumos/amd64
ios/amd64
ios/arm64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
openbsd/mips64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
windows/arm

举例编译windows x64的:GOOS=windows GOARCH=amd64 go build ~/gotest
文件信息:gotest.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows
编译arm的:GOOS=linux GOARCH=arm go build ~/gotest
文件信息:gotest: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped

纯go代码,禁用cgo,随便编译跨平台可执行文件,默认编译出的都是静态连接的

有C代码,启用CGO -XGO

karalabe/xgo

最早的版本:https://github.com/karalabe/xgo
已经不维护了,最后的go支持到1.13.15,且不支持go mod,在此不再介绍。
浅谈go语言交叉编译_第7张图片

techknowlogick/xgo

仍在维护,支持go mod,支持go 1.15,1.16版本,https://github.com/techknowlogick/xgo
安装之类的按照github项目主页的readme来进行就可以。
如果需要使用特定的go版本,在docker pull的时候进行指定,如:docker pull techknowlogick/xgo:go-1.16.3
以frp为例,因为frp算是一个典型的go项目,引用了很多github上的库,有助于测试网络问题,同时在一个项目中需要编译两个可执行文件,启用了go mod。选择的xgo docker的tag是latest。
编译命令:~/go/bin/xgo --pkg cmd/frps .,参考这一节techknowlogick/xgo - Package selection
第一次编译,很明显因为网络问题挂了(注意这条命令不对,按上面写的命令输):
浅谈go语言交叉编译_第8张图片
错误:Get "https://proxy.golang.org/github.com/armon/go-socks5/@v/v0.0.0-20160902184237-e75332964ef5.mod": dial tcp 172.217.160.81:443: i/o timeout
解决方案:
配置环境变量export GOPROXY=https://goproxy.io,direct
或者在执行时带上环境变量:GOPROXY=https://goproxy.io,direct ~/go/bin/xgo --pkg cmd/frps .
成功编译:
浅谈go语言交叉编译_第9张图片
注意,默认编译出来的在当前目录,带包名目录的下面,也就是不在frp根目录下,而是在frp/github.com/fatedier/目录下生成可执行文件。
浅谈go语言交叉编译_第10张图片
可以看到编译出的是动态连接到ld-linux.so的,那么是否可以编译出静态连接的呢?
我尝试在运行xgo时加上参数-ldflags '-s -w -extldflags "-static"'但是会报警告:

/tmp/go-link-325119081/000004.o: In function `_cgo_26061493d47f_C2func_getaddrinfo':
/tmp/go-build/cgo-gcc-prolog:57: warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking

查阅资料显示会有一些依赖动态的加载扩展,但是并不是常用的功能,对于大多数程序可以安全的忽略该警告。(参考Statically compiling Go programs)
最终编译出的一些文件信息如下,不确定是否算是静态连接:
在这里插入图片描述

更新一下,运行xgo时加上参数-ldflags '-linkmode "external" -extldflags "-static"',完整命令~/xgo -goproxy 'https://goproxy.io,direct' -ldflags '-linkmode "external" -extldflags "-static"' -pkg cmd/frps -x -docker-image crazymax/xgo:latest .,编译出的可执行文件为静态连接。

文件信息:github.com/fatedier/frp-linux-arm-5: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=71378ee03acfeb967920dda792808d587ca772f3, not stripped

crazy-max/xgo

项目主页:https://github.com/crazy-max/xgo
特性:支持命令行指定goproxy
按官方文档的说法是,交叉编译使用CGO的Go代码时,尤其是访问一些特定于操作系统的功能时(例如查询cpu负载),需要配置和维护单独的构建环境才可以,而xgo省略了这些事情。
编译命令:~/xgo -goproxy 'https://goproxy.io,direct' -dest ~/frp -pkg cmd/frps -docker-image crazymax/xgo:latest .
编译过程:
浅谈go语言交叉编译_第11张图片
编译输出:
浅谈go语言交叉编译_第12张图片

手动编译

musl-cross

brew项目:https://github.com/FiloSottile/homebrew-musl-cross
编译器项目:https://github.com/richfelker/musl-cross-make
编译工具链:https://musl.cc/
mac下可以通过brew install FiloSottile/musl-cross/musl-cross安装编译器,在Go编译的时候指定CC=x86_64-linux-musl-gcc来选择需要的交叉编译器,通过CGO_LDFLAGS="-static"来指定静态编译。

xgo

查看xgo的dockerfile构建时的build.sh,可以看到实际编译时使用的命令,如arm-5的编译命令:
CC=arm-linux-gnueabi-gcc-6 CXX=arm-linux-gnueabi-g++-6 GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=1 CGO_CFLAGS="-march=armv5" CGO_CXXFLAGS="-march=armv5" CGO_LDFLAGS="-static" go build -x -trimpath --ldflags='-s -w -extldflags "-static"' -o frps ./cmd/frps
上面的是我根据实际情况修改的,那么这样我们安装了交叉编译器sudo apt install gcc-6-arm-linux-gnueabi g++-6-arm-linux-gnueabi,然后运行上述命令,可以得到编译后的文件信息
ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, stripped
这样也可以根据实际情况来手动编译了。

todo

  1. CGO_LDFLAGS="-static"--ldflags '-extldflags "-static"'有何区别?
  2. 目前没有遇到需要编译复杂的CGO程序的问题,暂时就写这么多了。

参考

《Go语言高级编程》 - 第2章 CGO编程
知乎专栏 - Go语言涉及CGO的交叉编译(跨平台编译)解决办法
知乎专栏 - 含有CGO代码的项目如何实现跨平台编译
Go官方文档 - Installing Go from source
Gist - Go (Golang) GOOS and GOARCH
Blog - Statically compiling Go programs
推荐 - CGO_ENABLED环境变量对Go静态编译机制的影响

你可能感兴趣的:(go,go语言,交叉编译,编译器)