使用GO交叉编译的相关流程
众所周知,Go的一大特色是可以在不安装编译链的情况下进行交叉编译,而且基本上只要是完整安装Go环境的机子都可以支持,而无需再额外安装交叉编译环境
通用配置
启动交叉编译由系统环境变量决定,具体来说需要使用的环境变量有
变量名 | 值 | 说明 |
---|---|---|
CGO_ENABLED | 0 | 使用交叉编译时必填 |
GOOS | windows/darwin/linux | 目标操作系统 |
GOARCH | amd64/arm/misp | 目标平台 |
关闭CGO是因为本质上缺少对应平台的工具链,因此需要指示编译器不要使用平台的C运行库,否则会在运行时提示缺少库文件。当使用Docker打包并配合诸如Alpine这种最小镜像时,即使是相同平台和操作系统也会因为精简了库文件也需要关闭。目前Go所有的标准库和主流库基本上都已经使用纯Go实现,已经不再需要CGO的支持了,因此这种改动适用于大部分场合是没啥问题的
如果一定要使用CGO支持,则需要准备对应平台的C和C++(如果需要)编译器,并显式打开CGO(在启动交叉编译时默认是关闭的),并指定编译器的位置,具体用到的变量名是CC和CXX,类似于这样
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 \ CC=./aarch64-unknown-linux-gnueabi-5.4.0-2.23-4.4.6/bin/aarch64-unknown-linux-gnueabi-gcc \ CXX=./aarch64-unknown-linux-gnueabi-5.4.0-2.23-4.4.6/bin/aarch64-unknown-linux-gnueabi-g++ \ AR=./aarch64-unknown-linux-gnueabi-5.4.0-2.23-4.4.6/bin/aarch64-unknown-linux-gnueabi-ar \ go build xxx.go
其中:
CC
指定C编译器位置CXX
指定C++编译器地址AR
指定链接器地址如果你用到了动态链接库,那还需要添加诸如
-I, -isystem, -L, -l
这些参数来指定库的位置,这些过程和普通的C编译过程是类似的CGO_ENABLED=1 GOOS=linux GOARCH=arm64 \ CC=./aarch64-unknown-linux-gnueabi-5.4.0-2.23-4.4.6/bin/aarch64-unknown-linux-gnueabi-gcc \ -Wall -std=c++11 -Llib -isystem/aarch64/usr/include -L/aarch64/lib -ldl -lpthread -Wl,-rpath-link,/aarch64/lib \ -L/aarch64/lib/aarch64-linux-gnu -L/aarch64/usr/lib -I/aarch64/usr/include -L/aarch64/usr/lib/aarch64-linux-gnu \ -ldl -lpthread -Wl,-rpath-link,/aarch64/usr/lib/aarch64-linux-gnu -lphonon -lcurl -lprotobuf \ go build xxx.go
如果需要静态编译,可以使用
-ldflags "-s -w -extldflags '-static'"
来控制后端编译器的行为来实现,其中-s -w
会省略符号表和调试信息,以及DWARF表以精简体积,同时还可以使用-trimpath
来精简路径信息。需要注意的是,如果使用了外部库而非源码,则必须保证库也是采用静态编译的,不然也是无法静态链接的,在linux中就需要是.a
的库文件而非.so
的。需要注意的一点是,即使是使用了静态编译,有些动态库仍然会使用动态链接(如使用了linux相关的系统调用),这会导致诸如file
工具在判断链接时发现有诸如/lib64/ld-linux-x86-64.so.2
这种而认为仍然为动态编译,实际是否将预期的库代码静态编译进了二进制文件中,需要以对应平台的ldd
工具输出为准
支持的操作系统GOOS
如下
aix | android | darwin |
dragonfly | freebsd | hurd |
js | linux | nacl |
netbsd | openbsd | plan9 |
solaris | windows | zos |
支持的目标平台架构GOARCH
如下
386 | amd64 | amd64p32 |
arm | armbe | arm64 |
arm64be | ppc64 | ppc64le |
mips | mipsle | mips64 |
mips64le | mips64p32 | mips64p32le |
ppc | riscv | riscv64 |
s390 | s390x | sparc |
sparc64 | wasm |
你也可以通过执行
go tool dist list
来获得所有支持的操作系统与平台组合
在Linux平台,只需要这样执行编译就行了
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -o run.arm
在Windows平台,CMD配置环境变量
SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=arm
go build
go build -o run.arm
但是凡是比较新的Windows命令行都默认使用PowerShell了,上述的方式不会生效,在这里要用这种方式
$env:CGO_ENABLED=0
$env:GOOS="linux"
$env:GOARCH="arm"
go build
build -o run.arm
编译完成后,直接传输到目标机器上运行即可
关于ARM平台
ARM平台有不同的版本,不同版本由于指令集的原因不一定兼容,go支持的arm版本有ARMv5
,ARMv6
,ARMv7
,分别对应5
,6
,7
,注意,这里特指GOARCH
为arm
和armbe
时的情况,另外armv8
由于是64位架构被划分到arm64
也不在此列,要指定ARM平台版本,只需要额外添加以下环境变量
GOARM=6
ARM平台常用小端序
注意:即使是已经明确了目标平台的版本,也不代表不会出现版本问题,已知在某个ARMv6l
的设备上必须以版本5
进行编译才能运行的情况,如果问题不能解决时可以尝试其他平台版本试试
在M系芯片的macOS上编译
若要在使用Arm架构的mac平台上进行交叉编译,推荐的工具链主要有两个:一个是基于musl
库的编译器,另一个就是使用标准libc
库的编译器,至于这两个C标准库实现的性能差异和区别,已经有很多相关的讨论,在这里不再赘述
- 基于
musl
的编译器
推荐使用homebrew-musl-cross这个项目,你也可以直接使用brew
安装
brew install filosottile/musl-cross/musl-cross
- 基于
libc
的编译器
推荐使用homebrew-macos-cross-toolchains这个项目,由于libc
的编译器比较大,建议选择性地安装需要编译的目标平台,毕竟一个平台的就顶上面一整个musl
的了,同样也是通过brew
来安装
brew tap messense/macos-cross-toolchains
# install x86_64-unknown-linux-gnu toolchain
# 编译x86 64位平台时使用
brew install x86_64-unknown-linux-gnu
# install aarch64-unknown-linux-gnu toolchain
# 编译Arm64平台时使用
brew install aarch64-unknown-linux-gnu
关于嵌入式设备(MIPS)
如果目标机器是嵌入式设备,如mips
设备上,那么可能会在一些设备上缺少浮点数运算器(FPU),这会导致即使是架构正确的情况下,也会输出Illegal instruction
,例如典型的Openwrt设备
这种情况下,一般有两种选择,一个是在Openwrt这类固件里找到软件模拟FPU的编译选项并打开,但是这需要重新编译固件,另一种方式是go 1.10+左右新增的模拟软浮点类型,只需要在编译时添加一个参数即可
GOMIPS=softfloat
软浮点选项仅适用于MIPS平台,MIPS平台常用大端序
但软浮点毕竟是模拟的,因此会导致浮点性能很差,不建议在上面跑计算密集型服务,而且尽量在硬件支持的情况下优先使用硬件支持
其他资料
确定目标机器平台架构
关于确定目标机器的平台架构,可以使用命令
uname -m
对照表格
架构 | 对应结果 |
---|---|
i386 | i386 i686 |
amd64 | x86_64 |
arm_garbage | arm armel |
arm | armv6l armv7l armhf |
arm64 | aarch64 armv8l |
mips | mips |
mips64 | mips64 |
注:
armv8l
由于缺少测试设备,不确定是属于arm64
还是arm64p32
或者,也可以通过查看/proc/cpuinfo
文件,并检查CPU信息得知
system type : MediaTek MT7621 ver:1 eco:3
machine : Xiaomi Mi Router 3G
processor : 0
cpu model : MIPS 1004Kc V2.15
BogoMIPS : 586.13
wait instruction : yes
microsecond timers : yes
tlb_entries : 32
extra interrupt vector : yes
hardware watchpoint : yes, count: 4, address/irw mask: [0x0ffc, 0x0ffc, 0x0ffb, 0x0ffb]
isa : mips1 mips2 mips32r1 mips32r2
ASEs implemented : mips16 dsp mt
...
字节序
其中需要注意的是,mips
或mips64
架构因为不会标明字节序,需要额外指令来确定字节序,一般来说只要用第一条就行了,报错尝试第二条(我的Ubnt路由第一次编译运行不成功就是踩的这个坑)
其余平台均可以采用此方式,只是用的不多
echo -n I | hexdump -o | awk '{print substr($2,6,1); exit}'
echo -n I | od -to2 | awk '{print substr($2,6,1); exit}'
对应字节序 | 对应平台 | 对应结果 |
---|---|---|
大端序 | mips / mips64 | 0 |
小端序 | mipsle / mips64le | 1 |
如果以上两个命令都无法正常执行,也可以通过下载目标机器上的一个可执行文件,例如/bin/sh
,然后使用file
命令查看ELF文件头来得知
sh: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-mipsel-sf.so.1, no section header
其中,LSB
就是小端序,即mipsle
这种,MSB
为大端序
你也可以在Go中文网的下载页,找到许多平台的对应静态文件,用于在目标机器上部署Go环境,不过大多时候没必要这么做
https://studygolang.com/dl