Introduction to Position Independent Code (PIC) Architecture: Setting Your Own Anchor on the Stack
When you write a standard program in C or C++ and compile it, the compiler and operating system handle a lot of work for you. Your code goes into the .text section, static variables go into .data, and zeroed variables into .bss. The OS loader prepares the memory locations of these sections before the program runs and links everything together.
But sometimes you encounter specific scenarios where you can’t rely on the OS loader, where you won’t know ahead of time where your code will be thrown in memory. This is where the rules change, and you need to write Position Independent Code (PIC).
So what is PIC, why do we need it, and how do we use the “Stack Anchor” technique to find our way in memory?
Why Do We Use PIC?
Position Independent Code is a code architecture that uses relative references instead of absolute references to memory addresses.
If you use a fixed memory address like mov rax, [0x401000] in your Assembly code and your code is loaded at 0x500000 at runtime, your program will crash immediately (Segmentation Fault). The data it’s looking for is no longer at that address. With PIC architecture, the code doesn’t say “go to this fixed address”; it says “go 50 bytes forward from where I currently am.” This way, no matter where you copy the code block in memory, it continues to work flawlessly as a complete unit.
Where PIC Saves the Day
- Dynamic linked libraries (.so / .dll files)
- Special executable blocks that need to run directly in memory, independent of the OS (Sectionless execution)
- Advanced memory injection architectures
The Challenge of “Sectionless” Living: Where Do We Store Our Data?
The biggest problem when writing PIC is storing data. Since we can’t use fixed addresses, we have to forget about traditional .data or .bss sections. We’re forced to embed all our data and variables directly inside the .text (code) section.
But here a fatal problem emerges: Memory Protection Rules.
The .text section is usually marked by the operating system as R-X (Readable and Executable). It’s not writable. If you try to assign a new value to a variable embedded in the .text section, you’ll get an access violation immediately.
If we want to access templates carried within our code AND write data to them at runtime, we need to dynamically create our own Read-Write (R-W) area. For this, the safest haven is the stack.
The Solution: The “Stack Anchor” Technique
In the projects I’ve developed, I use a method where I set my own anchor on the stack to manage variables safely and avoid getting tangled with OS protections. This architecture provides the perfect foundation for processing incoming data (such as network packets or file contents).
Here’s the basic building block of this architecture:
Step 1: Creating a Safe Area and Setting the Anchor
sub rsp, 0x8000 ; We're carving out a massive 32KB area on the stack
and rsp, -16 ; We're aligning the stack to a 16-byte boundary (critical for syscalls)
mov rbp, rsp ; We're anchoring the RBP register to this safe haven
What these three lines do is remarkable. They take the current stack pointer (RSP), pull it way down (0x8000) to avoid overwriting existing data, apply 16-byte alignment, and then anchor the RBP register here. Now, no matter where in our code we are, we can safely access our own Read/Write area anytime by saying [rbp + offset].
Step 2: Copying Templates to Our Working Area
We’ve set the anchor. But we have some data templates embedded in our code (inside the Read-Only .text section)—like an ICMP packet header, an IP configuration, or a static string. To modify these, we need to transfer them to the safe R-W (Read-Write) area we just created.
This is where the rep movsb (Repeated Byte Copy) instruction takes the stage:
; --- STEP 2: TRANSFERRING TEMPLATES TO THE STACK ---
; We're establishing special offsets on the stack:
; icmp_packet copy -> [rbp + 0x100]
; delay_req copy -> [rbp + 0x200]
; Transfer our ICMP packet template to the safe stack area
lea rsi, [rel icmp_packet] ; SOURCE: Original template in our code (Read-Only)
lea rdi, [rbp + 0x100] ; DESTINATION: Our new space on the stack (Readable and Writable!)
mov rcx, 88 ; SIZE: The packet is 88 bytes
rep movsb ; Copy!
; ... Program continues ...
; --- STEP 3: DATA TEMPLATES ---
icmp_packet:
db 0x08, 0x00, 0x00, 0x00 ; Type, Code, Checksum...
; ... (88 bytes of static data) ...
What This Architecture Gained Us
Complete Independence: No fixed addresses (0x4000…) appear in the code. Everything is calculated relatively through RIP-relative addressing and base pointer (rbp).
Efficient Memory Usage: The dependency on .data or .bss sections is completely eliminated. Code is reduced to a single block.
Safe Manipulation: Templates that normally can’t be written to are transferred to a dynamic stack area and become modifiable at runtime. Now we can inject whatever data we want into the packet at [rbp + 0x100] and send it over the network.
Writing PIC means stepping out of the compiler’s comfort zone and confronting the fundamentals of architecture. As long as you set your anchor correctly, no matter how deep in memory you are, you’ll never lose your way.