今天的文章聊一下接下来要上线的一些功能。

简单说就是:我们现在可以把 Yaklang 脚本,编译成原生二进制文件,加上混淆,并且能真正跑起来。

但这不是一个突然冒出来的方向。

它其实是 yaklang 编译这条线上长期规划的一部分。

这件事为什么是现在#

过去很长时间,我们花了很多力气在做一件事:把各种语言的源码编译成统一的 SSA IR(中间表示)。

你如果关注过我们之前的文章,应该知道我们已经接入了不少语言的编译前端,并且做了大量的语法特性支持。在做这些的过程中,Yak SSA IR 本身也在慢慢趋于稳定。

而当中间这一层稳定下来以后,一个很自然的问题就出现了:

能不能继续往下走?不只是把源码编译成 IR 用来分析,而是把 IR 再往下编译成真正能跑的二进制?

这件事从技术上来说并不突兀。

编译这件事,本质上就是保持语义一致的前提下,一层一层做代码形式的转换——从源码到 AST,再到 IR,再到目标码。每一层都是在前一层的基础上,把语义保留下来,换成更接近机器能执行的形式。

所以当中间 IR 层趋于稳定,往后端走就是水到渠成的事情。

这条编译路径叫 AOT(Ahead-Of-Time,提前编译),意思是把代码提前编译成原生二进制,运行时不再临时编译或解释。它和 JIT(运行时边跑边编译)是两条不同的路。我们现在走的是 AOT 这一条。


现在做到了什么程度#

关于编译和分析,我们前面已经聊过很多了。

这篇文章,主要聊编译向后端走这一步。

编译链路已经完整跑通#

从一份 .yak 源码开始,整条路是这样的:

  1. 先把源码编译成 Yak SSA IR
  2. 再把 SSA IR 转成 LLVM IR
  3. 从 LLVM IR 出汇编、目标文件
  4. 链接对应的运行时库
  5. 最后拿到一个真正的原生二进制,可以直接跑,并且结果校验是对的

这五步,每一步都已经是跑通的状态。

不是“只能生成 LLVM IR 就停了”,是所有步骤都打通了。

语法支持已经覆盖了不少#

目前这条线可以处理的 Yaklang 代码,不是玩具级。已经支持的包括:

  • 基本算术、比较、控制流(if / for / return)
  • 函数调用、递归
  • defer / panic / recover / try-catch-finally
  • 异步调用(go 语句)和自动等待
  • 闭包基础能力
  • slice 的创建、追加和索引读写

这里面有些是简单收口,有些是踩了不少坑才做通的——比如 try-catch-finally 和闭包,在编译路径下的实现逻辑和在解释执行里完全是两套东西。

调用协议已经统一#

目前所有调用——不管是普通函数、builtin 分发、runtime shadow method、还是异步 go 调用——都统一接到了一套调用协议上。这件事单独看不算亮点,但它意味着以后加新能力不用每种调用都重新拆一遍,工程上会越来越省力。


混淆:不只是能编译,还要能保护#

如果只是编译成二进制,这件事还不够完整。

所以我们从一开始就在这条线上同时推进了混淆能力。

目前这条线已经接入了一整套混淆管线,包括:

- addsub:算术指令替换

- callret:调用返回混淆

- xor:LLVM 层的异或混淆

- opaque predicates:不透明谓词

- MBA:混合布尔算术变换

- virtualize:虚拟机化混淆

而且这些混淆器不是零散的——它们已经按三阶段(SSA 前 / SSA 后 / LLVM 层)做了分层,不同混淆器放在不同阶段,用 profile 统一配置。

换句话说:今天你已经可以指定一份混淆策略,把它和编译一起跑下来,拿到一份被混淆过的原生二进制。


LLVM 原生混淆能力也在测试接入#

上面那些是我们自己实现的混淆器。

除此之外,我们还在编译链路里留了一个 LLVM interop 层。它不限于我们自己的混淆器,而是可以直接挂接 LLVM 生态里已有的混淆 pass——不管是标准的 opt pass,还是第三方的混淆插件,都可以通过插件或 pass 管线接进来。

目前这一层开始测试,正在实验性支持阶段:

  • 支持 LLVM 新 Pass Manager 和 Legacy Pass Manager 两种模式
  • 支持按 pack 批量加载、按单个 plugin 加载、按 pass 名指定
  • 支持 LLVM 版本检测和能力校验
  • 内置 pack(比如 instcombine + simplifycfg 的组合)可以直接用

所以现在的情况是:我们自己的混淆器已经在跑,LLVM 生态里已有的混淆能力也正在接入和测试中。


还在继续补的事情#

到这里这条线的大概轮廓应该清楚了:主链路跑通了,混淆方向打开了,剩下重点在做稳定性和边界补齐。

具体来说:

混淆稳定性#

混淆器本身能跑,但在不同组合、不同场景、不同规模的项目下的行为和性能,还需要继续验证和收口。这一层会持续做深。

语法支持稳定性#

前面列的语法点都已经支持了,但复杂组合场景(比如闭包 + 对象 + 副作用叠加在一起)还需要更多测试覆盖。语法支持会一块一块补,稳定性也会一步一步提上来。

还有没实现的能力#

map 完整支持、更完整的 slice 能力、更细粒度的 LLVM 原生类型系统——这些还在研发阶段,也是后续会继续推进的方向。


怎么理解现在的状态#

SSA2LLVM 不是概念验证,也不是已经全面成熟的产品功能。它处在中间:主链路是跑通的,大部分基本语法是支持的,混淆管线是接好的,LLVM 原生混淆接入也在测试中。现在的工作重心是混淆稳定性和语法支持的稳定性,一步一步往深做、做扎实。

它不是替代当前执行路径,而是一条被认真推进的工程线。

而且它已经推进到了一个阶段:你真的可以拿一份 Yaklang 源码去编译出一份带混淆的原生二进制。


备注#

本文内容基于仓库内 SSA2LLVM 状态文档与工程实现整理,重点参考:

  • common/yak/ssa2llvm/STATUS.md
  • common/yak/ssa2llvm/PROJECT.md
  • common/yak/ssa2llvm/readme.md
  • common/yak/ssa2llvm/docs/link-prep.md
  • common/yak/ssa2llvm/compiler/llvm_interop.go
  • common/yak/ssa2llvm/llvminterop/doc.go
  • common/yak/ssa2llvm/obfuscation/obfuscator.go