ENIAC
Used Decimal system
Initially used to find ballistic trajectory
Team adjusted knobs and final trajectory was pushed to Punch Card
EDVAC (1949):
switched to true binary 0 and 1 machine code and stored those 0s and 1s as acoustic waves inside tubes of liquid mercury
introduced the Stored-Program Concept. The instructions to calculate a trajectory were converted into digital binary code (0s and 1s) and saved directly into the computer's memory
Assembly
replaced raw binary instructions with short, memorable text commands called mnemonics. Instead of typing 00001011, the programmer typed ADD
Hand written
To a human, an assembly instruction looks like a short text command (e.g., mov rax, 42). But to a computer CPU, an instruction is nothing more than a fixed-size sequence of 1s and 0s (a binary number) grouped into specific structural patterns called fields. [1, 2, 3, 4]
Here is how a machine code instruction is structured and decoded by the CPU hardware. [5, 6]
The Anatomy of an Instruction
Every instruction is broken up into distinct logical pieces. The most basic blueprint contains two primary components:
┌─────────────────────────────────┐
│ INSTRUCTION │
├────────────────┬────────────────┤
│ OPCODE │ OPERAND │
│ (What to do) │ (What to do to)│
└────────────────┴────────────────┘
- The Opcode (Operation Code): The unique binary ID that tells the CPU what math or logic circuit to activate (e.g., Add, Subtract, Load, Jump). [9, 10, 11, 12, 13]
- The Operand(s): The data fields that tell the CPU where to find the source inputs or where to place the output result (e.g., specific registers, or a memory address). [14, 15, 16, 17, 18]
A Real-World Example: Adding Two Numbers
Let’s look at exactly what a real instruction looks like on an x86_64 processor (Intel or AMD).
Suppose we want to add the number 5 directly to the EBX register. [19]
1. The Assembly Code (Human Readable)
2. The Machine Code (Hexadecimal Format)
Engineers often read binary using Hexadecimal shorthand to keep it concise:
3. The Pure Machine Code (What the CPU Actually Reads)
When loaded into RAM, it is exactly 3 bytes of raw binary voltage pulses:
10000011 11000011 00000101
How the CPU Decodes Those Bits
When the CPU's Instruction Decoder pulls those 24 bits (10000011 11000011 00000101) into its processing pipeline, it instantly breaks them down into hardware triggers: [20, 21]
10000011 (Byte 1: Opcode Matrix)
The CPU reads the first few bits and recognizes this specific number matches the hardwired hardware circuit for an "ADD operation involving an immediate value and a register." [22, 23, 24] 11000011 (Byte 2: ModR/M Field)
This byte is split into sub-blocks that point to target locations. The bits 011 explicitly route the output connection wires directly to the EBX Register hardware slot on the chip.00000101 (Byte 3: Immediate Data)
This is the raw, literal binary number 5 (4 + 1). The CPU channels this value directly into its Arithmetic Logic Unit (ALU) to combine with whatever is already sitting inside the EBX register.
RISC vs. CISC Instruction Sizes
Depending on the architecture of your device, instructions look physically different in memory:
- ARM / RISC (Apple Silicon, Smartphones): Uses Fixed-Length instructions. Every single instruction is exactly 32 bits (4 bytes) long. This makes it incredibly easy for the CPU to stream instructions fast and save battery power because it always knows exactly where the next instruction begins. [25, 26, 27, 28, 29]
- x86 / CISC (Intel, AMD PCs): Uses Variable-Length instructions. An instruction can be as short as 1 byte (like
0x90 for NOP/No-Operation) or as long as 15 bytes for incredibly complex memory-routing math. This makes the chip design highly complex but allows for dense, tightly packed code programs. [30, 31, 32, 33, 34]
Core Features of C
- Procedural and Structured: Programs are organized using distinct, reusable blocks called functions.
- Low-Level Hardware Access: Allows direct manipulation of computer memory registers and hardware addresses.
- Pointers: Variables that store the exact memory addresses of other variables for high-speed data handling.
- Statically Typed: All variable data types (like integers or characters) must be declared and checked before compiling.
- Extremely Portable: Source code can be easily compiled for completely different computer architectures with minimal changes.
- Minimalist Standard Library: Focuses on raw performance, avoiding heavy built-in features to keep the runtime footprint tiny.
New Features in C++ (On Top of C)
- Object-Oriented Programming (OOP): Introduced Classes and Objects to group data variables and functions together into single units.
- Encapsulation & Access Control: Added keywords (
public, private, protected) to secure data from unauthorized modification. - Polymorphism (Virtual Functions): Enables a single command to behave differently based on the specific type of object executing it.
- Inheritance: Allows a new class to automatically adopt the properties and code of an existing class, reducing duplicate work.
- Constructors & Destructors: Automatically allocate memory when an object is created and safely clean it up when deleted, preventing memory leaks.
- Function & Operator Overloading: Allows multiple functions to share the exact same name if they use different inputs, and lets standard symbols (like
+) manipulate user-defined objects. - References (
&): Introduced a safer, cleaner alternative to C pointers that acts as an alias for an existing variable without the risks of manual address arithmetic. - Standard Template Library (STL): Provided a massive, pre-built collection of ready-to-use data structures (like vectors, lists, and queues) and algorithms out of the box.
The C Compilation Process (4 Key Steps)
When you run a command like gcc main.c, the compiler executes four distinct steps behind the scenes to turn human-readable C code into a runnable binary executable file.
[ .c Source Code ]
│
▼ (Step 1) PREPROCESSING (Expands macros, includes headers)
[ .i Expanded Source ]
│
▼ (Step 2) COMPILATION (Translates C to Assembly)
[ .s Assembly Code ]
│
▼ (Step 3) ASSEMBLY (Translates Assembly to Binary Object)
[ .o / .obj Object Code ] ───▶ [ Linker ] ◀─── [ Pre-compiled Libraries ]
│
(Step 4) ▼ LINKING (Stitches blocks together)
[ Executable Binary ]
Step 1: Preprocessing
- What happens: The preprocessor (
cpp) cleans up the raw text of your .c source code file. - The actions: It strips away comments, copies the contents of header files (like
#include <stdio.h>) directly into the file, and replaces macro shorthand (like #define PI 3.14) with its actual value. - Output: A temporary, expanded pure text file (usually ending in
.i).
Step 2: Compilation
- What happens: The compiler core (
cc1) translates the high-level syntax of the preprocessed text file into low-level machine logic. - The actions: It analyzes syntax errors, maps data variables to hardware registries, and outputs the code using architecture-specific computer instructions.
- Output: An assembly language text file tailored to your specific CPU (usually ending in
.s).
Step 3: Assembly
- What happens: The assembler (
as) converts human-readable assembly instructions into raw machine language instructions. - The actions: It translates commands like
MOV or ADD directly into binary operational codes (opcodes). - Output: An unlinked machine-code binary file called an Object File (usually ending in
.o or .obj). This file cannot run yet because it contains placeholder addresses for unresolved functions.
Step 4: Linking
- What happens: The linker (
ld) stitches independent code blocks into a single, cohesive, self-contained system executable file. - The actions: If your program calls a standard library routine (like
printf), the linker finds its pre-compiled binary code inside the operating system library paths and links it. It also merges multiple user-written .o files into one layout and resolves memory addresses. - Output: The final executable file (e.g.,
a.out, main, or main.exe).
What Changed in this Process for C++?
The fundamental structural stages (Preprocessing $\rightarrow$ Compilation $\rightarrow$ Assembly $\rightarrow$Linking) remain identical in C++. However, because C++ introduces objects, classes, and function overloading, the compiler and linker had to adopt three major procedural changes on top of the standard C pipeline:
1. Name Mangling (During Compilation)
- The Challenge: In C, every function must have a completely unique name. In C++, you can have three distinct functions all named
print() as long as they accept different data inputs (Function Overloading). - The Change: To prevent the assembler and linker from getting confused by matching names, the C++ compiler automatically rewrites function names behind the scenes. A function like
print(int x) is mangled into something unique like _Z5printi. This ensures the linker can uniquely match function calls to their exact binary definitions.
2. VTable Generation (During Compilation)
- The Challenge: C++ supports runtime polymorphism, meaning a single function call to a base class might need to execute entirely different code depending on which inherited object subclass is active at runtime.
- The Change: The C++ compiler generates a hidden lookup array called a Virtual Table (VTable) for classes containing virtual functions. Instead of hardcoding a direct memory address during compilation, it emits instructions telling the processor to look up the target address inside the VTable at execution time.
3. Object Construction & Destructor Injections (During Compilation)
- The Challenge: C++ objects require precise memory initialization upon creation and automatic memory cleanup when they fall out of scope.
- The Change: The compiler steps through your functions and silently injects low-level constructor and destructor instructions directly into the assembly pipeline. It ensures that memory allocations, pointer tracking, and variable initialization happen automatically without requiring explicit manual code updates from the developer.
When you run a compiled C or C++ application, the CPU executes the raw machine instructions directly. However, whenever your program needs to interact with the outside world (like printing text, allocating memory, or reading a file), it cannot talk to the hardware directly; it must ask the Operating System (OS) for help via system calls. [1, 2, 3, 4, 5]
🪵 The Division of Labor
[ Your C/C++ Code ]
│
├──► Pure Math / Logic ──────► [ Direct CPU Execution ]
│
└──► Input/Output / Memory ──► [ OS Kernel API ] ──► [ Hardware / CPU ]
1. What C/C++ Executes DIRECTLY on the CPU
For standard logic, math, and data manipulation, C and C++ completely bypass the operating system. The compiler translates these lines directly into raw machine instructions. [6, 7]
- Examples: Adding numbers, loops (
for/while), array manipulation, sorting algorithms, and basic logic switches (if/else). - How it looks:
int a = 5;
int b = 10;
int c = a + b; // Compiles to a single CPU instruction: 'add'
- Performance: This runs at the absolute maximum speed of your processor chip because there is zero OS overhead. [8, 9, 10, 11, 12]
2. What C/C++ Must Route Through the OS API
The CPU operates in different security privileges. Normal applications run in User Mode (restricted access). The OS kernel runs in Kernel Mode (full hardware access). If your C++ code tries to touch hardware or cross process boundaries without the OS, the CPU will trigger a security fault and kill your program. [13, 14, 15, 16, 17]
- Examples: Writing to the screen (
std::cout), reading a file, connecting to the internet, or requesting heap memory (new or malloc). - How it looks:
- Under the Hood: The C++ Standard Library wraps this nice command into a low-level OS API request (e.g.,
write() on Linux, WriteFile() on Windows). The CPU handles a Software Interrupt, safely hands execution control over to the OS kernel, the OS interacts with the monitor hardware, and then control is handed back to your program. [18, 19, 20, 21, 22]
🔍 A Concrete Example: Allocating Memory
To see exactly how this hybrid approach works, look at what happens when you create a dynamic array in C++:
int* arr = new int[1000]; // 1. Calls OS API (Requires permission for memory)
arr[0] = 42; // 2. Direct CPU Instruction (Blazing fast math)
new int[1000]: Your program does not own the computer's physical RAM. The C++ runtime must drop down and call an OS memory allocation API (like brk or mmap on Linux). The OS updates its virtual page tables, grants your program a safe block of addresses, and returns control to C++. [23, 24] arr[0] = 42: Now that your program safely owns that block of memory, assigning the value 42 bypasses the OS entirely. The compiler converts this line into a raw mov (move) assembly instruction. The CPU moves the binary digits straight into its hardware cache registers.
🔓 Exception: Bare-Metal Coding (No OS)
There is one major exception to this rule: Embedded Systems. [25]
If you are writing C or C++ for an Arduino, a microwave controller, a car engine computer, or writing an Operating System kernel from scratch, there is no OS API to call. [26]
In these environments, C and C++ compilers are configured to interface 100% directly with the CPU and memory registers. Writing to an output pin or an LCD display means using pointers to overwrite specific, physical hardware memory addresses mapped directly to the processor's pins.
Because Java was designed to solve the problem of "Write Once, Run Anywhere" (WORA), it introduced a dual-stage execution model. It sits completely on top of a runtime engine written in C++, which handles the heavy lifting of interacting with the physical operating system.
Here is the exact breakdown of how this pipeline operates under the hood:
1. The Compilation Phase (Human to Bytecode)
- What Happens: When a developer writes code in a
.java file and compiles it (using javac), the compiler does not generate machine code or assembly for a real CPU. - The Output: It outputs a
.class file containing Bytecode. Bytecode is a highly optimized set of instructions designed for a hypothetical, universal computer—the JVM.
2. The JVM: Built on C++
- The Layer: The standard JVM (Oracle’s HotSpot) is a massive software application written primarily in C++ (along with some low-level Assembly).
- The Subsystem: Because it is built in C++, it has direct access to OS system calls, memory management, and threads. The JVM acts as a buffer shield between your Java code and the operating system.
3. The Execution Phase (Bytecode to Assembly)
When you run a Java application, the C++ engine of the JVM processes the bytecode using two distinct execution strategies:
[ .class Bytecode ]
│
▼
┌─────────────── JVM (Built in C++) ────────────────┐
│ │
│ ──▶ [ Interpreter ] ──▶ Translates line-by-line │
│ │
│ ──▶ [ JIT Compiler ] ─▶ Translates hot loops │
│ directly to Native Asm │
└───────────────────────────────────────────────────┘
│
▼
[ Native OS / CPU Assembly ]
- The Interpreter: The JVM reads the bytecode instructions line-by-line and translates them into corresponding C++ runtime functions, which then execute on the OS. This gets the program running instantly but is relatively slow.
- The JIT (Just-In-Time) Compiler: To speed things up, the JVM monitors the running code. If it finds a section of bytecode that is executed repeatedly (a "hot spot"), the JIT compiler instantly compiles that bytecode block directly into native CPU assembly code in memory, completely bypassing interpretation for subsequent loops.
Why Was This Shift Away From Native C++ Necessary?
| Metric | C++ Strategy | Java / JVM Strategy |
|---|
| Target | Compiles down to a specific OS/CPU architecture. | Compiles down to universal intermediate Bytecode. |
| Portability | Must recompile the entire codebase for every single platform. | The same .class file runs unmodified on any machine with a JVM. |
| Memory | Manual memory allocation (malloc, new, delete). | Automatic Garbage Collection handled by the C++ engine. |
| Safety | Crash-prone if pointers access illegal memory spaces. | Sandboxed environment prevents direct malicious memory corruption. |
1. Python is Built on C, Not C++
The standard, official version of Python that everyone downloads from Python.org is called CPython. As the name suggests, the entire Python executable, its runtime environment, and its core libraries are written in
pure C, not C++. [
1,
2,
3]
(Note: There are alternative versions of Python like PyPy, or Jython which is written in Java, but the standard engine is completely C-based). [
1,
2]
2. Python Does Not Convert Code to Assembly
Unlike a C++ compiler, the standard Python interpreter
never converts your Python code into CPU assembly language or machine code instructions. [
1,
2]
Instead, the process looks like this:
[ .py Source Code ]
│
▼ (Step 1: Automated Compilation)
[ .pyc Bytecode ] ──▶ Stored in __pycache__ folder
│
▼ (Step 2: Interpretation Loop)
┌─────────────── CPython Engine (Written in C) ──────────────┐
│ │
│ Reads Bytecode instructions one-by-one │
│ │
│ [ Bytecode: BINARY_ADD ] ──▶ Evaluates via C functions │
│ │
└────────────────────────────────────────────────────────────┘
│
▼
[ Directly Triggers Pre-Compiled C Executable Logic on CPU ]
Step 1: The Hidden Compilation (Source to Bytecode)
When you run a Python script, it doesn't immediately read the text line-by-line. First, Python’s internal C-compiler parses your entire
.py file and translates it into an intermediate low-level language called
Bytecode (compiled
.pyc files found in your
__pycache__ directory). [
1,
2]
Step 2: The Virtual Machine Loop (No Assembly Needed)
This bytecode is handed over to the
Python Virtual Machine (PVM). The PVM is just a massive loop written in C. It reads the bytecode instructions one at a time: [
1,
2]
- If your bytecode says
BINARY_ADD, the PVM does not generate an assembly ADD instruction for your computer chip. - Instead, the PVM calls a pre-compiled C function (like
PyNumber_Add()) that has already been turned into machine code when Python itself was installed on your computer.
Summary: Why This Matters
Because Python relies on a C-runtime engine to evaluate instructions on the fly rather than translating your code directly into native CPU assembly, it achieves incredible cross-platform flexibility. However, this extra layer of runtime interpretation is also the exact reason why Python runs significantly slower than pure compiled languages like C or C++. [
1,
2,
3]