|
The Design of the Go Assembler |
|
|
|
Gophercon |
|
12 July 2016 |
|
|
|
Rob Pike |
|
Google |
|
https://go.dev/ |
|
|
|
* Presentation on youtube.com |
|
|
|
Video is [[https://www.youtube.com/watch?v=KINIAgRpkDA][here]]. |
|
|
|
|
|
* Motivation |
|
|
|
_Why_Learn_Assembler_Language?_ |
|
|
|
_The_most_important_single_thing_to_realize_about_assembler_language_is_that_it_enables_the_programmer_to_use_all_System/360_machine_functions_as_if_he_were_coding_in_System/360_machine_language._ |
|
|
|
— A Programmer's Introduction to IBM System/360 Assembler Language, 1970, page 4 |
|
|
|
* We still need assembly language |
|
|
|
Once it was all you needed, then high-level languages like FORTRAN and COBOL came along. |
|
|
|
But still needed today: |
|
|
|
- environment bootstrap (operating system and program startup, runtime) |
|
- low-level library code such as stack management and context switching |
|
- performance (`math/big`) |
|
- access to features not exposed in language such as crypto instructions |
|
|
|
Also, perhaps most important: It's how we talk about the machine. |
|
|
|
Knowing assembly, even a little, means understanding computers better. |
|
|
|
* What does it look like? |
|
|
|
Some examples... |
|
|
|
* IBM System/360 |
|
|
|
.code asm/360.s.txt |
|
|
|
* Apollo 11 Guidance Computer |
|
|
|
.code asm/apollo.s.txt |
|
|
|
* PDP-10 |
|
|
|
.code asm/pdp10.s.txt |
|
|
|
(From the MIT PDP-10 Info file) |
|
|
|
* PDP-11 |
|
|
|
.code asm/pdp11.s.txt |
|
|
|
(From Unix v6 `as/as13.s`) |
|
|
|
* Motorola 68000 |
|
|
|
.code asm/68000.s.txt |
|
|
|
(From Wikipedia) |
|
|
|
* CRAY-1 |
|
|
|
.code asm/cray1.s.txt |
|
|
|
(From Robert Griesemer's PhD thesis) |
|
|
|
|
|
* Common structure |
|
|
|
Columnar layout with function and variable declarations, labels, instructions. |
|
|
|
Instructions: |
|
|
|
subroutine header |
|
label: |
|
instruction operand... ; comment |
|
... |
|
|
|
Operands: |
|
|
|
register |
|
literal constant |
|
address |
|
register indirection (register as address) |
|
... |
|
|
|
There are exceptions such as Cray (`A5` `A5+A14`) but they aren't conceptually different. |
|
|
|
CPUs are all pretty much the same. |
|
|
|
* Use that commonality |
|
|
|
We can use the common structure of all assemblers (CPUs, really) to construct a common grammar for all architectures. |
|
|
|
This realization took some time. |
|
|
|
The seeds were planted long ago. |
|
|
|
* Plan 9 assembly |
|
|
|
Around 1986, Ken Thompson wrote a C compiler for the National 32000 (Sequent SMP). |
|
Compiler generated pseudo-code, linker did instruction assignment. |
|
|
|
The "assembler" was just a way to write that pseudo-code textually. |
|
|
|
MOVW $0, var |
|
|
|
might become (hypothetical example) |
|
|
|
XORW R1, R1 |
|
STORE R1, var |
|
|
|
Note assembler emits the `MOVW`; the linker generates `XORW` and `STORE`. |
|
We call this _instruction_selection_. |
|
|
|
Or consider `RET`, which becomes `RET` or `JMP` `LR` or `JMP` `(R31)` or ... |
|
|
|
The assembler is just a way to hand-write the output the compiler produces. |
|
(Compiler does not feed assembler, unlike in many other systems.) |
|
|
|
* The pieces |
|
|
|
.image asm/arch1.png |
|
|
|
* The Plan 9 assemblers |
|
|
|
Assembler for each architecture was a separate C program with a Yacc grammar, |
|
adapted and partially rewritten for every architecture. |
|
|
|
`8a`, `6a`, `va` etc. corresponding to `8c`, `6c` `vc`, etc. |
|
(One-letter codes: `8` for 386, `6` for AMD64, `v` for MIPS, etc.) |
|
|
|
All very similar up front but different in detail. |
|
|
|
The earliest Go implementations used this design, adding Go compilers `8g`, `6g` but using the Plan 9 assemblers unchanged. |
|
|
|
The separation of (compiler/assembler)⇒linker allowed the Go linker to do more, including helping boot the runtime. |
|
|
|
* Go 1.3: Rearrange the pieces |
|
|
|
Goal: Move to a pure Go implementation. |
|
Preparation started in Go 1.3 |
|
|
|
New library that (in part) does instruction selection: `"liblink"` (as of 1.5, `"obj"`). |
|
Call it from the compiler. |
|
|
|
Thus the first part of the old linker is now in the compiler. |
|
The compiler now emits (mostly) real instructions, not pseudo-instructions. |
|
|
|
Result: Slower compiler, but faster build. |
|
Instruction selection for library code done once, not every time you link a program. |
|
|
|
Assemblers also use `obj`. |
|
|
|
For both compiler and assembler, the _input_ is unchanged. |
|
In fact the whole _process_ is the same, just arranged differently. |
|
|
|
* The old pieces |
|
|
|
.image asm/arch1.png |
|
|
|
|
|
* The new pieces |
|
|
|
.image asm/arch2.png |
|
|
|
|
|
* Go 1.5: C must Go |
|
|
|
More prep in Go 1.4, then in Go 1.5, all tooling moved to Go. |
|
|
|
Compiler and linker machine-translated from C to Go. |
|
The old `liblink` became a new suite of libraries, `obj/...`: |
|
|
|
- `cmd/internal/obj` (portable part) |
|
- `cmd/internal/obj/x86` (architecture-specific part) |
|
- `cmd/internal/obj/arm` (architecture-specific part) |
|
- ... |
|
|
|
Previous presentations about this work: |
|
|
|
- Russ Cox at Gophercon 2014 (out of date): [[https://youtube.com/watch?v=QIE5nV5fDwA]] |
|
- Rob Pike at Gopherfest 2015: [[https://youtube.com/watch?v=cF1zJYkBW4A]] |
|
* Go 1.5: Compiler and linker as single programs |
|
|
|
The many compilers (`6g`, `8g` etc.) were replaced with a single tool: `compile`. |
|
`GOOS` and `GOARCH` (only!) specify the target operating system and architecture. |
|
|
|
GOOS=darwin GOARCH=arm go tool compile prog.go |
|
|
|
Same for the linker: `6l`, `8l`, etc. become `go` `tool` `link`. |
|
|
|
How can a single binary handle all these architectures? |
|
|
|
Only one input language, only one output generator (the `obj` library). |
|
The target is configured when the tool starts. |
|
|
|
* Go 1.5 Assembler |
|
|
|
Unlike the old compilers, which shared much code, the old assemblers were all different programs. |
|
(Although they were very similar inside, they shared almost no code.) |
|
|
|
Proposal: Write a single `go` `tool` `asm` from scratch in Go, replacing all the old assemblers. |
|
|
|
`GOOS` and `GOARCH` tell you what the target is. |
|
|
|
But assembly language isn't Go. Every machine has a different assembly language. |
|
|
|
Well, not really! Not quite universal across machines, but ... |
|
|
|
* An example program |
|
|
|
Look at the generated assembly for this simple program: |
|
|
|
.code asm/add.go |
|
|
|
For each architecture, with some noise edited out: |
|
|
|
* 32-bit x86 (386) |
|
|
|
.code asm/386.s.txt |
|
|
|
* 64-bit x86 (amd64) |
|
|
|
.code asm/amd64.s.txt |
|
|
|
* 32-bit arm |
|
|
|
.code asm/arm.s.txt |
|
|
|
* 64-bit arm (arm64) |
|
|
|
.code asm/arm64.s.txt |
|
|
|
* S390 (s390x) |
|
|
|
.code asm/s390x.s.txt |
|
|
|
* 64-bit MIPS (mips64) |
|
|
|
.code asm/mips64.s.txt |
|
|
|
* 64-bit Power (ppc64le) |
|
|
|
.code asm/ppc64le.s.txt |
|
|
|
* Common grammar |
|
|
|
They all look the same. (Partly by design, partly because they _are_ the same.) |
|
|
|
The only significant variation is the names of instructions and registers. |
|
Many details hidden, such as what `RET` is. (It's a pseudo-instruction.) |
|
|
|
(Offsets are determined by size of `int`, among other things.) |
|
|
|
The fortuitous syntax originated in Ken's National 32000 assembler. |
|
|
|
With common syntax and the `obj` library, can build a single assembler for all CPUs. |
|
|
|
* Aside: Downside |
|
|
|
Not the same assembly notation as the manufacturers'. |
|
Can be offputting to outsiders. |
|
|
|
On the other hand, this approach uses the same notation on all machines. |
|
New architectures can arrive without creating or learning new notation. |
|
|
|
A tradeoff worth making. |
|
|
|
* Design of the Go 1.5 assembler |
|
|
|
The apotheosis of assemblers. |
|
|
|
New program, entirely in Go. |
|
|
|
Common lexer and parser across all architectures. |
|
Each instruction parsed into an instruction description. |
|
That becomes a data structure passed to the new `obj` library. |
|
|
|
The core of the assembler has very little per-machine information. |
|
Instead, tables are constructed at run time, flavored by `$GOARCH`. |
|
|
|
An internal package, `cmd/asm/internal/arch`, creates these tables on the fly. |
|
Machine details are loaded from `obj`. |
|
|
|
* An example: initializing the 386 |
|
|
|
.code asm/arch386._go /^import/,$ |
|
|
|
Parser just does string matching to find the instruction. |
|
|
|
* An example: ADDW on 386 |
|
|
|
Given an assembly run with `GOOS=386`, the instruction |
|
|
|
ADDW AX, BX |
|
|
|
is parsed into in a data structure schematically like: |
|
|
|
&obj.Prog{ |
|
As: arch.Instructions["ADDW"], |
|
From: obj.Addr{Reg: arch.Register["AX"]}, |
|
To: obj.Addr{Reg: arch.Register["BX"]}, |
|
... |
|
} |
|
|
|
That gets passed to the `obj` library for encoding as a 386 instruction. |
|
|
|
This is a purely mechanical process devoid of semantics. |
|
|
|
* Validation |
|
|
|
Assembler does some validation: |
|
|
|
- lexical and syntactic correctness |
|
- operand syntax |
|
- (with some variation. e.g.: `[R2,R5,R8,g]` only legal on ARM) |
|
|
|
But all semantic checking is done by the `obj` library. |
|
|
|
If it can be turned into real instructions, it's legal! |
|
|
|
* Testing |
|
|
|
New assembler was tested against the old (C-written) ones. |
|
|
|
A/B testing at the bit level: Same input must give same output. |
|
Also reworked some parts of `obj` packages for better diagnostics and debugging. |
|
|
|
Did `386` first, then `amd64`, `arm`, and `ppc`. Each was easier than the last. |
|
|
|
No hardware manuals were opened during this process. |
|
|
|
* Result |
|
|
|
One Go program replaces many C/Yacc programs, so it's easier to maintain. |
|
As a Go program it can have proper tests. |
|
|
|
Dependent on `obj`, so correctness and completeness are relatively simple to guarantee. |
|
|
|
New assembler almost 100% compatible with previous ones. |
|
Incompatibilities were mostly inconsistencies. |
|
|
|
Portability is easy now. |
|
|
|
A new instruction set just needs connecting it up with the `obj` library, |
|
plus a minor amount of architecture-specific tuning and validation. |
|
|
|
Several architectures have been added since the assembler was created, |
|
most by the open source community. |
|
|
|
* Tables |
|
|
|
To a large extent, the assembler is now table-driven. |
|
Can we generate those tables? |
|
|
|
The disassemblers (used by `go` `tool` `pprof`) are created by machine processing of PDFs. |
|
The architecture definition is machine-readable, so use it! |
|
|
|
Plan to go the other way: |
|
|
|
Read in a PDF, write out `obj` library definitions and bind to assembler. |
|
Why write by hand when you can automate? |
|
|
|
Hope to have this working soon; basics are already in place. |
|
|
|
Result: a largely machine-generated assembler. |
|
|
|
* Conclusion |
|
|
|
Assembly language is essentially the same everywhere. |
|
|
|
Use that to build a *true* common assembly language. |
|
|
|
Customize it on the fly using dynamically loaded tables. |
|
|
|
And one day: create those tables automatically. |
|
|
|
|
|
A portable solution to a especially non-portable problem. |