diff options
| author | Cliff Click <cliffc@acm.org> | 2026-09-20 14:49:32 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-09-20 14:49:32 -0700 |
| commit | 0487ef57a2b7f80fd6f9ed3f6aeea0b0ec88e89f (patch) | |
| tree | ce84334450dbe54ca9ea3c7f39419730b4f86678 /chapter21/README.md | |
| parent | 4f3d4859e9096ab8f15c3e9eb02d154c268b108d (diff) | |
| download | Simple-0487ef57a2b7f80fd6f9ed3f6aeea0b0ec88e89f.tar.gz Simple-0487ef57a2b7f80fd6f9ed3f6aeea0b0ec88e89f.zip | |
Add back copy coaleascing in ch21
Minor arm encoding fix.
Diffstat (limited to 'chapter21/README.md')
| -rw-r--r-- | chapter21/README.md | 954 |
1 files changed, 146 insertions, 808 deletions
diff --git a/chapter21/README.md b/chapter21/README.md index c5b41bc7..cd3a46ae 100644 --- a/chapter21/README.md +++ b/chapter21/README.md @@ -1,810 +1,148 @@ # Chapter 21: Instruction Encoding and ELF -## Meta-Issues / Code-Dev Issues of Instruction Selection, Register Allocation and Encodings - -The output of Encodings - conversion of machine Nodes to instruction bits - is -difficult to test in bulk without an execution strategy. We won't have an -execution strategy until all three of Instruction Selection, Register Allocation -and Encodings are done. Hence in and around the completion of Encoding we -expect to find lots of bugs in Instruction Selection and Register Allocation. -We certainly hand-inspect the output and fix obvious problems, but still lots -bugs linger until we can actually run the code. - -This means that other chapters have updates and bug-fixes to the work being -described here. - -Encodings are an entirely bit-picky nit-picky affair. There's no real rocket -science here, but instead there's an boundless opportunity for off-by-1 errors. -If you find a bug here, do not be surprised! Its almost surely a "shallow" -bug... and there's bound to be a few until Simple gets more programmer hours -under its belt. - - - -You can also read [this chapter](https://github.com/SeaOfNodes/Simple/tree/linear-chapter21) in a linear Git revision history on the [linear](https://github.com/SeaOfNodes/Simple/tree/linear) branch and [compare](https://github.com/SeaOfNodes/Simple/compare/linear-chapter20...linear-chapter21) it to the previous chapter. - -# Table of Contents - -1. [Instruction Encodings](#instruction-encodings) -2. [Infrastructure](#infrastructure) -3. [Endianness](#endianness) -4. [RISCV](#riscv) - - [Instruction formats](#instruction-formats) - - [Float](#float) - - [Indirect](#indirectmemop-2) - - [Branch](#branch) - - [Function Constant](#function-constant) - - [LUI](#lui) - - [Large constants](#large-constants) -5. [ARM](#arm) - - [Reg-form](#reg-form) - - [Shift immediates](#shift-immediates) - - [asri](#asrimmediate) - - [LSL(immediate)](#lslimmediate) - - [LSR(immediate)](#lsrimmediate) - - - [Logical Immediates](#logical-immediates) - - [Float](#float) - - [Function constant](#function-constant) - - [Immediate](#immediate) - - [Conditional flags](#conditional-flags) - - [Indirect](#indirectmemop-1) - - [Branching](#branching-) - - [Rip relative](#rip-relative) - - [Compare](#compare) -6. [AMD64](#amd64) - - [REX PREFIX](#rex-prefix) - - [Large constants](#large-constants) - - [MODR/M](#modrm) - - [SIB](#sib) - - [Displacement](#displacement) - - [Immediate](#immediate) - - [Float](#float) - - [Indirect](#indirectmemop) - - [Conditional flags](#conditional-flags) - - -This chapter will add instruction encodings. Instruction encoding refers to -the representation of machine instructions in a specific binary format defined -by the processor's instruction set architecture (ISA). - - -After completing instruction selection, the next step is to encode the -instructions before writing them into an ELF object file. - -To avoid ambiguity, we define the sources of the information we use to encode the instructions: - -Currently, we support these architectures: - -### RISC-V: - -For *riscv* we currently target [RVA23U64](https://msyksphinz-self.github.io/riscv-isadoc/html/). - -### ARMV8/AArch64 : -For *arm*(aarch64) we use this collection for the [encoding rules](https://docsmirror.github.io/A64/20a23-06/index.html). -*Note*: We only support 64 bits ARM encodings. - -### AMD64: -For *x86-64(amd64):*, we use the latest Intel manual for the [encoding rules](https://www.felixcloutier.com/x86/). - --------------------------------- - -## Instruction Encodings - - -## Infrastructure - -The instruction selection phase creates the machine friendly nodes from ideal -nodes. These machine friendly nodes define an *encoding* function that is -called from the Encoding driver found in CodeGen. - -```java -for( CFGNode bb : _code._cfg ) - for( Node n : bb._outputs ) - if( n instanceof MachNode mach ) { - int off = _bits.size(); - mach.encoding(this); - _opLen[n._nid] = (byte)(_bits.size()-off); - } -``` - -Based on the selected target, we either need to look out for little endian or -big endian encodings. These are being handled by `add4` which adds to the bit -stream in the correct order. As of now the bitstream is a simple -ByteArrayOutputStream structure. - -```java -@Override public void encoding( Encoding enc ) { - enc.add4(0); // adds 4 bytes to the bitstream -} -``` - -On *RISC-V*, where the documentation provides big-endian encodings, this -function also converts them to little-endian. For other architectures, no -conversion is necessary. - -## Endianness - -ARM, RISC-V, and x86 are all little-endian architectures in practice, meaning -the least significant byte is stored first. We ensure that instruction -encodings are generated in little-endian format accordingly. - -```java -// Little endian write of a 32b opcode -public void add4( int op ) { - _bits.write(op ); - _bits.write(op>> 8); - _bits.write(op>>16); - _bits.write(op>>24); -} -``` -For example, consider a 32-bit encoding for fixed-width instructions: -*10110101100111101011001001101101*. - -We append the least significant byte first, then the next byte, and so on. -``` -_bits.write(op ); -``` - -`_bits.write()` only appends the least significant byte. By shifting the value -to the right, we can bring the next byte into the least significant position, -allowing us to write each byte in sequence. - - --------------------------------- - -## RISCV - -RISCV instructions are all 32 bits wide, and are always little-endian. RISCV -is a RISC architecture, it prioritizes "register-to-register" forms, and has -very regular encodings. We present RISCV first because it is the simplest -encoding. - - -#### Instruction formats - -##### R-TYPE -This encoding format is used for reg-to-reg operations. -Such as `AddRISC`, `MulRISC` etc. -```java -public static int r_type(int opcode, int rd, int func3, int rs1, int rs2, int func7) { - return (func7 << 25) | (rs2 << 20) | (rs1 << 15) | (func3 << 12) | (rd << 7) | opcode; -} -``` - -##### I-TYPE -This layout is used for immediate formats. Such as `AddIRISC` - -```java - public static int i_type(int opcode, int rd, int func3, int rs1, int imm12) { - assert opcode >= 0 && rd >=0 && func3 >=0 && rs1 >=0 && imm12 >= 0; // Zero-extend by caller - return (imm12 << 20) | (rs1 << 15) | (func3 << 12) | (rd << 7) | opcode; - } -``` - -##### S-TYPE - -This encoding format is used for `StoreRISC`. - -```java -public static int s_type(int opcode, int func3, int rs1, int rs2, int imm12) { - assert imm12 >= 0; // Masked to high zero bits by caller - int imm_lo = imm12 & 0x1F; - int imm_hi = imm12 >> 5; - return (imm_hi << 25) | (rs2 << 20) | (rs1 << 15) | (func3 << 12) | (imm_lo << 7) | opcode; -} -``` - -##### B-TYPE - -This encoding layout is used by `BranchRISC`. -```java - // immf = first imm - // immd = second imm - // BRANCH -public static int b_type(int opcode, int immf, int func3, int rs1, int rs2, int immd) { - return (immd << 25 ) | (rs2 << 20) | (rs1 << 15) | (func3 << 12) | (immf << 7) | opcode; - } -``` - -##### U-TYPE -This layout is used for `AUIPC` and `LUI`(upper immediate is 20-bits). - -```java - public static int u_type(int opcode, int rd, int imm20) { - return (imm20 << 12) | (rd << 7) | opcode; - } -``` - -##### J-TYPE -Used for unconditional jumps. (`UJmpRISC`) -```java - public static int j_type(int opcode, int rd, int imm20) { - return imm20 << 12 | rd << 7 | opcode; - } -``` - -#### FLOAT -For loading float constants, we use indirect memory load(PC-relative). -``` -AUIPC dst,#hi20_constant_pool -Load dst,[dst+#low12_constant_pool] -``` - -#### INDIRECT(MemOp) - -[Load](https://msyksphinz-self.github.io/riscv-isadoc/html/rvi.html#lw): `lw rd,offset(rs1)` - -[Store](https://msyksphinz-self.github.io/riscv-isadoc/html/rvi.html#sw): `sw rs2,offset(rs1)` - -#### BRANCH -RISC-V is unusual because branch instructions include the comparison and branch target in one instruction. - -#### Immediates -We only encode the imm form for the *ALU* operations if they fit into 12 bits (signed). -Otherwise we do a Load Upper Immediate (high 20 bits) or both. - -``` -addiw a5,a5,123 -``` -vs -(do it with extra load): -``` -addw a5,a5,a4 -``` - -### FLOAT Constants -- Stored in the **constant pool**. -- Accessed via **PC-relative loads**. - -### INT Constants -- **12-bit constants** - - Use `ADDI` -- **20-bit constants** - - Use `LUI` -- **32-bit constants** - - Use `LUI` + `ADDI` combo - - - -#### Function Constants -In our RISC-V code, when we need to load a function-local constant, we store -the current PC into a register and use that as a base to access the -constant. However, we usually try to avoid loads, since simple arithmetic like -addition is faster and more efficient than accessing memory. - -```java -// auipc t0,0 -// addi t1,t0 + #0 -``` - - --------------------------------- - -## ARM -Arm instructions are all 32 bits wide, and are always little-endian. -Arm is a RISC architecture, it prioritizes "register-to-register" forms. - -#### Reg-form -We encode the regs forms as [shifted registers](https://docsmirror.github.io/A64/2023-06/orr_log_shift.html). -``` -orr reg1, reg2 -``` -``` -x0 = x0 | (x1 << 0) -``` -The same thing applies for all the other reg forms. -Because of this similarity, we can lift the encoding of the reg form to a common function. - -```java -short self = enc.reg(n); -short reg1 = enc.reg(n.in(1)); -short reg2 = enc.reg(n.in(2)); -int body = r_reg(opcode, 0, reg2, 0, reg1, self); -enc.add4(body); -``` -*imm6* and -*shift* -are irelevant for the reg form, so we just set them to 0. - -### Shift immediates -This refers to: *asri, lsli, lsri* -Encode to shift immediates if the shift amount is between [0, 63] inclusive. - -#### ASR(immediate) -For arithmetic Shift Right immediates, we set *immr* to the immediate we wish to encode, and imms to `63` = `111111`. -This is following the rules of [asri encoding](https://docsmirror.github.io/A64/2023-06/asr_sbfm.html). - -The `sf` bit is included in the opcode, `N` is implicitly added to the bitstream after the execution of the -`imm_shift` function. -```java -enc.add4(arm.imm_shift(0b100100110,_imm, 0b111111, rn, rd)); -``` - -#### LSL(immediate) - -```java -// UBFM <Xd>, <Xn>, #(-<shift> MOD 64), #(63-<shift>) -// immr must be (-<shift> MOD 64) = 64 - shift -enc.add4(arm.imm_shift(0b110100110, 64 - _imm, (64 - _imm) - 1, rn, rd)); -``` - -We know that `_imm` is never a negative number and is between 0 and 63, so we can safely use `64 - _imm` as the `immr` value. -We take away this form of the immediate from 64 to invert the bits. - -For *imms* - the following condition must hold *imms + 1 = immr*; -After subbing in the values we get: - -> imms + 1 = 64 - _imm; - -> imms = (64 - _imm) - 1; - -Hence, the `imms` value is `(64 - _imm) - 1`. - -#### LSR(immediate) -Same as *ASR*. - - -#### Logical Immediates -Logical immediates refer to *andi, xori, orri*. - -```java -// Can we encode this in ARM's 12-bit LOGICAL immediate form? -// Some combination of shifted bit-masks. -private static int imm12Logical(TypeInteger ti) { - if( !ti.isConstant() ) return -1; - if( !ti.isConstant() ) return -1; - long val = ti.value(); - if (val == 0 || val == -1) return -1; // Special cases are not allowed - int immr = 0; - // Rotate until we have 0[...]1 - while (val < 0 || (val & 1)==0) { - val = (val >>> 63) | (val << 1); - immr++; - } - int size = 32; - long pattern = val; - // Is upper half of pattern the same as the lower? - while ((pattern & ((1L<<size)-1)) == (pattern >> size)) { - // Then only take one half - pattern >>= size; - size >>= 1; - } - size <<= 1; - int imms = Long.bitCount(pattern); - // Pattern should now be zeros followed by ones 0000011111 - if (pattern != (1L<<imms)-1) return -1; - imms--; - if (size == 64) return 0x1000 | immr << 6 | imms; - return (32-size)<<1 | immr << 6 | imms; -} -``` - -imm12Logical returns the 12-bit immediate encoding for logical operations if it is possible to encode it, otherwise -1. -```java -int imm12; -return and.in(2) instanceof ConstantNode off && -off._con instanceof TypeInteger ti && (imm12 = imm12Logical(ti)) != -1 -? new AndIARM(and, imm12) -``` -```java -arm.imm_inst(enc,this,0b100100100,_imm); -``` - -#### Float -Similarly to x86, we have to subtract the offset to get the GPR register pair that corresponds to the *D* register. -``` -short dst = (short)(enc.reg(this ) - arm.D_OFFSET); -``` -For loading float constants we use the `ldr(literal)` instruction. -The offset is added to the PC register to get the address of the literal. - -```java -// Any number that can be expressed as +/-n * 2-r,where n and r are integers, 16 <= n <= 31, 0 <= r <= 7. -int body = arm.load_pc(0b01011100, 0, dst); -``` - -#### Large constants - -##### INT: We can use different combinations of movs(movz, movk, movn) - this allows us to avoid PC relative loading. - -##### FLOAT: We use the *LDR (immediate, SIMD&FP)* instruction relative to the PC - - -#### Function constant -To load a function constant we store the current PC into x0 and use that to access the constant. -Generally, we try to avoid load as adding is faster. -``` -adrp x0, 0 -add x0, x0, 0 -``` - -#### Conditional flags -In arm we set the conditional flags after a comparison - -this works similarly to x86. - -``` -int square(int a) { -// subs -// cset w8, eq // eq = none -bool da = a == 1; - // bne - if(da == 1) { - return 1; - } - return 2; -} -``` -In arm comparisons are done with `subss` which does a subtraction between the two -operands and also sets the flags. -`CSET` is used to look up the flags and save it out to a register. - -Later on `b.ne` relies on this flag again when it attempts to conditionally execute a branch. - -#### INDIRECT(MemOp) -Arm supports the following indirect forms: - -[Load](https://docsmirror.github.io/A64/2023-06/ldr_reg_gen.html): `ldr dst, [base + index]` where both, base and index are registers. - -[Load](https://docsmirror.github.io/A64/2023-06/ldr_imm_gen.html): `ldr dst, [base + #offset]` where base is a register and the offset is an immediate. - -[Store](https://docsmirror.github.io/A64/2023-06/str_reg_gen.html): `str dst, [base + index]` - -[Store](https://docsmirror.github.io/A64/2023-06/strb_imm.html): `str dst, [base + #offset]` - -If the offset is not a con -#### Branching -`B.cond` - branch [conditionally](https://developer.arm.com/documentation/dui0802/b/A32-and-T32-Instructions/Condition-codes?lang=en) to a label at a PC-relative offset. - -> cond = is one of the standard conditions. - - --------------------------------- - -## AMD64 X86-64 - -As opposed to riscv arhitectures, where the instruction width is fixed, -*x86-64* has variable width instructions. This is common with *CISC (Complex -Instruction Set Computer)* architectures, where the instruction width can vary -from 1 to 15 bytes. Since AMD64 supports many indirect addressing modes, the -goal with *CISC* in general is to complete a task in as few lines of assembly -as possible. - -### REX PREFIX - -Since we are targeting the 64-bit version of x86, we need to handle this -prefix. In 32-bit mode, however, it is typically unnecessary. - -Generally speaking the *REX* prefix must be encoded when: - - using one of the extended registers (R8 to R15, XMM8 to XMM15, YMM8 to YMM15, CR8 to CR15 and DR8 to DR15); - - using 64-bit operand size and the instruction does not default to 64-bit operand size(most ALU ops) - - using a 8-bit operand with RSI, RDI, RBP - -A *REX* prefix must not be encoded when: - - using one of the high byte registers AH, CH, BH or DH (not done currently in Simple) - -*Note:* When encoding SSE instruction, the *REX* prefix (0x40) must come after -the `SSE` prefix. In all other cases, it is ignored. - -In Simple, the REX prefix is just appended to the beginning of the bit stream -(except SEE float instructions). The layout is the following: - -| **Field** | **Length** | **Description** | -|:---------:|:----------:|---------------------------------------------------------------------------------| -| `0100` | 4 bits | Fixed bit pattern | -| `W` | 1 bit | When 1, a 64-bit operand size is used. Otherwise, 32-bit operand size is used. | -| `R` | 1 bit | Extension to the **MODRM.reg** field. | -| `X` | 1 bit | Extension to the **SIB.index** field. | -| `B` | 1 bit | Extension to the **MODRM.rm** or **SIB.base** field. | - - -```java - public static int REX_W = 0x48; -``` - -```java -public static int rex(int reg, int ptr, int idx, boolean wide) { - // assuming 64 bit by default so: 0100 1000 - int rex = wide ? REX_W : REX; - if( 8 <= reg && reg <= 15 ) rex |= 0b00000100; // REX.R - if( 8 <= ptr && ptr <= 15 ) rex |= 0b00000001; // REX.B - if( 8 <= idx && idx <= 15 ) rex |= 0b00000010; // REX.X - return rex; -} - - ... - enc.add1(x86_64_v2.rex(dst, src, 0)); -``` - -Setting THE `W` bit gives us: -`0b01001000 = 0x48;` - -#### Opcode -In our case, the opcode is a 1 byte field that specifies the operation to be performed. -``` -enc.add1(opcode()); // opcode -``` - -#### MODR/M - -| **Field** | **Length** | **Description** | -|:-------------:|:----------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **MODRM.mod** | 2 bits | In general, when this field is `b11`, then register-direct addressing mode is used; otherwise register-indirect addressing mode is used. -| **MODRM.reg** | 3 bits | This field can have one of two values:<br><br>• A 3-bit opcode extension, which is used by some instructions but has no further meaning other than distinguishing the instruction from other instructions.<br><br>• A 3-bit register reference, which can be used as the source or the destination of an instruction (depending on the instruction). The referenced register depends on the operand-size of the instruction and the instruction itself. The `REX.R`, `VEX.~R` or `XOP.~R` field can extend this field with 1 most-significant bit to 4 bits total. | -| **MODRM.rm** | 3 bits | Specifies a direct or indirect register operand, optionally with a displacement. The `REX.B`, `VEX.~B` or `XOP.~B` field can extend this field with 1 most-significant bit to 4 bits total. | - - -We deal with the MODR/M byte in a similar way: - -The first thing we need for the modrm byte is the 2 bits mod. - -##### mod - -The layout for mod is the following: -``` -public enum MOD { - INDIRECT, // [mem] - INDIRECT_disp8, // [mem + 0x12] - INDIRECT_disp32,// [mem + 0x12345678] - DIRECT, // mem -}; -``` - -##### reg -The reg field is a 3 bit field capable of encoding 15 different registers. - -##### r/m - -The rm field specifies a direct or indirect register operand and is capable of encoding 15 different registers. - - -```java -public static int modrm(MOD mod, int reg, int m_r) { - // combine all the bits - return (mod.ordinal() << 6) | ((reg & 0x07) << 3) | m_r & 0x07; -} -``` - -The `modrm` function is then used to build up the `ModR/M` byte. -Usually this follows after the opcode. - -``` -enc.add1(x86_64_v2.modrm(x86_64_v2.MOD.DIRECT, dst, src)); -``` - -Encoding a reg form might look like this: -``` -short dst = enc.reg(this ); // src1 -short src = enc.reg(in(2)); // src2 -enc.add1(x86_64_v2.rex(dst, src, 0)); -enc.add1(opcode()); // opcode -enc.add1(x86_64_v2.modrm(x86_64_v2.MOD.DIRECT, dst, src)); -``` - -#### SIB -The SIB byte has the following fields: - -| **Field** | **Length** | **Description** | -|:-------------:|:----------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **SIB.scale** | 2 bits | This field indicates the scaling factor of `SIB.index`, where **s** (as used in the tables) equals 2^SIB.scale^. | -| **SIB.index** | 3 bits | The index register to use. See Registers for the values to use for each of the registers. The `REX.X`, `VEX.~X` or `XOP.~X` field can extend this field with 1 most-significant bit to 4 bits total. | -| **SIB.base** | 3 bits | The base register to use. See Registers for the values to use for each of the registers. The `REX.B`, `VEX.~B` or `XOP.~B` field can extend this field with 1 most-significant bit to 4 bits total. | - -It has the same layout as the MODR/M byte. -```java -public static int sib(int scale, int index, int base) { - return (scale << 6) | ((index & 0x07) << 3) | base & 0x07; -} -``` - -```java -enc.add1(x86_64_v2.sib(_scale, idx, x86_64_v2.RBP)); -``` - -#### Displacement -Displacement can happen in 2 forms: -```java -public enum MOD { -INDIRECT, // [mem] -INDIRECT_disp8, // [mem + 0x12] -INDIRECT_disp32,// [mem + 0x12345678] -DIRECT, // mem -}; -``` - -```java -if( mod == MOD.INDIRECT_disp8 ) { - enc.add1(offset); -} else if( mod == MOD.INDIRECT_disp32 ) { - enc.add4(offset); -} -``` -The displacement goes before the immediate, so for example: -``` -sub [eax + 2], 4 -``` -``` -bytes.write(0x83); -bytes.write(modrm(INDIRECT8, 5, 0 /* eax */)); -bytes.write(2); // displcement goes first -bytes.write(4); -``` - -#### Immediate -The immediate byte/bytes go after the `ModR/M` byte, we are capable of encoding 4 bytes of immediates depending on the need. -```java -// immediate(4 bytes) 32 bits or (1 byte)8 bits -if( x86_64_v2.imm8(_imm) ) enc.add1(_imm); -else enc.add4(_imm); -``` - -#### INDIRECT(MemOp) -X86-64 support multiple indirect addressing modes: - - -#### Float -For float operations we use `SEE SIMD` instructions that operate on scalar single precision floating0-point values located in the -XMM registers or memory. -*Note*: When using XMM registers for ALU operations, the encoding is really similar with the exception that we need -to subtract the `XMM_OFFSET` from the register number. -We count the registers continuously, so we need to subtract the offset to get the GPR register pair that corresponds to the XMM register. - -```java -short dst = (short)(enc.reg(this ) - x86_64_v2.XMM_OFFSET); -``` - -```java -public static int XMM_OFFSET = 16; -``` - -| **X.Reg** | **8-bit GP** | **16-bit GP** | **32-bit GP** | **64-bit GP** | **80-bit x87** | **64-bit MMX** | **128-bit XMM** | **256-bit YMM** | **16-bit Segment** | **32-bit Control** | **32-bit Debug** | -|:--------:|:------------:|:-------------:|:-------------:|:-------------:|:-------------:|:-------------:|:--------------:|:--------------:|:-----------------:|:-----------------:|:---------------:| -| `0.000 (0)` | AL | AX | EAX | RAX | ST0 | MMX0 | XMM0 | YMM0 | ES | CR0 | DR0 | - -We see that `XMM0` corresponds to `RAX` and so on. - -#### Large constants - -##### INT: We just use `MOV r64, imm64` instruction form. - -##### FLOAT: We use relocation and use constant from memory. - -#### Function constant -To load a function constant we use lea which will load the address into the specified reg -relative to the instruction pointer. -`lea rax, [rip+disp32]` - -#### Conditional flags: -The FLAGS register is the status register that contains the current state of an x86 CPU. - -Let's consider a simple example that presents all the instructions that we need to encode for -conditional code. - -``` -int square(int a) { - // cmp DWORD PTR [rbp-0x8],0x1 - // `sete al` - bool da = a == 1; - - // cmp eax,0x1 - if(da == 1) { - return 1; - } - return 2; -} -``` - -First we need a compare instruction comparing `a` with 1. -``` -cmp DWORD PTR [rbp-0x8],0x1 -``` - -This compare instruction then sets the conditional flags: -> The CF, OF, SF, ZF, AF, and PF flags are set according to the result. - -We then use the `sete` instruction to save out the result of the comparison to a register: - -`sete al` - -For the if condition we do an extra comparison based on the result of the first bool. -``` - cmp eax,0x1 -``` - -And then a conditional branch that is relying on the flags set by the comparison(`a == 1`). -``` -jne 1159 <square(int)+0x29> -``` - - --------------------------------- - - -## Relocation - -Relocation allows code to be *relocated* to a new code offset. Code often -refers to other code and data; `call` instructions target subroutines, -`branches` target other instructions, large constants are often loaded from a -*constant pool*. - -### Local Relocation - -Local relocation patches up local, or self-referential, code; the most common -form is branch targets. When writing instructions into the `BAOS` (Byte Array -Output Stream) forwards branches towards future instructions have an unknown -offset - the offset depends on the size of the encodings in between the branch -and target. For `riscv` and `arm` targets this sounds easy (and instructions -are 4 bytes, so just count instructions)... except that some encodings use -multiple instructions so are not actually 4 bytes long. For X86's wild -psuedo-random instruction lengths the act of figuring out the opcode length is -just as hard as writing the opcode in the first place. - -X86 adds another wrinkle: short and long form branches. If a branch target -lies within a signed byte range (+/-127) X86 will use a 2 byte branch encoding, -otherwise a 6 byte encoding. Simple includes a pass to compact these branches -and use 2 bytes wherever possible - but this pass has to slide code around -which changes all the other branch offsets. The "compact" pass actually starts -optimistically assuming all branches can use the 2 byte form, and then expands -branches as needed (so really *expands* the code). - -Once the code is finally settled into "shape", a local relocation pass is made -to update all the self-references. Basically, the branch offsets in the byte -array get overwritten with the actual values. - -At the end of this pass, the code is locally correct at some offset (generally -0), but this is not the end - code is often *linked* against other code -generated in other compilations, and again various offsets will need to be -adjusted. - -### Global Relocation - -Code in one compilation unit wants to call code in another... this requires a -*global relocation*. A common example is a *loader* loading an *ELF* file, -which mostly happens every time a new program is run, and requires the -relocation information to be stored to disk (in e.g. the ELF file). JIT -systems will neeed to do this as well across different compilations, although -the relocation information can stay in memory. - -#### Getting Relocations to All Agree - -Since the relocations can be written to disk, the disk format, loader and "code -shape" all have to agree. Suppose we want to call from one compilation unit to -another - e.g. the program is calling `malloc` from `libc`. The caller is a -Simple using a machine-specific version of generic `CallNode` with the target -being C-compiled code. During loading, the placement of the `call` and `libc` -is determined by the loader; the two different blocks of code are written into -memory in some order (generally with lots of other blocks of code). Now we -need to patch the `call`'s target to be `malloc` in the `libc` block of code. - -For an X86 external call, the encoding is a 5-byte instruction, one byte of -opcode and 4 bytes of PC-relative target address. The needed patch is the -delta between the X86-specific `call` instruction *end* and the start of -`malloc`, and is written directly on the last 4 bytes of the `call`. The -encoding type in the ELF file has to describe this. - -For a RISCV external call, the encoding is 2 4-byte instructions each carrying -some of the bits. The `LUI` loads the target upper 20 bits, and then the -`JALR` adds in the lower 12 bits (32-bit absolute target range). The `LUI` -might be replaced with a `AUIPC` to get a 32-bit pc-relative range. Patching -here updates 20bits in one instruction and 12bits in another. Again the ELF -file encoding type has to describe this; using a X86-style patch here will just -crush the wrong bits and make a broken program. - -#### Large Constants - -Large constants are those which cannot be easily directly loaded into a -register, and instead are loaded from memory. For security reasons they are -often kept in a seperate address space with e.g. *read-only* permissions instead -of *execute* permissions. This means their final address, relative to the code, -varies according to placement - same as for calls crossing compilation units. - -Also large constants can be shared across compilation units (one can imagine -repeated uses of `pi` in HPC codes); sharing reduces cache footprint. In any -case, once the locations of the code and constants are known, the code needs to -be patched to load at the correct offset. Like the `call` example above, the -load instruction variant and patch variant all need to agree. An X86 might use -either a 4-byte address or an 8-byte address depending on X86 variant; the -RISCV probably uses some variant of an `LUI` and a `Load`, with the immediate -split into 20/12 bits. Also the RISCV probably wants to "share" the constant -pool 20 bits in the same `LUI`, but have different loads for different -constants coming out of the same pool. ARM will have yet another variant. - +This chapter turns the scheduled, register-allocated graph into executable +instructions. Each machine node writes its bytes; the compiler places blocks, +resolves local references, and exports an ELF object for the linker. We can now +run generated code, which also tests instruction selection, register allocation, +and calling conventions together. + +You can also read [this chapter](https://github.com/SeaOfNodes/Simple/tree/linear-chapter21) +in the linear revision history and +[compare it to Chapter 20](https://github.com/SeaOfNodes/Simple/compare/linear-chapter20...linear-chapter21). + +## From machine nodes to bytes + +Instruction selection chooses the operation and register constraints. Register +allocation assigns physical registers and stack slots. Encoding then combines +the opcode, assigned registers, and immediate operands into instruction bytes. +An instruction cannot choose a different register at this point: the surrounding +instructions already depend on the allocator's choices. + +[Encoding.java](src/main/java/com/seaofnodes/simple/codegen/Encoding.java) owns the +byte stream and records each node's offset and encoded length. Its driver: + +1. Lays out basic blocks and inserts or reverses branches as needed. +2. Calls `MachNode.encoding(Encoding)` in scheduled order. +3. Settles short and long instruction forms. +4. Patches references whose targets are inside this compilation. + +The `add1`, `add2`, `add4`, and `add8` helpers append little-endian values. Encoding +functions obtain allocated registers through `Encoding.reg`. Stack operands use +the function's frame layout, including space for saved registers and outgoing +arguments. A successful coloring alone does not establish a correct calling +convention; the generated program must preserve the caller's values too. + +## Three instruction sets + +The targets share this driver, but their machine nodes own the bit details. + +| Target | Main encoding concerns | +|---|---| +| RISC-V | Register and immediate fields in instruction words; larger constants and addresses need multiple instructions. | +| AArch64 | Register forms, restricted immediate encodings, and multi-instruction constant construction. | +| x86-64 | Variable instruction lengths, register prefixes, addressing forms, and short versus long branches. | + +For example, a small integer can fit directly in an instruction, while a large +one may require several instructions or a load from a constant pool. Consequently, +even a target with fixed-width instructions can emit different numbers of bytes +for different machine nodes. + +The [encoding reference](docs/encoding-reference.md) retains the detailed bit +layouts, worked examples, and ISA links. Those details matter when implementing +a particular instruction, but do not change the compiler pipeline described here. +The implementations live under +[node/cpus](src/main/java/com/seaofnodes/simple/node/cpus). + +## Relocation and ELF + +A forward branch cannot know its final displacement when its bytes are first +written. Its target's offset depends on all the intervening encodings. Shortening +or expanding another branch can move that target again. The layout pass settles +instruction sizes before local relocations patch the final displacements. + +External references need a later patch. For example, the compiler can emit a +call to `malloc` without knowing where the linker will place it. The object file +records the symbol, patch location, and relocation kind. That kind must agree +with the instruction: an x86 call displacement and a split RISC-V address occupy +different fields and cannot use the same patch operation. + +[ElfFile.java](src/main/java/com/seaofnodes/simple/codegen/ElfFile.java) writes the +object's code, data, symbols, and relocation records. Large constants introduce +the same placement problem as external calls: their eventual addresses must be +connected to the instructions that load them. Local relocation and object-file +relocation are two stages of the same obligation to make references correct. + +## Execute the result + +`make tests` includes C harnesses that link and run generated x86 code, and +RISC-V/AArch64 emulator checks. The tests compare results as well as spill counts. +Native process failures retain their full exit status. Emulator checks must read +their own result memory; matching another target's result is insufficient. + +Run `make spill-stats` to collect allocations from the same tests with assertions +enabled. It reports each test/CPU/ABI, then cohort totals. A spill-golden mismatch +is recorded without skipping the remaining targets or execution checks; any such +mismatch still makes the command fail. Structural allocator regressions are +excluded from the spill totals. + +## RegAlloc improvements: conservative copy coalescing + +A split inserts a copy between two live ranges. If both can safely become one +range, the copy can disappear before coloring. [Coalesce.java](src/main/java/com/seaofnodes/simple/codegen/Coalesce.java) +checks that the ranges do not interfere, intersects their allowed-register masks, +and combines their neighbors. It merges only when the resulting neighbor count +is smaller than the number of available registers. This simple conservative rule +preserves a color choice even if every neighbor takes a different register. + +Merging also updates neighboring adjacency lists and removes sample references +to the deleted copy. A rejected merge restores the original adjacency list. +Mask compatibility alone is insufficient: two values that are simultaneously +live cannot share a register merely because both permit it. + +Chapter 20's legality and progress fixes carry forward, including compatible +rematerialization and clobber-aware copy reuse. Chapter 21 retains its native +frame and ABI support. Stronger color bias and cheap-spill ranking are reserved +for Chapter 22, popular-use grouping for 23, cold-first loop splitting for 24, +and area/cost ranking for 25. + +These are measured sums with **this chapter's compiler**, default optimizer seed +123, on Windows x86-64. A split is a retained `SplitNode`, including register +moves; it is not necessarily a memory spill. Weighted counts multiply each split +by `8^loopDepth`. + +| Test cohort | Allocations | Splits | Loop-weighted splits | +|---|---:|---:|---:| +| Chapter 20 | 39 | 401 | 653 | +| Chapter 21 | 52 | 483 | 1,141 | +| Total | 91 | 884 | 1,794 | + +The Chapter 20 row freezes all 13 original inputs, including BrainFuck and +MergeSort, on three SystemV targets. They live in `Chapter20Test`. The revised +sources/ABI cases previously in that file are retained in `Chapter21AllocTest`; +together with `Chapter21Test` and the native BrainFuck/MergeSort tests they form +the Chapter 21 row. That row includes 17 ARM/SystemV, 17 RISC-V/SystemV, eight +x86/SystemV, and ten x86/Win64 compilations. Native host ABI changes affect this +row; compare the same host and targets. + +| Controlled comparison, same 91 compilations | Splits | Loop-weighted splits | +|---|---:|---:| +| Current compiler with coalescing disabled | 1,074 | 1,921 | +| Current compiler with coalescing | 884 | 1,794 | +| Original Chapter 21 snapshot, including later heuristics | 794 | 1,620 | + +The controlled run disables only the `Coalesce.coalesce` call in `graphColor`; +allocation, register-constraint, and runtime checks still pass. Its different +spill goldens are reported as failures, as intended for this experiment. + +Coalescing saves 190 moves (17.7%) and 127 weighted moves (6.6%) against the same +corrected compiler without coalescing. It is not uniformly beneficial; use the +summed measurements to judge the change. + +Deferring the later heuristics has a visible cost against the old snapshot: +weighted totals rise by 174 (10.7%). RISC-V BrainFuck rises from 28 to 211 in the +native case, while sieve improves from 257 to 178 on x86/Win64, 160 to 89 on +RISC-V, and 160 to 93 on ARM. These are reported costs of the staged tutorial, +not evidence that the simplified allocator beats the original combined one. +Chapter 20's 355 weighted splits also cannot be read as a coalescing baseline: +Chapter 21 changes machine lowering and implements native ABI obligations. +The controlled comparison above holds those changes fixed. |
