Fundamentals 24 min read

Understanding Linux User Stacks: How Threads Manage Memory and Calls

This article explains the role of the Linux user stack in multithreaded programs, covering its memory layout, how each thread gets a private stack, stack frame mechanics, growth and overflow handling, and practical examples of creating and debugging stacks with pthreads and gdb.

Deepin Linux
Deepin Linux
Deepin Linux
Understanding Linux User Stacks: How Threads Manage Memory and Calls

Linux User Stack Overview

The user stack is the memory region used by a process while executing in user mode. It resides at the high end of the process's virtual address space and grows downward. The stack stores function arguments, local variables, return addresses, and temporary data needed for function calls.

Per‑Thread Private Stacks

Stack allocation during thread creation

When pthread_create is called, the runtime allocates a private stack for the new thread, typically via mmap in the process's file‑mapping area. The default size is 1–8 MiB depending on the architecture. The stack can be explicitly set with pthread_attr_setstack:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/types.h>
#define STACK_SIZE (1024*1024) // 1 MiB example

void* thread_function(void* arg) {
    printf("Child thread is running
");
    return NULL;
}

int main() {
    pthread_t tid;
    void* stack = mmap(NULL, STACK_SIZE, PROT_READ|PROT_WRITE,
                      MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    if (stack == MAP_FAILED) { perror("mmap failed"); return 1; }
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setstack(&attr, stack, STACK_SIZE);
    int ret = pthread_create(&tid, &attr, thread_function, NULL);
    if (ret != 0) { perror("pthread_create failed"); munmap(stack, STACK_SIZE); return 1; }
    pthread_join(tid, NULL);
    pthread_attr_destroy(&attr);
    munmap(stack, STACK_SIZE);
    return 0;
}

How the User Stack Works

Memory allocation strategy

On Linux the stack is allocated with mmap. Default sizes are 8 MiB on 32‑bit and ARM64 systems. Users can enlarge the stack with the shell command ulimit -s (value in KiB) or programmatically with setrlimit(RLIMIT_STACK, …).

Stack frame layout

When a function is called, the kernel pushes the arguments, the return address, and space for local variables onto the stack, forming a stack frame. For example, calling int add(int a, int b) creates a frame that holds a, b, the return address, and the local variable result. The frame is removed when the function returns, restoring the stack pointer.

Stack growth and overflow handling

The stack grows toward lower addresses. Excessive recursion or large local arrays can exhaust the allocated space, causing a stack overflow. Linux raises SIGSEGV on overflow; many compilers insert guard pages (e.g., StackGuard) to detect the condition early.

Adjusting Stack Size

Typical programs run fine with the default 8 MiB. Deep recursion or large buffers may require more. To set a temporary limit to 16 MiB:

#include <stdio.h>
#include <sys/resource.h>
#include <unistd.h>

int main() {
    struct rlimit rl;
    if (getrlimit(RLIMIT_STACK, &rl) == -1) { perror("getrlimit"); return 1; }
    printf("Current soft limit: %ld
", rl.rlim_cur);
    printf("Current hard limit: %ld
", rl.rlim_max);
    rl.rlim_cur = 16 * 1024 * 1024; // 16 MiB
    if (setrlimit(RLIMIT_STACK, &rl) == -1) { perror("setrlimit"); return 1; }
    if (getrlimit(RLIMIT_STACK, &rl) == -1) { perror("getrlimit"); return 1; }
    printf("New soft limit: %ld
", rl.rlim_cur);
    printf("New hard limit: %ld
", rl.rlim_max);
    return 0;
}

Case Studies

Multithreaded server example

A typical POSIX‑threaded network server creates a new thread for each client connection. Each thread has its own private stack, ensuring that buffers and local variables do not interfere across connections.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8888
#define MAX_CLIENTS 100
#define BUFFER_SIZE 1024

void* handle_client(void* arg) {
    int client_socket = *(int*)arg;
    char buffer[BUFFER_SIZE] = {0};
    int valread = read(client_socket, buffer, BUFFER_SIZE);
    if (valread < 0) { perror("read failed"); close(client_socket); pthread_exit(NULL); }
    printf("Received from client: %s
", buffer);
    const char response[] = "HTTP/1.1 200 OK
Content-Type: text/html

Hello, World!";
    send(client_socket, response, strlen(response), 0);
    close(client_socket);
    pthread_exit(NULL);
}

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    socklen_t addrlen = sizeof(address);
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); }
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt))) { perror("setsockopt failed"); exit(EXIT_FAILURE); }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); }
    if (listen(server_fd, MAX_CLIENTS) < 0) { perror("listen failed"); exit(EXIT_FAILURE); }
    while (1) {
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen)) < 0) { perror("accept failed"); continue; }
        pthread_t tid;
        int *p = malloc(sizeof(int));
        *p = new_socket;
        if (pthread_create(&tid, NULL, handle_client, p) != 0) { perror("pthread_create failed"); free(p); close(new_socket); }
    }
    close(server_fd);
    return 0;
}

Debugging a stack overflow

Using gdb, the command thread apply all bt shows backtraces for all threads. If a thread crashes because a fixed‑size buffer on the stack is overrun, replace the buffer with a heap allocation:

void* handle_client(void* arg) {
    int client_socket = *(int*)arg;
    char *buffer = malloc(BUFFER_SIZE);
    if (!buffer) { perror("malloc failed"); close(client_socket); pthread_exit(NULL); }
    int valread = read(client_socket, buffer, BUFFER_SIZE);
    if (valread < 0) { perror("read failed"); free(buffer); close(client_socket); pthread_exit(NULL); }
    printf("Received from client: %s
", buffer);
    const char response[] = "HTTP/1.1 200 OK
Content-Type: text/html

Hello, World!";
    send(client_socket, response, strlen(response), 0);
    free(buffer);
    close(client_socket);
    pthread_exit(NULL);
}

Recursive stack overflow example

The following code demonstrates how uncontrolled recursion quickly exhausts stack space:

#include <stdio.h>

void recursive_function(int n) {
    char large_array[1024]; // 1 KiB per call
    printf("Level %d, address of array: %p
", n, (void*)large_array);
    recursive_function(n + 1);
}

int main() {
    recursive_function(1);
    return 0;
}

Without a termination condition, each call adds a new frame and the program eventually receives SIGSEGV due to stack overflow.

Best Practices

Give each thread a reasonable stack size (default 8 MiB is suitable for most workloads).

For deep recursion or large local buffers, increase the limit with ulimit -s (shell) or setrlimit (programmatic).

Avoid large fixed‑size arrays on the stack; prefer heap allocation ( malloc / free) for data whose size is not known at compile time.

Use guard pages or compiler options (e.g., -fstack-protector) to detect overflows early.

When debugging multithreaded programs, inspect each thread's stack with gdb to locate overflow sources.

C++LinuxthreadpthreadStack OverflowUser Stack
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.