diff options
Diffstat (limited to 'chapter21/README.md')
| -rw-r--r-- | chapter21/README.md | 668 |
1 files changed, 339 insertions, 329 deletions
diff --git a/chapter21/README.md b/chapter21/README.md index ec47ac3b..c5b41bc7 100644 --- a/chapter21/README.md +++ b/chapter21/README.md @@ -27,17 +27,16 @@ You can also read [this chapter](https://github.com/SeaOfNodes/Simple/tree/linea 1. [Instruction Encodings](#instruction-encodings) 2. [Infrastructure](#infrastructure) -3. [AMD64](#amd64) - - [REX PREFIX](#rex-prefix) - - [Large constants](#large-constants) - - [MODR/M](#modrm) - - [SIB](#sib) - - [Displacement](#displacement) - - [Immediate](#immediate) +3. [Endianness](#endianness) +4. [RISCV](#riscv) + - [Instruction formats](#instruction-formats) - [Float](#float) - - [Indirect](#indirectmemop) - - [Conditional flags](#conditional-flags) -4. [ARM](#arm) + - [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) @@ -53,15 +52,17 @@ You can also read [this chapter](https://github.com/SeaOfNodes/Simple/tree/linea - [Branching](#branching-) - [Rip relative](#rip-relative) - [Compare](#compare) - -5. [RISCV](#riscv) - - [Instruction formats](#instruction-formats) - - [Float](#float) - - [Indirect](#indirectmemop-2) - - [Branch](#branch) - - [Function Constant](#function-constant) - - [LUI](#lui) +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 @@ -75,9 +76,6 @@ To avoid ambiguity, we define the sources of the information we use to encode t Currently, we support these architectures: -### AMD64: -For *x86-64(amd64):*, we use the latest Intel manual for the [encoding rules](https://www.felixcloutier.com/x86/). - ### RISC-V: For *riscv* we currently target [RVA23U64](https://msyksphinz-self.github.io/riscv-isadoc/html/). @@ -86,7 +84,10 @@ For *riscv* we currently target [RVA23U64](https://msyksphinz-self.github.io/ris 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 @@ -150,266 +151,134 @@ to the right, we can bring the next byte into the least significant position, allowing us to write each byte in sequence. -## 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. +## RISCV -In Simple, the REX prefix is just appended to the beginning of the bit stream -(except SEE float instructions). The layout is the following: +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. -| **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. | +#### Instruction formats +##### R-TYPE +This encoding format is used for reg-to-reg operations. +Such as `AddRISC`, `MulRISC` etc. ```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; +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; } - - ... - 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 +##### I-TYPE +This layout is used for immediate formats. Such as `AddIRISC` -The layout for mod is the following: -``` -public enum MOD { - INDIRECT, // [mem] - INDIRECT_disp8, // [mem + 0x12] - INDIRECT_disp32,// [mem + 0x12345678] - DIRECT, // mem -}; +```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; + } ``` -##### 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. +##### S-TYPE +This encoding format is used for `StoreRISC`. ```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; +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; } ``` -```java -enc.add1(x86_64_v2.sib(_scale, idx, x86_64_v2.RBP)); -``` +##### B-TYPE -#### Displacement -Displacement can happen in 2 forms: +This encoding layout is used by `BranchRISC`. ```java -public enum MOD { -INDIRECT, // [mem] -INDIRECT_disp8, // [mem + 0x12] -INDIRECT_disp32,// [mem + 0x12345678] -DIRECT, // mem -}; + // 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; + } ``` -```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); -``` +##### U-TYPE +This layout is used for `AUIPC` and `LUI`(upper immediate is 20-bits). -#### 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); +```java + public static int u_type(int opcode, int rd, int imm20) { + return (imm20 << 12) | (rd << 7) | opcode; + } ``` -#### 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. - +##### J-TYPE +Used for unconditional jumps. (`UJmpRISC`) ```java -short dst = (short)(enc.reg(this ) - x86_64_v2.XMM_OFFSET); + public static int j_type(int opcode, int rd, int imm20) { + return imm20 << 12 | rd << 7 | opcode; + } ``` -```java -public static int XMM_OFFSET = 16; +#### FLOAT +For loading float constants, we use indirect memory load(PC-relative). +``` +AUIPC dst,#hi20_constant_pool +Load dst,[dst+#low12_constant_pool] ``` -| **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. +#### INDIRECT(MemOp) -##### FLOAT: We use relocation and use constant from memory. +[Load](https://msyksphinz-self.github.io/riscv-isadoc/html/rvi.html#lw): `lw rd,offset(rs1)` -#### 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]` +[Store](https://msyksphinz-self.github.io/riscv-isadoc/html/rvi.html#sw): `sw rs2,offset(rs1)` -#### Conditional flags: -The FLAGS register is the status register that contains the current state of an x86 CPU. +#### BRANCH +RISC-V is unusual because branch instructions include the comparison and branch target in one instruction. -Let's consider a simple example that presents all the instructions that we need to encode for -conditional code. +#### 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. ``` -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; -} +addiw a5,a5,123 ``` - -First we need a compare instruction comparing `a` with 1. +vs +(do it with extra load): ``` -cmp DWORD PTR [rbp-0x8],0x1 +addw a5,a5,a4 ``` -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: +### FLOAT Constants +- Stored in the **constant pool**. +- Accessed via **PC-relative loads**. -`sete al` +### INT Constants +- **12-bit constants** + - Use `ADDI` +- **20-bit constants** + - Use `LUI` +- **32-bit constants** + - Use `LUI` + `ADDI` combo + - -For the if condition we do an extra comparison based on the result of the first bool. -``` - cmp eax,0x1 -``` +#### 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. -And then a conditional branch that is relying on the flags set by the comparison(`a == 1`). +```java +// auipc t0,0 +// addi t1,t0 + #0 ``` -jne 1159 <square(int)+0x29> -``` - -We'll see how this differs in ARM and RISCV. - ---- +-------------------------------- ## ARM Arm instructions are all 32 bits wide, and are always little-endian. @@ -588,126 +457,266 @@ If the offset is not a con > cond = is one of the standard conditions. ---- -## RISCV -RISCV instructions are all 32 bits wide, and are always little-endian. -RISCV is a RISC architecture, it prioritizes "register-to-register" forms. +-------------------------------- + +## 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. | -#### 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; + 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)); ``` -##### I-TYPE -This layout is used for immediate formats. Such as `AddIRISC` +Setting THE `W` bit gives us: +`0b01001000 = 0x48;` -```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; - } +#### Opcode +In our case, the opcode is a 1 byte field that specifies the operation to be performed. +``` +enc.add1(opcode()); // opcode ``` -##### S-TYPE +#### 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. -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; +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; } ``` -##### B-TYPE +The `modrm` function is then used to build up the `ModR/M` byte. +Usually this follows after the opcode. -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; - } +``` +enc.add1(x86_64_v2.modrm(x86_64_v2.MOD.DIRECT, dst, src)); ``` -##### U-TYPE -This layout is used for `AUIPC` and `LUI`(upper immediate is 20-bits). +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 u_type(int opcode, int rd, int imm20) { - return (imm20 << 12) | (rd << 7) | opcode; - } +public static int sib(int scale, int index, int base) { + return (scale << 6) | ((index & 0x07) << 3) | base & 0x07; +} ``` -##### 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; - } +```java +enc.add1(x86_64_v2.sib(_scale, idx, x86_64_v2.RBP)); ``` -#### FLOAT -For loading float constants, we use indirect memory load(PC-relative). +#### 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: ``` -AUIPC dst,#hi20_constant_pool -Load dst,[dst+#low12_constant_pool] +sub [eax + 2], 4 +``` +``` +bytes.write(0x83); +bytes.write(modrm(INDIRECT8, 5, 0 /* eax */)); +bytes.write(2); // displcement goes first +bytes.write(4); ``` -#### INDIRECT(MemOp) +#### 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); +``` -[Load](https://msyksphinz-self.github.io/riscv-isadoc/html/rvi.html#lw): `lw rd,offset(rs1)` +#### INDIRECT(MemOp) +X86-64 support multiple indirect addressing modes: -[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. +#### 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. -#### 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. +```java +short dst = (short)(enc.reg(this ) - x86_64_v2.XMM_OFFSET); +``` +```java +public static int XMM_OFFSET = 16; ``` -addiw a5,a5,123 + +| **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. + ``` -vs -(do it with extra load): +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; +} ``` -addw a5,a5,a4 + +First we need a compare instruction comparing `a` with 1. +``` +cmp DWORD PTR [rbp-0x8],0x1 ``` -### FLOAT Constants -- Stored in the **constant pool**. -- Accessed via **PC-relative loads**. +This compare instruction then sets the conditional flags: +> The CF, OF, SF, ZF, AF, and PF flags are set according to the result. -### INT Constants -- **12-bit constants** - - Use `ADDI` -- **20-bit constants** - - Use `LUI` -- **32-bit constants** - - Use `LUI` + `ADDI` combo - - +We then use the `sete` instruction to save out the result of the comparison to a register: -#### Function Constant -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. +`sete al` -```java -// auipc t0,0 -// addi t1,t0 + #0 +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 @@ -720,21 +729,22 @@ refers to other code and data; `call` instructions target subroutines, ### Local Relocation Local relocation patches up local, or self-referential, code; the most common -form is branch targets. When writing instructions into the `BAOS` (bit 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. - -Alson 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 to make space for the extra 4 bytes as needed, which changes all the -other branch offsets. +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 @@ -789,7 +799,7 @@ 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, one the locations of the code and constants are known, the code needs to +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 |
