-
Notifications
You must be signed in to change notification settings - Fork 63
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_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
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
## 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 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.
Output:
$ uptime
Days: 0 Hours: 1 Minutes: 23 Seconds: 45Information:
- Uptime in days, hours, minutes, seconds
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.0Location: 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)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
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 5Location: 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: 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:0Location: 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/1970Location: userspace/bin/cpuid.c
Displays CPU capabilities (x86-specific).
Output:
Currently a stub (no output).
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: 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)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)#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 - 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:
- Add to
userspace/bin/CMakeLists.txt:
set(PROGRAM_LIST
# ... other programs ...
hello_os.c
)- Rebuild:
cd build
make prog_hello_os
make filesystem
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 - 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 zeroGoal: 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.txtGoal: 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.txtGoal: 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/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
- File Systems - How programs access storage