Go Wiki:GcToolchainTricks

本页文档介绍了 `gc` 工具链(以及 Go 工具)一些不太为人所知(可能比较高级)的技巧。

不使用 `cgo` 的 C 代码

使用 `syso` 文件嵌入任意独立的 C 代码

基本上,你可以用 GNU as(1) 格式编写你的汇编语言,但要确保所有接口函数都使用 Go 的 ABI(一切都在栈上,等等,请阅读 Go 1.2 Assembler Introduction 了解更多细节)。

最重要的一步是将该文件编译为 file.syso(`gcc -c -O3 -o file.syso file.S`),并将生成的 syso 文件放在包的源代码目录中。然后,假设你的汇编函数名为 Func,你需要一个 cmd/asm 汇编文件作为存根来调用它。

TEXT ·Func(SB),$0-8 // please set the correct parameter size (8) here
    JMP Func(SB)

然后你只需在你的包中声明 Func 并使用它,`go build` 就可以拾取 syso 文件并将其链接到包中。

注意事项

  • 生成的二进制文件不会使用 cgo,开销只是一个可以完美进行分支预测的无条件 JMP。但是,请注意,因为它不使用 cgo,所以你的汇编函数运行在 Go 栈上,并且它不应该使用过多的栈空间(安全值为 100 字节以下),否则会发生可怕的事情。对于计算内核,这个要求并不算太苛刻。
  • 请确保你在 C 代码中包含了所有库依赖项。`libc` 不可用,最值得注意的是,`libgcc` 也不可用(尤其是在你使用 gcc `__builtin_funcs` 时,请使用 `nm(1)` 来仔细检查你的文件是否包含任何未定义符号)。
  • 也可以从 C 代码调用 Go 函数,但这留给读者作为练习。
  • 此技巧在所有 Go 1.x 版本中都受支持。
  • Go 链接器功能非常强大,你只需要为每种架构准备 .syso 文件,而不需要为每个操作系统/架构组合都准备(假设你没有使用特定于操作系统的构造,显然),并且 Go 链接器完全能够将例如 Mach-O 对象文件链接到 ELF 二进制文件中。因此,请确保你的 syso 文件命名为 `file_amd64.syso`、`file_386.syso` 等。

将数据打包到 Go 二进制文件中

有很多方法可以将数据打包到 Go 二进制文件中,例如:

  • 将数据文件 `zip` 起来,并将 zip 文件附加到 Go 二进制文件的末尾,然后使用 `zip -A prog` 来调整打包的 zip 头。你可以使用 `archive/zip` 将程序作为 zip 文件打开,并轻松访问其内容。有一些现有的包可以帮助完成这个任务,例如 https://pkg.go.dev/bitbucket.org/tebeka/nrsc; 这需要对程序二进制文件进行后处理,不适合需要静态数据的非 main 包。此外,你必须将所有数据文件收集到一个 zip 文件中,这意味着无法使用多个利用此方法的包。
  • 将二进制文件作为 `string` 或 `[]byte` 嵌入到 Go 程序中。不推荐使用此方法,不仅因为生成的 Go 源文件比二进制文件本身大得多,还因为静态的 `[]byte` 会减慢包的编译速度,并且 `gc` 编译器在编译时会消耗大量内存(这是 `gc` 的一个已知 bug)。例如,请参阅 tools/godoc/static 包。
  • 使用类似的 `syso` 技术来打包数据。使用 GNU `as(1)` 的 `.incbin` 伪指令预编译数据文件作为 syso 文件。

第三种选择的关键技巧是,`gc` 工具链的链接器能够轻松地将不同架构的 COFF 对象文件链接到二进制文件中,因此你不需要为所有支持的架构提供 syso 文件。只要 syso 文件不包含指令,你就可以只用一个来嵌入数据。

生成 COFF .syso 文件的汇编模板

/* data.S, as -o data.syso */
.section .rdata,"dr" /* put in COFF section .rdata */
.globl _bindataA /* no longer need to prepend package name here */
.globl _ebindataA
_bindataA:
.incbin "dataA"
_ebindataA:

.globl _bindataB /* no longer need to prepend package name here */
.globl _ebindataB
_bindataB:
.incbin "dataB"
_ebindataB:

以及另外两个文件,第一个是用于将切片组装给 Go 的 Plan 9 C 源文件。

/* slice.c */
#include "runtime.h"
extern byte _bindataA[], _bindataB[], _ebindataA, _ebindataB;

void ·getDataSlices(Slice a, Slice b) {
  a.array = _bindataA;
  a.len = a.cap = &_ebindataA - _bindataA;
  b.array = _bindataB;
  b.len = b.cap = &_ebindataB - _bindataB;
  FLUSH(&a);
  FLUSH(&b);
}

最后,是使用嵌入切片 的 Go 文件。

/* data.go */
package bindata

func getDataSlices() ([]byte, []byte) // defined in slice.c

var A, B = getDataSlices()

注意:你需要一个能够生成 COFF syso 文件的 `as(1)`,你可以在 Unix 上轻松构建一个。

wget http://ftp.gnu.org/gnu/binutils/binutils-2.22.tar.bz2   # any newer version also works
tar xf binutils-2.22.tar.bz2
cd binutils-2.22
mkdir build; cd build
../configure --target=i386-foo-pe --enable-ld=no --enable-gold=no
make
# use gas/as-new to assemble your data.S
# all the other file could be discarded.

缺点是,此方法似乎与 cgo 不兼容,因此至少目前为止,仅在你未使用 cgo 时使用它。我(minux)正在努力弄清楚它们为什么不兼容。

在可执行文件中包含构建信息

`gc` 工具链链接器 `cmd/link` 提供了一个 `-X` 选项,可用于在链接时将任意信息记录到 Go 字符串变量中。格式为 `importpath.name=val`。其中 `importpath` 是导入语句中使用的包名(或主包使用 `main`),`name` 是包中定义的字符串变量名,`val` 是你想要设置给该变量的字符串。在使用 go 工具时,请使用其 `-ldflags` 选项将 `-X` 选项传递给链接器。

假设此文件是 `company/buildinfo` 包的一部分。

package buildinfo

var BuildTime string

你可以使用 `go build -ldflags="-X 'company/buildinfo.BuildTime=$(date)'"` 来构建使用此包的程序,以记录构建时间。 (`$(date)` 的使用假设你使用的是类 Unix shell。)

字符串变量必须存在,它必须是一个变量,而不是常量,并且其值不能由函数调用初始化。使用 `-X` 选项中的错误名称不会有任何警告。你通常可以通过运行程序的 `go tool nm` 来找到要使用的名称,但如果包名包含任何非 ASCII 字符、`"` 或 `%` 字符,则会失败。


此内容是 Go Wiki 的一部分。