Skip to content

Userspace Programs

Enrico Fraccaroli (Galfurian) edited this page Jan 29, 2026 · 9 revisions

This page teaches you how to write userspace programs for MentOS.

What are Userspace Programs?

Userspace programs are applications that run in user mode (ring 3), separate from the kernel. They:

  • Run in their own isolated address space (0x00000000 - 0xBFFFFFFF)
  • Cannot directly access hardware or kernel memory
  • Interact with the kernel only through system calls
  • Use the C standard library for common functionality

MentOS provides 40+ built-in programs (ls, cat, shell, ps, etc.) that demonstrate these concepts.

How Programs Work

Program Lifecycle

1. Kernel loads ELF binary from disk
   ↓
2. Creates new process with isolated address space
   ↓
3. Maps program into memory (text, data, bss, stack)
   ↓
4. Jumps to _start (entry point in crt0.S)
   ↓
5. _start calls __libc_start_main
   ↓
6. __libc_start_main calls main(argc, argv, envp)
   ↓
7. Program executes
   ↓
8. main returns to __libc_start_main
   ↓
9. _start performs the exit syscall (int 0x80)
   ↓
10. Kernel cleans up process

Memory Layout

When a program is loaded:

0xBFFFFFFF  ┌─────────────────┐
            │     Stack       │  Grows downward
            │       ↓         │
            ├─────────────────┤
            │       ↑         │
            │     Heap        │  Grows upward (malloc/brk)
            ├─────────────────┤
            │   .bss (zeros)  │  Uninitialized data
            ├─────────────────┤
            │   .data         │  Initialized data
            ├─────────────────┤
0x0xxxxxxx  │   .text         │  Program code (randomized address)
            └─────────────────┘
0x00000000  (Invalid - NULL pointer)

Note: The .text section address is randomized per-program (between 0x10000000 and 0xB0000000) so symbols don't clash when debugging multiple programs.

Interacting with the Kernel

Programs use system calls to request kernel services:

// User calls libc function
int fd = open("/file.txt", O_RDONLY);

// libc wrapper invokes syscall
ssize_t n = read(fd, buffer, 100);

// Under the hood:
// 1. Set syscall number in eax
// 2. Set arguments in ebx, ecx, edx, esi, edi
// 3. Execute INT 0x80
// 4. CPU switches to kernel mode (ring 0)
// 5. Kernel handles syscall
// 6. Return to userspace with result in eax

Creating Your First Program

Let's create a simple hello program step by step.

Step 1: Write the Source Code

Create userspace/bin/hello.c:

/// @file hello.c
/// @brief A simple hello world program
/// @copyright (c) 2014-2024 This file is distributed under the MIT License.
/// See LICENSE.md for details.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    printf("Hello, MentOS!\n");
    
    // Show arguments if provided
    if (argc > 1) {
        printf("You passed %d argument(s):\n", argc - 1);
        for (int i = 1; i < argc; i++) {
            printf("  [%d]: %s\n", i, argv[i]);
        }
    }
    
    return 0;
}

What's happening:

  • #include <stdio.h> - Use standard I/O functions (printf)
  • main(argc, argv) - Entry point called by libc
  • printf() - Calls write() syscall internally
  • return 0 - Success exit code

Step 2: Add to Build System

Edit userspace/bin/CMakeLists.txt and add hello.c to the PROGRAM_LIST:

set(PROGRAM_LIST
    cat.c
    # ... other programs ...
    hello.c      # <-- Add this line
    # ... more programs ...
)

That's it! The CMake configuration automatically:

  1. Creates a build target prog_hello
  2. Links with libc
  3. Sets up the entry point (_start)
  4. Randomizes the .text address
  5. Outputs binary to filesystem/bin/hello

Step 3: Build and Test

# Build the program
cd build
## Available Programs (Overview)

MentOS ships with 40+ built-in programs. Instead of a long catalog here, use this page to **learn how programs work** and use the source tree for reference.

**Where to browse code:** `userspace/bin/`

**What you’ll find there:**

- **System & Core**: `init`, `login`, `shell`
- **File Operations**: `cat`, `ls`, `cp`, `rm`, `mkdir`, `rmdir`, `chmod`, `chown`, `touch`, `stat`, `head`, `more`
- **Process & System**: `ps`, `kill`, `nice`, `showpid`, `uptime`, `uname`, `date`, `poweroff`
- **Utilities**: `echo`, `env`, `pwd`, `clear`, `reset`, `id`, `logo`, `cpuid`
- **IPC Tools**: `ipcs`, `ipcrm`
- **Development**: `runtests`, `edit`, `man`

If you want to study a specific program, open its source in `userspace/bin/` and follow how it uses libc and syscalls. That’s where the real learning happens.

## Further Reading

- [[C Library]] - Available libc functions
- [[System Calls]] - Kernel interface reference
- [[Development Guide]] - Writing new programs
- [[Debugging]] - Debugging userspace programs

## Program Compilation
     42     1      S login
     45    42      S shell
     51    45      R ps

Process States:

  • S (INTERRUPTIBLE_SLEEP) - Waiting for event
  • R (RUNNING) - Currently executing
  • Z (ZOMBIE) - Terminated, awaiting parent reaping
  • D (UNINTERRUPTIBLE_SLEEP) - Cannot be interrupted

uptime - Show System Uptime

Location: userspace/bin/uptime.c

Displays system uptime.

Output:

$ uptime
Days: 0 Hours: 1 Minutes: 23 Seconds: 45

Information:

  • Uptime in days, hours, minutes, seconds

uname - System Information

Location: userspace/bin/uname.c

Shows system information.

Flags:

  • -a, --all - System name and kernel version
  • -r, --rev - Kernel version only
  • -i, --info - System name and kernel version
  • -h, --help - Show help

Output:

$ uname -a
MentOS 0.1.0

clear - Clear Screen

Location: userspace/bin/clear.c

Clears terminal screen.

Usage:

clear

User and Group Programs

id - Show User Identity

Location: userspace/bin/id.c

Displays user and group IDs.

Output:

$ id
uid=0(root) gid=0(root)

Development and Testing

echo - Print Arguments

Location: userspace/bin/echo.c

Outputs text to stdout.

Flags:

  • -n - Don't add newline
  • -e - Interpret escape sequences

Usage:

$ echo "Hello, World!"
Hello, World!

$ echo -n "No newline"
No newline$

env - Show Environment

Location: userspace/bin/env.c

Displays environment variables.

Usage:

$ env
PATH=/bin:/usr/bin
HOME=/root
USER=root
...

kill - Send Signal

Location: userspace/bin/kill.c

Sends signals to processes.

Usage:

kill 1234              # Send SIGTERM to PID 1234
kill -9 1234           # Send SIGKILL (force kill)
kill -l                # List all signals

nice - Run with Priority

Location: userspace/bin/nice.c

Adjusts the calling process priority (nice value).

Usage:

nice               # Show current nice value
nice 10            # Increase nice value by 10
nice -5            # Decrease nice value by 5

showpid - Display Process ID

Location: userspace/bin/showpid.c

Shows current process ID.

Usage:

$ showpid
5

stat - File Status

Location: userspace/bin/stat.c

Shows file metadata.

Output:

$ stat file.txt
File: file.txt
Size: 1.00 KB  Inode: 12
File type: regular file
Access: (0644/-rw-r--r--) Uid: (0/root) Gid: (0/root)
Access: 1970-1-1 0:0:0
Modify: 1970-1-1 0:0:0
Change: 1970-1-1 0:0:0

date - Show Current Date/Time

Location: userspace/bin/date.c

Displays current date and time.

Usage:

$ date
Seconds since 01/01/1970 : 0
It's  0: 0: 0, 0 weekday, 01/01/1970

cpuid - CPU Information

Location: userspace/bin/cpuid.c

Displays CPU capabilities (x86-specific).

Output:

Currently a stub (no output).

poweroff - Shutdown System

Location: userspace/bin/poweroff.c

Gracefully shuts down the system.

Usage:

poweroff           # Power off system

login - User Authentication

Location: userspace/bin/login.c

Authenticates user and starts session.

logo - Display Welcome Banner

Location: userspace/bin/logo.c

Shows ASCII art welcome screen.

Output:

 __  __                  _      ___    ____ 
|  \/  |   ___   _ __   | |_   / _ \  / ___|
| |\/| |  / _ \ | '_ \  | __| | | | | \___ \
| |  | | |  __/ | | | | | |_  | |_| |  ___) |
|_|  |_|  \___| |_| |_|  \__|  \___/  |____/

MentOS - Mentoring Operating System

runtests - Test Suite

Location: userspace/bin/runtests.c

Runs automated tests.

Usage:

runtests          # Run all tests

Program Compilation

Programs are compiled and linked with libc:

// Example program: userspace/bin/echo.c
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    for (int i = 1; i < argc; i++) {
        printf("%s", argv[i]);
        if (i < argc - 1) printf(" ");
    }
    printf("\n");
    return 0;
}

Build Process:

# In CMakeLists.txt
add_executable(prog_echo echo.c)
target_include_directories(prog_echo PRIVATE ${CMAKE_SOURCE_DIR}/lib/inc)
target_link_libraries(prog_echo libc)

Filesystem Organization

Programs in the filesystem image:

files/
├── bin/              # Executable programs
│   ├── cat
│   ├── chmod
│   ├── cp
│   ├── echo
│   ├── init
│   ├── ls
│   ├── mkdir
│   ├── ps
│   ├── pwd
│   ├── rm
│   ├── shell
│   └── (40+ more)
├── etc/              # System configuration
│   ├── passwd        # User database
│   ├── shadow        # Password database
│   ├── group         # Group database
│   ├── inittab       # Init configuration
│   └── ...
├── home/             # User home directories
├── proc/             # Process filesystem (virtual)
└── usr/              # User programs (optional)

Key Patterns

Standard Program Structure

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[], char *envp[]) {
    // Parse arguments
    for (int i = 1; i < argc; i++) {
        char *arg = argv[i];
        if (arg[0] == '-') {
            // Handle flags
        } else {
            // Handle positional arguments
        }
    }
    
    // Perform operation
    
    // Return exit code
    return 0;  // Success
    // return 1;  // Error
}

Signal Handling

#include <signal.h>

void handle_signal(int sig) {
    if (sig == SIGINT) {
        printf("Interrupted\n");
        exit(0);
    }
}

int main() {
    signal(SIGINT, handle_signal);
    // ... main code
}

Process Creation

#include <unistd.h>

pid_t pid = fork();
if (pid == 0) {
    // Child process
    execve("/bin/program", argv, envp);
    exit(1);  // If exec fails
} else if (pid > 0) {
    // Parent process
    wait(NULL);  // Wait for child
}

Hands-On Exercises

Exercise 1: Write Your First MentOS Program

Goal: Create a simple program and run it on MentOS.

Program - userspace/bin/hello_os.c:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[], char *envp[]) {
    printf("Hello from MentOS!\n");
    printf("My PID: %d\n", getpid());
    printf("I have %d arguments\n", argc);
    
    for (int i = 0; i < argc; i++) {
        printf("  arg[%d]: %s\n", i, argv[i]);
    }
    
    return 0;
}

Build and Test:

  1. Add to userspace/bin/CMakeLists.txt:
set(PROGRAM_LIST
    # ... other programs ...
    hello_os.c
)
  1. Rebuild:
cd build
make prog_hello_os
make filesystem
make qemu
  1. In MentOS shell:
# Run with no arguments
/bin/hello_os

# Run with arguments
/bin/hello_os arg1 arg2 arg3

Expected output:

Hello from MentOS!
My PID: 42
I have 4 arguments
  arg[0]: /bin/hello_os
  arg[1]: arg1
  arg[2]: arg2
  arg[3]: arg3

Exercise 2: Implement a Simple Calculator

Goal: Parse command-line arguments and perform operations.

Program - userspace/bin/calc.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc != 4) {
        printf("Usage: calc <number> <operator> <number>\n");
        printf("Operators: +, -, *, /\n");
        return 1;
    }
    
    int a = atoi(argv[1]);
    char op = argv[2][0];
    int b = atoi(argv[3]);
    
    int result = 0;
    char success = 1;
    
    switch (op) {
        case '+': result = a + b; break;
        case '-': result = a - b; break;
        case '*': result = a * b; break;
        case '/':
            if (b == 0) {
                printf("Error: Division by zero\n");
                success = 0;
            } else {
                result = a / b;
            }
            break;
        default:
            printf("Unknown operator: %c\n", op);
            success = 0;
    }
    
    if (success) {
        printf("%d %c %d = %d\n", a, op, b, result);
    }
    
    return success ? 0 : 1;
}

Test:

/bin/calc 10 + 5        # Output: 10 + 5 = 15
/bin/calc 20 - 3        # Output: 20 - 3 = 17
/bin/calc 4 '*' 6       # Output: 4 * 6 = 24
/bin/calc 15 / 3        # Output: 15 / 3 = 5
/bin/calc 10 / 0        # Error: Division by zero

Exercise 3: Create an Echo Program

Goal: Read stdin and write to stdout (like Unix echo).

Program - userspace/bin/echo2.c:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    // Echo all arguments, space-separated
    for (int i = 1; i < argc; i++) {
        printf("%s", argv[i]);
        if (i < argc - 1) {
            printf(" ");
        }
    }
    printf("\n");
    
    return 0;
}

Enhancement - Add -n flag (no newline):

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
    int newline = 1;  // Default: add newline
    int start = 1;
    
    // Check for -n flag
    if (argc > 1 && strcmp(argv[1], "-n") == 0) {
        newline = 0;
        start = 2;
    }
    
    // Print arguments
    for (int i = start; i < argc; i++) {
        printf("%s", argv[i]);
        if (i < argc - 1) {
            printf(" ");
        }
    }
    
    if (newline) {
        printf("\n");
    }
    
    return 0;
}

Test:

/bin/echo2 Hello World
/bin/echo2 -n No newline

# Use output redirection
/bin/echo2 test > output.txt
/bin/cat output.txt

Exercise 4: Implement wc (Word Count)

Goal: Read input and count lines, words, and characters.

Program - userspace/bin/wc.c:

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: wc <file>\n");
        return 1;
    }
    
    int fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        printf("Error: cannot open %s\n", argv[1]);
        return 1;
    }
    
    int lines = 0, words = 0, chars = 0;
    int c;
    int in_word = 0;
    
    while ((c = fgetc(fd)) != EOF) {
        chars++;
        
        if (c == '\n') {
            lines++;
            in_word = 0;
        } else if (c == ' ' || c == '\t' || c == '\n') {
            in_word = 0;
        } else if (!in_word) {
            words++;
            in_word = 1;
        }
    }
    
    if (in_word) lines++;  // Count last line
    
    printf("%d %d %d %s\n", lines, words, chars, argv[1]);
    
    close(fd);
    return 0;
}

Test:

# Create a test file
echo -e "Hello World\nThis is a test" > /tmp/test.txt

# Count it
/bin/wc /tmp/test.txt
# Expected: 2 5 26 /tmp/test.txt

Exercise 5: Multi-Process Program (Shell Launcher)

Goal: Use fork/exec to launch other programs.

Program - userspace/bin/launcher.c:

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

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: launcher <program> [args...]\n");
        return 1;
    }
    
    // Print parent process info
    printf("[Parent] PID %d launching: %s\n", getpid(), argv[1]);
    
    pid_t pid = fork();
    
    if (pid == 0) {
        // Child: prepare arguments and exec
        char *child_argv[] = {argv[1], argv[2], NULL};
        char *child_envp[] = {NULL};
        
        printf("[Child] PID %d executing %s\n", getpid(), argv[1]);
        
        execve(argv[1], child_argv, child_envp);
        
        // If we get here, exec failed
        printf("[Child] ERROR: exec failed\n");
        return 1;
    } else if (pid > 0) {
        // Parent: wait for child
        int status;
        printf("[Parent] Waiting for child %d...\n", pid);
        
        waitpid(pid, &status, 0);
        
        int exit_code = WEXITSTATUS(status);
        printf("[Parent] Child exited with code: %d\n", exit_code);
    } else {
        printf("Fork failed!\n");
        return 1;
    }
    
    return 0;
}

Test:

# Launch a program through the launcher
/bin/launcher /bin/echo Hello from launcher

# Verify process hierarchy
/bin/ps

Challenge: Build Your Own Shell

Advanced Goal: Implement a basic shell that:

  1. Reads commands from user
  2. Parses arguments (space-separated)
  3. Forks and execs the program
  4. Waits for completion
  5. Loops for next command

Hints:

  • Use fgets() to read input
  • Use strtok() to parse arguments
  • Use fork/execve/wait pattern
  • Implement built-in commands (cd, exit, history)

Further Reading

Clone this wiki locally