The Core Problem
When you build a linked list, binary tree, or any pointer-based structure inside a shared memory segment, you naturally want to store pointer values. But here is the trap: the kernel may attach the same shared memory segment at different virtual addresses in different processes. An absolute pointer (a real memory address) stored by process A is meaningless in process B.
The solution is simple: store offsets (distance in bytes from the start of the segment) instead of absolute addresses.
When you call shmat(shmid, NULL, 0), the NULL second argument means “kernel, you choose the address”. The kernel scans the calling process’s virtual address space for a free region big enough to fit the segment. Since each process has its own independent layout โ its own stack, heap, shared libraries loaded at varying positions, and prior mappings โ the free region found for process A will commonly differ from the one found for process B.
| 0xFFFF0000 [stack] |
| 0xB7E00000 โ baseaddr_A |
| libc.so |
| 0x08048000 [text] |
Same physical
memory
| 0xFFFF0000 [stack] |
| 0xC0010000 extra_lib.so |
| 0xB6F00000 โ baseaddr_B |
| libc.so |
| 0x08048000 [text] |
0xB7E00000 in Process A and at 0xB6F00000 in Process B. If Process A stores the absolute address 0xB7E00100 (pointing 256 bytes into the segment), Process B reads that value and tries to dereference 0xB7E00100 โ which in Process B’s address space is not inside the shared segment. Result: crash or silent data corruption.
/*
* WRONG โ storing absolute pointers inside shared memory.
* This works only if both processes happen to attach at the same address,
* which is never guaranteed.
*/
#include <stdio.h>
#include <sys/shm.h>
struct BadNode {
int value;
struct BadNode *next; /* absolute pointer โ DANGEROUS in shm */
};
int main(void)
{
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0660);
struct BadNode *base = shmat(shmid, NULL, 0);
struct BadNode *node1 = base;
struct BadNode *node2 = base + 1;
node1->value = 10;
node2->value = 20;
/*
* WRONG: stores the absolute virtual address of node2.
* In this process node2 might be at 0xB7E00010.
* In another process the same physical memory could be at 0xB6F00010.
* node1->next = 0xB7E00010 is garbage in the other process.
*/
node1->next = node2; /* WRONG */
printf("node1->next = %p (absolute โ wrong in another process)\n",
(void *)node1->next);
shmctl(shmid, IPC_RMID, NULL);
shmdt(base);
return 0;
}
Instead of storing the target’s absolute address, store the distance (in bytes) from the start of the shared memory segment. Every process knows where its own baseaddr is, so every process can reconstruct the correct real pointer from the same offset value.
/*
* CORRECT โ store offsets, not absolute pointers.
*
* To store: *p = (target - baseaddr);
* To restore: real_ptr = baseaddr + *p;
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <unistd.h>
struct GoodNode {
int value;
long next_offset; /* bytes from segment base; -1 means no next */
};
int main(void)
{
int shmid;
char *baseaddr;
shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0660);
baseaddr = shmat(shmid, NULL, 0); /* baseaddr = start of shm in THIS process */
struct GoodNode *node1 = (struct GoodNode *)(baseaddr + 0);
struct GoodNode *node2 = (struct GoodNode *)(baseaddr + sizeof(struct GoodNode));
node1->value = 10;
node2->value = 20;
node2->next_offset = -1; /* last node */
/*
* Store the OFFSET of node2 from the segment base.
* Offset = (char *)node2 - baseaddr = sizeof(struct GoodNode)
* This value is the same number regardless of where the segment is attached.
*/
node1->next_offset = (char *)node2 - baseaddr;
printf("[Parent] baseaddr=%p node1 offset=0 node2 offset=%ld\n",
baseaddr, node1->next_offset);
/* Fork โ child attaches the SAME segment independently */
if (fork() == 0) {
/* Child calls shmat() โ may get a DIFFERENT virtual address */
char *child_base = shmat(shmid, NULL, 0);
printf("[Child] child_base=%p (may differ from parent's baseaddr)\n",
child_base);
/* Walk the list using offset arithmetic */
struct GoodNode *cur = (struct GoodNode *)child_base;
int step = 0;
while (cur) {
printf("[Child] node[%d].value = %d\n", step++, cur->value);
if (cur->next_offset == -1) break;
/* Reconstruct real pointer using child's own baseaddr */
cur = (struct GoodNode *)(child_base + cur->next_offset);
}
shmdt(child_base);
exit(0);
}
wait(NULL);
shmctl(shmid, IPC_RMID, NULL);
shmdt(baseaddr);
return 0;
}
/*
* Compile: gcc -o offset_demo offset_demo.c
* Output (addresses vary):
* [Parent] baseaddr=0xb7e40000 node1 offset=0 node2 offset=16
* [Child] child_base=0xb7e30000 (may differ from parent's baseaddr)
* [Child] node[0].value = 10
* [Child] node[1].value = 20
*
* Correct even though child's base differs from parent's.
*/
| baseaddr (offset 0) |
. . . data . . . | target node (offset = 256) |
. . . data . . . |
| โโโโโโโโโโโโโโ 256 bytes (the stored offset value) โโโโโโโโโ | |||
/* ---------- STORING (write side) ---------- */
char *baseaddr = shmat(shmid, NULL, 0); /* start of shm in THIS process */
char *target = baseaddr + 256; /* some node 256 bytes into the segment */
long *p = (long *)(baseaddr + 128);/* location where we store the reference */
/* Store the offset โ not the absolute address */
*p = target - baseaddr; /* *p = 256 */
/* NEVER do this: */
/* *p = (long)target; stores 0xB7E00100 โ useless in another process */
/* ---------- RESTORING (read side) ---------- */
/* In another process, baseaddr may be different */
char *baseaddr2 = shmat(shmid, NULL, 0); /* e.g. 0xB6F00000 */
long *p2 = (long *)(baseaddr2 + 128); /* same relative position, different absolute addr */
/* Reconstruct the correct pointer using THIS process's base */
char *target2 = baseaddr2 + *p2; /* baseaddr2 + 256 โ correct! */
/* ---------- SUMMARY ---------- */
/* Store: offset = (target - baseaddr)
* Restore: real_ptr = (baseaddr + offset)
*
* The offset (256) is the same number in all processes.
* The real_ptr differs per process โ and that is correct and expected.
*/
When all nodes are the same size, you can skip offset arithmetic entirely and use integer array indices as references. Index 3 always means array[3] regardless of where in virtual memory array starts.
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#define MAX_NODES 64
struct IndexNode {
int value;
int next_index; /* array index of next node; -1 = end */
};
struct ShmPool {
int head; /* index of first node, -1 = empty */
int free_head; /* index of first free slot */
struct IndexNode nodes[MAX_NODES];
};
int main(void)
{
int shmid = shmget(IPC_PRIVATE, sizeof(struct ShmPool), IPC_CREAT | 0660);
struct ShmPool *pool = shmat(shmid, NULL, 0);
/* Initialise free list */
pool->head = -1;
pool->free_head = 0;
for (int i = 0; i < MAX_NODES - 1; i++)
pool->nodes[i].next_index = i + 1;
pool->nodes[MAX_NODES - 1].next_index = -1;
/* Insert three nodes (prepend) */
for (int v = 1; v <= 3; v++) {
int idx = pool->free_head;
pool->free_head = pool->nodes[idx].next_index;
pool->nodes[idx].value = v * 10;
pool->nodes[idx].next_index = pool->head;
pool->head = idx;
}
/* Walk the list */
printf("List: ");
for (int i = pool->head; i != -1; i = pool->nodes[i].next_index)
printf("%d ", pool->nodes[i].value);
printf("\n");
/*
* In any other process that attaches this same segment:
* struct ShmPool *p2 = shmat(shmid, NULL, 0);
* Index 0, 1, 2 mean the same thing relative to p2 โ no absolute address trouble.
*/
shmctl(shmid, IPC_RMID, NULL);
shmdt(pool);
return 0;
}
/* Output: List: 30 20 10 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <unistd.h>
typedef struct {
int value;
long left_off; /* offset from shm base; -1 = no child */
long right_off;
} BSTNode;
#define MAX_NODES 32
typedef struct {
long root_off; /* offset to root; -1 = empty */
int count;
BSTNode nodes[MAX_NODES];
} BST;
/* Macros to convert between offset and pointer */
#define OFF2PTR(base, off) \
((off) == -1L ? NULL : (BSTNode *)((char *)(base) + (off)))
#define PTR2OFF(base, ptr) \
((long)((char *)(ptr) - (char *)(base)))
static void insert(BST *tree, int val)
{
if (tree->count >= MAX_NODES) return;
BSTNode *n = &tree->nodes[tree->count++];
n->value = val;
n->left_off = n->right_off = -1;
long noff = PTR2OFF(tree, n);
if (tree->root_off == -1) { tree->root_off = noff; return; }
BSTNode *cur = OFF2PTR(tree, tree->root_off);
for (;;) {
if (val < cur->value) {
if (cur->left_off == -1) { cur->left_off = noff; break; }
cur = OFF2PTR(tree, cur->left_off);
} else {
if (cur->right_off == -1) { cur->right_off = noff; break; }
cur = OFF2PTR(tree, cur->right_off);
}
}
}
static void inorder(BST *tree, long off)
{
if (off == -1) return;
BSTNode *n = OFF2PTR(tree, off);
inorder(tree, n->left_off);
printf("%d ", n->value);
inorder(tree, n->right_off);
}
int main(void)
{
int shmid = shmget(IPC_PRIVATE, sizeof(BST), IPC_CREAT | 0660);
BST *tree = shmat(shmid, NULL, 0);
tree->root_off = -1;
tree->count = 0;
int vals[] = {50, 30, 70, 20, 40, 60, 80};
for (int i = 0; i < 7; i++) insert(tree, vals[i]);
printf("[Parent] In-order: ");
inorder(tree, tree->root_off);
printf("\n");
if (fork() == 0) {
BST *t2 = shmat(shmid, NULL, 0); /* may attach at different address */
printf("[Child] In-order: ");
inorder(t2, t2->root_off);
printf("\n");
shmdt(t2);
exit(0);
}
wait(NULL);
shmctl(shmid, IPC_RMID, NULL);
shmdt(tree);
return 0;
}
/*
* Output:
* [Parent] In-order: 20 30 40 50 60 70 80
* [Child] In-order: 20 30 40 50 60 70 80
*/
| Approach | Safe? | Best For | Notes |
|---|---|---|---|
| *p = target | โ Never | โ | Stores absolute address โ meaningless in any process that attaches at a different address. |
| *p = target – baseaddr | โ Correct | Variable-size or complex layouts | Offset is a universal value. Each process rebuilds the pointer with its own baseaddr. |
| array[index] | โ Correct | Fixed-size node pools | Simplest approach when all elements are the same size. |
Answer: shmat(shmid, NULL, 0) lets the kernel choose the attachment address independently for each process. The same segment may be at 0xB7E00000 in process A and 0xB6F00000 in process B. An absolute pointer stored by A, say 0xB7E00200, is not inside the segment in B’s address space at all. Dereferencing it causes a segfault or silent data corruption.
Answer: Store a relative byte offset from the segment’s start:
Store: *p = (target - baseaddr);
Restore: real_ptr = baseaddr + *p;
baseaddr is the return value of shmat() in the current process. The offset is the same number in every process; only the reconstructed pointer differs.
Answer: You can pass a fixed address as the second argument to shmat(shmid, fixed_addr, SHM_RND). However this is fragile: the address may already be occupied in some process (shmat fails), different processes may have different library layouts, and ASLR can cause conflicts. Offsets are always the portable, safe solution.
Answer: When all nodes are the same size, cast the shared memory as an array of fixed-size structures and use integer indices as references. Index 5 always means the 5th element of the array, regardless of where the array starts in virtual memory.
Answer: Use ptrdiff_t (from <stddef.h>) or long. On 64-bit systems these are 8 bytes. Using int (4 bytes, max ~2 GB) is only safe if the segment is guaranteed to be smaller than 2 GB. ptrdiff_t is the standard C type for pointer differences and is the most portable choice.
Answer: Only immediately after fork() in the child, before any exec(). After exec() the address space is completely replaced and shared memory must be re-attached. Also, grandchildren, unrelated processes, or processes restarted after a crash will all attach at potentially different addresses. Relying on address equality is not a safe general strategy.
Answer: baseaddr is the virtual address at which the shared memory segment starts in the current process. It is the return value of shmat() in that process. It is a process-local variable โ it must not be stored inside the shared memory itself. Each process saves its own copy of baseaddr and uses it for all offset calculations.
Learn how to delete, inspect, change permissions, and lock a shared memory segment.
