liveBook · Manning

Opcode

As explained in Rust in Action: Systems programming concepts and techniques

An opcode is a symbolic code that identifies an instruction in a programming language or instruction set.

An opcode is a number that tells a CPU emulator which operation to perform. In CHIP-8, an opcode can also identify the registers or other values that the operation uses. The same bytes in memory become instructions when the emulator reads and executes them. section 5.7.1 section 5.8

Decode one addition instruction

Split 0x8014 into four hexadecimal digits: 8 selects a two-register operation, 0 selects register 0, 1 selects register 1, and 4 selects addition. With 5 in register 0 and 10 in register 1, executing it adds 10 to 5. Masks and shifts extract the digits; a match selects the addition method. section 5.7.1

Listing 5.20 Reading the opcode
 impl CPU {
   fn read_opcode(&self) -> u16 {                      #1
     self.current_operation                            #1
   }                                                   #1
 
   fn run(&mut self) {
     // loop {                                         #2
       let opcode = self.read_opcode();
 
       let c = ((opcode & 0xF000) >> 12) as u8;        #3
       let x = ((opcode & 0x0F00) >>  8) as u8;        #3
       let y = ((opcode & 0x00F0) >>  4) as u8;        #3
       let d = ((opcode & 0x000F) >>  0) as u8;        #3
 
       match (c, x, y, d) {
           (0x8, _, _, 0x4) => self.add_xy(x, y),      #4
           _  =>  todo!("opcode {:04x}", opcode),      #5
       }
     // }                                              #6
   }
 
   fn add_xy(&mut self, x: u8, y: u8) {
     self.registers[x as usize] += self.registers[y as usize];
   }
 }

Follow the masks, shifts, and match arm that turn a 16-bit opcode into a register operation. section 5.7.1

To run more than one instruction, store opcodes in RAM. The emulator reads two bytes at the current memory position, combines them into a 16-bit opcode, advances the position by two bytes, and executes it. An opcode of 0x0000 stops this loop. section 5.7.3

Listing 5.24 Reading an opcode from memory
 8 fn read_opcode(&self) -> u16 {
 9   let p = self.position_in_memory;
10   let op_byte1 = self.memory[p] as u16;
11   let op_byte2 = self.memory[p + 1] as u16;
12  
13   op_byte1 << 8 | op_byte2       #1
14 }

See how two successive memory bytes become one opcode. section 5.7.3

Opcodes can change where execution goes next. CALL (0x2nnn) transfers execution to address nnn while saving the return position on a stack; RETURN (0x00EE) restores that position. A function containing two addition opcodes followed by RETURN can be written as three 16-bit values or six bytes loaded into RAM. section 5.7.4

Comparisons and changes to the current memory position provide control flow without a CPU-level while or for instruction. Opcodes are not limited to CPUs: characters from the Base 16 alphabet also serve as opcodes in a LOGO-like language for drawing avatars. section 5.7.5 section 10.4

How Rust in Action: Systems programming concepts and techniques covers it

How deep it goes
Works through nibble extraction, byte assembly, dispatch, and stack-backed calls; defers fuller CHIP-8 coverage to companion code.
Examples
Decodes 0x8014 as register addition and loads addition and return opcodes as bytes for a callable function.
What it assumes
Introduces operations and registers before decoding, then builds from one addition instruction toward memory-backed execution.
What you can do after
Build it from scratch in Rust by decoding CHIP-8 opcodes and extending an emulator from addition to memory-backed calls.
How it connects
Links opcodes to registers, operands, RAM, program counters, stacks, and control flow.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Rust in Action: Systems programming concepts and techniques. The section links point to the parts of the book it draws on.