minio/c2goasm
GitHub: minio/c2goasm
将 C/C++ 编译器生成的汇编代码转换为 Go 汇编,使 Go 项目能零开销调用底层高性能 C/C++ 实现。
Stars: 1347 | Forks: 115
# c2goasm: C 转为 Go 汇编
## 简介
这是一个将 C/C++ 编译器生成的汇编代码转换为 Golang 汇编代码的工具。它旨在与 [asm2plan9s](https://github.com/minio/asm2plan9s) 结合使用,以便为 C/C++ 代码自动生成纯 Go 封装(例如,可以利用编译器的 SIMD intrinsics 或 `template<>` 代码)。
操作流程如下:
```
$ c2goasm -a /path/to/some/great/c-code.s /path/to/now/great/golang-code_amd64.s
```
你可以通过传入 `-f` 参数,选择使用 [asmfmt](https://github.com/klauspost/asmfmt) 对代码进行精美的格式化。
本项目是作为开发 [Simd](https://github.com/fwessels/go-cv-simd) 的 Go 封装的一部分而开发的。不过,它也应该适用于其他项目和库。但请记住,它的目的不是在单次操作中“移植”完整的 C/C++ 项目,而是针对每个函数/源文件按情况进行处理(并创建相应的高级 Go 代码来调用汇编代码)。
## 命令行选项
```
$ c2goasm --help
Usage of c2goasm:
-a Immediately invoke asm2plan9s
-c Compact byte codes
-f Format using asmfmt
-s Strip comments
```
## 一个简单的例子
下面是一个执行 AVX2 intrinsics 计算的简单 C 函数:
```
void MultiplyAndAdd(float* arg1, float* arg2, float* arg3, float* result) {
__m256 vec1 = _mm256_load_ps(arg1);
__m256 vec2 = _mm256_load_ps(arg2);
__m256 vec3 = _mm256_load_ps(arg3);
__m256 res = _mm256_fmadd_ps(vec1, vec2, vec3);
_mm256_storeu_ps(result, res);
}
```
将其编译为汇编代码,会得到以下结果
```
__ZN14MultiplyAndAddEPfS1_S1_S1_: ## @_ZN14MultiplyAndAddEPfS1_S1_S1_
## BB#0:
push rbp
mov rbp, rsp
vmovups ymm0, ymmword ptr [rdi]
vmovups ymm1, ymmword ptr [rsi]
vfmadd213ps ymm1, ymm0, ymmword ptr [rdx]
vmovups ymmword ptr [rcx], ymm1
pop rbp
vzeroupper
ret
```
运行 `c2goasm` 将生成以下 Go 汇编代码(例如,保存在 `MultiplyAndAdd_amd64.s` 中)
```
//+build !noasm !appengine
// AUTO-GENERATED BY C2GOASM -- DO NOT EDIT
TEXT ·_MultiplyAndAdd(SB), $0-32
MOVQ vec1+0(FP), DI
MOVQ vec2+8(FP), SI
MOVQ vec3+16(FP), DX
MOVQ result+24(FP), CX
LONG $0x0710fcc5 // vmovups ymm0, yword [rdi]
LONG $0x0e10fcc5 // vmovups ymm1, yword [rsi]
LONG $0xa87de2c4; BYTE $0x0a // vfmadd213ps ymm1, ymm0, yword [rdx]
LONG $0x0911fcc5 // vmovups yword [rcx], ymm1
VZEROUPPER
RET
```
这需要伴随以下 Go 代码(在 `MultiplyAndAdd_amd64.go` 中)
```
//go:noescape
func _MultiplyAndAdd(vec1, vec2, vec3, result unsafe.Pointer)
func MultiplyAndAdd(someObj Object) {
_MultiplyAndAdd(someObj.GetVec1(), someObj.GetVec2(), someObj.GetVec3(), someObj.GetResult()))
}
```
正如你可能已经猜到的那样,为了让参数名称能够被推导出来(并让 `go vet` 成功执行),必须将 amd64.go 文件放置到位。
## 针对 cgo 的基准测试
我们针对 Go 1.7.5 和 1.8.1 版本,运行了 `c2goasm` 与 `cgo` 的基准对比测试。你可以在 `test/` 目录中找到 `c2goasm` 基准测试,并在 `cgocmp/` 目录中找到 `cgo` 测试。以下是这两个版本的结果:
```
$ benchcmp ../cgocmp/cgo-1.7.5.out c2goasm.out
benchmark old ns/op new ns/op delta
BenchmarkMultiplyAndAdd-12 382 10.9 -97.15%
```
```
$ benchcmp ../cgocmp/cgo-1.8.1.out c2goasm.out
benchmark old ns/op new ns/op delta
BenchmarkMultiplyAndAdd-12 236 10.9 -95.38%
```
如你所见,Golang 1.8 比 1.7.5 有了显著的性能提升(38.2%),但相比直接调用由 `c2goasm` 封装的汇编代码,它依然慢了约 20 倍。
## 已转换的项目
- [go-cv-simd (WIP)](https://github.com/fwessels/go-cv-simd)
## 内部机制
基本流程是:(在序言部分)按照 C 代码的预期设置栈和寄存器,并在退出子程序时(在结语部分)恢复回 Golang 环境,并在需要时传回返回值。更详细地说:
- 使用符合 Golang 规范的修饰符定义汇编子程序,包括所需的栈空间以及参数和返回值的总大小。
- 在启动 C 代码之前,将函数参数从 Golang 栈加载到寄存器中,并将超过 6 个的参数存储到 C 栈空间中。
- 为 C 代码预留并设置栈空间。根据 C 代码的具体情况,可能需要将栈指针对齐到特定的边界(这对于利用了 SIMD 指令(如 AVX 等)的代码尤为重要)。
- (如果需要)生成常量表,并将所有基于 `rip` 的引用替换为 Go 存放该表位置的正确偏移量。
## 限制
- (目前)参数必须是 64 位大小,意味着要么是一个值,要么是一个指针(未来将会解除此限制)
- 最多 14 个参数(硬性限制——如果你触发了这个限制,也许你无论如何都应该重新思考你的 API 设计……)
- 通常不支持 `call` 语句(因此请内联你的 C 代码),但对于像 `memset` 和 `memcpy` 这样的函数有一些例外(参见 `clib_amd64.s`)
## 从 C/C++ 生成汇编
例如,对于使用 cmake 的项目,可以通过以下方式查看汇编目标列表
```
$ make help | grep "\.s"
```
查看生成汇编的实际命令
```
$ make -n SimdAvx2BgraToGray.s
```
## 支持的 Golang 架构
目前仅支持 AMD64 架构。此外,ARM64 应该也能以类似的方式正常工作,但目前尚缺乏相关支持。
## 兼容的编译器
已测试过以下编译器:
- OSX/darwin 下的 `clang` (Apple LLVM 版本)
- linux 下的 `clang`
编译器参数:
```
-masm=intel -mno-red-zone -mstackrealign -mllvm -inline-threshold=1000 -fno-asynchronous-unwind-tables -fno-exceptions -fno-rtti
```
| 参数 | 说明 |
|:----------------------------------| :--------------------------------------------------|
| `-masm=intel` | 输出 Intel 语法的汇编代码 |
| `-mno-red-zone` | 不要在栈指针下方写入数据(避免使用 [红区 (red zone)](https://en.wikipedia.org/wiki/Red_zone_(computing))) |
| `-mstackrealign` | 使用显式的栈初始化 |
| `-mllvm -inline-threshold=1000` | 提高 inline 启发式算法的阈值(默认=255) |
| `-fno-asynchronous-unwind-tables` | 不要生成 unwind 表(用于调试目的) |
| `-fno-exceptions` | 禁用异常处理 |
| `-fno-rtti` | 禁用运行时类型信息 |
以下参数仅在 `clang -cc1` 前端模式下可用(参见[下文]()):
| 参数 | 说明 |
|:----------------------------------| :------------------------------------------------------------------|
| `-fno-jump-tables` | 不要使用可能为 `select` 语句生成的跳转表 |
#### `clang` 与 `clang -cc1`
根据 clang 的 [FAQ](https://clang.llvm.org/docs/FAQ.html#driver),`clang -cc1` 是前端,而 `clang` 是该前端的(主要兼容 GCC 的)驱动程序。要查看驱动程序传递给前端的所有选项,请像这样使用 `-###`:
```
$ clang -### -c hello.c
"/usr/lib/llvm/bin/clang" "-cc1" "-triple" "x86_64-pc-linux-gnu" etc. etc. etc.
```
#### clang 的命令行参数
要查看所有的命令行参数,可以使用 `clang --help` 或 `clang --help-hidden`(针对 clang 驱动程序),或者使用 `clang -cc1 -help`(针对前端)。
#### 进一步优化与微调
使用 LLVM 优化器([opt](http://llvm.org/docs/CommandGuide/opt.html))可以进一步优化代码生成。使用 `opt -help` 或 `opt -help-hidden` 查看所有可用选项。
可以通过 `clang` 的 `-mllvm ` 选项传递参数,例如上面讨论过的 `-mllvm -inline-threshold=1000`。
此外,LLVM 还允许你通过[函数属性](http://llvm.org/docs/LangRef.html#function-attributes)来微调特定函数,例如 `define void @f() alwaysinline norecurse { ... }`。
#### 关于 GCC 的支持呢?
目前 GCC 生成的代码无法开箱即用。然而,从根本上说 GCC 没有理由不能工作(欢迎提交 PR)。
## 资源
- [Go 汇编入门](https://github.com/teh-cmc/go-internals/blob/master/chapter1_assembly_primer/README.md)
- [汇编中的 Go 函数](https://github.com/golang/go/files/447163/GoFunctionsInAssembly.pdf)
- [x86-64 上的栈帧布局](http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64)
- [Compiler Explorer (交互式)](https://go.godbolt.org/)
## 许可证
c2goasm 在 Apache License v2.0 下发布。你可以在 LICENSE 文件中找到完整的文本。
## 贡献
欢迎贡献代码,请为任何功能增强提交 PR。
标签:EVTX分析, Go语言, SIMD, SOC Prime, 代码转换, 开发工具, 快速连接, 日志审计, 汇编语言, 程序破解, 编译器工具, 自动回退