-
Notifications
You must be signed in to change notification settings - Fork 1
Development Guide
Frank edited this page Jan 18, 2026
·
2 revisions
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.
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
- 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)
# Clone and build
git clone https://github.com/yourusername/cupid-os.git
cd cupid-os
make
# Test in QEMU
make run-
Build frequently:
make && make run - Test in QEMU: Keep a terminal with QEMU running
- Iterate quickly: Make small changes, test immediately
-
Debug with GDB:
make debugfor breakpoint debugging
-
Prefix with module name:
keyboard_init(),memory_alloc() -
Use snake_case:
get_cpu_frequency(),print_hex_value() -
Be descriptive:
timer_get_uptime_ms()notget_time()
-
snake_case:
cursor_position,interrupt_count - Global variables: Prefix with module name when necessary
-
Local variables: Keep short but meaningful:
i,len,ptr
-
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_tnotpde_t
-
ALL_CAPS:
VGA_WIDTH,MAX_COMMAND_LENGTH -
Prefix with module:
KEYBOARD_BUFFER_SIZE,MEMORY_PAGE_SIZE
- 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();
}
}#ifndef MODULE_NAME_H
#define MODULE_NAME_H
// Declarations here
#endif- 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);-
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
}Each module follows this pattern:
-
Header file (
.h): Public interface, types, constants -
Implementation file (
.c): Private functions, implementation - Clear separation: Public vs private functions
#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#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;
}- 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- 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
}- Plan the feature: Understand requirements and impact
- Choose location: Decide which module/component to extend
- Implement incrementally: Add small pieces, test frequently
- Add shell command: Provide visible proof of functionality
- Update documentation: Wiki pages and comments
-
Create driver files:
drivers/newdriver.canddrivers/newdriver.h - Define interface: Initialization, read/write/control functions
- Handle interrupts: If needed, add ISR and IRQ setup
- Add to build: Update Makefile with new object file
-
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;
}-
Add command function:
static void shell_cmd(const char* args) -
Add to command table: Include in
commands[]array - Handle arguments: Parse command-line arguments
- 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}When syscall interface is added:
- Define syscall numbers: Add to enum in kernel
- Implement handler: Add case in syscall dispatcher
- Add user library: Wrapper functions for user code
- Validate parameters: Check bounds and permissions
- Shell commands: Manual testing via shell interface
-
Build verification:
make && make runfrequently - Edge cases: Test with invalid inputs, boundary conditions
# Run with GDB server
make debug
# In another terminal
gdb
(gdb) target remote localhost:1234
(gdb) break kernel_main
(gdb) continueprint("DEBUG: variable = ");
print_int(variable);
print("\n");print("Memory at 0x1000: ");
print_hex(*(uint32_t*)0x1000);
print("\n");# In shell
testpf
# Use fault shell to inspect CR2, error codes-
Measure timing: Use
timer_get_uptime_ms() - Profile code: Add timing around suspect functions
- Optimize hotspots: Focus on frequently called code
- 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
- 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
- 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
- Buffer overflows: Always check bounds
- Use-after-free: Clear pointers after freeing
- Uninitialized variables: Initialize everything
- 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
- Port I/O: Use correct port numbers and data sizes
- Timing dependencies: Hardware may need delays
- State preservation: Save/restore hardware state
- Code comments: Read existing implementations
- Wiki pages: Architecture and feature documentation
- OS literature: "Operating Systems: Three Easy Pieces"
- Community: GitHub issues and discussions
- 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
- Fork the repository
-
Create feature branch:
git checkout -b feature-name - Make changes: Follow conventions, test frequently
- Test thoroughly: All existing functionality still works
- Update documentation: Wiki pages, comments
- Submit pull request: Clear description of changes
- 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:
- Home - Project overview
- Building - Build system details
- Shell Guide - Using the shell interface
Getting Started
Shell & Scripting
Compilers
- CupidC Compiler
- CupidC Language Reference
- CupidC 2D Graphics Library
- CupidASM Assembler
- Floating Point
Programs & Docs
GUI
Kernel
Filesystems
Hardware