While you might be content to write a kernel that functions on only x86 IBM compatible computers, the truth is there are many other platforms on which to write a kernel. These range from the long dead, such as Amiga and Alpha Processor-based computers, to the relevant ARM architectures. Traditionally, the operating system was specific to an architecture, which allowed user programs to take full advantage of their hardware designs. UNIX however, changed this with the introduction of the C programming language, which allowed UNIX (And other programs) to be ported to almost every system ever conceived with only minor adjustments. An operating system's core, the kernel is designed to deal with the hardware, so the Operating System can be oblivious to hardware changes. However, most kernel code can be reused on different architectures that share similar hardware, such as USB, and PCI buses.
C and C++ code can be crossed compiled onto many systems, so chances are if you can target gcc to run on it, you can write a kernel for it. Some hardware will be easier and more thoroughly documented than others, but in any case, you should fully understand the system in which you're developing for, and read all it's manuals and specifications. Once you feel confident with it, you can begin.
Your kernel will have functions that contain what is called generic and targeted code. For example: a puts() implementation will be generic, while the screen driver that puts() uses to draw the string to the output device would be targeted to a specific implementation. Before you begin trying to code for more than one platform, separate your sources so any targeted functions are in a different location than generics. This creates an abstraction layer between hardware drivers and pure software. That way, all you need to do to port your kernel to a new platform is re-write the targeted functions for the platform, and the generic code can run on top of it.
Targeted code can be further divided into two categories: architecture-specific code and machine-specific code. Architecture-specific code is code that differs between different processor architectures, like I/O, trap dispatching, and paging. Machine-specific code is code that differs between machines with the same processor architectures, like IRQ controllers, system timers, real time clocks, and multiprocessor information. The NT kernel has a module containing machine-specific code called the Hardware Abstraction Layer, or HAL.
After the targeted files are separated, you can make new targeted functions for each platform you want to port to. The generic code will (usually) run without issue on top of the targeted code. A brief side note, make sure to delete the object files after you re-target, as object files are not cross compatible.
Once your architecture-specific code has been separated, you need to begin writing drivers for the new platform. Even though you've modified your kernel to be able to run on top of any hardware, you still need to write the drivers that have code specific to your platform. For example, if you had an x86-only kernel before, and you separated out the targeted code and wrote targeted code for the Raspi, in order for your OS to actually interface with the user, you have to write device drivers for hardware like the screen and audio controllers.
Example Targeted Functions
- Booting (usually assembly stubs)
- CPU Specific functions (I/O, Trap Dispatching, Paging, Context Switching, SMP)
- Timers (PIT, RTC, APIC Timer, HPET)
Example Generic Functions
- malloc() (NOT paging!)
- Task Scheduler
- String parsing functions
- Buffered I/O (gets, putchar, etc)