Everyone Says Assembly Is Untyped—Everyone Is Wrong
Everyone Says Assembly Is Untyped—Everyone Is Wrong
每个人都说汇编是无类型的——他们都错了
TL;DR: I believe Odin’s inline assembly is currently the best out of any language. The most important aspects are of this article listed below. I am not aware of any other assembly (GCC/Clang/Rust/Go…) that would combine all of these aspects: TL;DR: 我认为 Odin 的内联汇编目前是所有编程语言中最好的。本文列出了其最重要的几个方面。据我所知,没有任何其他语言(GCC/Clang/Rust/Go 等)能同时具备以下所有特性:
- Inline assembly is organized into asm “templates”, similar to and callable as procedures.
- 内联汇编被组织成汇编“模板”,类似于过程(procedure)并可像过程一样被调用。
- asm templates integrate with rest of the code, through bindings specifying clobbers, pinned, tied, and scratch registers.
- 汇编模板通过绑定机制与代码的其他部分集成,明确指定了破坏(clobbers)、固定(pinned)、绑定(tied)和暂存(scratch)寄存器。
- Assembly syntax is unified across ISAs and consistent with Odin syntax.
- 汇编语法在不同指令集架构(ISA)之间是统一的,并与 Odin 语法保持一致。
- Assembly is fully type checked, just like rest of Odin code.
- 汇编代码像 Odin 的其他代码一样,经过了完整的类型检查。
- Understanding that assembly is actually typed.
- 理解汇编实际上是有类型的。
- Real semantic diagnostics via core:rexcode encoding tables.
- 通过
core:rexcode编码表实现真正的语义诊断。 - It was built in ~7 days.
- 它是在大约 7 天内构建完成的。
I have been asked why Odin even bothers having its own custom inline assembler at all. Isn’t inline assembly a solved problem? You take a string, you hand it to the assembler, and you let it sort out the rest. Everyone from GCC to Clang to Rust (Rust’s inline assembly is a little more sophisticated because of the macro system, but not that much more) does more or less this. The wheel has been invented, right? 有人问我,为什么 Odin 还要费心去开发自己的自定义内联汇编器?内联汇编难道不是一个已经解决的问题吗?你只需要拿一个字符串,把它交给汇编器,剩下的让它去处理就行了。从 GCC 到 Clang 再到 Rust(Rust 的内联汇编因为宏系统的存在稍微复杂一点,但也仅此而已),大家或多或少都是这么做的。轮子不是已经被发明出来了吗?
This is precisely the design I did not want, and precisely the design that most languages have settled for. My goal from the beginning was an inline assembler that actually integrates with the rest of the language rather than feeling bolted on the side. And I honestly believe that what Odin has ended up with is the best inline assembly system in any language right now. I don’t say that lightly, and by the end of this article I hope you’ll at least understand why I believe that to be true. 这恰恰是我不想要的设计,也是大多数语言所妥协的设计。从一开始,我的目标就是构建一个能真正与语言其余部分集成,而不是感觉像是在侧面“强行拼凑”上去的内联汇编器。我真心认为,Odin 目前所拥有的,是所有语言中最好的内联汇编系统。我不是随口说说,希望读完本文后,你至少能理解我为什么这么认为。
The String-Based Nonsense
基于字符串的荒谬做法
Let’s start with the thing I was reacting against. Here is what a trivial “add one” looks like in GCC-style extended asm using x86 AT&T/GAS syntax: 让我们从我所反对的东西开始。以下是一个在 GCC 风格扩展汇编中使用 x86 AT&T/GAS 语法的简单“加一”操作:
int dst;
asm ("movl %1, %0\n\t"
"addl $1, %0"
: "=r" (dst) // outputs
: "r" (src) // inputs
: /* clobbers */);
Look at this and ask yourself: what does the compiler (as opposed to the assembler) understand here? The answer is “almost nothing”. The body is a string. “=r” and “r” are explicit constraint strings, another little stringly-typed DSL glued to the side of the real DSL. The %0 and %1 are positional references into a list you have to count by hand. And if you get any of it wrong, the error you get back is not from the compiler that knows your types and semantics; it is from the assembler, much later on, pointing at generated text that was not written by you.
看看这段代码,问问自己:编译器(相对于汇编器而言)在这里理解了什么?答案是“几乎什么都没理解”。主体是一个字符串。"=r" 和 "r" 是显式的约束字符串,这是粘在真正 DSL 旁边的一套基于字符串类型的微型 DSL。%0 和 %1 是指向列表的位置引用,你必须手动计数。如果你弄错了任何地方,你得到的错误信息不是来自了解你的类型和语义的编译器,而是来自汇编器,而且是在很久之后,指向一段并非由你编写的生成文本。
This is the sort of thing that happens when a feature is designed as an escape-hatch first rather than as a part of the language. Nobody seems to have sat down and asked “what would inline assembly look like if it respected the type system, the calling conventions, the constant system, and other things (like multiple-return-value semantics) of the host language?”. Rather, they asked “how do I bodge some assembly into this function with the least amount of compiler work?”, and a string was the answer. These kinds of inline assemblers ignore all of the aspects of the host language, and just bodge it in. I didn’t; I designed one from scratch. 当一个功能被设计为“逃生舱”(escape-hatch)而非语言的一部分时,就会发生这种情况。似乎没有人坐下来思考过:“如果内联汇编尊重宿主语言的类型系统、调用约定、常量系统以及其他特性(如多返回值语义),它会是什么样子?”相反,他们问的是:“我怎样才能用最少的编译器工作量,把一些汇编代码塞进这个函数里?”,而字符串就是答案。这类内联汇编器忽略了宿主语言的所有方面,只是强行拼凑。我没有这样做;我是从零开始设计的。
A Brief History of Bolting It On
“强行拼凑”的简史
Strings are not the only way this has been done, and it is worth looking at what previous languages/compilers have done, because some of these approaches are a heck of a lot better than what GCC/Clang did, and unfortunately this development has stopped in compiler space. 字符串并不是实现这一功能的唯一方式,值得回顾一下以前的语言/编译器所做的工作,因为其中一些方法比 GCC/Clang 的做法要好得多,遗憾的是,这种发展在编译器领域已经停滞了。
MSVC
Microsoft’s C compilers had a genuinely different approach. MSVC’s __asm was statement-based, not string-based. You wrote a block of real instructions, and (this is the good part) you referenced your C variables and labels directly by name, and the compiler resolved them for you:
微软的 C 编译器采取了一种真正不同的方法。MSVC 的 __asm 是基于语句的,而不是基于字符串的。你编写的是一个真实的指令块,并且(这是好的一面)你可以直接通过名称引用 C 变量和标签,编译器会为你解析它们:
int add_one(int x) {
__asm {
mov eax, x // 'x' 是 C 参数,由编译器解析
inc eax
} // eax 中的值即为返回值,按惯例
}
No constraint strings. No %0. No counting operands. Compared to the GCC contraption this is honestly pleasant to read, and for a long time it was how an enormous amount of Windows systems code got written. So why did it disappear? Firstly, it was x86-only. When Microsoft moved to x64 (and later ARM64) they did not port it. The official guidance became “use compiler intrinsics, or write a separate .asm file and run it through MASM”. One of the stated constraints for the x64 compiler was to have no inline assembler at all. A whole approach was thrown away at the ISA boundary rather than generalized across it. Secondly, even where it existed, the compiler did not really understand the block. It resolved your symbol names, but it carried no explicit clobber information; the optimizer largely treated the region as an opaque fence to be conservative around. It knew what x was. It did not give any feedback to the user as to what the instructions did.
没有约束字符串,没有 %0,也不用数操作数。与 GCC 的那套玩意儿相比,这读起来确实令人愉悦,很长一段时间里,大量的 Windows 系统代码都是这样编写的。那么它为什么消失了呢?首先,它仅限于 x86。当微软转向 x64(以及后来的 ARM64)时,他们没有移植它。官方指导变成了“使用编译器内置函数,或者编写单独的 .asm 文件并通过 MASM 运行”。x64 编译器的既定限制之一就是完全不提供内联汇编器。一种完整的方法在 ISA 边界处被抛弃,而不是在不同 ISA 之间进行泛化。其次,即使在它存在的地方,编译器也没有真正理解这个代码块。它解析了你的符号名称,但没有携带任何显式的破坏信息;优化器在很大程度上将该区域视为一个不透明的屏障,从而采取保守策略。它知道 x 是什么,但它没有向用户提供关于这些指令具体做了什么的任何反馈。
Turbo Pascal If you go back further, you’ll find Turbo Pascal, which I have an obvious fondness for, as I do for Pascals in general. For its inline assembly, it had two mechanisms, and together they bracket the entire design space quite nicely. 如果追溯得更远,你会发现 Turbo Pascal,我对它以及一般的 Pascal 语言都有明显的偏爱。对于内联汇编,它有两种机制,它们共同很好地涵盖了整个设计空间。
The first mechanism was the inline directive, and it is the purest possible statement of “the compiler understands nothing”. You gave it machine code as a sequence of numeric constants—actual opcodes, as bytes:
第一种机制是 inline 指令,它是“编译器什么都不懂”这一理念最纯粹的体现。你以数字常量序列的形式提供机器码——即实际的操作码字节:
procedure Cli; inline($FA); { $FA = CLI 指令 }
procedure Nops; inline($90/$90); { 两个 NOP 字节 }
That is not an assembler. This is you being the assembler, by hand, with the compiler faithfully copying your bytes into the stream. It is the ur-escape-hatch (Odin keeps this exact capability as the #byte directive, but as one directive among many inside a checked template, not as the entire interface).
那不是汇编器。那是你自己在充当汇编器,手动操作,而编译器只是忠实地将你的字节复制到流中。这是最原始的“逃生舱”(Odin 保留了这一功能作为 #byte 指令,但它只是受检查模板中的众多指令之一,而不是整个接口)。
The second mechanism, which was added in Turbo Pascal 6.0, was the built-in assembler: the asm ... end block and the assembler procedure directive. This approach is much better as it has real mnemonics, and, like MSVC after it, you could name your Pascal variables and parameters directly:
第二种机制是在 Turbo Pascal 6.0 中添加的内置汇编器:asm ... end 块和 assembler 过程指令。这种方法要好得多,因为它有真正的助记符,而且像后来的 MSVC 一样,你可以直接命名 Pascal 变量和参数: