如何将Go程序编译至OpenWRT

前言

通常而言,OpenWRT 对应的都是一个路由器,先不考虑在上面安装常用的脚本语言(如 Python)的复杂度,即使能够安装,也需要占据大量的空间,并且由于其性能低下,运行 Python 将会非常缓慢。

尽管可以用 Bash 脚本作为替代,但是 Bash 的编写体验非常不友好。而 Go 作为跨平台编译性语言,其不需要再对应平台安装繁杂的虚拟机,只需要本机交叉编译即可。而且与 C 等语言不同,交叉编译仅仅只需要简单的一行环境变量。

查看 OpenWRT 系统架构

使用uname -acat /proc/cpuinfo可以查看路由器的系统架构,得知我们到底要如何编译程序

NEWIFI3:/home/root # cat /proc/cpuinfo
system type             : MediaTek MT7621 SoC
processor               : 0
cpu model               : MIPS 1004Kc V2.15
BogoMIPS                : 583.68
wait instruction        : yes
microsecond timers      : yes
tlb_entries             : 32
extra interrupt vector  : yes
hardware watchpoint     : yes, count: 4, address/irw mask: [0x0000, 0x0000, 0x0ff8, 0x0000]
ASEs implemented        : mips16 dsp mt
shadow register sets    : 1
kscratch registers      : 0
core                    : 0
VPE                     : 0
VCED exceptions         : not available
VCEI exceptions         : not available

processor               : 1
cpu model               : MIPS 1004Kc V2.15
BogoMIPS                : 583.68
wait instruction        : yes
microsecond timers      : yes
tlb_entries             : 32
extra interrupt vector  : yes
hardware watchpoint     : yes, count: 4, address/irw mask: [0x0000, 0x0000, 0x0000, 0x0000]
ASEs implemented        : mips16 dsp mt
shadow register sets    : 1
kscratch registers      : 0
core                    : 0
VPE                     : 1
VCED exceptions         : not available
VCEI exceptions         : not available

processor               : 2
cpu model               : MIPS 1004Kc V2.15
BogoMIPS                : 583.68
wait instruction        : yes
microsecond timers      : yes
tlb_entries             : 32
extra interrupt vector  : yes
hardware watchpoint     : yes, count: 4, address/irw mask: [0x0000, 0x0000, 0x0ff8, 0x0000]
ASEs implemented        : mips16 dsp mt
shadow register sets    : 1
kscratch registers      : 0
core                    : 1
VPE                     : 0
VCED exceptions         : not available
VCEI exceptions         : not available

processor               : 3
cpu model               : MIPS 1004Kc V2.15
BogoMIPS                : 583.68
wait instruction        : yes
microsecond timers      : yes
tlb_entries             : 32
extra interrupt vector  : yes
hardware watchpoint     : yes, count: 4, address/irw mask: [0x0ffc, 0x0000, 0x0ff8, 0x0000]
ASEs implemented        : mips16 dsp mt
shadow register sets    : 1
kscratch registers      : 0
core                    : 1
VPE                     : 1
VCED exceptions         : not available
VCEI exceptions         : not available

可以看到,这里路由器采用 MIPS 架构,同时并未提及 fpu(浮点数运算单元),因此可以默认为不存在浮点数运算单元(如果存在应该会有fpu: yes
但 MIPS 本身包括大端序和小端序两种情况。尽管网上有很多快速判断大端序和小端序的代码,但是大多依赖于一些路由器不自带的指令(如lscpuod)。尽管大部分都是非常普遍的工具,但是 OpenWRT 仍然不自带。这里使用 hexdump 来获取字节序:

> hexdump  -s 5 -n 1 -C  /bin/ls
00000005    01    |.|
00000006

> hexdump  -n 8 -C /bin/busybox
00000000    7f 45 4c 46 01 01 01 00    |.ELF....|
00000008

> hexdump  -n 8 -C /bin/ls
00000000    7f 45 4c 46 01 01 01 00    |.ELF....|
00000008

通过查看这些程序的第 6 个字节,来判断字节序。01 表示小端序,00 表示大端序

在这里,就可以得知路由器为 mipsle 架构(mips little endian)。(mips 默认为大端序,所以大端序直接称为 mips)

Go交叉编译

go build会从环境变量读取相关参数确定如何编译程序,默认自然是编译到当前操作系统。
如果需要编译到其他系统,需要确定两点:

  • 要编译到什么系统:这将涉及底层系统调用
  • 要编译到什么 CPU:这将涉及机器码如何生成

从上一步可知,CPU 为 mips 架构,而 OpenWRT 自然是 Linux 系统。

因此需要在环境变量写入GOOS=linuxGOARCH=mipsle

由于上面提到不存在 fpu,因此需要使用GOMIPS=softfloat来让程序使用其他指令执行浮点运算(当然也可以在 OpenWRT 系统层面设置模拟浮点运算)

将这些编译指令写成go generate命令即可一件实现交叉编译

package main

import ("fmt")

//go:generate bash -c "GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -o sayHi"
func main(){
    fmt.Println("Hi")
}

将其编译出,并通过scp发送到路由器中,测试运行

$ go generate
$ go scp sayHi root@router.oyohyee.com:~/sayHi
sayHi
100% 2020KB   2.3MB/s   00:00 

运行即可看到输出了Hi

补充内容

cpu架构

CPU 是实际的运算器,也即各种语言被编译成机器码后,CPU 通过读取机器码来执行相应的运算。
如果使用的机器码不是当前 CPU 认识的机器码,那么程序将会无法运行。每种 CPU 所认识的机器码就是指令集。指令集分为复杂指令集(cisc)和精简指令集(risc)。
在实际使用中,复杂指令集一般指复杂指令为主、精简指令为辅的架构;精简指令集指精简指令为主、复杂指令为辅的架构,分类并不是那么如名字那么绝对。

复杂指令集

复杂指令集通常用于电脑,通过将各种可能的操作在设计阶段进行深度优化,来提升运算性能,但其代价是功耗更大。
举一个简单但可能不恰当的例子:如果要计算 9*9=81,别的 CPU 可能还需要一点一点算,但是复杂指令集的 CPU 可以直接背乘法口诀,迅速算出结果。

如果提到 86 和 CPU,很容易联想到 8086 单片机,但 8086 单片机本身是 16 位的 CPU。其后衍生出的 80286、80386 则是 32 位的 CPU。在这里,可以理解成,这些单片机采用了同一套类似的指令集,由于他们都以 86 结尾,因此该指令集被简称为 x86 指令集。其中 x86 指令集本身是包含 16 位、32 位、64 位这些不同的总线长度的硬件。
而由于 16 位已经成为历史,x86 架构最辉煌的时期则是 32 位总线长度,其开创于 80386,该 CPU 全称为 Intel 30386(i386)。在很多地方,也使用 i386 来表示 32 位架构。

在 32 位时期,Intel 的 Intel Architecture, 32-bit(IA-32)处于绝对统治地位,因此包括 AMD 在内的其他厂商,都采用该指令集来构建 CPU。因此如果要下载 32 位程序,可以直接选择带有 x86、i386、32 字样的选项即可。

到了 64 位使其,Intel 推出了 IA-64,但这与之前的 IA-32 并不兼容,因此被市场抛弃。占据主流的是由 AMD 推出的兼容 x86 的 64 位架构,称为 x86-64,正式名称为 AMD64。后续 Intel 推出的 64 位 CPU 也都采用该架构。如果要下载 64 位程序,选择带有 x86-64、amd64、64 字样的选项即可。

精简指令集

精简指令集用于小型设备(如手机、路由器)。以手机为例,其所需要给用户提供的功能大多并不需要复杂的计算能力,同时又受限于电池容量,因此使用计算性能较低但更节能的精简指令集更为合适。

精简指令集主要有 ARM 和 MIPS。前者广泛应用于各种手机 CPU,三星、苹果、高通都通过向 ARM 公司购买 ARM 技术专利来将其用于自己的 CPU 中。后者是一个学院类型的架构,相对而言专利授权非常简单(学院派更推崇开源精神),广泛应用于各种嵌入式设备(省钱)。

字节序

字节序是数字在内存中存储的顺序。

  • 大端序(Big Endian),高字节存于内存低地址,低字节存于内存高地址。
  • 小端序(Little Endian),高字节存于内存高地址,低字节存于内存低地址。

用一个通俗的例子来看,如果在纸上写上12345678,我们的书写顺序是从左向右,因此先读的(内存低地址)是数字的高位(12),后读的(内存高地址)是数字的低位(78)。这也就表明,我们的阅读习惯是大端序。

大端序和小端序影响的实际上是字节数组的顺序,而非数字本身的顺序。无论是大端序还是小端序,单个字节都是相同的,但是字节数组的顺序可能是相反。

通常我们使用的操作系统(x86、amd64)都是小端序,而网络字节序则是大端序。

原文链接

https://www.ohyee.cc/post/note_compile_go_to_openwrt

版权声明:
作者:Ne-21
链接:https://blog.gocos.cn/archives/219.html
来源:云淡风轻
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
海报
如何将Go程序编译至OpenWRT
前言 通常而言,OpenWRT 对应的都是一个路由器,先不考虑在上面安装常用的脚本语言(如 Python)的复杂度,即使能够安装,也需要占据大量的空间,并且由于其性……
<<上一篇
下一篇>>