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, 代码转换, 开发工具, 快速连接, 日志审计, 汇编语言, 程序破解, 编译器工具, 自动回退