├── README.md ├── C └── readme.md └── assembly └── README.md /README.md: -------------------------------------------------------------------------------- 1 | This guide is a journey from low-level to high-level programming. 2 | It starts at the metal—assembly, memory, registers—and climbs up through C, Rust, and modern languages, exploring how things work under the hood, how to write efficient code, and how to truly understand what your programs are doing. Whether you're debugging raw opcodes or writing sleek high-level abstractions, there's something here for you. 3 | -------------------------------------------------------------------------------- /C/readme.md: -------------------------------------------------------------------------------- 1 | # From Bare Metal to C: Low-Level Foundations of High-Level Programming 2 | 3 | Programming has come a long way from the days of manually toggling bits and writing raw machine code. Yet, even today’s **high-level code ultimately boils down to low-level operations** on hardware. In this article, we’ll take an in-depth journey from the hardware’s perspective up through the C programming language – often dubbed a “portable assembly” – and see how C acts as a bridge between **register-level machine execution and high-level software design**. We’ll dive into how modern CPUs execute instructions, how C maps to those operations, what the C compilation process entails, and how to write efficient C code by **“thinking like the machine.”** Along the way, we’ll explore memory management, performance optimization, debugging at the assembly level, and best practices to avoid the common pitfalls of low-level programming. 4 | 5 | Experienced developers will find a conversational yet technically rich discussion, complete with code snippets, assembly excerpts, diagrams, and compiler insights. Let’s start at the very bottom: the hardware foundation that underpins all of our code. 6 | 7 | ## The Hardware Foundation 8 | 9 | At the lowest level, everything a computer does is powered by the CPU and memory. Understanding how a CPU works with memory gives valuable insight into how languages like C operate under the hood. 10 | 11 | ### CPU Instructions and Registers 12 | 13 | A modern CPU executes instructions in a cycle of **fetching, decoding, and executing** operations. At any moment, the CPU is reading instructions (machine code) from memory, decoding them to determine the operation, and performing that operation using its internal circuits and registers ([Chapter  3. Computer Architecture](https://bottomupcs.com/ch03.html#:~:text=The%20CPU%20executes%20instructions%20read,are%20two%20categories%20of%20instructions)) ([Chapter  3. Computer Architecture](https://bottomupcs.com/ch03.html#:~:text=Executing%20a%20single%20instruction%20consists,fetching%2C%20decoding%2C%20executing%20and%20storing)). The CPU’s **registers** are small storage slots built into the processor – think of them as the CPU’s own variables. Practically all arithmetic or logical operations happen in registers. There are generally two kinds of instructions the CPU runs ([Chapter  3. Computer Architecture](https://bottomupcs.com/ch03.html#:~:text=The%20CPU%20executes%20instructions%20read,are%20two%20categories%20of%20instructions)): 14 | 15 | - **Load/Store instructions:** These transfer data between memory and registers (for example, loading a value from a memory address into a register, or storing a register’s value back to memory). 16 | - **Operate instructions:** These perform computations on data in registers (for example, adding the values of two registers, bitwise ANDing bits, etc.). 17 | 18 | In essence, to do anything useful, the CPU first **loads data from RAM into registers**, operates on it (addition, multiplication, bit shifts, etc.), and then **stores results back to memory** if needed ([Chapter  3. Computer Architecture](https://bottomupcs.com/ch03.html#:~:text=The%20CPU%20executes%20instructions%20read,are%20two%20categories%20of%20instructions)). For example, adding a number to a value in memory would internally involve loading that memory value into a register, adding the number to it using the CPU’s arithmetic logic unit (ALU), and then storing the result back out to memory ([Chapter  3. Computer Architecture](https://bottomupcs.com/ch03.html#:~:text=1,values%20from%20registers%20to%20memory)). 19 | 20 | The CPU can only directly manipulate data that’s in its registers, which are extremely fast but limited in number and size (often 32-bit or 64-bit). Memory (RAM) is much larger but also much slower than registers. This is why **efficient programs minimize memory access** – once data is in registers, the CPU can crunch away at high speed. Modern CPUs even have multiple execution units and pipelines so they can process several instructions concurrently, but the fundamental model of moving data in/out of registers and performing operations remains true ([Chapter  3. Computer Architecture](https://bottomupcs.com/ch03.html#:~:text=The%20CPU%20executes%20instructions%20read,are%20two%20categories%20of%20instructions)) ([Chapter  3. Computer Architecture](https://bottomupcs.com/ch03.html#:~:text=)). 21 | 22 | ### Memory, Stack, and Heap: The Lifeblood of Execution 23 | 24 | When a program runs, the system carves out different regions of memory for different purposes. Broadly, we can think of three key areas of memory in a typical program’s process space ([Making Sense of Stack and Heap Memory | by Niraj Ranasinghe | Xeynergy Blog](https://blog.xeynergy.com/making-sense-of-stack-and-heap-memory-b13cda940bbc#:~:text=When%20your%20computer%20runs%20an,in%20how%20your%20application%20works)): 25 | 26 | - **Code (Text) Segment:** This is where the compiled machine code instructions of your program reside. The CPU fetches instructions from here. 27 | - **Stack Segment (Call Stack):** A region of memory that handles function call management and **local variables**. The stack operates as a Last-In-First-Out (LIFO) structure – each time a function is called, a *stack frame* (also called an activation record) is pushed onto the stack to hold that function’s local variables, return address, and bookkeeping info. When the function returns, its frame is popped off the stack ([Making Sense of Stack and Heap Memory | by Niraj Ranasinghe | Xeynergy Blog](https://blog.xeynergy.com/making-sense-of-stack-and-heap-memory-b13cda940bbc#:~:text=The%20stack%20represents%20a%20reserved,transition%20between%20different%20execution%20contexts)). This automatic allocation and deallocation makes the stack very efficient for managing **scope and control flow**. 28 | - **Heap Segment:** A region for **dynamic memory allocation**. Unlike the stack, the heap is a more flexible pool of memory where the program can request and free chunks of memory at runtime (e.g., via C’s `malloc` and `free`). Memory on the heap persists until freed (or until the program ends), allowing data to outlive the function that created it. The heap does not have the neat LIFO discipline; you can allocate and free in arbitrary order, which is powerful but requires the programmer to manage it explicitly ([Making Sense of Stack and Heap Memory | by Niraj Ranasinghe | Xeynergy Blog](https://blog.xeynergy.com/making-sense-of-stack-and-heap-memory-b13cda940bbc#:~:text=Heap)). 29 | 30 | Each part plays a specific role in program execution ([Making Sense of Stack and Heap Memory | by Niraj Ranasinghe | Xeynergy Blog](https://blog.xeynergy.com/making-sense-of-stack-and-heap-memory-b13cda940bbc#:~:text=When%20your%20computer%20runs%20an,in%20how%20your%20application%20works)). The **stack** grows and shrinks as functions are called and return, tracking the “depth” of our function call hierarchy and providing a place for each function’s local data ([Making Sense of Stack and Heap Memory | by Niraj Ranasinghe | Xeynergy Blog](https://blog.xeynergy.com/making-sense-of-stack-and-heap-memory-b13cda940bbc#:~:text=The%20stack%20represents%20a%20reserved,transition%20between%20different%20execution%20contexts)). The **heap** grows (and can also shrink) as the program requests memory for dynamic structures like dynamically sized arrays, linked lists, trees, etc. ([Making Sense of Stack and Heap Memory | by Niraj Ranasinghe | Xeynergy Blog](https://blog.xeynergy.com/making-sense-of-stack-and-heap-memory-b13cda940bbc#:~:text=Heap)). Meanwhile, the CPU continuously executes instructions from the **code segment**, operating on data that might be in registers, the stack, or the heap. 31 | 32 | **Function Call Mechanics (Stack Frames):** When a function is invoked, the system uses the stack to manage the call. For instance, on a typical x86 architecture, what happens under the hood is roughly: 33 | 34 | 1. The caller evaluates function arguments and pushes them to the stack (for x86) or places them in registers (on x86-64, the first several arguments go in registers like RDI, RSI, etc.). 35 | 2. The CPU’s `CALL` instruction is executed, which pushes the **return address** (the address of the next instruction in the caller, so the CPU knows where to come back) onto the stack, and then jumps to the function’s start address. 36 | 3. Upon entering the function, a *prologue* sequence runs: often the function will push the old base/frame pointer (EBP/RBP on x86) onto the stack and then set the base pointer to the current stack pointer. This creates a new frame so the function can reference its arguments and locals at fixed offsets from the base pointer. It may also decrement the stack pointer (`SUB` instruction) to reserve space for local variables. At this point, the stack frame for the function is established ([Cracking Assembly — Stack Frame Layout in x86 | by Sruthi K | Medium](https://medium.com/@sruthk/cracking-assembly-stack-frame-layout-in-x86-3ac46fa59c#:~:text=As%20we%20know%2C%20stack%20grows,point%20to%20this%20stack%20location)). 37 | 4. Inside the function, local variables are accessed as offsets from the base pointer (or stack pointer). For example, on 32-bit x86, a local int might live at `-4(%ebp)` (4 bytes below the base pointer), whereas a function argument might be at `8(%ebp)` (just above the saved return address) ([Cracking Assembly — Stack Frame Layout in x86 | by Sruthi K | Medium](https://medium.com/@sruthk/cracking-assembly-stack-frame-layout-in-x86-3ac46fa59c#:~:text=another%20function%2C%20it%20first%20pushes,point%20to%20this%20stack%20location)). The CPU simply treats these as memory accesses at specific addresses. 38 | 5. When the function is ready to return, it loads the return value in the appropriate register (e.g., EAX/RAX for integers on x86/x64), then executes an *epilogue*: typically restoring the saved base pointer (`POP EBP`) and executing the `RET` instruction. `RET` will pop the saved return address off the stack and jump to it, thus returning control to the caller. The stack is automatically cleaned up in this process (or in some conventions the caller cleans part of it). 39 | 40 | To illustrate, here’s a simplified example of what assembly instructions for a function prologue and epilogue might look like in x86-64 assembly (AT&T syntax): 41 | 42 | ```asm 43 | pushq %rbp # save caller's base pointer 44 | movq %rsp, %rbp # set this function's base pointer 45 | subq $16, %rsp # allocate 16 bytes for local variables 46 | 47 | ... (function body) ... 48 | 49 | addq $16, %rsp # free local variable space 50 | popq %rbp # restore caller's base pointer 51 | retq # return to caller (pop return address and jump) 52 | ``` 53 | 54 | Every function call thus results in a new stack frame being pushed. The **call stack** not only holds local data, but also provides the mechanism to return to the correct place and to trace the nested calls. If you ever use a debugger to get a backtrace, it’s essentially walking these saved frame pointers and return addresses to list which functions called which. 55 | 56 | **Control Flow at the Machine Level:** High-level constructs like loops and conditionals are implemented with **branch instructions** at the CPU level. A simple `if` statement in C might compile to a compare instruction (or an arithmetic instruction affecting CPU flags) followed by a conditional branch (jump) instruction. For example, an `if (x == 0)` check might compile into an instruction that tests if `x` is zero (this could be done by OR-ing `x` with itself, which sets the zero flag if `x` was zero) and then a `JE` (Jump if Equal) or `JZ` (Jump if Zero) instruction to branch to the appropriate block ([Chapter  3. Computer Architecture](https://bottomupcs.com/ch03.html#:~:text=Apart%20from%20loading%20or%20storing%2C,loops%20and%20decision%20statements%20work)) ([Chapter  3. Computer Architecture](https://bottomupcs.com/ch03.html#:~:text=For%20example%2C%20a%20statement%20like,branch%20past%20the%20body%20code)). Similarly, loops use a combination of compare/test instructions and jumps (or sometimes specialized loop instructions) to repeat sections of code. 57 | 58 | In summary, **“control flow” is just fancy talk for changing the CPU’s instruction pointer**. Normally, the CPU’s instruction pointer (program counter) just increments sequentially, executing instructions one by one ([Chapter  3. Computer Architecture](https://bottomupcs.com/ch03.html#:~:text=Apart%20from%20loading%20or%20storing%2C,loops%20and%20decision%20statements%20work)). A *branch* instruction modifies the instruction pointer to jump to a new location, implementing behaviors like “if condition is true, go to this part of code, else skip it” or “jump back to the start of the loop to repeat.” This is how constructs like `if/else`, `while`, `for`, and `switch` are realized in machine code – via combinations of conditional and unconditional jumps that alter the flow of execution. 59 | 60 | ### Putting It Together: An Example 61 | 62 | Let’s tie these concepts together with a quick example in C and what it means at the low level. Consider this simple C function: 63 | 64 | ```c 65 | int add(int a, int b) { 66 | int sum = a + b; 67 | if (sum == 42) { 68 | return 0; 69 | } 70 | return sum; 71 | } 72 | ``` 73 | 74 | At the machine level (on an x86-64 system using System V ABI), a possible translation might be: 75 | 76 | - The integers `a` and `b` are passed in registers (EDI and ESI respectively on x86-64 for 32-bit ints). 77 | - The function sets up a stack frame (push rbp; mov rbp, rsp). 78 | - It performs the addition: `addl %esi, %edi` (adding `b` into `a`, with the result in EDI). 79 | - It moves the result into a local storage or a register (in this case, EDI already has the sum). 80 | - Compares the sum with 42: `cmpl $42, %edi`. 81 | - If equal, jumps to a section that loads 0 into the return register and goes to the function epilogue. 82 | - Otherwise, it just continues to the epilogue, where `%edi` (holding the sum) is typically already the return value. 83 | - Cleans up stack frame and returns. 84 | 85 | This is a rough sketch, but it demonstrates how closely C code maps to straightforward sequences of loads, stores, ALU operations, and branches. The **C compiler’s job is to translate high-level constructs into these low-level instructions**. As we’ll see, this is why C is often considered only a thin abstraction over assembly. 86 | 87 | ## C as a “Portable Assembly” 88 | 89 | One of the biggest reasons C became so popular is that it gives programmers **low-level control and high performance** while still being (somewhat) easier to write and maintain than pure assembly. C has been called a “portable assembly language” because C code can run on almost any machine (thanks to compilers for different architectures) but still provides **minimal abstractions** over the hardware ([C | Computer vision | Fandom](https://computervision.fandom.com/wiki/C#:~:text=C%20is%20a%20relatively%20minimalist,it%20operates%20with%20the%20hardware)). 90 | 91 | ### Close to the Metal 92 | 93 | By design, C is a relatively minimalist language that operates close to the hardware. There’s no heavy runtime or interpreter standing between your C code and the machine – once compiled, your C program *is* machine code running directly on the CPU. This was a deliberate design choice: C was created to write operating systems and system software, so it needed to be **efficient** and **able to access hardware features** directly ([C | Computer vision | Fandom](https://computervision.fandom.com/wiki/C#:~:text=C%20is%20a%20relatively%20minimalist,it%20operates%20with%20the%20hardware)). Many languages (especially those developed after C) introduce features like garbage collection, extensive runtime type checking, or virtual machines, which add convenience but incur overhead. C opts to give the programmer **both the power and the responsibility** – you manage memory manually, you ensure indices are in range, etc. – which allows compiled C code to be as performant as hand-written assembly in many cases. 94 | 95 | In fact, C’s design was so successful in this regard that it’s been used as an **intermediate language** by many compilers for other languages. Instead of generating assembly directly, some compilers output C code, which is then compiled with a C compiler for portability. As Dennis Ritchie (co-creator of C) noted, C has been widely used as an intermediate representation “essentially, as a portable assembly language” for a variety of compilers ([](https://www.nokia.com/bell-labs/about/dennis-m-ritchie/chist.pdf#:~:text=an%20intermediate%20representation%20,Meyer%2088)). This speaks to C’s close-to-the-metal nature and universal availability. 96 | 97 | ### C Maps to Hardware Operations 98 | 99 | Almost every construct in C has a predictable lower-level representation: 100 | 101 | - **Basic data types** (`int`, `char`, `float`, etc.) correspond to fundamental hardware-supported types (integers of various sizes, floating-point numbers as supported by the FPU). For example, an `int` might be 32 bits, which the CPU can load into a 32-bit register and add in one instruction. 102 | - **Pointers** in C are essentially addresses in memory. If you have a pointer to an `int` (e.g. an `int *p`), you can think of it as “this register holds a memory address, which should point to a 4-byte integer value.” Using the pointer (e.g. `*p`) causes the program to load from or store to that memory address. Pointer arithmetic is defined in terms of the size of the pointed-to type – adding 1 to an `int*` advances the address by 4 bytes (assuming 4-byte ints) to point to the next integer. In assembly, this is just integer arithmetic on addresses. For instance, `p + 5` translates to the address `p + 5*4` in bytes ([ Pointers and Memory Allocation 103 | ](https://ics.uci.edu/~dhirschb/class/165/notes/memory.html#:~:text=%60%20b,is%20merely%20shorthand%20for%20this)), and `*(p + 5)` would cause a memory access at that computed address ([ Pointers and Memory Allocation 104 | ](https://ics.uci.edu/~dhirschb/class/165/notes/memory.html#:~:text=%60%20b,is%20merely%20shorthand%20for%20this)). 105 | - **Arrays** are tightly related to pointers. In C, an array is laid out as a contiguous block of memory. Accessing `arr[i]` is internally computed as *(arr + i)* – i.e., start at the beginning address of `arr` and offset by `i` elements ([ Pointers and Memory Allocation 106 | ](https://ics.uci.edu/~dhirschb/class/165/notes/memory.html#:~:text=%60%20b,is%20merely%20shorthand%20for%20this)). The “subscript” operation is essentially pointer arithmetic followed by a memory access. This contiguous layout means arrays are very efficient and predictable: element `i` is exactly `i * sizeof(element)` bytes from the start. The C standard guarantees this contiguous layout for arrays. 107 | - **Structures (`struct`)** in C are aggregates of other types, and C guarantees that struct members are stored in memory in the order they are declared (with potential padding in between, which we’ll discuss later). This means a struct can often be treated as a simple block of memory where different byte offsets correspond to different fields. In fact, this is why C has been a popular choice for systems programming – you can map hardware registers or file formats directly onto structs and access the fields, because you know the memory layout. For example: 108 | ```c 109 | struct Color { 110 | uint8_t red; 111 | uint8_t green; 112 | uint8_t blue; 113 | }; 114 | ``` 115 | This struct will occupy 3 bytes (maybe 4 with padding) and you know `red`, `green`, `blue` will be adjacent in memory in that order. Writing `myColor.green = 0xFF;` will simply write a byte value to an address that is 1 byte offset from the start of the struct. 116 | 117 | Because of this transparent mapping, C code often corresponds *line-by-line* with a few machine instructions. A statement like `x = y + z;` where x, y, z are integers might compile to a single CPU instruction (ADD) that operates on registers (assuming y and z are in registers). An array index like `arr[i]` becomes a multiplication and addition to compute an address, then a load/store. A function call becomes an actual series of push/jump instructions. There isn’t a lot of “magic” hidden behind the scenes. 118 | 119 | This is why C is prized for its efficiency and why it’s still heavily used in systems programming (operating systems, embedded systems, compilers, etc.) ([C | Computer vision | Fandom](https://computervision.fandom.com/wiki/C#:~:text=The%20C%20programming%20language%20is,C%20Code%20Examples)) ([C | Computer vision | Fandom](https://computervision.fandom.com/wiki/C#:~:text=C%20is%20a%20relatively%20minimalist,it%20operates%20with%20the%20hardware)). You can more or less **anticipate what assembly will be generated** for a piece of C code, which means you can reason about performance characteristics such as how many instructions something might take or how memory is accessed. C gives you high-level *syntax* (you write `for` loops and array indexing instead of jumps and pointer math explicitly), but those high-level constructs have straightforward translations to low-level operations. 120 | 121 | However, C’s power comes with caveats: because it doesn’t force a lot of safety checks, the onus is on the programmer to avoid errors. Issues like accessing invalid memory, overflowing an integer, or misusing pointers are not prevented by the language – and if you do them, the CPU will happily execute the resulting invalid operations, often leading to crashes or other unintended behavior. We’ll talk more about these pitfalls and undefined behaviors in a later section. 122 | 123 | To summarize this section: **C is like a thin veneer over the machine**. It abstracted away the tedium of raw assembly without introducing heavy runtime overhead. As a result, it’s often called “portable assembly language” – you get the performance of low-level code with the portability of a high-level language ([C | Computer vision | Fandom](https://computervision.fandom.com/wiki/C#:~:text=C%20is%20a%20relatively%20minimalist,it%20operates%20with%20the%20hardware)). Many other languages have since built on the concepts C introduced, but C remains a foundational language for understanding how software maps to hardware. 124 | 125 | ## The C Compilation Pipeline 126 | 127 | So, how do we go from human-readable C code to the actual binary instructions that the CPU executes? This is done by the **C compilation pipeline**, which consists of several stages. Knowing these stages not only helps to understand what happens to your code before it runs, but also reveals places where you can inspect or optimize the process (e.g., looking at compiler assembly output or understanding linker errors). The typical stages in compiling a C program are: **Preprocessing, Compilation, Assembly, Linking, and Loading** ([ 128 | 129 | The Four Stages of Compiling a C Program 130 | 131 | ](https://www.calleluks.com/the-four-stages-of-compiling-a-c-program/#:~:text=Compiling%20a%20C%20program%20is,Preprocessing%2C%20compilation%2C%20assembly%2C%20and%20linking)) ([linking.html](https://www.cs.fsu.edu/~baker/opsys/notes/linking.html#:~:text=The%20first%20phase%20of%20the,detect%20different%20kinds%20of%20errors)). Let’s go through each in order. 132 | 133 | ### 1. Preprocessing 134 | 135 | The first phase is the **preprocessor**, which handles all the directives that start with `#` in your code. This is a simple text substitution tool, not specific to C syntax per se. It processes **`#include`** directives by literally inserting the contents of header files, handles **`#define`** macros (replacing macro calls with their expansions), and evaluates conditional compilation directives (`#ifdef`, `#if`, etc.) ([ 136 | 137 | The Four Stages of Compiling a C Program 138 | 139 | ](https://www.calleluks.com/the-four-stages-of-compiling-a-c-program/#:~:text=Preprocessing)). By the end of preprocessing, your source code is transformed into a *pure C code file* with no preprocessor directives – all those have been resolved. You can imagine it as producing a `.i` file (if your source was `program.c`, the preprocessed output might be `program.i`). If you pass the `-E` flag to GCC (the compiler), it will stop after preprocessing and output this intermediate code ([ 140 | 141 | The Four Stages of Compiling a C Program 142 | 143 | ](https://www.calleluks.com/the-four-stages-of-compiling-a-c-program/#:~:text=Before%20interpreting%20commands%2C%20the%20preprocessor,and%20stripping%20comments)). 144 | 145 | For example, if you had: 146 | ```c 147 | #include 148 | #define SIZE 10 149 | 150 | int main() { 151 | int arr[SIZE]; 152 | printf("Hello, world!\n"); 153 | return 0; 154 | } 155 | ``` 156 | After preprocessing, it might become (sketching roughly): 157 | ```c 158 | int main() { 159 | int arr[10]; 160 | printf("Hello, world!\n"); 161 | return 0; 162 | } 163 | ``` 164 | with the actual content of `` header inlined at the top. Comments would be stripped, macro uses replaced, etc. The compiler never actually sees `SIZE` or the include directive – those are handled in this phase. 165 | 166 | ### 2. Compilation (to Assembly) 167 | 168 | Next comes the actual **compilation** phase (sometimes called the “compiler proper”). Here, the compiler takes the preprocessed C code and translates it into **assembly code** specific to the target architecture ([ 169 | 170 | The Four Stages of Compiling a C Program 171 | 172 | ](https://www.calleluks.com/the-four-stages-of-compiling-a-c-program/#:~:text=The%20second%20stage%20of%20compilation,an%20intermediate%20human%20readable%20language)). This is where the compiler does most of its heavy lifting: parsing the C code into an abstract syntax tree, performing optimizations, and generating an equivalent sequence of low-level instructions. The output of this phase is human-readable assembly language (for example, a `.s` file containing x86-64 assembly if you’re targeting a PC). 173 | 174 | This stage is crucial because it’s where the high-level C is mapped to low-level operations. Modern compilers like GCC and Clang do this through an intermediate representation (IR) – they don’t go directly from C to assembly in one step, but conceptually we can think of it as this stage. The existence of an assembly output stage also means you can inject **inline assembly** in your C code, and the compiler will merge it appropriately into the output ([ 175 | 176 | The Four Stages of Compiling a C Program 177 | 178 | ](https://www.calleluks.com/the-four-stages-of-compiling-a-c-program/#:~:text=readable%20language)). 179 | 180 | If you want to see the assembly output, you can run `gcc -S program.c`, which tells GCC to compile to assembly and stop (the `-S` flag). The result might be a `.s` file with lots of low-level code. For example, compiling our earlier simple `main` might produce assembly that starts like this: 181 | 182 | ```asm 183 | _main: 184 | pushq %rbp ## Prologue: save base pointer 185 | movq %rsp, %rbp ## set base pointer to stack pointer 186 | subq $16, %rsp ## allocate stack space 187 | leaq .L.str(%rip), %rdi ## load address of string into RDI (1st arg to puts) 188 | callq _puts ## call puts() 189 | movl $0, %eax ## prepare return value 0 190 | addq $16, %rsp ## Epilogue: restore stack pointer 191 | popq %rbp ## restore base pointer 192 | retq ## return 193 | ``` 194 | 195 | This corresponds to our `printf("Hello, world!")` call (which gets turned into a call to `puts` in this case) and returning 0 from main. You can see how function setup/teardown works and how the string is passed in a register (RDI). Don’t worry if not every instruction is clear; the key point is that *your C is now basically assembly*. This assembly still needs to be turned into actual machine code, which is what the next phase does. 196 | 197 | ### 3. Assembly (Assembler) 198 | 199 | The assembly code output from the compiler is still human-readable text. The next stage is to run an **assembler**, which converts this assembly text into **object code** (relocatable machine code). In the object code, opcodes and registers are encoded in binary, and addresses are mostly left as placeholders to be filled in later. The assembler essentially does a direct translation: each assembly instruction is translated to its binary encoding, and symbols (like labels or external function names) are recorded for later resolution. 200 | 201 | If you run `gcc -c program.c`, it will stop after assembly and produce an object file, e.g., `program.o` ([ 202 | 203 | The Four Stages of Compiling a C Program 204 | 205 | ](https://www.calleluks.com/the-four-stages-of-compiling-a-c-program/#:~:text=To%20save%20the%20result%20of,c%60%20option%20to%20%60cc)). This file contains the **machine instructions** for your code, but it’s not yet a full executable. It may have references to external symbols (like library functions it called) that are not resolved. Object files also contain additional metadata and are often in a format like ELF (on Linux) or COFF (on Windows). At this point, if you looked at `program.o` under a hex dump, you’d see binary data representing the instructions (and perhaps the string literal "Hello, world!" embedded in the data section) ([ 206 | 207 | The Four Stages of Compiling a C Program 208 | 209 | ](https://www.calleluks.com/the-four-stages-of-compiling-a-c-program/#:~:text=Running%20the%20above%20command%20will,one%20of%20the%20following%20commands)). 210 | 211 | ### 4. Linking 212 | 213 | Most programs consist of multiple source files and also use libraries. The **linker’s job** is to take all the compiled object files and **combine them into a single executable**, resolving references between them ([ 214 | 215 | The Four Stages of Compiling a C Program 216 | 217 | ](https://www.calleluks.com/the-four-stages-of-compiling-a-c-program/#:~:text=The%20object%20code%20generated%20in,This%20process%20is%20called%20linking)). For example, if `main.o` calls a function `myFunction` that is defined in `util.o`, the compiler in each file doesn’t know about the other – it just leaves a symbol “myFunction” to be resolved. The linker will take `main.o` and `util.o`, locate the symbol addresses, and patch the code so that `CALL myFunction` in `main.o` points to the right place in `util.o`. Likewise, it includes any library object code needed. In our example with `printf`, the call to `puts` is an external library call. The linker will find `puts` in the C standard library and include the necessary code so that at runtime, `puts` is correctly invoked ([ 218 | 219 | The Four Stages of Compiling a C Program 220 | 221 | ](https://www.calleluks.com/the-four-stages-of-compiling-a-c-program/#:~:text=linking)). 222 | 223 | During linking, all the pieces are rearranged and stitched together to form the final **executable file** ([ 224 | 225 | The Four Stages of Compiling a C Program 226 | 227 | ](https://www.calleluks.com/the-four-stages-of-compiling-a-c-program/#:~:text=The%20object%20code%20generated%20in,This%20process%20is%20called%20linking)). The linker also handles relocation – if one object assumes it will be loaded at a certain address but the final arrangement is different, it adjusts addresses accordingly. The output of linking is your program (on Unix often called `a.out` by default, or whatever you specified with `-o`). At this point, you have an actual binary that could be loaded into memory and run by the operating system. 228 | 229 | ### 5. Loading and Execution 230 | 231 | Though not often discussed in “compiling,” the final step is **loading**. When you run your program (e.g., by typing `./program` in a shell), the operating system’s loader takes over. It **loads the executable file into memory**, typically mapping the code segment as read-execute, the data segments as read-write, etc., sets up the stack, and then jumps to the program’s entry point (for a C program, this is typically the start of the `main` function, after some runtime setup). If dynamic libraries are used, this is also when the dynamic linker will load needed `.so` or `.dll` files and resolve those references. 232 | 233 | In summary, to get from C source to running program, we went through: 234 | 235 | 1. **Preprocess** (`.c` + `.h` -> pure `.c`). 236 | 2. **Compile** (pure `.c` -> `.s` assembly). 237 | 3. **Assemble** (`.s` -> `.o` machine code, incomplete). 238 | 4. **Link** (`.o` + libraries -> executable). 239 | 5. **Load** (executable -> running program in memory). 240 | 241 | Each of these steps can be observed or tuned. For instance, compiler flags control optimization during the compilation phase, and linker scripts can control memory layout at linking. 242 | 243 | ### Compiler Optimizations (GCC/Clang Insights) 244 | 245 | Modern compilers do far more than just a 1:1 translation of C to naive assembly. They include sophisticated **optimization passes** that improve performance and sometimes code size. At a high level, the compiler’s optimizer tries to make your code run faster (or smaller) *without changing what it does*. This involves a variety of techniques ([Optimizations and Flags for C++ compilers | Nerd For Tech](https://medium.com/nerd-for-tech/compiler-optimizations-boosting-code-performance-without-doing-much-95f1182a5757#:~:text=During%20the%20optimization%20phase%2C%20the,inlining%2C%20and%20dead%20code%20elimination)): 246 | 247 | - **Constant Folding:** Evaluate constant expressions at compile time. If you write `x = 2 * 3;`, the compiler will just substitute `x = 6;`. It can propagate constants through code and eliminate branches that depend on constants. 248 | - **Common Subexpression Elimination:** If the same calculation is done multiple times, the compiler might compute it once and reuse the result. 249 | - **Dead Code Elimination:** Code that can never execute or whose result is never used will be removed. For example, an `if (0) { ... }` block or setting a variable that is never read. 250 | - **Function Inlining:** The compiler may replace a function call with the body of the function (especially if it’s small), avoiding the call overhead and enabling further optimizations across what was once a call boundary ([Optimizations and Flags for C++ compilers | Nerd For Tech](https://medium.com/nerd-for-tech/compiler-optimizations-boosting-code-performance-without-doing-much-95f1182a5757#:~:text=During%20the%20optimization%20phase%2C%20the,inlining%2C%20and%20dead%20code%20elimination)). 251 | - **Loop Optimizations:** There are many of these – e.g., *loop unrolling* (repeating the loop body multiple times per iteration to decrease loop overhead), *loop fusion/fission*, *strength reduction* (turning expensive operations in loops into cheaper ones, like using addition instead of multiplication when possible), and more. 252 | - **Vectorization:** Using SIMD instructions to operate on multiple data elements in parallel when iterating over arrays (if the compiler can prove it’s safe). 253 | - **Register Allocation:** Deciding which values to keep in registers vs memory to minimize slow memory accesses (this is a big part of compiler optimization). 254 | - **Instruction Scheduling:** Reordering instructions to avoid pipeline stalls on the CPU while preserving logical correctness. 255 | 256 | Compilers like GCC and Clang have multiple optimization levels (e.g., `-O0` for none, `-O2` for a good balance, `-O3` for more aggressive, `-Os` to optimize for size, etc.). At higher levels, they apply more of these transformations. The result is that the final assembly can look quite different from a straightforward translation of the source – it’s often *better* than what a human would write by hand. For instance, the compiler might completely **omit a calculation** if it determines the result isn’t needed, or it might **inline and merge functions** to reduce overhead. 257 | 258 | However, these optimizations rely on the compiler’s understanding of the code’s behavior. If you invoke *undefined behavior* in C (like overflow a signed int, use an uninitialized variable, etc.), the compiler is allowed to assume that situation never happens, and it may optimize in ways that seem surprising. We’ll revisit this in the Best Practices section, but it’s a crucial point: *trust the compiler*, but also *write code that the compiler can reason about safely*. 259 | 260 | As an example of optimization, consider: 261 | 262 | ```c 263 | for (int i = 0; i < 1000; ++i) { 264 | array[i] = i * 2; 265 | } 266 | ``` 267 | 268 | At `-O0`, the compiled code might literally increment `i` and multiply by 2 on each loop iteration. But at `-O2`, a compiler could unroll the loop to do, say, 4 assignments per iteration, or use vectorized instructions to do multiple `i*2` operations in parallel. The output assembly could be quite different and much faster. 269 | 270 | In summary, the compilation pipeline not only translates C to machine code but also applies numerous optimizations behind the scenes. Understanding these phases can help you appreciate what the compiler does for you – and also how to leverage flags or inspect intermediate outputs (with tools like `-E`, `-S`, or using tools like **Godbolt Compiler Explorer** online) to better grasp how your C code is transformed. 271 | 272 | ## Memory and Performance in C 273 | 274 | One of the reasons to “think low-level” as a C programmer is to write code that makes good use of the hardware’s memory hierarchy and avoids costly operations. In this section, we’ll look at how to write memory-efficient, cache-friendly C code, how memory management in C works (and can go wrong), and how data alignment and layout affect performance. 275 | 276 | ### Cache-Aware Coding: Locality Matters 277 | 278 | Modern CPUs have a hierarchy of caches (L1, L2, L3, etc.) that sit between the blazing-fast CPU registers and the much slower main memory. To get the most performance, programs should maximize their use of the caches by accessing memory in a **cache-friendly pattern**. The key concept here is **locality**: 279 | 280 | - **Spatial Locality:** If a program accesses memory at address X, it’s likely to soon access nearby addresses (e.g., X+4, X+8, etc.). CPUs fetch memory in chunks called **cache lines** (often 64 bytes). So accessing memory sequentially means after the first access, subsequent ones might hit the cache line already loaded ([Memory Layout And Cache Optimization For Arrays](https://blog.heycoach.in/memory-layout-and-cache-optimization-for-arrays/#:~:text=,data%20can%20reduce%20cache%20misses)). 281 | - **Temporal Locality:** If a program accesses memory at address X, it’s likely to access X again in the near future. Keeping recently used data in cache means we can reuse it quickly ([Memory Layout And Cache Optimization For Arrays](https://blog.heycoach.in/memory-layout-and-cache-optimization-for-arrays/#:~:text=,data%20can%20reduce%20cache%20misses)). 282 | 283 | **Arrays** in C play very well with caches due to spatial locality. Since arrays are contiguous in memory, iterating through an array in order (`0,1,2,...`) will access elements that are next to each other in memory. The hardware will fetch a cache line and you’ll get many elements worth of data in one go. Contrast this with a data structure like a linked list: each node might be scattered in memory (heap allocations can be anywhere), so walking a linked list can jump all over memory, causing many cache misses. It’s not that C can’t do linked lists – it certainly can – but an array might be *much* faster for iterative access patterns because of how the hardware caches work ([Memory Layout And Cache Optimization For Arrays](https://blog.heycoach.in/memory-layout-and-cache-optimization-for-arrays/#:~:text=accessed%20again%20soon,in%20memory%20to%20leverage%20the)). 284 | 285 | A classic example: Compare iterating a 2D array in row-major order vs column-major order. In C (row-major storage), iterating row by row is much more cache-friendly than column by column. If you loop column-by-column, each access is far apart in memory (assuming a large row length), causing the CPU to constantly load new cache lines and evict others. Row-by-row iteration, on the other hand, steps through memory linearly. 286 | 287 | **Writing cache-aware code** in C often means organizing data and loops to take advantage of locality. Some tips include grouping related data together (e.g., in structs or contiguous arrays) if they’ll be used together, iterating in the natural memory order of your data, and sometimes blocking or tiling loops to work on chunks that fit in cache. The differences can be dramatic: algorithms that are theoretically the same complexity can differ by orders of magnitude in practice due to cache effects. 288 | 289 | Another consideration is **alignment**. Data that straddles cache lines or isn’t properly aligned may incur performance penalties. Fortunately, by default, C’s allocation and layout rules ensure reasonable alignment for basic types (and the compiler will align struct members to suitable boundaries, often inserting padding). Still, when doing low-level memory allocations (like allocating a raw byte buffer), you might need to ensure alignment for vector instructions or other purposes (there are aligned allocation functions or posix_memalign for this). 290 | 291 | In summary, **to optimize C code, use the cache wisely**: access memory contiguously when possible and try to reuse data that’s already in cache before moving on. Use arrays or structures of arrays (SoA) versus lots of pointers when high performance is needed, since indirection can defeat locality ([Memory Layout And Cache Optimization For Arrays](https://blog.heycoach.in/memory-layout-and-cache-optimization-for-arrays/#:~:text=%2A%20Cache,in%20memory%20to%20leverage%20the)). These principles (spatial/temporal locality) are fundamental to writing high-performance C and C++ code and are often more important than micro-optimizing arithmetic operations. 292 | 293 | ### Manual Memory Management and Pitfalls 294 | 295 | C gives you direct control over memory, which is a double-edged sword. On one hand, you can manage memory very efficiently for your needs. On the other, **bugs in memory management are common** and can be catastrophic (crashes, security vulnerabilities). 296 | 297 | Key aspects of memory management in C: 298 | 299 | - **Stack Allocation (automatic):** When you declare a local variable inside a function, memory for it is allocated on the stack automatically when the function is called, and freed when the function returns. This is fast and simple – just moving the stack pointer – but the lifetime is limited to the scope of the function. 300 | - **Heap Allocation (dynamic):** Using functions like `malloc`, `calloc`, `realloc` (and freeing with `free`), you can request memory from the heap. This memory remains until you free it (or the program exits). Heap allocation is more flexible but also more expensive than stack allocation (involves interacting with the OS or heap manager, finding a suitable block, etc.). 301 | 302 | **Common pitfalls** in C memory management include: 303 | 304 | - **Memory Leaks:** Forgetting to free memory you allocated with `malloc`. Over time, this can cause a program to use more and more memory. In long-running programs (like servers), leaks are especially harmful. Tools like *Valgrind* (discussed later) can help catch leaks by showing what wasn’t freed. 305 | - **Dangling Pointers:** Using a pointer after the memory it points to has been freed. This can lead to bizarre behavior because that memory might now be reused for something else. For example, if you `free(p)` and then do `*p = 5`, you’re potentially corrupting whatever new allocation took that memory (or corrupting the heap structures). Accessing freed memory is a form of **undefined behavior** – anything can happen, and it often results in crashes. 306 | - **Buffer Overflows:** This occurs when you write outside the bounds of an array or buffer. For example, if you have `char buf[10];` and you try to write 20 characters into it, you will overflow. On the stack, this might overwrite the return address or other local variables (a classic cause of security vulnerabilities, e.g., stack smashing attacks). On the heap, it might overwrite control structures of the heap allocator or adjacent objects. In C, **there’s no automatic bounds checking**, so it’s on you to ensure you only access valid indices. A buffer overflow typically causes corruption that leads to a crash or exploitable condition. It’s critical to avoid by careful coding and testing. 307 | - **Uninitialized Memory:** If you allocate memory via `malloc`, it contains indeterminate data (malloc doesn’t zero it out, unlike `calloc`). Reading from it before initializing it is undefined behavior. Similarly, local stack variables are not automatically initialized in C. Always initialize variables before use. 308 | - **Incorrect use of `malloc`/`free`:** Allocating with one heap function and freeing with another’s data, e.g., freeing memory not obtained from `malloc`/`realloc` (or double-freeing the same pointer) is undefined behavior. Also, mismatched `malloc`/`free` across library boundaries can be an issue if different runtimes are in play. 309 | 310 | One notorious aspect of C is **undefined behavior (UB)**. Many of these memory errors (buffer overflow, use-after-free, etc.) fall under UB. When you invoke UB, the C standard says anything can happen – the program isn’t required to handle it in any consistent way. In practice, UB often leads to crashes or weird results. Compilers also assume UB *does not happen*, which means they may optimize code in ways that make no sense if you *do* trigger UB (for instance, the compiler might assume a pointer isn’t null and eliminate a check, because dereferencing a null is UB so it concludes “this must never be null”) ([What Every C Programmer Should Know About Undefined Behavior #1/3 - The LLVM Project Blog](https://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html#:~:text=seemingly%20reasonable%20things%20in%20C,highly%20recommend%20reading%20John%27s%20article)). **Avoiding undefined behavior is a cornerstone of C best practices.** 311 | 312 | To manage memory safely in C: 313 | 314 | - Be very mindful of allocations and frees. Every `malloc` should eventually have a corresponding `free`. A good habit is to consider ownership of allocated memory – who is responsible for freeing it? Document that in your code or comments. 315 | - Use tools to help detect memory errors. For example, *Valgrind’s Memcheck* will catch many misuse of memory like out-of-bounds or use-after-free (it can make your program run ~20-30x slower, but it’s invaluable for debugging) ([Valgrind](https://valgrind.org/docs/manual/quick-start.html#:~:text=The%20Valgrind%20tool%20suite%20provides,to%20crashes%20and%20unpredictable%20behaviour)) ([Valgrind](https://valgrind.org/docs/manual/quick-start.html#:~:text=Your%20program%20will%20run%20much,and%20leaks%20that%20it%20detects)). 316 | - Prefer stack allocation when feasible (for small objects or fixed-size buffers) because it’s simpler and less error-prone (no need to manually free). 317 | - When using the heap, consider using higher-level abstractions or libraries that provide safer usage (for example, using dynamic array libraries or smart pointers in C++). In plain C, functions like `strdup` (which mallocs and copies a string) or allocating structs instead of raw pointers can help keep things clear. 318 | - Always check the result of `malloc` for `NULL` (out of memory), especially in critical systems (though on modern desktop OSes, if you run out of memory things are already not great, but on embedded it’s important). 319 | 320 | ### Alignment and Padding: Data Structure Layout 321 | 322 | As hinted earlier, how you arrange your data in structs can impact both memory footprint and performance due to alignment. CPUs often require or prefer that multi-byte values be aligned on certain boundaries (e.g., a 4-byte `int` aligned to a 4-byte address). To satisfy this, compilers will insert **padding bytes** in structs when necessary. 323 | 324 | For instance, consider this struct: 325 | 326 | ```c 327 | struct Example { 328 | char c; 329 | int x; 330 | char d; 331 | }; 332 | ``` 333 | 334 | On a 32-bit system, an `int` typically needs 4-byte alignment. The struct might be laid out as: `c` at offset 0 (1 byte), then 3 bytes of padding, `x` at offset 4 (4 bytes), then `d` at offset 8 (1 byte), then 3 more bytes of padding to make the struct size a multiple of 4 (perhaps total size 12). So even though you have 1+4+1 = 6 bytes of real data, `sizeof(Example)` might be 12. The padding ensures that `x` fell on a 4-byte boundary as required ([Struct Padding in C: Overview, Examples, Visuals | by Kyra Krishna | mycsdegree | Medium](https://medium.com/mycsdegree/struct-padding-in-c-overview-examples-visuals-96888cae82fe#:~:text=Since%20char%20,padding%20in%20between%20data%20types)). 335 | 336 | If you reorder the struct as: 337 | 338 | ```c 339 | struct Example2 { 340 | int x; 341 | char c; 342 | char d; 343 | }; 344 | ``` 345 | 346 | Now `x` is at offset 0 (aligned), `c` at offset 4, `d` at offset 5, and maybe 2 bytes padding to align the overall struct size to 4 bytes. `sizeof(Example2)` would be 8 in this case. We saved 4 bytes by reordering (depending on compiler and architecture alignment rules). The data and their alignment requirements determine padding – each type is stored at an offset that is a multiple of its alignment, often its size or a platform-defined value ([Struct Padding in C: Overview, Examples, Visuals | by Kyra Krishna | mycsdegree | Medium](https://medium.com/mycsdegree/struct-padding-in-c-overview-examples-visuals-96888cae82fe#:~:text=Well%2C%20C%20uses%20something%20called,of%20the%20largest%20data%20type)). 347 | 348 | Why does alignment matter for performance? Misaligned accesses (like reading a 4-byte int from an address not divisible by 4) can be slower, or even forbidden in some architectures (requiring multiple instructions to piece the value together). So the compiler aligns data to avoid that penalty. 349 | 350 | **Struct padding** can be an issue if you’re concerned with memory footprint. For example, in an array of millions of structs, padding is wasted space. You can sometimes reduce padding by reordering fields from largest to smallest (so that each new field finds the next available alignment easier). However, be careful: the most natural ordering might make the code more readable, and premature micro-optimizations might not be worth it unless profiling shows a problem. 351 | 352 | In performance-sensitive scenarios, you also want to consider **false sharing and cache alignment**, but that’s a deep topic – essentially, if two frequently used variables happen to sit in the same cache line, they might cause cache coherence traffic in multi-core systems. Padding or aligning structures to cache-line sizes can mitigate that in concurrent programs. 353 | 354 | Another thing to mention is that C offers the `alignof` and `aligned_alloc` in C11 (and in compilers, attributes like `__attribute__((aligned(n)))`) if you need specific alignment beyond the default. And if you need to pack a structure with no padding (for, say, reading a binary file format), many compilers have a pragma or attribute (`#pragma pack` or `__attribute__((packed))`) to do that – but accessing misaligned packed fields can be slower, so use with caution. 355 | 356 | In summary, **understand your data layout**. The C standard guarantees an order for struct members and contiguity for array elements. Use that knowledge to design memory layouts that are efficient. Be mindful of padding – it can be necessary for performance, but in some cases you can rearrange or use smaller types to save space. And always consider how your data will be accessed; align fields in a way that benefits access patterns. For example, it could be better to have all fields used together close in memory (to fit in one cache line) rather than spread out. 357 | 358 | ## Debugging and Profiling at the Low Level 359 | 360 | C’s closeness to hardware means that when bugs or performance issues arise, you often need to inspect things at a low level to fully understand what’s happening. Fortunately, there are powerful tools to aid in this process. We’ll discuss using a debugger (gdb), memory analysis tools (Valgrind), and performance profilers (perf), as well as the insights you can gain from inspecting assembly output. 361 | 362 | ### Debugging with GDB – Stepping through Assembly 363 | 364 | The GNU Debugger (gdb) is an essential tool for C programmers. When you compile your program with debugging symbols (`-g` flag), gdb lets you run the program under controlled conditions, set breakpoints, examine variables, and even inspect CPU registers and memory directly. In other words, it allows you to **see what is going on inside your program while it runs** ([Notes on using the debugger gdb](https://www.usna.edu/Users/cs/lmcdowel/courses/ic220/S21/resources/gdb.html#:~:text=Notes%20on%20using%20the%20debugger,To%20start)). 365 | 366 | Some things you can do with gdb that are especially useful at the low level: 367 | 368 | - **Set Breakpoints:** You can pause execution at certain lines or upon entering certain functions. This lets you inspect the state at that point. 369 | - **Step Through Code:** Execute the program one line at a time (or even one assembly instruction at a time with the `stepi` command). You can watch how variables change and how control flow is proceeding. 370 | - **Examine Variables and Memory:** You can print the value of variables (`p var_name`) or even examine memory at an address (`x/16wx 0xADDRESS` to examine 16 words of memory in hex, for example). Gdb knows the symbols and can help you inspect complex structures too. 371 | - **Inspect Registers:** With `info registers` you can see the contents of CPU registers at the current point – useful if you’re debugging at the assembly level or dealing with low-level operations. 372 | - **Backtrace:** If your program crashes (say due to a segmentation fault from a bad pointer), gdb can show you a stack backtrace (`bt`) indicating which function calls were active, helping pinpoint the origin of the crash. 373 | 374 | One of the great things with gdb is that you can mix C-level debugging with assembly-level. For instance, you might set a breakpoint at a function, then use the `disassemble` command to see the assembly of that function, then use `stepi` to go instruction by instruction. This is a fantastic way to learn how a piece of C code translates to assembly by *observing it live*. You can also modify registers or memory on the fly (though that’s more for the adventurous or for exploiting bugs). 375 | 376 | A typical gdb session might look like: 377 | ```bash 378 | gcc -g -O0 -o myprog myprog.c # compile with symbols, no optimizations for clarity 379 | gdb ./myprog 380 | ``` 381 | Then within gdb: 382 | ``` 383 | break main # set breakpoint at main 384 | run # run until breakpoint 385 | step # step into next line (entering functions as needed) 386 | next # move to next line (staying in current function) 387 | print x # print value of variable x 388 | info registers # see registers 389 | disassemble # show assembly of current function 390 | stepi # step one assembly instruction 391 | continue # resume program until next breakpoint or end 392 | ``` 393 | …and so on. Gdb has a learning curve but is extremely powerful. It even can attach to running processes or core dumps, which is invaluable for diagnosing issues that are hard to reproduce. 394 | 395 | In the context of our discussion, using gdb to inspect assembly and CPU state is a great way to verify what the compiler did with your C code. If something is misbehaving, sometimes examining the assembly can reveal issues (for example, maybe you expected a value to be updated, but the assembly shows the compiler optimized it out due to UB). It’s also useful for debugging optimized code, though at higher optimizations the correspondence between C and assembly gets less direct (variables might live only in registers or be optimized away, etc., which can confuse source-level debugging). 396 | 397 | ### Using Valgrind to Hunt Memory Bugs 398 | 399 | Memory errors can be some of the toughest bugs in C, because a memory corruption in one place can cause a crash in a completely unrelated part of the code, far removed in time and space from the root cause. This is where **Valgrind** (specifically the Memcheck tool) is a lifesaver. 400 | 401 | Valgrind is essentially an emulator that runs your program on a synthetic CPU, monitoring every memory access. It can detect things like: 402 | 403 | - **Invalid reads and writes:** e.g., accessing memory that wasn’t allocated (buffer overflow or use-after-free). 404 | - **Use of uninitialized memory:** using variables or memory that was never given a value. 405 | - **Memory leaks:** allocated memory that wasn’t freed by program exit. 406 | - **Double frees or improper frees:** freeing memory twice or freeing pointers not allocated. 407 | 408 | For example, if you had a bug where you wrote one element past the end of an array allocated from the heap, Valgrind would output an error like: 409 | 410 | ``` 411 | ==19182== Invalid write of size 4 412 | ==19182== at 0x804838F: f (example.c:6) 413 | ==19182== by 0x80483AB: main (example.c:11) 414 | ==19182== Address 0x1BA45050 is 0 bytes after a block of size 40 alloc'd 415 | ==19182== at 0x1B8FF5CD: malloc (vg_replace_malloc.c:130) 416 | ==19182== by 0x8048385: f (example.c:5) 417 | ==19182== by 0x80483AB: main (example.c:11) 418 | ``` 419 | 420 | This is an actual example of Valgrind catching a heap block overrun ([Valgrind](https://valgrind.org/docs/manual/quick-start.html#:~:text=%3D%3D19182%3D%3D%20Invalid%20write%20of%20size,c%3A11)). It tells you an invalid write of 4 bytes happened at line 6 of example.c in function `f`, and that the address written was just past a block of size 40 allocated at line 5 (presumably a malloc of 40 bytes). This pinpoints a buffer overflow. Without Valgrind, such an error might just cause a crash later or silently corrupt data. With Valgrind, you get an immediate diagnostic. 421 | 422 | To use Valgrind, you compile your program normally (debug symbols help to get file/line info), then run: 423 | ``` 424 | valgrind --leak-check=yes ./myprog 425 | ``` 426 | Valgrind will run the program much slower than normal (20-30x slower) ([Valgrind](https://valgrind.org/docs/manual/quick-start.html#:~:text=memory%20leak%20detector)), but it will spit out warnings for any issues it finds. After the program ends, if `--leak-check=yes` is on, it will also report any memory leaks (listing how many bytes were lost and where they were allocated). 427 | 428 | It’s common in development to run your test suite under Valgrind to catch memory regressions. Many open source projects won’t accept code until it’s Valgrind-clean (no errors reported). It’s not perfect – some things (like custom allocator misuse or data races in threaded programs) are beyond its scope – but for a C/C++ dev, Valgrind is an indispensable tool for ensuring memory correctness. 429 | 430 | ### Profiling Performance with perf (and understanding assembly output) 431 | 432 | When it comes to performance, especially on Linux, **perf** is an extremely powerful profiling tool. It interfaces with hardware performance counters to measure where your program spends time, cache misses, branch mispredictions, etc. With perf, you can find out which functions or even which lines of code are hot (using the most CPU cycles). 433 | 434 | A typical usage is: 435 | ``` 436 | perf record -g ./myprog 437 | perf report 438 | ``` 439 | This will sample the program as it runs and then give you an interactive report of which functions used the most CPU (inclusive of call graph if `-g` is used). You might see output like: 440 | ``` 441 | Overhead Command Shared Object Symbol 442 | 50.23% myprog myprog [.] hotFunction 443 | 30.11% myprog libc.so.6 [.] memcpy 444 | 15.00% myprog myprog [.] anotherFunction 445 | ... 446 | ``` 447 | This tells you where the “hot spots” are. Perf can even show annotated assembly for a given function, with the percentage of samples on each instruction. For example, you can drill down into `hotFunction` and see which assembly instructions consumed the most cycles, which often correlates with certain source lines (if optimization didn’t rearrange too much). This helps you identify performance bottlenecks – maybe a particular loop is taking most of the time, or a certain math operation is expensive. 448 | 449 | Perf is a complex tool (and on Windows, Visual Studio’s profiler or AMD’s VTune would play similar roles). But the idea is: measure first, don’t guess. Perhaps you wrote a piece of C code and you suspect a certain part is slow – using a profiler will either confirm or surprise you by pointing somewhere else. Sometimes the issue is not in the C code you wrote but in how the compiler transformed it. For instance, maybe an optimization didn’t kick in and your code is doing something in a slow way; by looking at assembly or performance counters (like cache misses), you might discover it’s actually memory-bound, not CPU-bound, etc. 450 | 451 | Another related tool is compiler *optimization reports* (for example, `-fsave-optimization-record` in clang or `-fopt-info` in GCC) which can tell you if the compiler failed to optimize something (like failed to inline or vectorize) and why, guiding you to tweak code. 452 | 453 | In addition to sampling profilers, sometimes you might instrument code manually or use simpler tools like `gprof` (with `-pg` flag) – though `gprof` is less used nowadays compared to perf and other modern profilers. 454 | 455 | **Disassembling compiled code** is also a valid way to analyze performance or correctness. You can use tools like `objdump -d -M intel myprog` to dump the assembly of your program. By studying that, you may learn exactly what instructions were generated. This can reveal, for example, if a loop was vectorized (you’ll see SSE/AVX instructions), or if a function was inlined (you won’t see a call to it), or if the compiler added some padding or alignment (you might see no-op instructions used for alignment). It’s a skill in itself to read compiler-generated assembly, but it’s incredibly useful for the performance-minded developer. 456 | 457 | As an illustration, suppose you have a function that does some mathematical computations in a loop, and you expected the compiler to use SIMD. If performance is lacking, you check the assembly: if you only see scalar `addss` or `mulss` (scalar single-precision ops) and not packed `addps/mulps`, you know vectorization didn’t happen. You might then adjust your code (maybe use `restrict` pointers or ensure alignment or use builtin vector types) to help the compiler vectorize, and try again. 458 | 459 | To connect with earlier parts: *profiling and examining assembly is the ultimate way to verify that your high-level intentions match the low-level reality*. It closes the loop in thinking like the machine. 460 | 461 | ## Best Practices & Philosophies for Low-Level C Programming 462 | 463 | Writing good C code – code that’s efficient, correct, and maintainable – requires a certain mindset. Here we’ll summarize some best practices and guiding philosophies for “thinking like the machine” while writing in C, as well as common pitfalls to avoid. 464 | 465 | ### Think in Terms of Cost 466 | 467 | Adopt a mental model of what your C code does at the machine level. You don’t have to hand-count every cycle, but know the relative cost of operations: 468 | - Memory access is slower than register access. If you can reuse a value stored in a variable instead of recomputing or reloading it, it’s usually beneficial. 469 | - Sequential code is generally faster than code with a lot of jumps (due to branch prediction, etc.), so loops should be as simple as possible for the CPU to predict. 470 | - Function calls have overhead (pushing/popping, etc.), so very small helper functions might be inlined for performance – you can do this manually or trust the compiler. 471 | - Understand complexity: a O(n^2) algorithm in C might beat an O(n log n) in Python for small n, but at large n, the algorithmic complexity will dominate no matter how efficient the C is. 472 | 473 | In practice, write clear code first, then optimize the hotspots. But while writing, an experienced C programmer keeps an eye on how the code might compile. For example, if you write a loop that appends to a dynamic array and the array grows one element at a time, you might realize “this will reallocate many times, which is costly; better to allocate in chunks.” That’s thinking ahead in terms of cost. 474 | 475 | ### Manage Memory Deliberately 476 | 477 | Be very deliberate with memory ownership and lifetimes. A few habits: 478 | - **Initialize everything.** It’s worth the slight effort to initialize variables (or use tools to zero-out structures) to avoid accidental use of garbage values. 479 | - **Use `const` and `restrict` when applicable.** `const` correctness helps catch mistakes and can sometimes help optimizations. `restrict` (in C99) tells the compiler that a pointer is the only reference to that memory, which can enable more aggressive optimizations by avoiding potential aliasing. 480 | - **Prefer stack allocation for small/temp data.** It’s automatic and avoids fragmentation and leaks. 481 | - **For heap allocation, consider wrappers.** For example, writing a small function to create and destroy a struct can centralize the `malloc`/`free` and make ownership clear (this is a poor man’s constructor/destructor pattern). 482 | - **Set freed pointers to NULL.** Then if you accidentally use them, it’s more likely to crash immediately (NULL deref) rather than corrupt random memory. 483 | 484 | ### Embrace Tools and Warnings 485 | 486 | Use your compiler’s warnings (compile with `-Wall -Wextra` and so on). These can catch a lot of mistakes like using an uninitialized variable, ignoring the return value of `malloc` (which might indicate an error), etc. Many C projects treat warnings as errors to enforce clean builds. 487 | 488 | Employ static analysis tools or sanitizers (AddressSanitizer, UndefinedBehaviorSanitizer available in GCC/Clang) for extra checking during development. These can catch out-of-bounds and use-after-free at runtime with less overhead than Valgrind, and UBsan can flag undefined behaviors (like signed integer overflow) when they occur. 489 | 490 | ### Undefined Behavior – Don’t Even Go There 491 | 492 | Undefined behavior in C is infamous and often misunderstood. It arises from things like: 493 | - Null pointer dereference 494 | - Buffer overflow 495 | - Using a pointer that wasn’t returned from a memory allocation (or has already been freed) 496 | - Signed integer overflow (yes, `INT_MAX + 1` is UB in C) 497 | - Violating strict aliasing rules (accessing the same memory through different types in incompatible ways) 498 | - Not having a `return` in a non-void function and then using the return value 499 | - Many other corner cases (like modifying a variable twice in one sequence without an intervening sequence point, e.g., `i = i++ + 1;` is UB). 500 | 501 | The motto with UB is **“don’t cross the streams.”** Once you invoke undefined behavior, all bets are off. As one article humorously put it, the program might format your hard drive or make demons fly out of your nose ([What Every C Programmer Should Know About Undefined Behavior #1/3 - The LLVM Project Blog](https://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html#:~:text=seemingly%20reasonable%20things%20in%20C,highly%20recommend%20reading%20John%27s%20article)). Practically, it often just does something unexpected. But modern compilers take advantage of the license UB gives them to optimize. For instance, since signed overflow is UB, a compiler might assume `a + 1 > a` will always be true (which is false if `a` was INT_MAX, but the compiler assumes that never happens) – this could eliminate a needed check if your code inadvertently relied on wraparound. 502 | 503 | **Best practice:** avoid code that even *hints* at UB. Use unsigned if you need modulo arithmetic. Be careful with pointer casts. Always stay within array bounds. If you’re not absolutely sure something is safe, don’t do it, or isolate it and test it thoroughly. 504 | 505 | ### Keep It Simple and Clear 506 | 507 | It might sound counterintuitive in a discussion about low-level details, but one of the philosophies of good C coding is to write code that is as simple and clear as possible, *especially* in critical low-level parts. This makes it easier to reason about correctness and performance. Clever one-liners or complicated macro hacks might save a few lines, but can introduce bugs or confuse the compiler’s optimizer. 508 | 509 | Remember that nowadays compilers are very good at their job. Writing convoluted code to micro-optimize often backfires – either the compiler would have done it anyway, or you make the code hard to maintain. A classic example is manually unrolling loops or reordering code in strange ways – sometimes it helps, but often compilers can do these optimizations. Focus on the higher-level algorithm and memory access pattern first. 510 | 511 | ### Test on Real Hardware 512 | 513 | If you are tuning for performance, test on the actual target hardware. The “low-level” characteristics (cache sizes, memory latency, branch predictor) differ between CPUs. Something fast on one machine might be slower on another if it doesn’t fit in that one’s cache, etc. So profile and test in context. 514 | 515 | ### Learn from History and Others 516 | 517 | C has been around for decades, and a lot of smart people have written about how to use it effectively. Classics like *“The C Programming Language”* (Kernighan & Ritchie) will give you the idiomatic basics. More advanced resources like *“Expert C Programming”* or *“Modern C”* cover idioms and pitfalls. And since C is used in high-performance systems, reading code from projects like the Linux kernel, libc, or embedded systems can teach a lot about low-level thinking. They often contain highly tuned code with careful considerations for the hardware. 518 | 519 | ### Checklist Mentality 520 | 521 | When writing or reviewing C code, especially low-level parts, it helps to have a mental (or literal) checklist: 522 | - Are all variables initialized before use? 523 | - Could any index go out of bounds? 524 | - Did we allocate enough space (including that `+1` for string null terminator, etc.)? 525 | - Are we freeing everything we malloc, no more, no less? 526 | - Is integer arithmetic likely to overflow? If so, should we use larger types or check for overflow? 527 | - Do we properly handle error cases (e.g., what if malloc returns NULL, what if file IO fails)? 528 | - Did we consider alignment requirements if doing low-level memory tricks? 529 | - Are we avoiding unnecessary work in hot loops? 530 | 531 | This might seem like a lot, but with practice it becomes second nature. And using the tools and compiler warnings helps enforce many of these. 532 | 533 | --- 534 | 535 | To wrap up, programming in C with a low-level perspective is immensely rewarding for those who enjoy understanding what the machine *actually* does. By tracing the evolution from hardware instructions, through assembly, to the C code we write, we gain a deeper insight into software performance and behavior. C stands at a sweet spot: it abstracts the truly tedious parts of assembly while still exposing the full power of the machine to the developer. By mastering the hardware fundamentals, the compilation pipeline, memory management, and debugging/profiling tools, you can write C code that is not only correct and robust but also blazingly fast and efficient. In the end, **thinking like the machine** makes you a better programmer in any language – but in C, it’s practically a requirement and a virtue. Happy coding, and may your pointers always be valid and your caches always hit! 536 | 537 | **Sources:** 538 | 539 | - Bottom-up explanation of CPU instructions and operations ([Chapter  3. Computer Architecture](https://bottomupcs.com/ch03.html#:~:text=The%20CPU%20executes%20instructions%20read,are%20two%20categories%20of%20instructions)) ([Chapter  3. Computer Architecture](https://bottomupcs.com/ch03.html#:~:text=Apart%20from%20loading%20or%20storing%2C,loops%20and%20decision%20statements%20work)) 540 | - Stack vs Heap roles in memory management ([Making Sense of Stack and Heap Memory | by Niraj Ranasinghe | Xeynergy Blog](https://blog.xeynergy.com/making-sense-of-stack-and-heap-memory-b13cda940bbc#:~:text=The%20stack%20represents%20a%20reserved,transition%20between%20different%20execution%20contexts)) ([Making Sense of Stack and Heap Memory | by Niraj Ranasinghe | Xeynergy Blog](https://blog.xeynergy.com/making-sense-of-stack-and-heap-memory-b13cda940bbc#:~:text=Heap)) 541 | - Function call mechanics on x86 architecture ([Cracking Assembly — Stack Frame Layout in x86 | by Sruthi K | Medium](https://medium.com/@sruthk/cracking-assembly-stack-frame-layout-in-x86-3ac46fa59c#:~:text=As%20we%20know%2C%20stack%20grows,point%20to%20this%20stack%20location)) 542 | - C as a “portable assembly” and low-level characteristics ([C | Computer vision | Fandom](https://computervision.fandom.com/wiki/C#:~:text=C%20is%20a%20relatively%20minimalist,it%20operates%20with%20the%20hardware)) ([](https://www.nokia.com/bell-labs/about/dennis-m-ritchie/chist.pdf#:~:text=an%20intermediate%20representation%20,Meyer%2088)) 543 | - Pointer arithmetic and array indexing equivalence ([ Pointers and Memory Allocation 544 | ](https://ics.uci.edu/~dhirschb/class/165/notes/memory.html#:~:text=%60%20b,is%20merely%20shorthand%20for%20this)) 545 | - Compilation stages: preprocessing, assembly, linking ([ 546 | 547 | The Four Stages of Compiling a C Program 548 | 549 | ](https://www.calleluks.com/the-four-stages-of-compiling-a-c-program/#:~:text=Preprocessing)) ([ 550 | 551 | The Four Stages of Compiling a C Program 552 | 553 | ](https://www.calleluks.com/the-four-stages-of-compiling-a-c-program/#:~:text=The%20object%20code%20generated%20in,This%20process%20is%20called%20linking)) 554 | - Compiler optimizations and techniques ([Optimizations and Flags for C++ compilers | Nerd For Tech](https://medium.com/nerd-for-tech/compiler-optimizations-boosting-code-performance-without-doing-much-95f1182a5757#:~:text=During%20the%20optimization%20phase%2C%20the,inlining%2C%20and%20dead%20code%20elimination)) 555 | - Cache locality principles for arrays and memory access ([Memory Layout And Cache Optimization For Arrays](https://blog.heycoach.in/memory-layout-and-cache-optimization-for-arrays/#:~:text=,like%20arrays%20over%20linked%20lists)) 556 | - Structure padding and alignment example ([Struct Padding in C: Overview, Examples, Visuals | by Kyra Krishna | mycsdegree | Medium](https://medium.com/mycsdegree/struct-padding-in-c-overview-examples-visuals-96888cae82fe#:~:text=Since%20char%20,padding%20in%20between%20data%20types)) 557 | - GDB usage for low-level inspection ([Notes on using the debugger gdb](https://www.usna.edu/Users/cs/lmcdowel/courses/ic220/S21/resources/gdb.html#:~:text=Notes%20on%20using%20the%20debugger,To%20start)) 558 | - Valgrind detecting memory errors (sample output) ([Valgrind](https://valgrind.org/docs/manual/quick-start.html#:~:text=%3D%3D19182%3D%3D%20Invalid%20write%20of%20size,c%3A11)) 559 | - Perf usage for performance profiling ([Linux perf Examples](https://www.brendangregg.com/perf.html#:~:text=the%20,a%20tree%2C%20annotated%20with%20percentages)) 560 | - Understanding undefined behavior and its risks ([What Every C Programmer Should Know About Undefined Behavior #1/3 - The LLVM Project Blog](https://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html#:~:text=seemingly%20reasonable%20things%20in%20C,highly%20recommend%20reading%20John%27s%20article)) 561 | 562 | -------------------------------------------------------------------------------- /assembly/README.md: -------------------------------------------------------------------------------- 1 | # Assembly Language Adventure 2 | 3 | **Ever peeked under the hood of your C code and wondered what really runs on the CPU?** This guide takes you on a journey into **assembly programming** – the human-readable form of machine code – with a curiosity-driven approach. Aimed at developers comfortable with C or similar languages, we’ll bridge high-level concepts to low-level reality. You’ll learn core assembly concepts and write code for modern CPUs (x86_64, ARM64, RISC-V) and even GPUs. By the end, you’ll know why assembly still matters and how to harness it for performance, debugging, and fun. 4 | 5 | **Table of Contents** 6 | 1. [Introduction](#introduction) 7 | 2. [Core Concepts](#core-concepts) 8 | - Registers, Memory, Stack, Instructions, Addressing 9 | - x86_64 vs ARM vs RISC-V Architectures 10 | 3. [Toolchain Setup](#toolchain-setup) 11 | - Assembling and Linking (NASM & GAS on Linux) 12 | - Notes: macOS & Windows 13 | 4. [Hands-On Examples](#hands-on-examples) 14 | - Hello World 15 | - Arithmetic & Control Flow 16 | - Functions and the Call Stack 17 | - Writing `strlen` and `memcpy` in assembly 18 | 5. [Inline Assembly in C](#inline-assembly-in-c) 19 | 6. [Debugging and Reverse Engineering](#debugging-and-reverse-engineering) 20 | 7. [Modern Relevance](#modern-relevance) 21 | - SIMD (SSE/AVX) on CPUs 22 | - GPU Assembly (NVIDIA PTX, AMD GCN, SPIR-V) 23 | 8. [Challenges & Fun Projects](#challenges--fun-projects) 24 | 9. [Resources](#resources) 25 | 26 | ## Introduction 27 | 28 | Assembly language is a **low-level programming language** that uses human-readable mnemonics to represent machine instructions ([ 29 | Unveiling the Power of Assembly Level Language 30 | ](https://www.digikey.com/en/maker/blogs/2024/unveiling-the-power-of-assembly-level-language#:~:text=Assembly%20language%20is%20a%20low,drivers%2C%20operating%20systems%2C%20and%20embedded)). Unlike C or Python, which are abstracted from hardware, assembly corresponds *directly* to CPU instructions. This close-to-the-metal control comes at the cost of complexity – you manage registers, memory addresses, and CPU flags manually. So, why learn assembly in the era of optimizing compilers and high-level languages? 31 | 32 | - **Hardware Control & Systems Programming:** Some tasks require precise hardware access. Operating system kernels, device drivers, and firmware often use assembly for things like context switching or interrupt handling ([ 33 | Unveiling the Power of Assembly Level Language 34 | ](https://www.digikey.com/en/maker/blogs/2024/unveiling-the-power-of-assembly-level-language#:~:text=%2A%20Low,efficient%20performance%20in%20devices%20like)). Assembly lets you manipulate CPU registers and special instructions that high-level languages might not expose. 35 | - **Performance Tuning:** In critical code paths (e.g. multimedia codecs, cryptography, game engines), hand-tuned assembly can maximize performance by using specialized instructions and avoiding unnecessary overhead ([ 36 | Unveiling the Power of Assembly Level Language 37 | ](https://www.digikey.com/en/maker/blogs/2024/unveiling-the-power-of-assembly-level-language#:~:text=,This%20knowledge%20is%20valuable)) ([Assembly Language: A Comprehensive Overview - DEV Community](https://dev.to/nitindahiyadev/assembly-language-a-comprehensive-overview-3on0#:~:text=,level%20code%20is%20necessary)). While modern compilers are excellent, a deep understanding of assembly helps you write C code that the compiler can optimize well – or occasionally to outperform the compiler with custom routines. 38 | - **Reverse Engineering & Security:** When analyzing malware or debugging at the binary level, you’ll encounter raw assembly ([ 39 | Unveiling the Power of Assembly Level Language 40 | ](https://www.digikey.com/en/maker/blogs/2024/unveiling-the-power-of-assembly-level-language#:~:text=,software%20across%20different%20hardware%20architectures)). Reverse engineers and security researchers read assembly to understand or modify program behavior. If you’ve ever used a debugger or disassembler, knowing assembly is invaluable for making sense of what you see. 41 | - **Education & Curiosity:** Learning assembly deepens your appreciation of how software runs. It demystifies what languages like C compile down to, and helps you understand concepts like the call stack, function calling conventions, and memory management at a fundamental level. This knowledge can make you a better high-level programmer too ([ 42 | Unveiling the Power of Assembly Level Language 43 | ](https://www.digikey.com/en/maker/blogs/2024/unveiling-the-power-of-assembly-level-language#:~:text=,indispensable%20for%20diagnosing%20problems%20and)). 44 | 45 | **Pro Tip:** *Even if you don’t write assembly daily, knowing it enables you to use tools like debuggers and profilers more effectively. You’ll be able to read compiler output, optimize critical loops, and understand compiler diagnostics (like “undefined behavior” cases) by seeing what assembly is generated.* 46 | 47 | **Common Pitfall:** *Assembly can differ by architecture – code written for x86_64 won’t run on ARM or RISC-V. Throughout this guide, we’ll highlight differences so you can learn portably. Keep in mind assembly is **not portable** across CPU types by nature ([Assembly Language: A Comprehensive Overview - DEV Community](https://dev.to/nitindahiyadev/assembly-language-a-comprehensive-overview-3on0#:~:text=,it%20less%20accessible%20to%20beginners)), but core concepts carry over.* 48 | 49 | In the following sections, we’ll build a strong foundation in assembly and then dive into examples. You’ll write a “Hello World” in assembly, implement functions, integrate assembly with C, and even explore GPU shaders. Let’s start by understanding the key concepts common to all assembly languages. 50 | 51 | ## Core Concepts 52 | 53 | At its heart, assembly programming revolves around a few key ideas: **registers**, **memory**, the **stack**, CPU **instructions**, and **addressing modes**. We’ll introduce each concept and relate it to what you know from C. Finally, we’ll compare how three popular architectures (x86_64, ARM64, and RISC-V) implement these ideas. 54 | 55 | ### Registers: CPU’s Working Variables 56 | 57 | **Registers** are small, fast storage locations inside the CPU. You can think of them as the CPU’s built-in variables. Arithmetic and logic operations typically happen in registers. For example, adding two numbers in assembly usually means loading them into registers, using an ADD instruction, and storing the result back to memory (if needed). 58 | 59 | Each CPU architecture has a fixed set of registers. x86_64, for instance, has 16 general-purpose registers (named RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, and R8–R15) each 64 bits wide, plus special registers like an instruction pointer (RIP) and flags register for status bits ([x86 assembly language - Wikipedia](https://en.wikipedia.org/wiki/X86_assembly_language#:~:text=x86%20processors%20feature%20a%20set,These%20registers%20are%20categorized%20into)). ARM’s AArch64 (64-bit ARMv8) has 31 general-purpose 64-bit registers (X0–X30) and a program counter (PC) that isn’t directly accessible as a general register ([A64 - ARM - WikiChip](https://en.wikichip.org/wiki/arm/a64#:~:text=A64%20has%2031%20general,a%20normal%20directly%20accessible%20register)). RISC-V (RV64) defines 31 general 64-bit registers (x1–x31) and uses register x0 as a constant zero always ([Registers - RISC-V - WikiChip](https://en.wikichip.org/wiki/risc-v/registers#:~:text=RISC,return%20address%20on%20a%20call)). Despite different naming, these all serve a similar purpose: they hold values that the CPU instructions will operate on. 60 | 61 | Some registers have conventional roles. For example, on x86_64, RAX is often used for returning values from functions, and the RSP (stack pointer) tracks the top of the stack. On ARM64, X0–X7 are used to pass function arguments and return values, and there is a dedicated stack pointer register (SP). RISC-V uses x1 as the return address register (link register) by convention (when you call a function, the return address is stored in x1) ([Registers - RISC-V - WikiChip](https://en.wikichip.org/wiki/risc-v/registers#:~:text=RISC,return%20address%20on%20a%20call)). Don’t worry about memorizing all these yet – with practice, you’ll get familiar with the important ones. 62 | 63 | **Common Pitfall:** *Registers are precious and limited. If you run out of registers to hold your data, you must spill data to memory. Also, **registers are volatile** across certain operations: for instance, a function call may overwrite some registers (per calling convention). We’ll discuss calling conventions later, but always be mindful which registers you need to preserve (save/restore) when writing assembly functions.* 64 | 65 | ### Memory and Addressing 66 | 67 | **Memory** in assembly is accessed through addresses. In C, you work with variables and pointers; in assembly, you often work with the memory addresses directly. Each variable in your program lives at some address in memory. Assembly instructions can load from or store to those addresses. 68 | 69 | Addressing modes define how an instruction references memory. For example, x86_64 supports complex addressing like `MOV RAX, [RBX + RCX*4 + 16]` which loads data from an address computed as **RBX + RCX*4 + 16**. This could correspond to something like `array[RCX]` where RBX is the base address of the array, RCX is an index, and 4 is the size of each element. ARM64 and RISC-V use simpler **load/store** addressing – typically an instruction like `LDR X1, [X2, #16]` to load from the address in X2 plus 16-byte offset (ARM64), or `lw x5, 8(x6)` in RISC-V to load a 32-bit word from address *x6 + 8*. These addressing modes cover common patterns (accessing a local variable at a fixed offset from a base pointer, indexing into an array, etc.). 70 | 71 | In assembly, you often label memory locations with names (just like variables). For example, you might define a message string as: 72 | 73 | ```asm 74 | section .data 75 | msg: db "Hello, World!", 0 ; declare bytes with a terminating 0 76 | ``` 77 | 78 | Here `msg` is a label for the address where the string is stored. To access it, you use `[msg]` in Intel syntax or `msg(%rip)` in AT&T syntax (for x86_64 relative addressing), or on RISC-V you might load its address into a register first. We will see concrete examples of this in the coming sections. 79 | 80 | **Endianness:** Different architectures store multi-byte values in memory with different byte orders. x86_64 and ARM (in almost all modern settings) are *little-endian* (least significant byte at the smallest address). Some systems (historically certain mainframes or network byte order) use *big-endian*. RISC-V can be either but is typically little-endian as well. Endianness matters when interpreting raw memory, but assembly instructions abstract this for you when you load/store (you just have to use the correct size). 81 | 82 | ### The Stack 83 | 84 | The **stack** is a special region of memory that supports *last-in, first-out* (LIFO) access. In high-level languages, the stack is used for function calls: it stores function parameters, local variables, return addresses, and saved registers. In assembly, you directly manipulate the stack using the stack pointer register (like RSP on x86_64, SP on ARM64, or x2 on RISC-V). 85 | 86 | When a function is called in assembly, a *stack frame* is typically created. This frame contains the return address (pushed automatically by a `CALL` instruction on x86 or by manual instruction on RISC architectures), and space for local variables and saved registers. Here’s what a simplistic stack frame might contain, in order: 87 | 88 | ([File:Call-stack-layout.svg - Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Call-stack-layout.svg)) *Figure: Call stack layout for nested calls (two stack frames: one for `DrawSquare` in blue and one for `DrawLine` in green). Each frame holds that function’s **parameters**, **return address**, and **local variables** ([Call stack - Wikipedia](https://en.wikipedia.org/wiki/Call_stack#:~:text=ImageCall%20stack%20layout%20for%20upward,is%20the%20currently%20executing%20routine)) ([Call stack - Wikipedia](https://en.wikipedia.org/wiki/Call_stack#:~:text=The%20stack%20frame%20at%20the,in%20push%20order)).* 89 | 90 | In assembly, you manage the stack with **PUSH** and **POP** instructions (on x86), or by adjusting the stack pointer explicitly (on RISC architectures). A typical function prologue in x86_64 might be: 91 | 92 | ```asm 93 | push rbp ; save caller's base pointer 94 | mov rbp, rsp ; set this function’s base pointer 95 | sub rsp, 32 ; reserve 32 bytes for local variables 96 | ``` 97 | 98 | And the corresponding epilogue: 99 | 100 | ```asm 101 | add rsp, 32 ; free local variables 102 | pop rbp ; restore caller’s base pointer 103 | ret ; pop return address into IP and jump 104 | ``` 105 | 106 | On ARM64, a similar prologue might use `STP` (store pair) to push registers and adjust SP, and an epilogue with `LDP` to pop. RISC-V uses instructions like `addi sp, sp, -32` to allocate and `lw ra, 28(sp)` etc. to restore saved data. 107 | 108 | The key idea is the same: the stack grows and shrinks as functions are called and return, maintaining a record of active function invocations (the *call stack*). This is how your program “remembers” where to return after a function call and keeps each function’s locals and temporaries separate. 109 | 110 | **Common Pitfall:** *Stack mismanagement is a major source of bugs in assembly! If you push something and forget to pop it (or adjust the stack pointer), your stack will be imbalanced, often leading to crashes when returning from functions. Always clean up the stack properly and adhere to the platform’s calling convention (e.g., who is responsible for removing function arguments from the stack on x86’s 32-bit stdcall vs cdecl, etc.).* 111 | 112 | ### CPU Instructions and Operations 113 | 114 | **Instructions** are the commands that tell the CPU to do something – add, subtract, load, store, jump, call, etc. In assembly, each instruction typically corresponds 1:1 with a machine opcode. Broad categories of instructions include: 115 | 116 | - **Data Movement:** e.g. `MOV` (move data between registers or between memory and register), `PUSH`/`POP` (stack operations), `LEA` (load effective address, useful for pointer arithmetic). 117 | - **Arithmetic/Logic:** e.g. `ADD`, `SUB`, `MUL`, `DIV`, bitwise `AND`, `OR`, `XOR`, shifts (`SHL`, `SHR`), etc. These usually set CPU status flags (like zero flag, carry flag) based on the result. 118 | - **Control Flow:** e.g. `JMP` (unconditional jump/goto), `JE/JNE` (jump if equal/not equal, i.e. conditional jumps based on flags), `CALL` (call subroutine), `RET` (return from subroutine), and in higher-level sense, conditional moves or architecture-specific branches. 119 | - **Special:** Each architecture has special instructions, e.g. x86 has `CPUID` (to query CPU capabilities), `RDTSC` (read time-stamp counter), etc., ARM64 has `MSR/MRS` to access system registers, and so on. We won’t cover them all, but knowing they exist helps – you can drop to assembly for these rare needs. 120 | 121 | One distinction: **CISC vs RISC.** x86_64 is a CISC (Complex Instruction Set Computing) architecture – it has many instructions, some quite complex (string operations, BCD arithmetic, etc.), and instructions have variable length. ARM64 and RISC-V are RISC (Reduced Instruction Set Computing) – fewer instruction types, fixed-length 32-bit instructions, and typically you must use simpler instructions (e.g., you can’t perform memory-to-memory arithmetic, you must load to registers, operate, then store). In practice, modern x86_64 and ARM64 both achieve high performance; as an assembly programmer, you just write in the style the ISA expects (x86 might let you combine more in one instruction, ARM requires more separate steps). RISC-V is a clean-slate design emphasizing simplicity and extensibility (it has modular extensions for atomic ops, floating point, etc., rather than one big ISA). 122 | 123 | ### Addressing Modes 124 | 125 | We touched on addressing modes earlier, but to summarize: **addressing modes** describe how an instruction locates its operands (especially operands in memory). Common addressing modes across architectures include: 126 | 127 | - **Immediate:** The operand is a constant encoded in the instruction. e.g. `mov eax, 5` (moves the value 5 into EAX). 128 | - **Register:** The operand is a register. e.g. `mov eax, ebx` (copy EBX into EAX). 129 | - **Direct (Absolute) Memory:** The instruction specifies a memory address directly. e.g. on x86, `mov eax, [0x1000]` would load the 32-bit value at memory address 0x1000 into EAX. 130 | - **Register Indirect:** The address of the operand is held in a register. e.g. `mov eax, [rbx]` loads EAX from the address in RBX. 131 | - **Base + Offset:** A constant offset is added to a register to form the address. e.g. `mov eax, [rbp-8]` might load a local variable at offset -8 from RBP (common for function locals). 132 | - **Base + Index*Scale + Offset:** (x86-specific mostly) Uses two registers and a scale. For example, `mov eax, [rax + rbx*4 + 16]` as mentioned, useful for array indexing (base address in RAX, index in RBX, element size 4 bytes, offset 16 bytes perhaps for some header). 133 | 134 | ARM64 addressing modes often allow an offset or index as well, but they are more limited (e.g., offset must be a small immediate or the index is implicitly scaled for certain instructions). RISC-V keeps it simple: one register + an immediate offset (e.g., `lw t0, 8(s1)` loads from address in s1 + 8). More complex addressing like index scaling must be done with explicit arithmetic in RISC-V. 135 | 136 | **Pro Tip:** *You can use the assembler to do arithmetic for you with labels and offsets. As seen in the string example with `len_Hello: equ $-Hello`, good assemblers (NASM, GAS) can compute constants like string lengths or structure offsets at assembly time. Leverage these to keep your code clear – treat your assembly code a bit like writing C, with meaningful labels and computed offsets, rather than hard-coding magic numbers everywhere.* 137 | 138 | ### x86_64 vs ARM64 vs RISC-V: A Quick Overview 139 | 140 | Let’s summarize the three architectures we’ll encounter: 141 | 142 | - **x86_64 (AMD64):** This is the 64-bit extension of the classic Intel x86. It’s CISC, little-endian, with variable-length instructions (anywhere from 1 to 15 bytes per instruction). It has 16 general-purpose registers (64-bit), which can also be accessed in 32-bit (`EAX`), 16-bit (`AX`), and 8-bit (`AL/AH`) subsets for backward compatibility. The calling convention on Unix (System V AMD64 ABI) uses registers RDI, RSI, RDX, RCX, R8, R9 for the first six arguments, and RAX for return value. The CPU has separate registers for segment tracking (mostly legacy in 64-bit), an instruction pointer (RIP), and flags (status bits like Zero, Carry, Sign, Overflow, etc.). x86_64 has a rich set of instructions including specialized ones for string operations (`REP MOVSB` to copy memory, etc.) and bit-manipulations. Memory is addressed via segmentation (mostly flat in modern OS) and paging under the hood, but in assembly you mostly use flat 64-bit addresses or RIP-relative addressing for PC-relative access. It also features **SIMD/vector registers** (XMM, YMM, ZMM) for multimedia and scientific computing (we’ll explore those later). 143 | 144 | - **ARM64 (AArch64):** A 64-bit RISC architecture used in most smartphones, tablets, and now Apple Macs (Apple Silicon), servers, and more. It has 31 general-purpose 64-bit registers (conventionally named X0–X30) ([A64 - ARM - WikiChip](https://en.wikichip.org/wiki/arm/a64#:~:text=A64%20has%2031%20general,a%20normal%20directly%20accessible%20register)). Register X30 is often used as the link register (holds return address on calls) instead of pushing it on stack, though the AArch64 ABI still typically uses the stack for return addresses when needed (it depends on the calling sequence). ARM64 uses a *zero* register (XZR) which reads as 0 and discards writes, similar to RISC-V’s x0 ([A64 - ARM - WikiChip](https://en.wikichip.org/wiki/arm/a64#:~:text=A64%20has%2031%20general,a%20normal%20directly%20accessible%20register)). It also has a separate stack pointer register (SP). Instructions are fixed 32-bit in length and most follow a uniform syntax like `OP , , `. ARM64 is load-store: you can’t do `add [mem], reg` in one go; you must load, add in a register, then store. It has extensive support for conditional execution (older ARM had condition codes on every instruction; AArch64 simplifies this but still has conditional branches and select instructions). ARM64 also includes Neon (SIMD) and can have Crypto extensions, etc. It’s also little-endian by default. The ARM64 calling convention (AAPCS for ARM64) uses X0–X7 for arguments and return (similar to x86_64’s first arguments in registers) and requires 16-byte stack alignment. 145 | 146 | - **RISC-V (RV64GC for example):** A modern open ISA that is gaining traction in academia and industry (you might encounter it in microcontrollers or experimental processors, and some Linux-capable RISC-V CPUs are emerging). RISC-V is pure RISC and very minimalist in design. It has 32 registers (x0–x31, but x0 is wired to 0 constant) ([Registers - RISC-V - WikiChip](https://en.wikichip.org/wiki/risc-v/registers#:~:text=RISC,return%20address%20on%20a%20call)). There is a program counter (PC) not accessible directly via these registers. One register (conventionally x1) is used as return address (link register) and x2 as stack pointer, x3–x7 as other pointers/temps, etc., according to the RISC-V ABI. Instruction format is fixed 32-bit, with a small number of instruction formats. It relies heavily on the assembler for synthesizing larger immediates (for example, loading a 32-bit constant might take two instructions `LUI` and `ADDI`). RISC-V’s base integer ISA is very small, but real-world uses include extensions like “M” (multiplication/division), “A” (atomic ops), “F” and “D” (floating point), etc. (RV64GC implies the base plus general extensions). Calling convention (on UNIX) uses a mix of registers and stack: a0–a7 (which are x10–x17) for arguments and returns, and the stack for additional arguments. Like ARM, RISC-V is load-store and requires explicit load/store for memory access. It’s also little-endian in most implementations. 147 | 148 | Despite these differences, *assembly programming techniques* are similar across architectures. You allocate registers for your variables, manage the stack for function calls, and use loops and branches for control flow. We’ll illustrate examples primarily in an *assembly-agnostic way* (pseudo-assembly) and give specific syntax for x86_64 (NASM, Intel syntax) and occasionally notes for ARM64 and RISC-V. Once you grasp one assembly language, others become easier to learn because you recognize the same patterns (just different syntax and registers). 149 | 150 | **Summary of Key Differences:** 151 | 152 | | Aspect | x86_64 (AMD64) | ARM64 (AArch64) | RISC-V (RV64) | 153 | |------------------------|-----------------------------------------|----------------------------------------|-----------------------------------| 154 | | Instruction Set | CISC, variable-length (1–15 bytes) | RISC, fixed 32-bit length | RISC, fixed 32-bit length | 155 | | Gen. Purpose Registers | 16 (64-bit) ([x86 assembly language - Wikipedia](https://en.wikipedia.org/wiki/X86_assembly_language#:~:text=x86%20processors%20feature%20a%20set,These%20registers%20are%20categorized%20into)) | 31 (64-bit) + SP ([A64 - ARM - WikiChip](https://en.wikichip.org/wiki/arm/a64#:~:text=A64%20has%2031%20general,a%20normal%20directly%20accessible%20register)) | 31 (64-bit) + x0=0 ([Registers - RISC-V - WikiChip](https://en.wikichip.org/wiki/risc-v/registers#:~:text=RISC,return%20address%20on%20a%20call)) | 156 | | Special Registers | RIP (instr pointer), FLAGS, segment regs| PC (not GPR), separate SP and zero reg | PC (not GPR), x1 link, x2 SP, x0=0 | 157 | | Endianness | Little-endian | Little-endian (bi-endian capable) | Little-endian (bi-endian capable) | 158 | | Memory Model | Flat 64-bit address space (with paging) | Flat 64-bit address space | Flat 64-bit address space | 159 | | Addr. Modes | Many (reg, reg+reg*scale+off, etc.) | Load/store, reg + imm offset | Load/store, reg + imm offset | 160 | | Calling Convention (Unix) | First 6 args in regs, rest on stack; caller cleans up variadic args; callee-saved: RBX, RBP, R12–R15 | First 8 args in X0–X7, rest on stack; callee-saved: X19–X28, etc. | First 8 args in a0–a7, rest on stack; callee-saved: s0–s11 (x8–x9, x18–x27) | 161 | 162 | Don’t worry about memorizing the table – refer back as needed. As we do examples, we’ll point out which registers or instructions are in use for each architecture. 163 | 164 | **Pro Tip:** *When learning assembly for a new architecture, write simple C functions and compile to assembly (`gcc -S`) for that architecture. This lets you see exactly what assembly the compiler generates, which is a great way to learn conventions. Websites like **Godbolt Compiler Explorer** are excellent for this: you can write C code and see the assembly output side by side ([Assembly - Compiler Explorer](https://godbolt.org/noscript/assembly#:~:text=Compiler%20Explorer%20is%20an%20interactive,and%20many%20more%29%20code)).* 165 | 166 | Next, let’s set up our environment and toolchain so we can assemble and run our own code. 167 | 168 | ## Toolchain Setup 169 | 170 | To write and run assembly, you’ll need an **assembler** (to turn your `.asm` or `.s` file into machine code) and a **linker** (to combine it into an executable, unless you’re writing raw binary). We’ll focus on Linux for examples, using two popular assemblers: **NASM (Netwide Assembler)** and **GAS (GNU Assembler)**. NASM uses Intel syntax by default, while GAS (the assembler used by `gcc`) historically uses AT&T syntax for x86. We’ll primarily use Intel syntax in this guide for clarity, but it’s good to know both. Modern GAS can accept Intel syntax too (with a `.intel_syntax noprefix` directive). 171 | 172 | ### Setting up on Linux (NASM & GAS) 173 | 174 | - **NASM:** Install via your package manager (e.g., `sudo apt install nasm` on Debian/Ubuntu, or `sudo dnf install nasm` on Fedora, Homebrew on Mac: `brew install nasm`). NASM outputs object files in various formats (ELF, COFF, Mach-O, etc.) via the `-f` option. 175 | - **GAS (as/gcc):** The GNU assembler is typically installed as part of the build-essential tools. You can invoke it directly as `as` or through `gcc`. Using `gcc` can simplify linking. For example, `gcc -o prog prog.s` will assemble and link `prog.s` in one step (assuming it’s in GAS syntax). 176 | 177 | **Writing & Assembling:** 178 | 179 | Write your assembly in a text file, e.g. `hello.asm`. Here’s a simple x86_64 example we’ll use (Linux syscall for write, Intel syntax): 180 | 181 | ```asm 182 | section .data 183 | msg: db "Hello, World!", 0xA ; our message string with newline 184 | len: equ $ - msg ; length of the string (computed by assembler) 185 | 186 | section .text 187 | global _start 188 | _start: 189 | ; Linux write(int fd, const void *buf, size_t count) syscall 190 | mov rax, 1 ; syscall number 1 = sys_write 191 | mov rdi, 1 ; fd 1 = stdout 192 | mov rsi, msg ; pointer to message 193 | mov rdx, len ; length of message 194 | syscall ; invoke kernel 195 | 196 | ; Linux exit(int status) syscall 197 | mov rax, 60 ; syscall number 60 = sys_exit 198 | xor rdi, rdi ; status = 0 (xor sets rdi to 0) 199 | syscall ; exit 200 | ``` 201 | 202 | Save that in `hello.asm`. To assemble and link on Linux using NASM and the GNU linker: 203 | 204 | ```bash 205 | nasm -f elf64 hello.asm -o hello.o # assemble to ELF64 object 206 | ld hello.o -o hello # link to executable (static, no libc) 207 | ``` 208 | 209 | This produces an executable `hello`. Run it: `./hello` should print "Hello, World!" to the terminal. 210 | 211 | Alternatively, using GAS with AT&T syntax, you’d write a similar program in a `.s` file (with directives like `.global _start`, and `%rax, %rdi` etc. for registers). You could then do: 212 | 213 | ```bash 214 | as hello.s -o hello.o # assemble with GNU assembler 215 | ld hello.o -o hello # link 216 | ``` 217 | 218 | Or even: `gcc -nostdlib -no-pie -o hello hello.s` to assemble & link in one step (using no C library, no PIE for simplicity). 219 | 220 | **AT&T vs Intel Syntax (x86):** Don’t be confused by the two syntax styles. AT&T syntax is used by GAS by default on x86: it prefixes registers with `%` and immediates with `$`, and the operand order is `source, destination` (opposite of Intel). For example, the Intel `mov rax, 5` would be `movq $5, %rax` in AT&T. Likewise `mov rax, [rbx]` (Intel) would be `movq (%rbx), %rax` in AT&T. Most Linux assembly tutorials use AT&T since it’s the GAS default ([x86 Assembly/GNU assembly syntax - Wikibooks, open books for an open world](https://en.wikibooks.org/wiki/X86_Assembly/GNU_assembly_syntax#:~:text=Examples%20in%20this%20article%20are,assembly%20language%20Syntax%20for%20a)), but you can switch GAS to Intel mode with the directive `.intel_syntax noprefix`. In this guide, we’ll stick to Intel syntax for consistency with NASM. Just remember if you see `%rax` and lots of `$`, you’re looking at AT&T style. The instructions themselves are the same; only the notation differs ([x86 Assembly/GNU assembly syntax - Wikibooks, open books for an open world](https://en.wikibooks.org/wiki/X86_Assembly/GNU_assembly_syntax#:~:text=Examples%20in%20this%20article%20are,assembly%20language%20Syntax%20for%20a)). 221 | 222 | **Linking and Running:** On Linux, if you wrote your program to use Linux syscalls (as above), you don’t need the C library at all. We called the kernel directly with `syscall`. We used `_start` as the entry label, which the linker knows to use as entry point by default in a static binary. If you want to interface with C code or use libc functions (like `printf` or `puts`), you’d typically write a proper function (say `main`) and compile with gcc *or* call into libc via extern symbols. For example, you could extern declare `printf` and use the PLT (Procedure Linkage Table) via a CALL, but that’s a bit advanced for now. Our examples will mostly use syscalls or be standalone. 223 | 224 | ### macOS and Windows Notes 225 | 226 | - **macOS:** macOS uses Mach-O format instead of ELF. You can still use NASM (with `-f macho64`) and the `ld` that comes with Xcode’s command-line tools. The syscall interface on macOS is different (different numbers and you use `syscall` as well but with Mach-O specifics). Often, it’s easier to write assembly that calls C library functions on macOS. For instance, calling `_printf` from your assembly _start is a way to print. Alternatively, use the Apple provided `as` (which uses AT&T syntax) and link normally. Keep in mind macOS default executables are PIE (position independent), so you might need special flags or use PC-relative addressing (RIP-relative) as we did with `lea rsi, [rel Hello]` in our example. The example above could be adapted with minor changes for Mach-O. 227 | 228 | - **Windows:** On Windows, the typical assembler is MASM (Microsoft Macro Assembler) with its own syntax (closer to Intel). If using GCC (MinGW) on Windows, you can use GAS as well (COFF object format). Windows 64-bit has a different calling convention (Microsoft x64): only 4 args in registers (RCX, RDX, R8, R9) and you must allocate space for “shadow store” on the stack. Also, Windows uses different syscalls (often you don’t call them directly; you go through OS DLLs). For learning, many just use a Linux environment or the Windows Subsystem for Linux to play with assembly. But if you do use Windows, consider starting by writing a DLL in assembly or using MASM to call Win32 API functions (like MessageBox) as an interesting exercise. 229 | 230 | Regardless of OS, the core assembly concepts remain the same. Now that our toolchain is ready, let’s get our hands dirty with some examples! 231 | 232 | ## Hands-On Examples 233 | 234 | It’s time to write some assembly! We’ll start simple and build up: 235 | 236 | - A classic **"Hello World"** program. 237 | - Basic **arithmetic and loops** (control flow). 238 | - Using **functions and the call stack** (calling conventions). 239 | - Implementing libc-like **`strlen` and `memcpy`**. 240 | 241 | The examples will primarily use x86_64 Linux assembly (Intel syntax) for concreteness, but we’ll discuss how they translate to ARM64 and RISC-V as well. 242 | 243 | ### Hello World in Assembly 244 | 245 | We saw an example in the toolchain section. Let’s break it down step by step, as it illustrates many fundamentals: 246 | 247 | ```asm 248 | section .data 249 | msg: db "Hello, Assembly!\n", 0 ; the string plus a null terminator (0) 250 | len: equ $ - msg ; length of the string (bytes) 251 | 252 | section .text 253 | global _start 254 | _start: 255 | ; sys_write(stdout, msg, len) 256 | mov rax, 1 ; syscall 1 = write 257 | mov rdi, 1 ; file descriptor 1 = stdout 258 | mov rsi, msg ; address of string to write 259 | mov rdx, len ; number of bytes 260 | syscall ; invoke Linux kernel 261 | 262 | ; sys_exit(0) 263 | mov rax, 60 ; syscall 60 = exit 264 | xor rdi, rdi ; exit code 0 (xor zeroes rdi) 265 | syscall 266 | ``` 267 | 268 | **What’s happening here?** We defined a data section with our message. In the text (code) section, we declared a `_start` label (the entry point). We then loaded registers according to Linux syscall convention: on x86_64 Linux, `rax` holds the syscall number, and `rdi, rsi, rdx, r10, r8, r9` are used for up to 6 arguments. So to call the `write` system call, we placed 1 in `rax` (write’s number), 1 in `rdi` (stdout), the address of our message in `rsi`, the length in `rdx`, then used the `syscall` instruction to trap into the kernel. After that, we prepare `exit` in a similar way and `syscall` again. 269 | 270 | On ARM64 Linux, the same idea works but registers differ: syscall number goes in `x8`, arguments in `x0`–`x5`, and you use `svc #0` to trigger the call. So an ARM64 Hello World (syscall version) would do: `mov x8, #64` (sys_write is 64 on ARM64), `mov x0, #1` (stdout), `adr x1, msg` (address of msg), `mov x2, #len`, then `svc #0`. And exit would be `mov x8, #93` (exit), `mov x0, #0`, `svc #0`. On RISC-V, system calls use `a7` for number and `a0`–`a5` for args, and the instruction `ecall`. The key point: *printing “Hello World” in assembly often involves invoking the OS directly*, which is a bit low-level. Alternatively, you could call C’s `printf` by setting up a call to the function provided by libc (but that involves linking to libc). 271 | 272 | **Output:** Running the program should print "Hello, Assembly!" followed by a newline. The null terminator isn’t actually needed for the syscall (since we provide length), but if we were calling C’s `printf("%s")`, it would expect a C-string (null-terminated). We put it anyway out of habit. 273 | 274 | Congratulations, you just walked through what it takes to print text using pure assembly! It’s more involved than `printf("Hello, Assembly!\n");` in C, but now you see what happens underneath – the interaction with the OS via registers. 275 | 276 | **Common Pitfall:** *Forgetting to set the exit syscall (or an infinite loop) will make your program continue executing whatever bytes follow in memory, leading to bizarre behavior or a crash. High-level languages automatically call `exit` (or return from main); in bare assembly, you must explicitly exit or your process will keep running.* 277 | 278 | ### Arithmetic and Control Flow (Loops and Branches) 279 | 280 | Let’s do some simple math in assembly. Suppose we want to sum the numbers 1 through 5 and print the result. In C you might do: 281 | 282 | ```c 283 | int sum = 0; 284 | for(int i=1; i<=5; ++i) { 285 | sum += i; 286 | } 287 | printf("%d\n", sum); 288 | ``` 289 | 290 | Now assembly (x86_64 Linux, using our already running program approach for brevity): 291 | 292 | ```asm 293 | xor eax, eax ; sum = 0 (eax=0) 294 | mov ecx, 1 ; i = 1 (using ecx as counter) 295 | loop_start: 296 | add eax, ecx ; sum += i 297 | inc ecx ; i++ 298 | cmp ecx, 6 ; compare i to 6 299 | jne loop_start ; if i != 6, jump back to loop_start 300 | 301 | ; at this point, EAX = sum 302 | ``` 303 | 304 | This loop uses ECX as the counter (`i`). We initialized ECX to 1, and EAX (as 32-bit register for sum) to 0. Each iteration adds ECX to EAX, increments ECX, and checks if ECX is 6 (meaning the loop ran i = 1..5). The CMP instruction sets flags by computing `ecx - 6` (without storing), and `JNE` (jump if not equal) goes back if the zero flag is not set (i.e., ECX was not 6). When ECX becomes 6, the loop ends with EAX holding the result. EAX=15 (0xF) because 1+2+3+4+5 = 15. 305 | 306 | To print EAX, we could call a write or print function. For simplicity, we might just exit with that code (so you can see it via `$?` in shell). But let’s say we want to output it as a character. That’s more complicated (convert int to string in assembly), so we'll take a shortcut: assume we know the result is one-digit and add '0'. 307 | 308 | ```asm 309 | add eax, 0x30 ; convert 15 to ASCII (15 + '0' = '?') 310 | ; Oops, 15 is 0x0F, adding 0x30 gives 0x3F, which is ASCII '?', not "15". 311 | ; So this trick only works for 0-9. Real int-to-string needs a loop or library call. 312 | ``` 313 | 314 | So scratch that; a real int-to-decimal conversion is a good challenge (you’d divide by 10, etc.). For now, trust the sum is correct in EAX. 315 | 316 | **Translating to ARM64 or RISC-V:** Loops look very similar. ARM64 example: 317 | 318 | ```asm 319 | mov w0, #0 // sum = 0 320 | mov w1, #1 // i = 1 321 | loop_start: 322 | add w0, w0, w1 // sum += i (w0 = w0 + w1) 323 | add w1, w1, #1 // i++ 324 | cmp w1, #6 325 | b.ne loop_start // branch if not equal 326 | ``` 327 | 328 | RISC-V example: 329 | 330 | ```asm 331 | li t0, 0 # sum = 0 (t0 register) 332 | li t1, 1 # i = 1 (t1 register) 333 | loop_start: 334 | add t0, t0, t1 # sum += i 335 | addi t1, t1, 1 # i++ 336 | li t2, 6 337 | bne t1, t2, loop_start # if i != 6, continue loop 338 | ``` 339 | 340 | As you see, the logic and flow are analogous – set up initial registers, do operations, use compare-and-branch or a conditional branch instruction. 341 | 342 | **If/else and comparisons** in assembly are done with combinations of compare/test and conditional jumps. For example, in x86: 343 | 344 | ```asm 345 | cmp eax, ebx 346 | jle label ; jump if less or equal (signed) -> this would jump to label if EAX <= EBX 347 | ``` 348 | 349 | ARM64: 350 | 351 | ```asm 352 | cmp x0, x1 353 | ble label ; branch if less or equal (uses condition flags set by cmp) 354 | ``` 355 | 356 | RISC-V doesn’t have a single <=, but has `blt` (branch if less than) and you can combine with an equality check or adjust logic accordingly. 357 | 358 | ### Functions and the Call Stack 359 | 360 | Writing functions in assembly means handling **calling conventions**: rules for how arguments are passed, how the stack is managed, and how return values are given back. Let’s write a simple function in x86_64 that adds two numbers (like `int add(int a, int b)`), and call it from an assembly “main”. 361 | 362 | In x86_64 System V ABI (Linux, macOS ABI is similar for simple cases), the first two integer arguments are in RDI and RSI, and the return value in RAX. We’ll adhere to that. 363 | 364 | ```asm 365 | section .text 366 | global main 367 | main: 368 | ; We’ll call add(2, 3) 369 | mov edi, 2 ; prepare first arg (a=2) in EDI 370 | mov esi, 3 ; prepare second arg (b=3) in ESI 371 | call add ; call the function 372 | ; upon return, EAX = result 373 | ; print or use EAX as needed (here we’ll just exit with result as status) 374 | mov edi, eax ; set exit code = result 375 | mov eax, 60 ; sys_exit 376 | syscall 377 | 378 | add: 379 | push rbp ; standard prologue 380 | mov rbp, rsp 381 | ; (for this simple function, we don't really need to use RBP or stack, but for demonstration) 382 | mov eax, edi ; move first arg (a) into EAX 383 | add eax, esi ; add second arg (b) 384 | pop rbp ; restore base pointer 385 | ret ; return to caller (places sum in EAX as return value) 386 | ``` 387 | 388 | Here, `main` is our entry (we’d link this with the C startup or call it from _start if writing standalone). We manually loaded arguments and did `call add`. The `call` instruction on x86 pushes the return address (next instruction’s address) onto the stack and jumps to `add`. In `add`, we pushed RBP (callee-saved register) and set up RBP as the frame pointer. We didn’t actually allocate any local variables on the stack, so we didn’t decrement RSP. We perform the addition by using EDI and ESI (which were set by caller) – copying EDI into EAX, then adding ESI. Why copy EDI to EAX first? Because on x86_64, by convention the return value should be in RAX, and also because add instructions require a destination. We could have done `mov eax, edi` then `add eax, esi` (as shown), or directly `lea eax, [rdi + rsi]` using LEA (which is a common trick to do arithmetic without affecting flags). After that, we restore RBP and use `ret`, which will pop the saved return address into RIP, so execution goes back to the instruction after the `call` in main. At that point, EAX has the sum. We then put that into EDI (for exit) and exit. 389 | 390 | **What about ARM64 and RISC-V functions?** The idea is the same but with different registers: 391 | 392 | - ARM64 AAPCS: First 8 args in X0–X7, return in X0. So an `add` function would expect X0 and X1 as arguments, and put result in X0. ARM64 has link register (X30 or alias LR) instead of pushing return address, so a typical function is: 393 | 394 | ```asm 395 | add_func: 396 | stp x29, x30, [sp, -16]! ; push frame pointer and link register 397 | mov x29, sp ; set frame pointer 398 | add w0, w0, w1 ; result = a + b (32-bit, assuming ints) 399 | ldp x29, x30, [sp], 16 ; pop frame pointer and return address 400 | ret ; return (jump to X30) 401 | ``` 402 | 403 | When calling it, you’d do `mov w0, #2; mov w1, #3; bl add_func` (BL = branch with link, which sets X30). 404 | 405 | - RISC-V ABI: First 8 args in a0–a7, return in a0. RISC-V doesn’t have a link register that’s not also a general register (it uses ra = x1 for return address). So: 406 | 407 | ```asm 408 | add_func: 409 | add a0, a0, a1 # a0 = a0 + a1, simple enough for this function 410 | ret # ret is a pseudoinstruction for jr ra (jump to return addr in ra) 411 | ``` 412 | 413 | For such a simple leaf function, we didn’t need to save any registers. In a more complex function, you’d use stack to save ra (the return address) if you call other functions, etc., and any s-registers (callee-saved) you use. 414 | 415 | Understanding the calling convention is critical when writing or interfacing assembly with other code. It tells you which registers you can use freely (temporary registers that the caller doesn’t expect preserved) and which you must preserve (callee-saved registers like RBX, RBP on x86_64, or X19–X28 on ARM64, etc.). In our `add` example, we only used EAX (caller-saved) and didn’t touch any callee-saved regs except RBP which we handled. 416 | 417 | **Pro Tip:** *Make use of calling convention documents and cheat sheets. They specify, for each architecture, how parameters are passed and what the callee needs to save. For instance, x86_64 SysV says RBX, RBP, R12–R15 must be preserved by callee (if used). If you violate this, weird bugs happen when your function returns and the caller’s registers are clobbered. So always adhere to the ABI.* 418 | 419 | ### Reimplementing `strlen` and `memcpy` 420 | 421 | As an exercise, let’s write two commonly used C functions in assembly: `strlen` (string length) and `memcpy`. We’ll do them in a generic way for x86_64. These functions will demonstrate loops, memory access, and a bit of efficiency consideration. 422 | 423 | #### `strlen` in assembly (x86_64 SysV ABI) 424 | 425 | C signature: `size_t strlen(const char *s);` – returns the length of string `s` not counting the null terminator. The input pointer is in RDI, and we return length in RAX. 426 | 427 | A straightforward assembly implementation: 428 | 429 | ```asm 430 | global strlen 431 | strlen: 432 | push rbp 433 | mov rbp, rsp 434 | mov rax, rdi ; RAX will be our length counter, start at pointer 435 | ; we will find the null terminator 436 | .loop: 437 | cmp byte [rax], 0 ; compare the byte at address RAX to 0 438 | je .done ; if zero, found end of string 439 | inc rax ; otherwise, increment pointer (and this will count bytes) 440 | jmp .loop 441 | .done: 442 | sub rax, rdi ; length = end_pointer - start_pointer 443 | pop rbp 444 | ret 445 | ``` 446 | 447 | We used RAX to scan through the string byte by byte until we hit a 0. Initially, we set `rax = rdi` (so RAX points to the start of string). In the loop, `[rax]` gets the byte at RAX. If it’s 0, we jump to done. If not, increment RAX and loop. After the loop, RAX points to the null terminator, RDI still points to start, so `length = RAX - RDI`. We compute that and return (length in RAX as required). 448 | 449 | This is a simple implementation. It’s not the fastest (a faster one might check in larger chunks, e.g. 8 bytes at a time using 64-bit loads and bit tricks to find zero faster, or use SSE instructions). But it’s clear. 450 | 451 | On ARM64, similarly, you might do a loop loading a byte with `ldrb w2, [x0, #offset]` etc., or use their optimized instructions (they have some tricks like `CBZ` (compare and branch if zero) that could shorten the code). 452 | 453 | #### `memcpy` in assembly (x86_64 SysV ABI) 454 | 455 | C signature: `void *memcpy(void *dest, const void *src, size_t n);` – copy n bytes from src to dest, return dest. ABI: dest (void*) in RDI, src (const void*) in RSI, n (size_t) in RDX, return dest in RAX (which on return should equal original RDI). 456 | 457 | A simple implementation: 458 | 459 | ```asm 460 | global memcpy 461 | memcpy: 462 | push rbp 463 | mov rbp, rsp 464 | mov rax, rdi ; save dest in RAX for return 465 | 466 | mov rcx, rdx ; RCX = n (counter) 467 | mov r8, rsi ; use r8 as src pointer 468 | mov r9, rdi ; use r9 as dest pointer 469 | 470 | .copy_loop: 471 | test rcx, rcx 472 | jz .done ; if count = 0, done 473 | mov bl, [r8] ; load byte from [src] (BL is lower 8 bits of RBX, we can use any byte-register) 474 | mov [r9], bl ; store byte to [dest] 475 | inc r8 ; src++ 476 | inc r9 ; dest++ 477 | dec rcx ; count-- 478 | jmp .copy_loop 479 | 480 | .done: 481 | pop rbp 482 | ret 483 | ``` 484 | 485 | This copies byte by byte. It’s clear but not optimized (real `memcpy` would copy words or use SIMD for large blocks, and handle overlaps carefully – note: standard memcpy doesn’t need to handle overlapping regions, that’s what `memmove` is for). We used RBX’s BL just as a convenient byte register; we could have also used AL by loading into AL and storing from AL. We had to use an additional register (like R8, R9) because we can’t modify RSI/RDI directly if we still need them – actually in this simple loop we could have just used RSI and RDI and incremented them (since we already saved RAX for dest). Perhaps simpler: 486 | 487 | ```asm 488 | memcpy: 489 | push rbp 490 | mov rbp, rsp 491 | mov rax, rdi ; return value = dest (save it) 492 | .copy_loop: 493 | test rdx, rdx 494 | je .done 495 | mov bl, [rsi] ; load byte from src 496 | mov [rdi], bl ; store to dest 497 | inc rsi 498 | inc rdi 499 | dec rdx 500 | jmp .copy_loop 501 | .done: 502 | pop rbp 503 | ret 504 | ``` 505 | 506 | This modifies RDI, RSI, RDX but according to the calling convention, those are caller-saved anyway (and we don’t need the original values after copying). We return original dest in RAX, which we saved at start. 507 | 508 | For small copies, this is fine. For large `n`, this will be slow (byte by byte). You could copy 8 bytes at a time (`mov qword [rdi], [rsi]` is not a single instruction in x86; you’d do `mov rbx, [rsi]` then `mov [rdi], rbx` for example). Or use `rep movsb` which is a built-in x86 instruction to copy a block (set RCX, RSI, RDI then `rep movsb`). But understanding the explicit loop is important. 509 | 510 | **Testing our assembly functions:** It’s a good practice to test these by writing a small driver in C or assembly. For example, after assembling `strlen` and `memcpy` into object files, you could call them from a C program to verify correctness. Or you could write a little assembly at `_start` to use them (like copy a string to a buffer with memcpy, then call strlen on it, etc., and exit with the result or print it). 511 | 512 | ### Inline Assembly in C 513 | 514 | Sometimes you don’t want to write a whole function in assembly, but just one or two instructions within a C function – perhaps to use a special CPU instruction or to manually optimize a small piece. **Inline assembly** lets you embed assembly instructions right inside your C code. We’ll focus on GCC-style inline assembly (supported by GCC and Clang). It’s a powerful feature but can be tricky to get right. 515 | 516 | Here’s a simple example: suppose we want to add two integers using an assembly instruction. It’s pointless for addition (C can do that), but serves as a demo of syntax: 517 | 518 | ```c 519 | #include 520 | int main() { 521 | int a = 5, b = 7, sum; 522 | asm("addl %%ebx, %%eax" 523 | : "=a" (sum) /* output: sum in EAX */ 524 | : "a" (a), "b" (b) /* inputs: a in EAX, b in EBX */ 525 | ); 526 | printf("%d + %d = %d\n", a, b, sum); 527 | return 0; 528 | } 529 | ``` 530 | 531 | Let’s break the weird string: In the `asm("...")` block, we first have the assembly template: `"addl %%ebx, %%eax"`. We use `%%` to escape percent signs for registers (because `%` has meaning in the format string of inline assembly). Then we have `: "=a" (sum)` which tells the compiler that this inline asm will produce an output in register EAX (the `"=a"` constraint means EAX is an output, `=` means write-only, and it will be assigned to the C variable `sum`). Next, `: "a" (a), "b" (b)` tells the compiler to put C variable `a` into EAX (`"a"` constraint) and `b` into EBX (`"b"` constraint) before executing the asm. There’s a third section for clobbered registers (none in this case aside from outputs) and a fourth for clobbered memory or conditions (not used here). 532 | 533 | What happens is: the compiler ensures EAX contains `a` and EBX contains `b`, then runs our `addl %ebx, %eax`, then takes EAX value as `sum`. If compiled and run, it prints `5 + 7 = 12`. Great, we made the CPU do it via our inline asm (a bit roundabout for addition, but illustrative). 534 | 535 | **Inline assembly is useful for:** 536 | 537 | - Using special instructions that have no direct C equivalent (e.g., x86’s `rdtsc` to read the cycle counter, or privileged instructions like `invlpg`). 538 | - Tiny optimized sequences that the compiler might not produce by itself. 539 | - Embedding *hardware-specific tricks*: for instance, maybe using a bit-scan instruction (`bsf`/`bsr`) to find the first set bit faster than a loop, or an SSE instruction for which you don’t have an intrinsic readily. 540 | 541 | **Constraints and Clobbers:** The extended asm syntax (GNU) is powerful – you can specify registers or let compiler allocate them (`"r"` for any register), you can have multiple outputs, and you must list any registers you modify that are not outputs as *clobbers*. Also if your assembly modifies memory or depends on memory, you might need `"memory"` in clobber list to tell compiler not to optimize around that. Using `asm volatile` prevents the compiler from optimizing it away or reordering it (useful for hardware I/O instructions or timing measurements). If your asm has no outputs, you likely need `volatile` to avoid it being removed as dead code. 542 | 543 | **Example: Using `RDTSC` (Read Time-Stamp Counter)**: 544 | 545 | ```c 546 | unsigned long long read_tsc() { 547 | unsigned int hi, lo; 548 | asm volatile("rdtsc" : "=a"(lo), "=d"(hi)); 549 | return ((unsigned long long)hi << 32) | lo; 550 | } 551 | ``` 552 | 553 | Here, `rdtsc` puts the lower 32 bits in EAX and upper 32 in EDX. We mark those as outputs (`"=a"` and `"=d"` for lo and hi). We use `volatile` because `rdtsc` is a timing instruction – we don’t want the compiler to move or remove it thinking it has no side-effects. No inputs in this case. We then combine hi and lo to get the full 64-bit value. 554 | 555 | **Common Pitfall:** *Forgetting to tell the compiler about side effects.* If your inline asm writes to a register that you don’t list as output or clobber, the compiler might assume that register’s original value is still valid after the asm. Or if your asm touches memory (say you implemented a byte copy in inline asm) but you don’t mark memory as changed, the compiler might not realize it needs to reload data from memory later. Always list all outputs and clobbers correctly. When in doubt, use `asm volatile` with `"memory"` clobber to be safe, though that may prevent some optimizations.* 556 | 557 | **Note:** MSVC on Windows has a different inline asm syntax (`__asm { ... }`) that’s more like writing raw assembly, but it’s only for 32-bit. On 64-bit, MSVC doesn’t support inline asm; you’d use intrinsics or separate asm files. GCC’s extended asm works on all platforms it supports (with appropriate constraints, e.g., use `"r"` for any general register, etc.). 558 | 559 | ### Debugging and Reverse Engineering 560 | 561 | Assembly programming goes hand-in-hand with debugging at the machine level. Here we cover tools and techniques for debugging your assembly programs and for reverse-engineering binaries (which is basically reading assembly that you didn’t write). 562 | 563 | #### GDB (GNU Debugger) 564 | 565 | GDB is a powerful debugger for programs on Linux (and other OS via ports like lldb on macOS, or windbg on Windows with different commands). With GDB you can step through your assembly code, inspect registers, and examine memory. A few useful GDB commands for assembly: 566 | 567 | - `layout asm` – if using the TUI mode, this splits the screen to show assembly source as you step. 568 | - `break *0x401000` – set a breakpoint at an address (you can also `break _start` if symbol is known). 569 | - `display/8i $rip` – each step, display 8 instructions at the current instruction pointer. 570 | - `si` (stepi) – step one instruction (machine instruction). 571 | - `info registers` – show all registers and their contents. 572 | - `x/16bx $rsp` – examine memory at RSP (stack pointer) as 16 bytes in hex (`b` for byte, `x` for hex). You can also use formats like `x/4gx` to show four 8-byte values (g = giant word, 8 bytes) at some address. 573 | - `info breakpoints` and `continue` and others are similar to high-level debugging. 574 | 575 | For example, if we run our earlier `hello` program in GDB: 576 | 577 | ``` 578 | $ gdb ./hello 579 | (gdb) break _start 580 | Breakpoint 1 at 0x401000 581 | (gdb) run 582 | Starting program: ./hello 583 | 584 | Breakpoint 1, 0x0000000000401000 in _start () 585 | (gdb) stepi 586 | ... (step through instructions) ... 587 | (gdb) info registers 588 | rax 0x1 1 589 | rbx 0x0 0 590 | rcx 0x0 0 591 | rdx 0xd 13 592 | rsi 0x402000 (whatever address of msg) 593 | rdi 0x1 1 594 | rip 0x401005 <_start+5> (pointing to next instruction) 595 | ... 596 | ``` 597 | 598 | You can see the registers set up for the write syscall (RAX=1, RDI=1, RSI pointing to message, RDX=13 length). If you continue stepping to the `syscall` instruction and then step, you’ll jump to kernel (which GDB can’t follow) and come back with the result (RAX will have number of bytes written). GDB is extremely useful in catching assembly bugs like forgetting to set a register or pushing/popping inconsistently. 599 | 600 | **Reverse Engineering Tools:** If you’re not debugging your own code, but analyzing someone else’s binary, you often use **objdump** or dedicated disassemblers. 601 | 602 | #### objdump 603 | 604 | `objdump -d -Mintel program.exe` will disassemble the executable (or object file) for you (use `-M att` for AT&T, default may vary). This is a quick way to see the assembly of a compiled C program, or to check that your assembly object file has the instructions you expect. 605 | 606 | Example, using our earlier `hello` (in Intel syntax): 607 | 608 | ``` 609 | $ objdump -d -Mintel hello 610 | 611 | hello: file format elf64-x86-64 612 | 613 | Disassembly of section .text: 614 | 615 | 0000000000401000 <_start>: 616 | 401000: b8 01 00 00 00 mov eax, 0x1 617 | 401005: bf 01 00 00 00 mov edi, 0x1 618 | 40100a: 48 8d 35 f1 0f 00 00 lea rsi, [rip + 0xff1] # 0x402000 619 | 401011: ba 0e 00 00 00 mov edx, 0xe 620 | 401016: 0f 05 syscall 621 | 401018: b8 3c 00 00 00 mov eax, 0x3c 622 | 40101d: 31 ff xor edi, edi 623 | 40101f: 0f 05 syscall 624 | ``` 625 | 626 | You can see the machine code bytes on the left and the instructions on the right. This matches our assembly (with some slight differences: we wrote `mov rax,1` which is encoded as `b8 01 00 00 00`, etc., and the assembler optimized our `mov rsi, msg` into a RIP-relative LEA). The comment shows the actual address of `msg`. This is extremely helpful for verifying or reverse-engineering code. 627 | 628 | #### strace and ltrace 629 | 630 | While not exactly assembly tools, these are great for understanding what a program is doing at a high level: 631 | 632 | - `strace ./program` will show you all **system calls** the program makes. For our `hello`, for example, `strace` would log something like: 633 | 634 | ([x86 assembly language - Wikipedia](https://en.wikipedia.org/wiki/X86_assembly_language#:~:text=%24%20strace%20.%2Fhello%20,exited%20with%200)) 635 | 636 | This confirms the program called `write(1, "Hello, World!\n", 14)` and then `exit(0)`. Strace is useful when debugging low-level issues or just to see if your syscalls are being made correctly (e.g., if you forgot to set a length, you might see a weird value in strace output). 637 | 638 | - `ltrace ./program` shows **library calls** made (calls to libc functions). If your assembly calls `printf`, this can show the parameters passed to `printf` at runtime (which can catch calling convention mistakes, like not cleaning the stack properly or passing wrong types). 639 | 640 | #### Reading Disassembly & Hex Dumps 641 | 642 | When reversing, you might not have symbol names or comments. It takes practice to read raw disassembly. Some tips: 643 | 644 | - Identify function boundaries by looking for typical prologue/epilogue patterns (push ebp; mov ebp,esp ... ret). 645 | - Keep track of register usage: note which registers seem to hold pointers vs counters vs values, by seeing how they are used (e.g., a register used in `[rax + rdx*4]` is likely a base pointer or array, etc.). 646 | - Use a tool like **IDA Pro, Ghidra, or radare2** for more automated analysis – they can often reconstruct pseudocode or at least label branches and data references. However, understanding assembly yourself is still crucial for tough cases. 647 | - Comment the disassembly as you figure out pieces. For example, you might deduce that a certain sequence is computing a length (it looks like our `strlen` loop). Add a comment in your notes "maybe computing strlen". This is essentially reverse-engineering work. 648 | 649 | **Debugging your own assembly** is easier because you have source and symbols. Make liberal use of GDB (and possibly tools like **Valgrind** if you suspect memory issues – Valgrind will detect invalid memory accesses even in assembly, though it might not give you the exact instruction easily, it will give the address). 650 | 651 | **Common Pitfall:** *Assembly bugs can be silent. If you mis-manage the stack or registers, your program might crash far away from the actual bug. For instance, corrupting the return address on stack will crash on `ret` with no obvious clue. If you suspect such issues, use GDB watchpoints (e.g., `watch *addr`) to catch when a memory location changes, or step through carefully paying attention to stack pointer alignment and values.* 652 | 653 | Armed with debugging tools, you can iterate on your assembly code much like you would in C, albeit with a bit more attention needed. Next, we’ll venture into *modern* assembly topics – how today’s hardware (CPUs and GPUs) utilize assembly for performance. 654 | 655 | ## Modern Relevance 656 | 657 | Assembly isn’t just about simple arithmetic and loops. Modern processors have advanced features that assembly programmers can tap into: **vector/SIMD instructions** on CPUs and the massively parallel nature of **GPUs**. In this section, we’ll give an overview of these, plus how compilers automatically use them. 658 | 659 | ### SIMD Instructions on CPUs (SSE, AVX, NEON, etc.) 660 | 661 | **SIMD (Single Instruction, Multiple Data)** instructions allow one instruction to perform the same operation on multiple pieces of data at once. Think of adding two arrays of 4 ints (C would loop 4 times, but a SIMD add can do 4 additions in one go). Intel introduced MMX and then SSE (Streaming SIMD Extensions) on x86, and it evolved to AVX and AVX-512 on modern CPUs. ARM has NEON for SIMD, and even RISC-V has vector extensions. 662 | 663 | **Registers:** SSE introduced 128-bit XMM registers (xmm0–xmm15 on x86_64). AVX widened these to 256-bit YMM, and AVX-512 to 512-bit ZMM registers. ARM NEON uses 128-bit Q registers (overlaid on 64-bit D registers). These registers can be treated as containing multiple lanes of data: e.g., an XMM register can hold 4 32-bit floats (4x32 = 128). Then an instruction like `ADDPS xmm0, xmm1` (Add Packed Single-precision) will add those 4 floats in xmm1 to the 4 floats in xmm0, producing 4 results simultaneously ([Cornell Virtual Workshop > Understanding GPU Architecture > GPU Characteristics > Design: GPU vs. CPU](https://cvw.cac.cornell.edu/gpu-architecture/gpu-characteristics/design#:~:text=The%20figure%20below%20illustrates%20the,times%20larger%20than%20the%20caches)). 664 | 665 | **Example (x86 with SSE):** 666 | 667 | ```asm 668 | movups xmm0, [rdi] ; load 4 floats from array1 (rdi points to array1) 669 | movups xmm1, [rsi] ; load 4 floats from array2 (rsi points to array2) 670 | addps xmm0, xmm1 ; xmm0 = xmm0 + xmm1 (element-wise add of 4 floats) 671 | movups [rdx], xmm0 ; store 4 result floats into result array (rdx points to result) 672 | ``` 673 | 674 | With this, we computed 4 float additions with one instruction. Without SIMD, we’d loop 4 times doing scalar `ADDSS` (scalar float add) or normal `FLD/FST` x87 operations historically. 675 | 676 | Modern C/C++ compilers can auto-vectorize simple loops. For instance, if you write a loop adding two arrays, `gcc -O3` might produce code similar to the above (using SSE/AVX) if the data is aligned and loop count is known, etc. You can also use **intrinsics** in C (like `_mm_add_ps` for SSE) which give you direct access to these instructions without writing inline asm. 677 | 678 | **AVX and AVX-512:** Wider registers allow doing 8 floats at a time (AVX) or 16 floats at a time (AVX-512). The principles remain – you load as wide as possible, do operations, store back. AVX introduced non-aligned versions (like `VMOVUPS` similar to MOVUPS, and aligned `VMOVAPD` for aligned moves). A consideration with AVX-512 is it has masking and so on, but that’s beyond our scope. 679 | 680 | On ARM NEON, you’d use instructions like `ADDV` or such (NEON syntax is different but conceptually similar – you operate on vectors of 2,4,8 elements depending on type). 681 | 682 | **When to use SIMD assembly:** If you’re working in performance critical code where vectorization doesn’t happen automatically or you can do it more optimally, assembly (or intrinsics) can help. Example domains: image processing, linear algebra, cryptography. Assembly might let you use an instruction like `PEXTRB` or some bit-level SIMD operation that compilers might not utilize on their own. 683 | 684 | **Pro Tip:** *Before writing manual SIMD assembly, check if compiler intrinsics or built-ins can do what you need – they are easier to maintain and often achieve near-assembly performance. Use manual assembly only if you have verified the compiler isn’t producing optimal code.* 685 | 686 | ### GPU Assembly and Shaders (NVIDIA PTX, AMD GCN, SPIR-V) 687 | 688 | CPUs are great for sequential speed and moderate parallelism (few cores, each very fast). GPUs, on the other hand, have **hundreds or thousands of smaller cores** designed for massive parallelism (SIMD across many threads). GPU programming is usually done in high-level parallel languages (CUDA, OpenCL, shader languages like HLSL/GLSL), but under the hood, these are translated to GPU-specific assembly or intermediate languages. 689 | 690 | **NVIDIA PTX:** PTX is an intermediate ISA for CUDA GPUs ([1. Introduction — PTX ISA 8.7 documentation - NVIDIA Docs Hub](https://docs.nvidia.com/cuda/parallel-thread-execution/#:~:text=Hub%20docs,ISA)). When you write a CUDA kernel in C++, it gets compiled to PTX, which is then either JIT-compiled to the actual hardware ISA (SASS) by the driver. PTX is human-readable assembly-like code. For example: 691 | 692 | ```ptx 693 | .visible .entry add_kernel( .param .u64 param0, .param .u64 param1, .param .u64 param2 ) 694 | { 695 | .reg .u32 t1, t2, t3; 696 | .reg .u64 addr_out; 697 | ld.param.u64 addr_out, [param0]; // load output pointer 698 | ld.param.u32 t1, [param1]; // load value1 699 | ld.param.u32 t2, [param2]; // load value2 700 | add.u32 t3, t1, t2; // t3 = t1 + t2 701 | st.global.u32 [addr_out], t3; // store t3 to output pointer 702 | ret; 703 | } 704 | ``` 705 | 706 | This PTX kernel `add_kernel` takes three parameters: an output memory address, and two 32-bit values, adds the two values, and stores the result. It uses a few PTX directives: `.reg .u32` to declare registers, `ld.param` to load kernel parameters, `add.u32` to add 32-bit ints, etc. PTX is not exactly the native assembly of the GPU, but it’s a low-level representation. (NVIDIA doesn’t publicly document the exact binary ISA in detail – they provide PTX as the stable interface ([Cornell Virtual Workshop > Understanding GPU Architecture > GPU Characteristics > Design: GPU vs. CPU](https://cvw.cac.cornell.edu/gpu-architecture/gpu-characteristics/design#:~:text=This%20diagram%2C%20which%20is%20taken,the%20figure%20does%20suggest%20that)).) 707 | 708 | **AMD GCN/RDNA:** AMD GPUs have their own assembly (often referred to by architecture generation like GCN for older, RDNA for newer). AMD *does* release ISA documentation ([AMD GPU architecture programming documentation - AMD GPUOpen](https://gpuopen.com/amd-gpu-architecture-programming-documentation/#:~:text=We%E2%80%99ve%20been%20releasing%20the%20Instruction,DirectX%C2%AE10%20era%20back%20in%202006)), and there are tools to compile and analyze it (such as the AMD Shader Analyzer). Writing AMD GPU assembly directly is rare except in driver development or tooling, but if you ever look at a compiled OpenCL program for AMD, you might see text like `v_add_f32` (vector add) etc., which is AMD’s assembly. 709 | 710 | **SPIR-V:** This is an intermediate language for Vulkan and OpenCL, akin to PTX but for the broader ecosystem. It’s a binary format, often not hand-written (you’d write GLSL/HLSL and compile to SPIR-V). There are SPIR-V assembly syntaxes for debugging (it looks like a list of instructions and ids) but it’s not as human-friendly as PTX. 711 | 712 | **GPU vs CPU assembly style:** GPUs are designed for throughput over latency. A GPU “assembly” program (a kernel) is executed by potentially thousands of threads in parallel. Control flow is often handled via predication (masking off lanes) or by having threads diverge (which can hurt performance if not aligned). Memory instructions may refer to different memory spaces (global, shared, local). There are often special registers for thread indices, etc. For example, in PTX you might see `mov.u32 %r0, %tid.x;` to get the thread’s X index. GPUs use a SIMT (single instruction multiple thread) model – groups of threads execute the same instruction in lockstep (a “warp” in NVIDIA terms, e.g., 32 threads). So the assembly is implicitly parallel: an `add.u32` in a warp context means 32 additions happening for 32 threads at once. 713 | 714 | ([Cornell Virtual Workshop > Understanding GPU Architecture > GPU Characteristics > Design: GPU vs. CPU](https://cvw.cac.cornell.edu/gpu-architecture/gpu-characteristics/design)) *Figure: High-level comparison of CPU vs GPU architecture. CPUs have a few cores with large control and cache (left), GPUs have many more smaller ALUs (green) with a smaller control logic slice (small orange/purple on left of GPU block) ([Cornell Virtual Workshop > Understanding GPU Architecture > GPU Characteristics > Design: GPU vs. CPU](https://cvw.cac.cornell.edu/gpu-architecture/gpu-characteristics/design#:~:text=The%20figure%20below%20illustrates%20the,times%20larger%20than%20the%20caches)) ([Cornell Virtual Workshop > Understanding GPU Architecture > GPU Characteristics > Design: GPU vs. CPU](https://cvw.cac.cornell.edu/gpu-architecture/gpu-characteristics/design#:~:text=1,core%20are%20individually%20more%20capable)). This hardware design means GPUs rely on running thousands of threads for performance, using simpler control per thread.* 715 | 716 | In assembly terms, this means a GPU core’s ISA might assume many threads – e.g., there are instructions for barrier synchronization between threads, or for loading from texture memory, etc., that have no equivalent in CPU ISA. 717 | 718 | **Compilers and Optimization:** 719 | 720 | Modern compilers are *very good* at utilizing hardware features: 721 | - Auto-vectorization (as mentioned) will try to use SIMD instructions. You can help it by aligning data and writing loops in a straightforward way. 722 | - Auto-parallelization to GPU isn’t done by normal C compilers, but there are directive-based approaches (OpenMP offloading, OpenACC) that can move loops to GPU kernels behind the scenes. Typically, though, you explicitly write GPU code (like CUDA) which the compiler (nvcc) turns into PTX/SASS. 723 | - Compilers also use specialized instructions for certain C operations. For instance, `__builtin_popcount(x)` in GCC will compile to the POPCNT instruction on x86 if available. A division by a constant might compile into a `mul` and shift sequence (using LEA or IMUL) instead of an actual DIV at runtime, if that’s faster. 724 | - Link-time optimization and whole-program optimization can even rearrange code to improve cache and branch prediction. While this isn’t “assembly” from the coder’s view, the compiler is essentially playing with assembly to optimize it. 725 | 726 | **When to go low-level:** 727 | 728 | - If profiling shows a hotspot where the compiler isn’t using a vector instruction that you know exists, you might use an intrinsic or small asm block. 729 | - If working with hardware where you must use specific instructions (e.g., special I/O ports, or waiting for a specific CPU instruction like PAUSE in spinlocks). 730 | - In cryptography or compression algorithms, where data-dependent branches hurt, sometimes assembly can be written to use bit tricks and SIMD to process data in fixed-time. 731 | - Writing a compiler or OS, where you generate or manipulate assembly directly. 732 | - Fun and competition: some people enjoy writing hand-tuned assembly for things like code golf or old-school demo scenes. 733 | 734 | But most application-level code benefits more from algorithm improvements than hand-written assembly. Use it judiciously where it counts. 735 | 736 | **Common Pitfall:** *Writing assembly for modern architectures without testing on actual hardware variations. An assembly routine optimized for one microarchitecture might perform poorly on another (e.g., using AVX-512 could actually slow down code on CPUs that downclock when AVX-512 is active). Always consider the portability and maintenance cost. Sometimes sticking to intrinsics or high-level code lets the compiler decide based on the target CPU.* 737 | 738 | We’ve now seen assembly from basic to advanced. To solidify this knowledge, nothing beats practice. In the next section, we suggest some challenges and projects for you to try. 739 | 740 | ## Challenges & Fun Projects 741 | 742 | Ready to push your assembly skills further? Here are some project ideas and challenges that range from practical to whimsical: 743 | 744 | - **Write a Bootloader (x86):** A bootloader is a tiny program that BIOS/UEFI loads from disk to start an OS. In real-mode x86 (16-bit), write 512 bytes that print a message or load a second stage. You’ll learn about BIOS interrupts, real vs protected mode, and size optimization (512 bytes max, ending with 0xAA55 magic). This is a great low-level journey – you can find guides on writing an x86 "Hello World" boot sector ([Writing an x86 “Hello world” boot loader with assembly - Medium](https://medium.com/@g33konaut/writing-an-x86-hello-world-boot-loader-with-assembly-3e4c5bdd96cf#:~:text=Once%20it%20finds%20512%20byte,This%20bootloader)). Assemble with NASM to binary (`-f bin`) and test in QEMU or Bochs. It’s like living in the DOS era! 745 | 746 | - **Manual Hex Decoding:** Take a machine code buffer (an array of `uint8_t` bytes) and try to manually disassemble a few instructions. For example, 0x48 0x89 0xE5 corresponds to `mov rbp, rsp` in x86_64. Can you write a small program that given some hex bytes, figures out what instructions they are? This will force you to reference an opcode table and understand instruction encoding. (It’s how disassemblers work internally). 747 | 748 | - **Hand-written vs Compiler Optimized Assembly:** Pick a simple algorithm (like computing Fibonacci numbers, or string copy). Write it in C and compile with optimizations, and also write your own assembly version. Compare the assembly (use `objdump -d` or compiler’s assembly output). Which is faster or smaller? For example, try a naive `strlen` vs one that uses 64-bit loads. Often, compilers do a very good job ([How can writing in assembly produce a faster program than ... - Reddit](https://www.reddit.com/r/AskComputerScience/comments/xcmviv/how_can_writing_in_assembly_produce_a_faster/#:~:text=Reddit%20www,that%20humans%20write%20into)), but you might identify cases where you can hint the compiler or use different strategies. This gives insight into compiler optimization strategies. 749 | 750 | - **Tiny GPU Shader in PTX or SPIR-V:** If you have CUDA, try writing a kernel in PTX directly and running it via the CUDA driver API (advanced, but educational – or use inline PTX in a CUDA C++ program ([CUDA PTX assembly syntax - Medium](https://medium.com/@wanghs.thu/cuda-ptx-assembly-syntax-b81d8080c290#:~:text=Cuda%20PTX,j))). Alternatively, write a simple vertex or fragment shader in SPIR-V assembly (there are tools to assemble SPIR-V from its textual form) that does something trivial (like output a color). This helps you appreciate shader languages and how GPU instructions differ from CPU. For instance, in SPIR-V you’ll deal with a very different kind of “assembly” for shader stage inputs/outputs. 751 | 752 | - **SIMD Optimized Routine:** Take a function like `memcpy`, `memset`, or a simple image filter (like converting RGB to grayscale), and write a SIMD version in assembly or intrinsics. Compare performance with the standard version. You might use SSE/AVX on x86 or NEON on ARM. Tools like Godbolt can show you compiler’s version to beat. It’s a fun challenge to see if you can micro-optimize better than `-O3` for a specific task. 753 | 754 | - **Contribute to an Open Source OS or Compiler:** If you want real-world experience, consider contributing to something like the [Linux kernel](https://github.com/torvalds/linux) (many device drivers or arch-specific code involve assembly) or a compiler backend (like [LLVM’s X86 or RISC-V backend](https://llvm.org)). Even writing an optimization pass that recognizes a pattern and emits a fancy instruction can be a contribution. 755 | 756 | These projects range in difficulty. Start with small experiments (e.g., writing one function in assembly, or stepping through some code in a debugger) and work your way up. 757 | 758 | Remember, assembly is as much an art as it is science. It rewards creative thinking and attention to detail. It can be frustrating when things don’t work (that blank screen when your bootloader fails, or the segmentation fault from a register mistake), but the payoff is a deeper understanding of how computers work at the lowest level. 759 | 760 | Happy hacking! 761 | 762 | ## Resources 763 | 764 | To continue your assembly journey, here are some recommended resources: 765 | 766 | - **Books & Tutorials:** 767 | - *Programming from the Ground Up* by Jonathan Bartlett – an excellent introduction to x86 assembly using Linux (ELF, AT&T syntax) for beginners. 768 | - *Computer Systems: A Programmer’s Perspective* by Bryant and O’Hallaron – not assembly-specific, but has great chapters on machine code and assembly, focusing on x86-64, and how high-level code maps to it. 769 | - *The Art of Assembly Language* by Randall Hyde – a comprehensive book (uses a high-level assembler, HLA, but concepts apply generally; also available free online in parts). 770 | - *RISC-V Assembly Language Programming* (various online PDFs and the official RISC-V Reader by Patterson & Waterman) – if you want to dive into RISC-V specifically. 771 | - *ARM 64-bit Assembly* by Ray Seyfarth – a gentle introduction targeting ARM Cortex-A (64-bit ARMv8). 772 | 773 | - **Websites and Online Docs:** 774 | - **x86 Instruction Reference** – Felix Cloutier’s online reference is great for checking instruction details (timing, encoding) for x86 ([x86 and amd64 instruction reference](https://www.felixcloutier.com/x86/#:~:text=x86%20and%20amd64%20instruction%20reference,ADC%2C%20Add%20With%20Carry)). 775 | - **Wikibooks: x86 Assembly** – community-maintained tutorials and references, covers NASM and GAS usage ([x86 Assembly/GNU assembly syntax - Wikibooks, open books for an open world](https://en.wikibooks.org/wiki/X86_Assembly/GNU_assembly_syntax#:~:text=Examples%20in%20this%20article%20are,assembly%20language%20Syntax%20for%20a)). 776 | - **OSDev Wiki** – if you’re doing low-level stuff like bootloaders or OS kernels, this wiki is a goldmine of info (e.g., the OSDev Bare Bones tutorial). 777 | - **ARM Developer Documentation** – Arm’s official docs on AArch64, including the ARM Architecture Reference Manual (highly detailed). 778 | - **RISC-V Specifications** – the official unprivileged and privileged spec PDFs on riscv.org (if you want the exact definitions of the ISA). 779 | 780 | - **Communities:** 781 | - **/r/asm** and **/r/Assembly_language** on Reddit – active subreddits where people discuss assembly problems, share projects, and help each other ([/r/asm - where every byte counts - Reddit](https://www.reddit.com/r/asm/#:~:text=r%2Fasm%3A%20Welcome%20to%20,in%20all%20Instruction%20Set%20Architectures)). Great for asking questions or seeing what others are up to. 782 | - **Stack Overflow** – the assembly tag has many Q&A; just be specific in your questions. 783 | - **Compiler Explorer (Godbolt)** – not a community but a tool; you can share links with others to discuss code-gen. Many compiler experts hang out on their Slack/Discord too. 784 | 785 | - **Tools:** 786 | - **Godbolt Compiler Explorer** – ([Assembly - Compiler Explorer](https://godbolt.org/noscript/assembly#:~:text=Compiler%20Explorer%20is%20an%20interactive,and%20many%20more%29%20code)) an online tool to type C/C++/Rust/etc code and see the assembly output for various compilers. Invaluable for learning and checking what assembly your code produces. 787 | - **GDB/LLDB** – debuggers to step through assembly. Pair with a good IDE or TUI mode for ease. 788 | - **objdump, readelf, nm** – binutils for inspecting binaries and object files (symbol tables, sections). 789 | - **Radare2 or Cutter** – open-source reverse engineering tools, for exploring binaries (disassembly, control flow graphs) in a more visual way than objdump. 790 | - **IDA Pro/Ghidra** – if you get serious about reverse engineering, these are the go-to disassemblers/decompilers (IDA is commercial, Ghidra is free from NSA). 791 | - **AMD GPU Open Documentation** – AMD’s official GPU ISA docs and tools ([AMD GPU architecture programming documentation - AMD GPUOpen](https://gpuopen.com/amd-gpu-architecture-programming-documentation/#:~:text=We%E2%80%99ve%20been%20releasing%20the%20Instruction,DirectX%C2%AE10%20era%20back%20in%202006)), if you venture into GPU shader assembly. 792 | - **NVIDIA PTX Guide** – NVIDIA’s documentation for PTX ISA (available on their docs hub) ([1. Introduction — PTX ISA 8.7 documentation - NVIDIA Docs Hub](https://docs.nvidia.com/cuda/parallel-thread-execution/#:~:text=Hub%20docs,ISA)). 793 | 794 | - **Miscellaneous:** 795 | - **Hacker’s Delight** by Henry Warren – a book on bit tricks and low-level algorithms, useful for writing clever assembly routines (e.g., computing parity, bitscan, etc.). 796 | - **CPU Manuals** – Intel and AMD’s software developer manuals (massive, but the final word on x86), and ARM’s Architecture Reference Manual for ARMv8 – these are exhaustive references for the truly curious or if you need specifics on, say, how exactly the flags are affected by each instruction. 797 | 798 | Assembly programming is a deep field – there’s always more to learn, whether it’s a new ISA, optimization technique, or simply decoding the latest instruction set extensions. Keep experimenting and refer to these resources when you hit a roadblock. Over time, you’ll develop that instinct for “what the compiler is doing” and how to bend the machine to your will, one instruction at a time. Good luck, and have fun exploring the world of assembly! 799 | --------------------------------------------------------------------------------