I've written a bootloader so far, a 2nd stage loader and some parts of the kernel, but all without a file system. That's right, file systems all need to be programmed.
The following link is an excellent tutorial on os development. It shows exactly what the first steps are into getting the kernel ready. It assumes the use of GRUB to load this kernel and you can follow through the example with standard Linux and the ability to mount a single file as a file system on your /mnt. (then you need to create some kind of image still, but check out qemu for more information).
For my research, I am using mostly x86 instruction sets, no other processors yet. It all starts out pretty easy. The boot sector of the booting device (512 bytes) is loaded into memory at 0x7C00. Then it starts executing. In those 512 bytes, you basically load a 2nd stage loader from a determined memory location and then you have more bytes to fiddle around with the system. Any file systems used on the system need to be implemented separately.
Eventually, the kernel is loaded. This can already be a 32-bit protected mode kernel or perhaps starts off in 16-bit. I'm assuming 32-bit PM (cause that's how GRUB eventually calls the kernel) and then you'll generally do the following:
- Set something that is called "Global Descriptor Table". This is a table with pointers to code segments, data segments, etc... in 32-bit PM.
- Set an interrupt descriptor table. This is for the processor to indicate to the kernel that some operation on the processor failed (exceptions) and also to indicate to the kernel that some hardware has some new data to process. For example, hitting a key on the keyboard creates that interrupt.
- Initializing video
- Initializing keyboard
- Running a while loop that continuously hlt's the processor until the next interrupt occurs.
Hardware may also cause an interrupt to occur, in which case the applications on top of the kernel will need to receive this interrupt events and either type the character in this edit box or another, depending on the focus.
Anyway, that's where OS's take off from. It receives hardware interrupts and timer interrupts, schedules the tasks with CPU time-slices and divides the CPU between those tasks. How this is actually implemented is mostly determined by the OS designer. But it's all not tooo difficult really (to understand the concept, not the implementation).
The applications may run in their own "application-space" with virtualized memory. That means that memory is paged in pages. From the point of view of the application, it is running in an area of memory of "0x0000:0x0000". For development purposes, this makes it very easy. It might of course actually be located somewhere else.
The CPU can be instructed to not let this application access memory outside a certain range. If the application attempts, the CPU will raise an "access violation" (as you may be familiar with on NT for example). And voilá!!! The kernel will need to treat the application and the error. So the OS very closely uses the hardware capabilities of the CPU to do all those tasks, including divide by zero, etc... It's not all too difficult to see that concept now.
Some parts of the kernel need to be written in asm, cannot be done in C. For example loading the gdt mentioned before, the IDT and some other operations like processor locks in SMP programming and spinlocks (atomic operations).
What the kernel really does is also up to the developer. You could consider writing an OS that only does databases (dedicated machine for databases), or you might want to develop a gaming OS (with direct hardware access for example with standardized hardware to make things simpler). There are no limits!