Skip to content

Development Guide

Frank edited this page Jan 18, 2026 · 2 revisions

Development Guide

This guide covers the development practices, coding conventions, and architecture patterns used in cupid-os. Whether you're fixing bugs, adding features, or learning about OS development, this guide will help you contribute effectively.

Project Structure

cupid-os follows a clean, modular architecture:

cupid-os/
├── boot/              # Bootloader (16-bit real mode → 32-bit protected mode)
│   └── boot.asm
├── kernel/            # Core kernel components
│   ├── kernel.c/.h    # Main kernel initialization
│   ├── idt.c/.h       # Interrupt Descriptor Table
│   ├── isr.asm        # Interrupt Service Routines (stubs)
│   ├── irq.c/.h       # IRQ management and dispatch
│   ├── pic.c/.h       # Programmable Interrupt Controller
│   ├── memory.c/.h    # Physical memory management
│   ├── paging.c       # Virtual memory and paging
│   ├── shell.c/.h     # Command-line interface
│   ├── fs.c/.h        # In-memory filesystem
│   ├── math.c/.h      # Mathematical utilities
│   ├── string.c/.h    # String manipulation
│   └── types.h        # Common type definitions
├── drivers/           # Hardware device drivers
│   ├── keyboard.c/.h  # PS/2 keyboard driver
│   ├── timer.c/.h     # System timer
│   ├── pit.c/.h       # Programmable Interval Timer
│   └── speaker.c/.h   # PC speaker
├── link.ld            # Linker script (defines memory layout)
├── Makefile           # Build configuration
└── wiki/              # Documentation

Development Environment

Prerequisites

  • NASM (Netwide Assembler) ≥ 2.15
  • GCC (32-bit support required) ≥ 7.0
  • GNU Make ≥ 4.0
  • QEMU ≥ 4.0 (for testing)
  • GDB (optional, for debugging)

Quick Setup

# Clone and build
git clone https://github.com/yourusername/cupid-os.git
cd cupid-os
make

# Test in QEMU
make run

Recommended Workflow

  1. Build frequently: make && make run
  2. Test in QEMU: Keep a terminal with QEMU running
  3. Iterate quickly: Make small changes, test immediately
  4. Debug with GDB: make debug for breakpoint debugging

Coding Conventions

Naming Conventions

Functions

  • Prefix with module name: keyboard_init(), memory_alloc()
  • Use snake_case: get_cpu_frequency(), print_hex_value()
  • Be descriptive: timer_get_uptime_ms() not get_time()

Variables

  • snake_case: cursor_position, interrupt_count
  • Global variables: Prefix with module name when necessary
  • Local variables: Keep short but meaningful: i, len, ptr

Types and Structs

  • typedef structs: End with _t: keyboard_state_t, timer_config_t
  • Enums: End with _t: key_state_t, memory_flags_t
  • Use meaningful names: page_directory_entry_t not pde_t

Constants

  • ALL_CAPS: VGA_WIDTH, MAX_COMMAND_LENGTH
  • Prefix with module: KEYBOARD_BUFFER_SIZE, MEMORY_PAGE_SIZE

Code Style

Formatting

  • Indentation: 4 spaces (no tabs)
  • Line length: Aim for 80-100 characters
  • Braces: K&R style (opening brace on same line)
void function_name(int param) {
    if (condition) {
        do_something();
    } else {
        do_other_thing();
    }
}

Header Guards

#ifndef MODULE_NAME_H
#define MODULE_NAME_H

// Declarations here

#endif

Comments

  • Function comments: Brief description of purpose and parameters
  • Complex logic: Explain why, not what
  • TODO/FIXME: Mark areas needing attention
/**
 * Initialize the VGA text mode display
 * Sets up cursor position and clears screen
 */
void init_vga(void);

/* Calculate memory address from page table indices */
uint32_t get_physical_address(uint32_t pd_index, uint32_t pt_index);

Error Handling

  • Assert assumptions: Use assert() for impossible conditions
  • Return error codes: For recoverable errors
  • Fail fast: Crash on critical errors (appropriate for OS development)
// Good: Clear contract, early return
page_t* allocate_page(void) {
    if (free_pages == 0) {
        return NULL;  // Caller must handle
    }
    // ... allocation logic
}

Architecture Patterns

Module Organization

Each module follows this pattern:

  1. Header file (.h): Public interface, types, constants
  2. Implementation file (.c): Private functions, implementation
  3. Clear separation: Public vs private functions

Header File Structure

#ifndef MODULE_H
#define MODULE_H

#include "types.h"

// Constants
#define MODULE_CONSTANT 42

// Types
typedef struct {
    int field;
} module_config_t;

// Public functions
void module_init(void);
int module_do_something(int param);

#endif

Implementation Structure

#include "module.h"

// Private constants
#define INTERNAL_BUFFER_SIZE 256

// Private functions
static void internal_helper(void) {
    // Implementation
}

// Public function implementations
void module_init(void) {
    // Initialization code
}

int module_do_something(int param) {
    // Implementation
    return result;
}

Memory Management

  • No dynamic allocation in ISRs: Interrupt handlers cannot allocate memory
  • Fixed-size buffers: Use static arrays where possible
  • Resource ownership: Clear who owns and frees memory
// Good: Clear ownership, no leaks possible
char buffer[256];
int length = read_data(buffer, sizeof(buffer));
// Use buffer...
// Buffer automatically cleaned up

Interrupt Handling

  • Keep ISRs short: Do minimal work, delegate to deferred handlers
  • Save/restore state: Let assembly stubs handle register saving
  • Use IRQ numbers: Abstract away PIC remapping
// ISR: Minimal, fast
void keyboard_isr(struct registers* regs) {
    uint8_t scancode = inb(KEYBOARD_DATA_PORT);
    keyboard_enqueue_scancode(scancode);
    pic_send_eoi(IRQ_KEYBOARD);
}

// Deferred handler: Complex processing
void keyboard_process_events(void) {
    // Process queued scancodes
}

Adding New Features

General Process

  1. Plan the feature: Understand requirements and impact
  2. Choose location: Decide which module/component to extend
  3. Implement incrementally: Add small pieces, test frequently
  4. Add shell command: Provide visible proof of functionality
  5. Update documentation: Wiki pages and comments

Adding a Device Driver

  1. Create driver files: drivers/newdriver.c and drivers/newdriver.h
  2. Define interface: Initialization, read/write/control functions
  3. Handle interrupts: If needed, add ISR and IRQ setup
  4. Add to build: Update Makefile with new object file
  5. Initialize in kernel: Call init function from kernel_main()

Example structure:

// drivers/newdriver.h
#ifndef NEWDRIVER_H
#define NEWDRIVER_H

void newdriver_init(void);
int newdriver_read_data(void);

#endif

// drivers/newdriver.c
#include "newdriver.h"

void newdriver_init(void) {
    // Hardware initialization
}

int newdriver_read_data(void) {
    // Read from hardware
    return data;
}

Adding Shell Commands

  1. Add command function: static void shell_cmd(const char* args)
  2. Add to command table: Include in commands[] array
  3. Handle arguments: Parse command-line arguments
  4. Provide feedback: Clear output, error messages
static void shell_example(const char* args) {
    if (!args || args[0] == '\0') {
        print("Usage: example <parameter>\n");
        return;
    }
    print("Example command executed with: ");
    print(args);
    print("\n");
}

// Add to commands array:
{"example", "Example command for demonstration", shell_example}

Adding System Calls (Future)

When syscall interface is added:

  1. Define syscall numbers: Add to enum in kernel
  2. Implement handler: Add case in syscall dispatcher
  3. Add user library: Wrapper functions for user code
  4. Validate parameters: Check bounds and permissions

Testing and Debugging

Unit Testing

  • Shell commands: Manual testing via shell interface
  • Build verification: make && make run frequently
  • Edge cases: Test with invalid inputs, boundary conditions

QEMU Debugging

# Run with GDB server
make debug

# In another terminal
gdb
(gdb) target remote localhost:1234
(gdb) break kernel_main
(gdb) continue

Common Debug Techniques

Printf Debugging

print("DEBUG: variable = ");
print_int(variable);
print("\n");

Memory Inspection

print("Memory at 0x1000: ");
print_hex(*(uint32_t*)0x1000);
print("\n");

Page Fault Testing

# In shell
testpf
# Use fault shell to inspect CR2, error codes

Performance Analysis

  • Measure timing: Use timer_get_uptime_ms()
  • Profile code: Add timing around suspect functions
  • Optimize hotspots: Focus on frequently called code

Best Practices

Code Quality

  • Readability first: Code is read more than written
  • Small functions: Each function should do one thing well
  • Clear abstractions: Hide complexity behind clean interfaces
  • Documentation: Comments explain intent, not implementation

System Design

  • Minimal interfaces: Expose only what needs to be public
  • Layered architecture: Clear separation between components
  • Fail-fast philosophy: Crash early on critical errors
  • Resource awareness: Memory, interrupts, and CPU cycles are precious

Maintenance

  • Regular cleanup: Remove dead code, fix warnings
  • Refactor ruthlessly: Improve code as you understand it better
  • Version control: Commit working states, meaningful messages
  • Documentation sync: Keep wiki and code comments current

Common Pitfalls

Memory Issues

  • Buffer overflows: Always check bounds
  • Use-after-free: Clear pointers after freeing
  • Uninitialized variables: Initialize everything

Concurrency Issues

  • ISR data races: Don't access shared data in ISRs
  • Reentrancy: Functions called from ISRs must be reentrant
  • Interrupt disabling: Know when interrupts are disabled

Hardware Interaction

  • Port I/O: Use correct port numbers and data sizes
  • Timing dependencies: Hardware may need delays
  • State preservation: Save/restore hardware state

Getting Help

Resources

  • Code comments: Read existing implementations
  • Wiki pages: Architecture and feature documentation
  • OS literature: "Operating Systems: Three Easy Pieces"
  • Community: GitHub issues and discussions

Asking Questions

  • Show code: Include relevant code snippets
  • Describe symptoms: What you expected vs. what happened
  • Include environment: Compiler version, QEMU version
  • Minimal reproduction: Smallest example that shows the issue

Contributing Workflow

  1. Fork the repository
  2. Create feature branch: git checkout -b feature-name
  3. Make changes: Follow conventions, test frequently
  4. Test thoroughly: All existing functionality still works
  5. Update documentation: Wiki pages, comments
  6. Submit pull request: Clear description of changes

Pull Request Guidelines

  • One feature per PR: Keep changes focused
  • Working code: PR should build and run
  • Clear commits: Logical commit history
  • Updated tests: Any new functionality is testable

Remember: cupid-os is about learning and experimentation. Don't be afraid to break things, understand why they broke, and fix them. Every bug is a learning opportunity!

For more information:

Clone this wiki locally