Recent from talks
Contribute something
Nothing was collected or created yet.
Machine code
View on Wikipedia

| Program execution |
|---|
| General concepts |
| Types of code |
| Compilation strategies |
| Notable runtimes |
|
| Notable compilers & toolchains |
|
In computing, machine code is data encoded and structured to control a computer's central processing unit (CPU) via its programmable interface. A computer program consists primarily of sequences of machine-code instructions.[1] Machine code is classified as native with respect to its host CPU since it is the language that CPU interprets directly.[2] A software interpreter is a virtual machine that processes virtual machine code.
A machine-code instruction causes the CPU to perform a specific task such as:
- Load a word from memory to a CPU register
- Execute an arithmetic logic unit (ALU) operation on one or more registers or memory locations
- Jump or skip to an instruction that is not the next one
An instruction set architecture (ISA) defines the interface to a CPU and varies by groupings or families of CPU design such as x86 and ARM. Generally, machine code compatible with one family is not with others, but there are exceptions. The VAX architecture includes optional support of the PDP-11 instruction set. The IA-64 architecture includes optional support of the IA-32 instruction set. And, the PowerPC 615 can natively process both PowerPC and x86 instructions.
Assembly language
[edit]
Assembly language provides a relatively direct mapping from a human-readable source code to machine code. The source code represents numerical codes as mnemonics and labels.[3] For example, NOP represents the x86 architecture opcode 0x90. While it is possible to write a program in machine code, doing so is tedious and error-prone. Therefore, programs are usually written in assembly or, more commonly, in a high-level programming language.
Instruction set
[edit]A machine instruction encodes an operation as a pattern of bits based on the specified format for the machine's instruction set.[nb 1][4]
Instruction sets differ in various ways. Instructions of a set might all be the same length or different instructions might have different lengths; they might be smaller than, the same size as, or larger than the word size of the architecture. The number of instructions may be relatively small or large. Instructions may or may not have to be aligned on particular memory boundaries, such as the architecture's word boundary.[4]
An instruction set needs to execute the circuits of a computer's digital logic level. At the digital level, the program needs to control the computer's registers, bus, memory, ALU, and other hardware components.[5] To control a computer's architectural features, machine instructions are created. Examples of features that are controlled using machine instructions:
- segment registers[6]
- protected address mode[7]
- binary-coded decimal (BCD) arithmetic[8]
The criteria for instruction formats include:
- Instructions most commonly used should be shorter than instructions rarely used.[4]
- The memory transfer rate of the underlying hardware determines the flexibility of the memory fetch instructions.
- The number of bits in the address field requires special consideration.[9]
Determining the size of the address field is a choice between space and speed.[9] On some computers, the number of bits in the address field may be too small to access all of the physical memory. Also, virtual address space needs to be considered. Another constraint may be a limitation on the size of registers used to construct the address. Whereas a shorter address field allows the instructions to execute more quickly, other physical properties need to be considered when designing the instruction format.
Instructions can be separated into two types: general-purpose and special-purpose. Special-purpose instructions exploit architectural features that are unique to a computer. General-purpose instructions control architectural features common to all computers.[10]
General-purpose instructions control:
- Data movement from one place to another
- Monadic operations that have one operand to produce a result
- Dyadic operations that have two operands to produce a result
- Comparisons and conditional jumps
- Procedure calls
- Loop control
- Input/output
Overlapping instruction
[edit]On processor architectures with variable-length instruction sets[11] (such as Intel's x86 processor family) it is, within the limits of the control-flow resynchronizing phenomenon known as the Kruskal count,[12][11][13][14][15] sometimes possible through opcode-level programming to deliberately arrange the resulting code so that two code paths share a common fragment of opcode sequences.[nb 2] These are called overlapping instructions, overlapping opcodes, overlapping code, overlapped code, instruction scission, or jump into the middle of an instruction.[16][17][18]
In the 1970s and 1980s, overlapping instructions were sometimes used to preserve memory space. One example were in the implementation of error tables in Microsoft's Altair BASIC, where interleaved instructions mutually shared their instruction bytes.[19][11][16] The technique is rarely used today, but might still be necessary to resort to in areas where extreme optimization for size is necessary on byte-level such as in the implementation of boot loaders which have to fit into boot sectors.[nb 3]
It is also sometimes used as a code obfuscation technique as a measure against disassembly and tampering.[11][14]
The principle is also used in shared code sequences of fat binaries which must run on multiple instruction-set-incompatible processor platforms.[nb 2]
This property is also used to find unintended instructions called gadgets in existing code repositories and is used in return-oriented programming as alternative to code injection for exploits such as return-to-libc attacks.[20][11]
Microcode
[edit]In some computers, the machine code of the architecture is implemented by an even more fundamental underlying layer called microcode, providing a common machine language interface across a line or family of different models of computer with widely different underlying dataflows. This is done to facilitate porting of machine language programs between different models.[21] An example of this use is the IBM System/360 family of computers and their successors.[22]
Examples
[edit]IBM 709x
[edit]The IBM 704, 709, 704x and 709x store one instruction in each instruction word; IBM numbers the bit from the left as S, 1, ..., 35. Most instructions have one of two formats:
- Generic
- S,1-11
- 12-13 Flag, ignored in some instructions
- 14-17 unused
- 18-20 Tag
- 21-35 Y
- Index register control, other than TSX
- S,1-2 Opcode
- 3-17 Decrement
- 18-20 Tag
- 21-35 Y
For all but the IBM 7094 and 7094 II, there are three index registers designated A, B and C; indexing with multiple 1 bits in the tag subtracts the logical or of the selected index registers and loading with multiple 1 bits in the tag loads all of the selected index registers. The 7094 and 7094 II have seven index registers, but when they are powered on they are in multiple tag mode, in which they use only the three of the index registers in a fashion compatible with earlier machines, and require a Leave Multiple Tag Mode (LMTM) instruction in order to access the other four index registers.
The effective address is normally Y-C(T), where C(T) is either 0 for a tag of 0, the logical or of the selected index registers in multiple tag mode or the selected index register if not in multiple tag mode. However, the effective address for index register control instructions is just Y.
A flag with both bits 1 selects indirect addressing; the indirect address word has both a tag and a Y field.
In addition to transfer (branch) instructions, these machines have skip instruction that conditionally skip one or two words, e.g., Compare Accumulator with Storage (CAS) does a three way compare and conditionally skips to NSI, NSI+1 or NSI+2, depending on the result.
MIPS
[edit]The MIPS architecture provides a specific example for a machine code whose instructions are always 32 bits long.[23]: 299 The general type of instruction is given by the op (operation) field, the highest 6 bits. J-type (jump) and I-type (immediate) instructions are fully specified by op. R-type (register) instructions include an additional field funct to determine the exact operation. The fields used in these types are:
6 5 5 5 5 6 bits [ op | rs | rt | rd |shamt| funct] R-type [ op | rs | rt | address/immediate] I-type [ op | target address ] J-type
rs, rt, and rd indicate register operands; shamt gives a shift amount; and the address or immediate fields contain an operand directly.[23]: 299–301
For example, adding the registers 1 and 2 and placing the result in register 6 is encoded:[23]: 554
[ op | rs | rt | rd |shamt| funct]
0 1 2 6 0 32 decimal
000000 00001 00010 00110 00000 100000 binary
Load a value into register 8, taken from the memory cell 68 cells after the location listed in register 3:[23]: 552
[ op | rs | rt | address/immediate] 35 3 8 68 decimal 100011 00011 01000 00000 00001 000100 binary
Jumping to the address 1024:[23]: 552
[ op | target address ]
2 1024 decimal
000010 00000 00000 00000 10000 000000 binary
Bytecode
[edit]Machine code is similar to yet fundamentally different from bytecode. Like machine code, bytecode is typically generated (i.e. by a compiler) from source code. But, unlike machine code, bytecode is not directly executable by a CPU. An exception is if a processor is designed to use bytecode as its machine code, such as the Java processor. If bytecode is processed by an software interpreter, then that interpreter is a virtual machine for which the bytecode is its machine code.
Storage
[edit]During execution, machine code is generally stored in RAM although running from ROM is supported by some devices. Regardless, the code may also be cached in more specialized memory to enhance performance. There may be different caches for instructions and data, depending on the architecture.[24]
From the point of view of a process, the machine code lives in code space, a designated part of its address space. In a multi-threading environment, different threads of one process share code space along with data space, which reduces the overhead of context switching considerably as compared to process switching.[25]
Readability
[edit]Machine code is generally considered to be not human readable,[26] with Douglas Hofstadter comparing it to examining the atoms of a DNA molecule.[27] However, various tools and methods support understanding machine code.
Disassembly decodes machine code to assembly language which is possible since assembly instructions can often be mapped one-to-one to machine instructions.[28]
A decompiler converts machine code to a high-level language, but the result can be relatively obfuscated; hard to understand.
A program can be associated with debug symbols (either embedded in the native executable or in a separate file) that allow it to be mapped to external source code. A debugger reads the symbols to help a programmer interactively debug the program. Example include:
- The SHARE Operating System (1959) for the IBM 709, IBM 7090, and IBM 7094 computers allowed for an loadable code format named SQUOZE. SQUOZE was a compressed binary form of assembly language code and included a symbol table.
- Modern IBM mainframe operating systems, such as z/OS, have available a symbol table named Associated data (ADATA). The table is stored in a file that can be produced by the IBM High-Level Assembler (HLASM),[29][30] IBM's COBOL compiler,[31] and IBM's PL/I compiler,[32] either as a separate SYSADATA file or as ADATA records in a Generalized object output file (GOFF).[33] This obsoletes the TEST records from OS/360, although it is still possible to request them and to use them in the TSO TEST command.
- Windows uses a symbol table[34] that is stored in a program database (.pdb) file.[35]
- Most Unix-like operating systems have available symbol table formats named stabs and DWARF. In macOS and other Darwin-based operating systems, the debug symbols are stored in DWARF format in a separate .dSYM file.
See also
[edit]- Endianness – Order of bytes in a computer word
- List of machine languages
- Machine code monitor – Software that was popular during the home computer era of the 1970s and 1980s
- Micro-Professor MPF-I – Microcomputer released by Multitech in 1981
- native executable – Data that causes a computer to follow indicated instructions
- Object code – Sequence of statements or instructions in a computer language
- P-code machine – Programming virtual machine
- Reduced instruction set computer – Processor executing one instruction in minimal clock cycles (RISC)
- Very long instruction word – Processor design that offloads complexity to the compiler
Notes
[edit]- ^ On early decimal machines, patterns of characters, digits and digit sign
- ^ a b While overlapping instructions on processor architectures with variable-length instruction sets can sometimes be arranged to merge different code paths back into one through control-flow resynchronization, overlapping code for different processor architectures can sometimes also be crafted to cause execution paths to branch into different directions depending on the underlying processor, as is sometimes used in fat binaries.
- ^ For example, the DR-DOS master boot records (MBRs) and boot sectors (which also hold the partition table and BIOS Parameter Block, leaving less than 446 respectively 423 bytes for the code) were traditionally able to locate the boot file in the FAT12 or FAT16 file system by themselves and load it into memory as a whole, in contrast to their counterparts in MS-DOS and PC DOS, which instead rely on the system files to occupy the first two directory entry locations in the file system and the first three sectors of IBMBIO.COM to be stored at the start of the data area in contiguous sectors containing a secondary loader to load the remainder of the file into memory (requiring SYS to take care of all these conditions). When FAT32 and logical block addressing (LBA) support was added, Microsoft even switched to require i386 instructions and split the boot code over two sectors for code size reasons, which was no option to follow for DR-DOS as it would have broken backward- and cross-compatibility with other operating systems in multi-boot and chain load scenarios, and as with older IBM PC–compatible PCs. Instead, the DR-DOS 7.07 boot sectors resorted to self-modifying code, opcode-level programming in machine language, controlled utilization of (documented) side effects, multi-level data/code overlapping and algorithmic folding techniques to still fit everything into a physical sector of only 512 bytes without giving up any of their extended functions.
References
[edit]- ^ Stallings, William (2015). Computer Organization and Architecture 10th edition. Pearson Prentice Hall. p. 776. ISBN 978-93-325-7040-5.
- ^ Gregory, Kate (2003-04-28). "Managed, Unmanaged, Native: What Kind of Code Is This?". Developer.com. Archived from the original on 2009-09-23. Retrieved 2008-09-02.
- ^ Dourish, Paul (2004). Where the Action is: The Foundations of Embodied Interaction. MIT Press. p. 7. ISBN 0-262-54178-5. Retrieved 2023-03-05.
- ^ a b c Tanenbaum 1990, p. 251
- ^ Tanenbaum 1990, p. 162
- ^ Tanenbaum 1990, p. 231
- ^ Tanenbaum 1990, p. 237
- ^ Tanenbaum 1990, p. 236
- ^ a b Tanenbaum 1990, p. 253
- ^ Tanenbaum 1990, p. 283
- ^ a b c d e Jacob, Matthias; Jakubowski, Mariusz H.; Venkatesan, Ramarathnam [at Wikidata] (20–21 September 2007). Towards Integral Binary Execution: Implementing Oblivious Hashing Using Overlapped Instruction Encodings (PDF). Proceedings of the 9th workshop on Multimedia & Security (MM&Sec '07). Dallas, Texas, US: Association for Computing Machinery. pp. 129–140. CiteSeerX 10.1.1.69.5258. doi:10.1145/1288869.1288887. ISBN 978-1-59593-857-2. S2CID 14174680. Archived (PDF) from the original on 2018-09-04. Retrieved 2021-12-25. (12 pages)
- ^ Lagarias, Jeffrey "Jeff" Clark; Rains, Eric Michael; Vanderbei, Robert J. (2009) [2001-10-13]. "The Kruskal Count". In Brams, Stephen; Gehrlein, William V.; Roberts, Fred S. (eds.). The Mathematics of Preference, Choice and Order. Studies in Choice and Welfare. Berlin / Heidelberg, Germany: Springer-Verlag. pp. 371–391. arXiv:math/0110143. doi:10.1007/978-3-540-79128-7_23. ISBN 978-3-540-79127-0. (22 pages)
- ^ Andriesse, Dennis; Bos, Herbert [at Wikidata] (2014-07-10). Written at Vrije Universiteit Amsterdam, Amsterdam, Netherlands. Dietrich, Sven (ed.). Instruction-Level Steganography for Covert Trigger-Based Malware (PDF). 11th International Conference on Detection of Intrusions and Malware, and Vulnerability Assessment (DIMVA). Lecture Notes in Computer Science. Egham, UK; Switzerland: Springer International Publishing. pp. 41–50 [45]. doi:10.1007/978-3-319-08509-8_3. eISSN 1611-3349. ISBN 978-3-31908508-1. ISSN 0302-9743. S2CID 4634611. LNCS 8550. Archived (PDF) from the original on 2023-08-26. Retrieved 2023-08-26. (10 pages)
- ^ a b Jakubowski, Mariusz H. (February 2016). "Graph Based Model for Software Tamper Protection". Microsoft. Archived from the original on 2019-10-31. Retrieved 2023-08-19.
- ^ Jämthagen, Christopher (November 2016). On Offensive and Defensive Methods in Software Security (PDF) (Thesis). Lund, Sweden: Department of Electrical and Information Technology, Lund University. p. 96. ISBN 978-91-7623-942-1. ISSN 1654-790X. Archived (PDF) from the original on 2023-08-26. Retrieved 2023-08-26. (1+xvii+1+152 pages)
- ^ a b "Unintended Instructions on x86". Hacker News. 2021. Archived from the original on 2021-12-25. Retrieved 2021-12-24.
- ^ Kinder, Johannes (2010-09-24). Static Analysis of x86 Executables [Statische Analyse von Programmen in x86 Maschinensprache] (PDF) (Dissertation). Munich, Germany: Technische Universität Darmstadt. D17. Archived from the original on 2020-11-12. Retrieved 2021-12-25. (199 pages)
- ^ "What is "overlapping instructions" obfuscation?". Reverse Engineering Stack Exchange. 2013-04-07. Archived from the original on 2021-12-25. Retrieved 2021-12-25.
- ^ Gates, William "Bill" Henry, Personal communication (NB. According to Jacob et al.)
- ^ Shacham, Hovav (2007). The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls (on the x86) (PDF). Proceedings of the ACM, CCS 2007. ACM Press. Archived (PDF) from the original on 2021-12-15. Retrieved 2021-12-24.
- ^ Kent, Allen; Williams, James G. (1993-04-05). Encyclopedia of Computer Science and Technology: Volume 28 - Supplement 13: AerosPate Applications of Artificial Intelligence to Tree Structures. CRC Press. pp. 33–34. ISBN 978-0-8247-2281-4.
- ^ Tucker, S. G. (1967-12-31). "Microprogram control for SYSTEM/360". IBM Systems Journal. 6 (4): 222–241. doi:10.1147/sj.64.0222. ISSN 0018-8670 – via IEEE Xplore.
- ^ a b c d e Harris, David; Harris, Sarah L. (2007). Digital Design and Computer Architecture. Morgan Kaufmann Publishers. ISBN 978-0-12-370497-9. Retrieved 2023-03-05.
- ^ Su, Chao; Zeng, Qingkai (2021). "Survey of CPU Cache-Based Side-Channel Attacks: Systematic Analysis, Security Models, and Countermeasures". Security and Communication Networks. 2021 (1) 5559552. doi:10.1155/2021/5559552. ISSN 1939-0122.
- ^ "CS 537 Notes, Section #3A: Processes and Threads". pages.cs.wisc.edu. School of Computer, Data & Information Sciences, University of Wisconsin-Madison. Retrieved 2025-07-18.
- ^ Samuelson 1984, p. 683.
- ^ Hofstadter 1979, p. 290.
- ^ Tanenbaum 1990, p. 398.
- ^ "Associated Data Architecture". High Level Assembler and Toolkit Feature.
- ^ "Associated data file output" (PDF). High Level Assembler for z/OS & z/VM & z/VSE - 1.6 -HLASM Programmer's Guide (PDF) (Eighth ed.). IBM. October 2022. pp. 278–332. SC26-4941-07. Retrieved 2025-02-14.
- ^ "COBOL SYSADATA file contents". Enterprise COBOL for z/OS.
- ^ "SYSADATA message information". Enterprise PL/I for z/OS 6.1 information. 2025-03-17.
- ^ "Appendix C. Generalized object file format (GOFF)" (PDF). z/OS - 3.1 - MVS Program Management: Advanced Facilities (PDF). IBM. 2024-12-18. pp. 201–240. SA23-1392-60. Retrieved 2025-02-14.
- ^ "Symbols for Windows debugging". Microsoft Learn. 2022-12-20.
- ^ "Querying the .Pdb File". Microsoft Learn. 2024-01-12.
Sources
[edit]- Hofstadter, Douglas R. (1979). Gödel, Escher, Bach: An Eternal Golden Braid. Basic Books. ISBN 0-465-02685-0. Retrieved 2025-02-10.
- Samuelson, Pamela (1984). "CONTU Revisited: The Case Against Copyright Protection for Computer Programs in Machine-Readable Form". Duke Law Journal. 33 (4): 663–769. doi:10.2307/1372418. hdl:hein.journals/duklr1984. JSTOR 1372418. Retrieved 2025-02-10.
- Tanenbaum, Andrew S. (1990). Structured Computer Organization, Third Edition. Prentice Hall. p. 398. ISBN 978-0-13-854662-5.
Further reading
[edit]- Hennessy, John L.; Patterson, David A. (1994). Computer Organization and Design. The Hardware/Software Interface. Morgan Kaufmann Publishers. ISBN 1-55860-281-X.
- Tanenbaum, Andrew S. (1999). Structured Computer Organization (Fourth ed.). Prentice Hall. ISBN 0-13-020435-8. Retrieved 2025-02-10.
- Brookshear, J. Glenn (2007). Computer Science: An Overview (Ninth ed.). Addison Wesley. ISBN 978-0-321-38701-1. Retrieved 2025-02-10.
Machine code
View on Grokipediaadd %rax, %rbx to add values in registers)—and then assembled into machine code by an assembler tool.[2] This binary output is linked with libraries to form an executable file, which the operating system loads into memory for the CPU to fetch, decode, and execute sequentially.[1] Due to its architecture-specific nature, machine code ensures optimal performance but lacks portability across different hardware platforms, necessitating recompilation for each target system.[2]
Historically, machine code emerged with the advent of stored-program computers in the mid-20th century, such as the EDSAC in 1949, where programmers manually entered binary instructions via switches or paper tape, marking the shift from wired or plugboard-based programming to software-defined control.[3] This direct hardware interaction laid the groundwork for modern computing, though its complexity prompted the development of assembly languages in the 1940s and high-level languages like FORTRAN in 1957 to abstract away binary details.[3] Today, machine code underpins critical systems including operating system kernels, device drivers, and embedded firmware, where efficiency and low-level hardware access are paramount.[1]
Fundamentals
Definition
Machine code is the lowest-level representation of computer programs, consisting of sequences of binary digits (0s and 1s) that a computer's central processing unit (CPU) can directly execute without any further translation or interpretation.[4] These binary instructions form the native language of the hardware, enabling the CPU to perform operations by fetching and processing them sequentially from memory.[5] Unlike source code, which is written in human-readable forms such as high-level programming languages or assembly mnemonics, machine code is inherently hardware-specific and not intended for direct human comprehension or editing.[6] It typically comprises operation codes (opcodes), which specify the action to be performed, and operands, which indicate the data or locations involved in that action.[7] Each instruction in machine code thus defines a precise operation—such as addition, data loading from memory, or conditional branching—and may incorporate addressing modes to determine how operands are accessed, such as immediate values, register contents, or memory addresses.[8] The concept of machine code originated in the era of early electronic computers during the 1940s, exemplified by the EDSAC completed in 1949, where programs were entered as binary instructions punched on paper tape.[9] This marked the realization of stored-program computing, establishing machine code as the foundational medium for instructing hardware to execute computational tasks.[3]Characteristics
Machine code consists of binary instructions encoded as sequences of bits, typically organized into fixed- or variable-length words such as 8-bit bytes or 32-bit words, which form the fundamental units processed by the CPU.[2] This binary format ensures direct compatibility with hardware logic gates, where each instruction is a pattern of 0s and 1s representing opcodes and operands. Signed integer values within these instructions or data are commonly represented using two's complement notation, which allows efficient arithmetic operations by treating negative numbers as the bitwise inverse plus one.[10] Floating-point representations, when used, adhere to the IEEE 754 standard, specifying formats like binary32 (single precision) with a sign bit, biased exponent, and mantissa for precise hardware handling of real numbers.[11] A core characteristic of machine code is its tight binding to a specific CPU architecture, such as x86 or ARM, where the valid instruction set, register layout, and addressing modes are defined by the processor's instruction set architecture (ISA).[1] This specificity extends to details like byte ordering, or endianness, with architectures employing either big-endian (most significant byte first, as in some PowerPC implementations) or little-endian (least significant byte first, as in x86) to interpret multi-byte values, impacting data portability across systems.[12] Such architecture dependence ensures optimized execution on target hardware but requires recompilation or emulation for compatibility with differing ISAs. Machine code exhibits high efficiency due to its minimal abstraction layer, incurring no interpretation overhead and enabling rapid direct execution by the CPU, often at clock speeds exceeding several gigahertz in modern processors.[13] However, this comes at the cost of limitation to primitive operations, including basic arithmetic (e.g., addition, subtraction), logical manipulations (e.g., AND, OR), and control flow mechanisms (e.g., jumps, branches), without support for higher-level constructs like loops or conditionals unless emulated through sequences of these basics. During runtime, machine code loaded into memory remains immutable; the program itself does not alter its instructions, as self-modifying code—once common for optimization—now incurs severe performance penalties from cache invalidations and pipeline flushes in modern superscalar processors, rendering it rare and generally deprecated.[14] Instruction lengths in machine code vary by architecture, influencing code density—the ratio of executable bytes to functional complexity—with reduced instruction set computing (RISC) designs typically using 1- to 4-byte instructions to balance simplicity and compactness.[15] For instance, base RISC-V instructions are 32 bits (4 bytes), while compressed variants reduce to 16 bits (2 bytes) for denser code in memory-constrained environments, directly affecting fetch efficiency and overall program size without compromising execution speed.[16]Generation and Relation to Higher Levels
Assembly Language
Assembly language is a low-level programming language that uses mnemonic codes, such as ADD for addition or MOV for data movement, and symbolic addresses to represent machine instructions in a human-readable form, with each assembly statement typically corresponding one-to-one to a single machine instruction.[17] These mnemonics serve as symbolic names for opcodes, while symbolic addresses allow programmers to reference memory locations without specifying exact binary values, which are resolved by the assembler during translation to machine code.[18] The syntax of assembly language includes labels for marking memory addresses or jump targets, assembler directives for controlling the assembly process, and macros for defining reusable code blocks. Labels are alphanumeric identifiers placed at the beginning of lines to denote locations, enabling jumps or data references without hardcoding addresses.[17] Directives, such as .data for defining data sections or ORG for setting the origin address, provide instructions to the assembler rather than generating machine code directly.[17] Macros expand to multiple instructions at assembly time, facilitating code reuse and reducing repetition, though their implementation varies by assembler.[19] Compared to writing directly in binary machine code, assembly language offers advantages in readability and maintainability, making debugging and performance optimization more accessible while preserving direct hardware control for fine-tuned operations like register manipulation.[20] Programmers can insert small, efficient code segments easily and achieve execution speeds close to machine code without the error-prone task of manual binary entry.[20] Assembly language development began in the late 1940s, with early forms appearing alongside stored-program computers; for instance, the EDSAC computer in 1949 incorporated an assembler known as Initial Orders, using single-letter mnemonics to simplify instruction entry on paper tape.[21] By the 1950s, assemblers had become standard as the first abstraction above machine code, predating high-level languages like Fortran.[22] Modern assemblers, such as NASM for x86 architectures and GAS (GNU Assembler) which supports multiple architectures including x86, ARM, and MIPS, continue this tradition with enhanced features for cross-platform development.[23] Assembly code is inherently platform-dependent, as its instructions and addressing modes are tailored to a specific CPU architecture, necessitating reassembly for different processors even if the source code structure remains similar.[24] The assembler translates this architecture-specific source into the corresponding binary machine code executable on that hardware.[17]Compilation from Higher-Level Languages
Machine code is produced from higher-level programming languages through a multi-stage compilation process that transforms abstract source code into platform-specific binary instructions. This automated translation enables developers to write code in expressive languages like C++ or Java while leveraging the efficiency of machine-executable instructions. The process begins with parsing the source code and culminates in generating object files containing machine code, which are then linked into runnable executables.[25] The compilation process originated with the FORTRAN compiler developed by IBM in 1957, marking the first successful implementation of automatic code translation from a high-level language to machine code for the IBM 704 computer.[26] Subsequent advancements have standardized the pipeline into key phases: lexical analysis, where the source code is scanned to identify tokens such as keywords and identifiers; syntax analysis or parsing, which builds a parse tree to verify grammatical structure; semantic analysis, ensuring type compatibility and scope rules; intermediate code generation, producing a platform-independent representation like three-address code; optimization, applying transformations for efficiency; and final code generation, where target-specific machine instructions (opcodes) are emitted. Relocation occurs during linking, adjusting addresses in the machine code to resolve references between object modules.[25] Compilers such as GCC (GNU Compiler Collection) and Clang implement this pipeline to generate machine code. For instance, GCC's front-end parses C++ source into an intermediate representation (GIMPLE), applies middle-end optimizations, and uses a back-end to produce assembly code, which the assembler (gas) converts to object files containing relocatable machine code.[25] The linker (ld) then resolves symbols, performs relocation to fix absolute addresses, and combines object files into an executable binary.[25] Some compilers, including Clang, can directly emit machine code in object files without an explicit assembly step for certain targets. Optimization techniques during compilation enhance the resulting machine code's performance and size. Dead code elimination removes unreachable or unused instructions, reducing program footprint and execution time, as implemented in GCC's optimization passes.[27] Instruction scheduling reorders operations to minimize pipeline stalls on modern processors, improving throughput without altering program semantics; for example, GCC uses this to exploit instruction-level parallelism.[27] In contrast to ahead-of-time compilation, just-in-time (JIT) compilation generates machine code at runtime for greater portability across architectures. JavaScript engines like V8 in Node.js and Chrome employ JIT to interpret and optimize hot code paths, translating bytecode to native machine instructions dynamically via stages including Ignition (interpreter) and TurboFan (optimizing compiler).[28] This approach, while incurring initial overhead, allows runtime adaptations such as architecture-specific optimizations. Some compilers use assembly language as an intermediate representation before final machine code emission, bridging high-level abstractions and low-level instructions.[25]Instruction Set Architecture
Instruction Components
Machine code instructions are composed of distinct structural elements that enable the processor to execute operations on data. The primary component is the opcode, a binary code that specifies the operation to be performed, such as addition or data movement. For instance, in a hypothetical 4-bit instruction set architecture (ISA), the opcode 0001 might denote an ADD operation.[29] Following the opcode are the operands, which provide the data or addresses required for the operation. Operands can include register identifiers, immediate values embedded directly in the instruction, or memory locations. Common types encompass register operands for fast access to processor registers, immediate operands for constant values, and indirect addressing where the operand points to a memory address containing the actual data.[30][29] Instructions are organized into specific formats that determine their overall length and layout. Reduced Instruction Set Computing (RISC) architectures typically employ fixed-length formats, such as 32 bits per instruction, to simplify decoding and fetching. In contrast, Complex Instruction Set Computing (CISC) architectures use variable-length formats, where instructions can range up to 15 bytes in the x86 family, allowing for greater flexibility but increasing complexity in execution.[31][32] Within these formats, instructions are divided into fields that encode the components. The opcode field commonly occupies 6 to 8 bits to distinguish operations, while register specifiers use about 5 bits each to select from a set of general-purpose registers. Additional function codes, often 6 bits in length, may specify sub-operations or variants within the same opcode, enhancing the instruction's versatility without expanding the overall format.[33] Addressing modes further refine operand interpretation, expanding flexibility by specifying how operands are computed, such as absolute addressing for direct memory locations, relative addressing based on the program counter, or indexed addressing that adds an offset to a base register. The PDP-11 architecture, introduced in 1970, provided eight addressing modes that significantly influenced subsequent ISA designs.[34][35]Overlapping Instructions
Overlapping instructions refer to a technique used in some early computer architectures to conserve memory by allowing the bytes of one instruction to serve as part of another instruction. This was particularly employed in the 1970s and 1980s when memory was expensive, enabling more compact code without fixed alignment. One example is in the implementation of error tables in the Burroughs B1700 and B1800 systems, where overlapping allowed efficient storage of diagnostic or correction data within executable code. The approach relies on careful encoding so that the CPU's instruction decoder can correctly interpret the intended instructions despite the shared bytes, often requiring specific alignment or hardware support. However, this complicates debugging, disassembly, and hardware design, as the sequential fetch-decode-execute cycle must account for potential ambiguities. In contrast to variable-length instructions, which use prefix-free codes for unambiguous parsing without byte sharing, overlapping is rare in modern ISAs due to these complexities. A historical example includes certain implementations in mainframes like the IBM System/360 variants, though primarily for specialized purposes rather than general code. Modern equivalents appear in code obfuscation or anti-tampering techniques, but for performance-critical systems, fixed- or compressed-length instructions (e.g., RISC-V's 16-bit compressed extensions achieving 20-30% code density improvements) are preferred to simplify pipelining.[36]Microcode
Microcode consists of low-level sequences of micro-operations, such as register loads, ALU operations, and memory accesses, that define the internal steps required to execute a single machine instruction visible to the programmer.[37][38] These micro-operations serve as firmware-like control signals stored in read-only memory (ROM) or writable control store within the CPU, translating higher-level instructions into primitive hardware actions.[39] In complex instruction set computing (CISC) architectures, microcode plays a key role by decomposing intricate machine instructions into simpler, reduced instruction set computing (RISC)-like primitives that the underlying hardware can execute efficiently.[40] This layer enables the implementation of complex operations without requiring extensive hard-wired logic for each instruction. In some systems, microcode is writable, allowing post-fabrication modifications for bug fixes, performance enhancements, or emulation of legacy instructions across different processor models.[41] Microcode was introduced commercially on a large scale in the IBM System/360 family of mainframes, with some models using it starting in 1965, marking a significant advancement in flexible instruction implementation across compatible hardware.[42] Early designs distinguished between horizontal microcode, which provides detailed, unencoded control signals for direct hardware manipulation to maximize parallelism, and vertical microcode, which employs higher-level encoding to compact instructions at the cost of additional decoding overhead.[43] In modern processors, such as those implementing the Intel x86 architecture, microcode maintains backward compatibility with evolving instruction sets and supports security updates delivered via BIOS or firmware, including mitigations for vulnerabilities like Spectre released in 2018.[44] By shifting instructional complexity from custom hardware circuitry to programmable control sequences, microcode simplifies CPU design, reduces development time, and facilitates adaptability without full hardware redesigns.[39]Examples
Historical: IBM 709x
The IBM 709x series, exemplified by the IBM 7090 introduced in 1959, utilized a 36-bit word architecture optimized for scientific and engineering computations, with machine code instructions stored as binary patterns in core memory. This system played a pivotal role in early space exploration, powering NASA's Mercury and Gemini programs for real-time trajectory computations, flight simulations, and data processing at facilities like the Goddard Space Flight Center.[45][46] Instructions followed fixed formats within the 36-bit word, often comprising a 6-bit opcode, a 12-bit decrement field for modifying addresses, a 3-bit tag for indexing, and a 15-bit base address, allowing for operations like addition with operand specification. For instance, the add instruction (opcode 0400 in octal) adds the contents of memory to the accumulator register; for location 000, it is represented as 040000 in octal, demonstrating direct addressing; with indexing via tag 1 and indirect addressing flagged, it could become 041000 for modified effective address calculation.[47][48][49] Key features of the 709x machine code included fixed-length 36-bit instructions, with support for indirect addressing and indexing. The 7094 variant introduced instruction overlap features like Store Lookahead and Transfer Lookahead to enhance execution efficiency. Programming typically involved the FORTRAN Assembly Program (FAP), which translated symbolic code into these binary instructions.[47][50][51] The architecture's design influenced later minicomputer developments by emphasizing flexible addressing and high-speed arithmetic, with 709x systems remaining in use through the 1970s for specialized simulations in aerospace and research.[52][53] A brief example of machine code for a simple loop adding successive values (e.g., incrementing an accumulator and branching on zero) in octal representation might appear as follows, assuming a tagged decrement for loop control (note: simplified and verified against manuals):+050000 // CLA 0 (clear and add from location 0)
+760100 // TZE LOOP (transfer on zero to loop start, tag 1)
+011001 // ADD DECR (add decrement for index update; pseudo-op example)
LOOP: +040000 // ADD from [memory](/page/Memory) at 0 to AC
+050000 // CLA 0 (clear and add from location 0)
+760100 // TZE LOOP (transfer on zero to loop start, tag 1)
+011001 // ADD DECR (add decrement for index update; pseudo-op example)
LOOP: +040000 // ADD from [memory](/page/Memory) at 0 to AC
Modern: MIPS
The MIPS architecture, a 32-bit reduced instruction set computer (RISC) design originating from research at Stanford University in the early 1980s and commercialized by MIPS Computer Systems starting in 1984, employs fixed-length 32-bit instructions to enhance decoding simplicity and pipeline efficiency.[55][56] This uniform instruction length eliminates the need for variable-length parsing, distinguishing MIPS from complex instruction set computer (CISC) designs and facilitating high-performance execution in pipelined processors.[57] A representative MIPS instruction is the ADD operation, which adds the contents of two registers and stores the result in a third, such asADD $t0, $t1, $t2. In binary form, this encodes as 000000 01001 01010 01000 00000 100000, where the fields comprise a 6-bit opcode (000000 for R-type instructions), 5-bit source register rs (t2 as 01010), 5-bit destination register rd ($t0 as 01000), 5-bit shift amount (00000), and 6-bit function code (100000 for addition).[58][59]
MIPS exemplifies a load/store architecture, where arithmetic and logical operations occur exclusively on registers using a three-operand format (source1, source2, destination), while memory access is restricted to dedicated load (e.g., LW) and store (e.g., SW) instructions.[60][56] This design promotes register-intensive code for speed and has found widespread adoption in embedded systems like routers and digital devices, as well as consumer electronics including the Sony PlayStation console, which utilized the MIPS R3000 processor.[61][62] MIPS notably avoids overlapping instructions to support straightforward pipelining without complex hazard resolution.[57]
The architecture evolved with the MIPS64 extension in the 1990s, expanding registers and addresses to 64 bits for handling larger datasets while maintaining backward compatibility with 32-bit code.[56] In 2018, under Wave Computing, MIPS launched the MIPS Open initiative, providing royalty-free access to the 32-bit and 64-bit ISA specifications under proprietary terms to encourage adoption, but the program was discontinued in 2019 amid company financial difficulties. As of 2024, MIPS has ceased development of its proprietary ISA and shifted focus to RISC-V-based architectures.[63][64]
For illustration, consider a basic arithmetic sequence in machine code that loads two values into registers, adds them, and stores the result:
# LW $t1, 0($s0) ; Load first value (assume address in $s0)
100011 10000 01001 0000000000000000
# LW $t2, 4($s0) ; Load second value
100011 10000 01010 0000000000000100
# ADD $t0, $t1, $t2 ; Add them
000000 01001 01010 01000 00000 100000
# SW $t0, 8($s0) ; Store result
101011 10000 01000 0000000000001000
# LW $t1, 0($s0) ; Load first value (assume address in $s0)
100011 10000 01001 0000000000000000
# LW $t2, 4($s0) ; Load second value
100011 10000 01010 0000000000000100
# ADD $t0, $t1, $t2 ; Add them
000000 01001 01010 01000 00000 100000
# SW $t0, 8($s0) ; Store result
101011 10000 01000 0000000000001000
Variants and Related Concepts
Bytecode
Bytecode represents an abstract set of instructions that serves as a platform-independent intermediate representation, executed by a virtual machine (VM) rather than directly by hardware processors. This design allows compiled code to run on any system equipped with a compatible VM, abstracting away hardware-specific details. Notable implementations include Java bytecode, which powers the Java Virtual Machine (JVM), the Common Intermediate Language (CIL), utilized within Microsoft's .NET runtime environment, and WebAssembly (Wasm), a binary instruction format for a stack-based virtual machine executed in web browsers and other environments.[65] The origins of bytecode trace back to the UCSD Pascal system, introduced in 1978, where it was implemented as p-code—a portable intermediate code interpreted by a software-based p-machine VM to achieve machine independence across diverse hardware like the Intel 8080 and Zilog Z80.[66] This innovation enabled the "write once, run anywhere" paradigm, later popularized by Java, by compiling high-level source code into a single bytecode form that could be deployed without platform-specific recompilation, though it introduces runtime interpretation overhead compared to native execution.[67] In terms of structure, bytecode typically employs compact instructions with a one-byte opcode denoting the operation, followed by variable-length operands providing necessary data or references. Most systems, including the JVM, .NET CIL, and WebAssembly, adopt a stack-based operational model, where instructions likeiload (load an integer onto the stack from a local variable) and iadd (pop two integers from the stack, add them, and push the result) manage computation via an operand stack, contrasting with the register-based approaches in some native architectures.
Bytecode generation occurs through compilation from higher-level languages; for example, the Java compiler (javac) translates .java source files into .class files containing bytecode, which the JVM then processes at runtime either by direct interpretation or via just-in-time (JIT) compilation into native machine code for efficiency. Similarly, .NET compilers produce CIL assemblies that the Common Language Runtime (CLR) JIT-compiles as needed. WebAssembly modules are compiled from languages like C++ or Rust and executed via JIT or ahead-of-time compilation in supporting runtimes. This process supports portability but requires VM support, distinguishing bytecode from hardware-specific binaries.[68]
A fundamental distinction of bytecode lies in its non-dependence on specific CPU binaries and its emphasis on pre-execution verification for security and correctness; the JVM's bytecode verifier, for instance, statically analyzes code to enforce type safety, prevent stack underflows or overflows, and block unauthorized access, ensuring safe execution even for untrusted code. This verification step, absent in native machine code loading, mitigates risks in distributed environments while maintaining the portability that defines bytecode's role as an intermediate form between source code and hardware execution.
Object Code
Object code refers to the relocatable form of machine code generated during the compilation process, stored in object files such as .o files on Unix-like systems or .obj files on Windows. These files contain machine instructions that are not yet fully addressable, including sections like .text for executable code and .data for initialized variables, along with a symbol table that lists unresolved external references to functions or variables defined elsewhere.[69][70] The relocatable nature allows the code to be positioned at any memory address during linking, with relocation records specifying adjustments needed for addresses.[71] Common formats for object files include the Executable and Linkable Format (ELF), developed in the late 1980s for System V Release 4 Unix and formalized in the 1995 Tool Interface Standard specification, and the Portable Executable (PE) format, introduced with Windows NT in 1993 as an extension of the Common Object File Format (COFF).[70][72][73] Both formats feature headers that identify the target architecture, such as x86 or ARM, and include metadata for entry points, section tables, and symbol information to facilitate subsequent processing.[70][73] Object files are produced by compilers or assemblers from source code or assembly, capturing the machine code output in a modular form. The linker then combines multiple object files and libraries, resolving unresolved symbols by matching definitions across modules and adjusting addresses via relocation entries to produce a final executable. This process supports integration with libraries, such as static archives (.a files) that embed code directly into the executable or dynamic shared objects (.so files) that defer resolution to runtime.[74] For production builds, debugging symbols—line numbers, variable names, and other metadata in the object files—are often stripped to reduce file size and enhance security, using tools like the GNU strip utility. The concept of object code evolved from early relocatable loaders in the 1950s, such as those developed for IBM systems like the IBM 701, where programs were assembled into relocatable modules that loaders could position in memory without full reassembly.[75] This approach addressed the limitations of absolute addressing in early computers, enabling modular programming and reuse across jobs.[75]Implementation Aspects
Storage and Representation
Machine code is typically stored in executable file formats that encapsulate the binary instructions along with necessary metadata for loading and execution by the operating system. Common formats include the Executable and Linkable Format (ELF), used primarily on Linux and other Unix-like systems; the Mach-O format, employed by macOS and iOS; and the Common Object File Format (COFF), an older standard that influenced formats like the Portable Executable (PE) on Windows.[76][77][78] These formats include metadata such as magic numbers—unique byte sequences at the file's beginning to identify the type, like 0x7F 'E' 'L' 'F' for ELF—and section tables that delineate regions for code, data, and symbols, enabling the loader to map them appropriately into memory.[79] Self-describing executable formats, which embed sufficient information for independent loading without external tools, emerged in the 1970s with the a.out format in early Unix systems on the PDP-11.[79] Modern formats like ELF and Mach-O build on this by supporting position-independent code (PIC), where instructions use relative addressing rather than absolute locations, facilitating address space layout randomization (ASLR) to enhance security against exploits.[80] Object code, as a precursor compiled from source but not yet linked into a final executable, often resides in these formats before the linking stage produces the persistent binary.[79] In memory, machine code is loaded into RAM as a sequence of contiguous bytes representing the processor-specific instructions, organized into distinct segments: the code (or text) segment for executable instructions, the data segment for initialized global variables, and the stack segment for runtime local variables and function calls.[81] These segments are typically mapped to virtual address spaces by the operating system's loader, ensuring isolation and efficient access.[82] For representation and debugging, machine code is often displayed in hexadecimal dumps, where each byte corresponds to an opcode or operand; for instance, in x86 architecture, the byte 0x8B encodes the MOV instruction from memory to a register.[83] In resource-constrained embedded systems, machine code may undergo compression techniques, such as dictionary-based or arithmetic coding methods, to reduce storage footprint while allowing on-the-fly decompression during loading.[84] On disk, machine code persists as binary files within these executable formats, with integrity often verified through cryptographic hashing like SHA-256 during software distribution to detect tampering or corruption.[85] This hashing ensures that the binary matches the expected digest provided by the distributor, maintaining trustworthiness in deployment.[86]Execution Process
The execution of machine code occurs through the fetch-decode-execute cycle, a fundamental process in the von Neumann architecture where instructions and data share a common memory space, enabling sequential program execution.[87] This cycle, repeated continuously by the central processing unit (CPU), processes binary instructions stored in memory to perform computations and control operations.[88] In the fetch stage, the CPU retrieves the next instruction from memory using the program counter (PC), a special register that holds the memory address of the instruction to be executed.[89] The PC's value is placed on the address bus to access the instruction memory, loading the binary code into the instruction register (IR), after which the PC is incremented to point to the subsequent instruction.[5] This step ensures linear progression through the program unless altered by jumps or other control flows.[90] During the decode stage, the control unit interprets the opcode (operation code) and operands within the fetched instruction, generating control signals to configure the CPU's datapath components such as the arithmetic logic unit (ALU), registers, and memory interfaces.[88] The opcode specifies the operation (e.g., add or load), while operands indicate source registers or immediate values, allowing the control unit to route data accordingly for the impending execution.[90] In complex instruction set architectures (CISC), microcode may assist this decoding by translating instructions into simpler sequences, though the high-level cycle remains unchanged.[91] The execute stage performs the specified operation, such as ALU computations on register data or memory accesses, and updates the PC if the instruction involves branching.[5] Results are typically written back to registers or memory, completing the cycle unless an interrupt or exception intervenes.[88] In the von Neumann model, interruptions via hardware interrupts (e.g., from I/O devices) or exceptions (e.g., division by zero) suspend normal execution, saving the current PC and state before transferring control to a handler routine, which restores flow upon resolution.[92] Modern CPUs enhance this cycle through pipelining, overlapping stages across multiple instructions to increase throughput, typically dividing execution into five stages: fetch, decode, execute (ALU operations), memory (data access), and writeback (result storage to registers).[93] This approach, common in reduced instruction set computing (RISC) designs, allows one instruction to complete per cycle in ideal conditions, though hazards like data dependencies require stalling or forwarding.[94] Further optimizations include superscalar execution, where the CPU issues and executes multiple instructions simultaneously using parallel pipelines, as introduced in the Intel Pentium processor in 1993 with dual integer units capable of two operations per cycle.[95] Branch prediction mitigates pipeline delays from conditional jumps by speculatively fetching instructions based on historical patterns (e.g., assuming backward branches are taken), flushing the pipeline only on mispredictions to maintain high instruction-level parallelism in modern processors.[96] These techniques, building on the core cycle, enable CPUs to achieve effective execution rates far exceeding one instruction per cycle despite the underlying von Neumann bottlenecks.[97]Human Aspects
Readability Challenges
Machine code, consisting of binary or hexadecimal representations of processor instructions, presents significant readability challenges due to its lack of inherent context and structure. Unlike higher-level languages that use descriptive keywords and abstractions, machine code appears as opaque sequences of bits or digits, such as10110000 01100001 for an x86 instruction to move a value to a register, making it difficult to discern operations, data flows, or program intent without additional tools or documentation.[98] This dense packing of instructions, where each byte or word encodes multiple elements like opcodes, operands, and addressing modes, obscures logical flow and control structures, requiring readers to mentally reconstruct the program's semantics from raw numerical data.[99]
The cognitive load imposed by machine code is particularly high, as humans must track low-level details such as register states, memory addresses, and flag changes across sequences of instructions, a process that exceeds typical working memory capacity without aids. Research on binary program comprehension highlights how this low-level granularity compounds uncertainty, forcing analysts to infer higher-level behaviors from granular hardware interactions, which increases mental effort and error rates.[99] For instance, understanding a simple loop in machine code involves simultaneously monitoring accumulator values and jump conditions, a task that demands sustained attention and often leads to fatigue or misinterpretation.[100]
Historically, these challenges were even more pronounced in early computing, where programmers entered machine code directly via wired panels or toggle switches, as seen with the ENIAC in 1945, leading to highly error-prone processes that could take hours for short programs and frequent debugging via manual verification.[101] Such methods amplified risks, exemplified by the 1962 Mariner 1 rocket failure, caused by a software bug in the guidance equations—a missing overbar in the source code—leading to incorrect computations, veering the vehicle off course and necessitating its destruction shortly after launch.[102]
Studies indicate that assembly language, with its mnemonic representations, improves comprehension and productivity over direct binary manipulation, as it reduces the need for numerical memorization and allows focus on logic rather than bit-level encoding.[103] Despite this, machine code's inherent limitations persist, including the absence of high-level abstractions like variables or functions, which prevents intuitive mapping to problem domains and maintains a fundamental barrier to human readability even with partial mitigations like disassembly.[99]
