-
Notifications
You must be signed in to change notification settings - Fork 69
Userspace Programs
This page teaches you how to write userspace programs for MentOS.
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.
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
↓
6. libc_start calls main(argc, argv, envp)
↓
7. Program executes
↓
8. main returns to libc_start
↓
9. libc_start calls exit(return_value)
↓
10. Kernel cleans up process
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.
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 eaxLet's create a simple hello program step by step.
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()- Callswrite()syscall internally -
return 0- Success exit code
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:
- Creates a build target
prog_hello - Links with libc
- Sets up the entry point (
_start) - Randomizes the
.textaddress - Outputs binary to
filesystem/bin/hello
# Build the program
cd build
make prog_hello
# Rebuild filesystem with new program
make filesystem
# Run MentOS
make qemu
# In MentOS shell:
$ hello
Hello, MentOS!
$ hello world foo bar
Hello, MentOS!
You passed 3 argument(s):
[1]: world
[2]: foo
[3]: barA more complex example showing file I/O:
/// @file wordcount.c
/// @brief Count lines, words, and characters in a file
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <ctype.h>
int main(int argc, char **argv)
{
if (argc < 2) {
printf("Usage: %s <file>\n", argv[0]);
return 1;
}
// Open file (uses sys_open syscall)
int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
// Count lines, words, chars
int lines = 0, words = 0, chars = 0;
int in_word = 0;
char c;
// Read file one byte at a time (uses sys_read syscall)
while (read(fd, &c, 1) > 0) {
chars++;
if (c == '\n') {
lines++;
}
if (isspace(c)) {
in_word = 0;
} else if (!in_word) {
in_word = 1;
words++;
}
}
// Close file (uses sys_close syscall)
close(fd);
printf("%d lines, %d words, %d chars in %s\n",
lines, words, chars, argv[1]);
return 0;
}Key concepts:
- Error handling with
perror() - File descriptor lifecycle: open → read → close
- Working with syscalls through libc wrappers
Creating child processes:
/// @file runner.c
/// @brief Execute a program and wait for it
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
if (argc < 2) {
printf("Usage: %s <program> [args...]\n", argv[0]);
return 1;
}
printf("Running: %s\n", argv[1]);
// Create child process (uses sys_fork syscall)
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) {
// Child process: execute the program
// (uses sys_execve syscall)
execve(argv[1], &argv[1], NULL);
// If we get here, execve failed
perror("execve");
exit(1);
}
// Parent process: wait for child
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("Program exited with code: %d\n", WEXITSTATUS(status));
} else {
printf("Program terminated abnormally\n");
}
return 0;
}Key concepts:
-
fork()creates identical copy of process - Child gets return value 0, parent gets child PID
-
execve()replaces process image -
waitpid()waits for child termination
#include <signal.h>
void handle_sigint(int sig)
{
printf("\nCaught SIGINT (Ctrl+C)\n");
exit(0);
}
int main()
{
signal(SIGINT, handle_sigint);
printf("Press Ctrl+C to exit...\n");
while (1) {
sleep(1);
}
return 0;
}#include <stdio.h>
int main()
{
char buffer[256];
printf("Enter your name: ");
// Read from standard input (fd 0)
if (fgets(buffer, sizeof(buffer), stdin)) {
printf("Hello, %s", buffer);
}
return 0;
}#include <stdio.h>
#include <errno.h>
#include <string.h>
int fd = open("/nonexistent", O_RDONLY);
if (fd < 0) {
// Option 1: Simple message
perror("open");
// Option 2: Custom message
printf("Failed to open file: %s\n", strerror(errno));
// Option 3: Check specific error
if (errno == ENOENT) {
printf("File does not exist\n");
}
return 1;
}When you add a program to PROGRAM_LIST, CMake automatically:
-
Creates executable target:
prog_<name> -
Includes libc headers: From
lib/inc/ -
Links with libc: Static library
libc.a -
Sets entry point:
-u_start(defined inlib/src/crt0.S) - Randomizes .text address: Prevents symbol conflicts
-
Sets linker flags:
-Ttext=<random> -e_start -melf_i386 -
Outputs to filesystem:
filesystem/bin/<name>
You can build individual programs:
make prog_hello # Build just hello
make programs # Build all programsPrograms must be in filesystem/bin/ to be accessible in MentOS:
# After building
make filesystem # Creates rootfs.img with all programs
# Programs are then accessible in MentOS:
$ ls /bin
cat chmod cp echo hello ls mkdir ...MentOS includes these built-in programs (see source in userspace/bin/):
-
init- First process, spawns login -
login- User authentication -
shell- Command interpreter with pipes and job control
-
cat- Display file contents -
ls- List directory contents -
cp- Copy files -
rm- Remove files -
mkdir- Create directory -
rmdir- Remove directory -
chmod- Change permissions -
chown- Change ownership -
touch- Create/update file -
stat- Show file metadata -
head- Show first lines -
more- Page through file
-
ps- List processes -
kill- Send signals -
nice- Run with priority -
showpid- Display PID -
uptime- System uptime -
uname- System information -
date- Show date/time -
poweroff- Shutdown system
-
echo- Print text -
env- Show environment -
pwd- Print working directory -
clear- Clear screen -
reset- Reset terminal -
id- Show user/group IDs -
logo- Display welcome banner -
cpuid- CPU information
-
ipcs- List IPC resources -
ipcrm- Remove IPC resources
-
runtests- Test suite runner -
edit- Simple text editor -
man- Manual pages
-
false- Always returns failure (for scripting) -
sleep- Delay execution
Each program demonstrates different aspects of system programming. Check their source code in userspace/bin/ for implementation examples.
- C Library - Available libc functions
- System Calls - Kernel interface reference
- Development Guide - Adding features
- Debugging - Debugging userspace programs
Location: userspace/bin/login.c
Prompts user for credentials and starts shell.
Flow:
1. Display "login:" prompt
2. Read username
3. Display "password:" prompt
4. Read and verify password
5. Set UID/GID to user's credentials
6. Change to user's home directory
7. Execute user's shell
Key Functions:
- Username validation (from
/etc/passwd) - Password verification (from
/etc/shadow) - Session setup
- Environment variables
Location: userspace/bin/shell.c
Interactive command interpreter.
Features:
- Command execution (with fork/exec)
- Built-in commands (cd, exit, help)
- Job control (background processes)
- Command history (if supported)
- Environmental variables ($VAR expansion)
- Pipe support (|) for command chaining
- Redirections (>, <, >>)
Built-in Commands:
cd <dir> # Change directory
exit [code] # Exit shell
help # Show help
export VAR=val # Set environment variableExample Interactions:
$ ls -la
total 256
drwxr-xr-x 5 root root 4096 Jan 1 1970 .
drwxr-xr-x 3 root root 4096 Jan 1 1970 ..
-rw-r--r-- 1 root root 256 Jan 1 1970 file.txt
$ cat file.txt
Hello from MentOS
$ echo "Test" > output.txt
$ ps
PID TTY STAT TIME COMMAND
1 ? S 0:01 init
42 ? S 0:00 login
44 #0 S 0:00 shell
49 #0 R 0:00 ps
$ ls | grep bin
binLocation: userspace/bin/ls.c
Lists files and directories.
Flags:
-
-l- Long format (permissions, size, date, name) -
-a- Show all files (including hidden) -
-h- Human-readable sizes -
-R- Recursive listing -
-S- Sort by size -
-t- Sort by time
Output Format:
drwxr-xr-x 2 root root 4096 Jan 1 1970 bin
-rw-r--r-- 1 root root 1024 Jan 1 1970 config.txt
lrwxrwxrwx 1 root root 14 Jan 1 1970 link -> /home/fileLocation: userspace/bin/cat.c
Outputs file contents to stdout.
Usage:
cat file.txt # Print file
cat file1.txt file2.txt # Print multiple files
cat > output.txt # Create file from stdinLocation: userspace/bin/cp.c
Copies files or directories.
Flags:
-
-r- Recursive (for directories) -
-f- Force overwrite
Usage:
cp source dest # Copy file
cp -r source/ dest/ # Copy directoryLocation: userspace/bin/rm.c
Deletes files.
Flags:
-
-r- Recursive (for directories) -
-f- Force (no confirmation)
Usage:
rm file.txt # Delete file
rm -rf directory/ # Delete directoryLocation: userspace/bin/mkdir.c
Creates directories.
Usage:
mkdir newdir # Create directory
mkdir -p a/b/c # Create nested (if supported)Location: userspace/bin/rmdir.c
Removes empty directories.
Usage:
rmdir emptydir # Remove only if emptyLocation: userspace/bin/pwd.c
Shows current directory path.
Usage:
$ pwd
/home/userLocation: userspace/bin/touch.c
Creates empty file or updates modification time.
Usage:
touch file.txt # Create empty file
touch -t timestamp file # Set time (if supported)Location: userspace/bin/head.c
Outputs first N lines of file (default 10).
Usage:
head file.txt # First 10 lines
head -20 file.txt # First 20 linesLocation: userspace/bin/more.c
Displays file contents with pagination.
Usage:
more file.txt # Page through file
cat file.txt | more # Pipe with moreLocation: userspace/bin/chmod.c
Modifies file permissions.
Usage:
chmod 644 file.txt # Set rwxr--r--
chmod +x script.sh # Add execute bit
chmod g+w file.txt # Add group writeLocation: userspace/bin/chown.c
Changes file owner and group.
Usage:
chown user:group file.txt # Change owner:group
chown 1000 file.txt # Change by UIDLocation: userspace/bin/ps.c
Shows running processes.
Output Columns:
- PID - Process ID
- TTY - Terminal (? = not attached)
- STAT - Process state (S=sleeping, R=running, Z=zombie)
- TIME - CPU time used
- COMMAND - Program name
Usage:
$ ps
PID TTY STAT TIME COMMAND
1 ? S 0:00 init
42 ? S 0:00 login
45 #0 S 0:00 shell
51 #0 R 0:00 psProcess States:
- S (INTERRUPTIBLE_SLEEP) - Waiting for event
- R (RUNNING) - Currently executing
- Z (ZOMBIE) - Terminated, awaiting parent reaping
- D (UNINTERRUPTIBLE_SLEEP) - Cannot be interrupted
Location: userspace/bin/uptime.c
Displays system uptime and load average.
Output:
$ uptime
12:34:56 up 1 day, 2:30, 3 users, load average: 0.25, 0.50, 1.00Information:
- Current time
- Uptime (days, hours, minutes)
- Number of logged-in users
- Load average (1 min, 5 min, 15 min)
Location: userspace/bin/uname.c
Shows system information.
Flags:
-
-a- All information -
-s- System name -
-r- Kernel release -
-v- Kernel version -
-m- Machine type
Output:
$ uname -a
MentOS 0.1.0 #1 Mentoring OS i686 GNU/LinuxLocation: userspace/bin/clear.c
Clears terminal screen.
Usage:
clearLocation: userspace/bin/id.c
Displays user and group IDs.
Output:
$ id
uid=0(root) gid=0(root) groups=0(root)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$Location: userspace/bin/env.c
Displays environment variables.
Usage:
$ env
PATH=/bin:/usr/bin
HOME=/root
USER=root
...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 signalsLocation: userspace/bin/nice.c
Runs program with modified priority.
Usage:
nice -n 10 command # Run with lower priority
nice -n -5 command # Run with higher priority (root only)Location: userspace/bin/showpid.c
Shows current process ID.
Usage:
$ showpid
5Location: userspace/bin/stat.c
Shows file metadata.
Output:
$ stat file.txt
File: file.txt
Size: 1024 Blocks: 8
Access: (0644/-rw-r--r--) Uid: 0 Gid: 0
Access: 1970-01-01 00:00:00.000000000
Modify: 1970-01-01 00:00:00.000000000
Change: 1970-01-01 00:00:00.000000000Location: userspace/bin/date.c
Displays current date and time.
Usage:
$ date
Thu Jan 1 00:00:00 1970Location: userspace/bin/cpuid.c
Displays CPU capabilities (x86-specific).
Output:
$ cpuid
CPUID: Family=6, Model=42, Stepping=1
Features: PAE, PSE, MMU, ...Location: userspace/bin/poweroff.c
Gracefully shuts down the system.
Usage:
poweroff # Power off systemLocation: userspace/bin/login.c
Authenticates user and starts session.
Location: userspace/bin/logo.c
Shows ASCII art welcome screen.
Output:
___ ___ _ ___ _____
| \/ | | | / _ \| _ \
| . . | __ _| |/ /_\ \ | | |
| |\/| |/ _` | || _ | | | |
| | | | (_| | || | | | |/ /
\_| |_/\__,_|_/\_| |_/___/
MentOS - Mentoring Operating SystemLocation: userspace/bin/runtests.c
Runs automated tests.
Usage:
runtests # Run all testsPrograms are compiled and linked with libc:
// Example program: programs/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(echo echo.c)
target_link_libraries(echo c) # Link with libcPrograms 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)#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
}#include <signal.h>
void handle_signal(int sig) {
if (sig == SIGINT) {
printf("Interrupted\n");
exit(0);
}
}
int main() {
signal(SIGINT, handle_signal);
// ... main code
}#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
}Goal: Create a simple program and run it on MentOS.
Program - programs/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:
- Add to
programs/CMakeLists.txt:
add_executable(hello_os hello_os.c)
target_link_libraries(hello_os PUBLIC c)
install(TARGETS hello_os DESTINATION ${MENTOS_ROOT_PATH}/bin)- Rebuild:
cd build
make
make qemu- In MentOS shell:
# Run with no arguments
/bin/hello_os
# Run with arguments
/bin/hello_os arg1 arg2 arg3Expected 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
Goal: Parse command-line arguments and perform operations.
Program - programs/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 zeroGoal: Read stdin and write to stdout (like Unix echo).
Program - programs/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 in shell pipeline
/bin/echo2 test | /bin/catGoal: Read input and count lines, words, and characters.
Program - programs/wc.c:
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: wc <file>\n");
return 1;
}
FILE *f = fopen(argv[1], "r");
if (!f) {
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(f)) != 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]);
fclose(f);
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.txtGoal: Use fork/exec to launch other programs.
Program - programs/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/psAdvanced Goal: Implement a basic shell that:
- Reads commands from user
- Parses arguments (space-separated)
- Forks and execs the program
- Waits for completion
- 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)
- System Calls - Syscalls used by programs
- C Library - Library functions available
- Development Guide - Writing new programs
- Filesystem - How programs access storage