Even though it's pretty rare to boot the system without a boot loader, it is still possible to do so by copying the raw kernel to a floppy disk. A command like cat zImage > /dev/fd0[/] will work perfectly on Linux, although some other Unix systems can do the task reliably only by using the dd command. The raw floppy image thus created can then be configured by using the rdev program, but I won't discuss it here.
The file called zImage is the compressed kernel image that lives in arch/i386/boot after you issued make zImage or make boot -- the latter invocation is the one I prefer, as it works unchanged on other platforms. If you built a big zImage, instead, the file is called bzImage, and lives in the same directory.
Booting an x86 kernel is a tricky task because of the limited amount of available memory. The Linux kernel tries to maximize usage of the low 640 kilobytes by moving itself around several times. But let's see in detail the steps performed by a zImage kernel; all the following pathnames are relative to arch/i386/boot.
The first sector (executing at 0x7c00) moves itself to 0x90000 and loads subsequent sectors after itself, getting them from the boot device using the firmware's funtions to access the disk. The rest of the kernel is then loaded to address 0x10000, allowing for a maximum size of half a meg of data -- but this is the compressed image. The boot-sector code lives in bootsect.S, a real-mode assembly file.
Then, code at 0x90200 (defined in setup.S) takes care of some hardware initialization and allows to change the default text mode (video.S). Text mode selection has become a compile-time option from 2.1.9 onwards.
Later, all the kernel is moved from 0x10000 (64K) to 0x1000 (4K). This move overwrites BIOS data stored in RAM, and no BIOS call can be performed after then. The first physical page is not touched because it is the so-called "zero-page", used in handling virtual memory.
At this point setup.S enters protected mode and jumps to 0x1000, where the kernel lives. All the available memory can be accessed now, and the system can begin to run.
The steps just shown used to be the whole story of booting when the kernel was small enough to fit half a meg -- the address range between 0x10000 and 0x90000. When the kernel was small it lived at 0x1000, but as features were added to the system it didn't fit half a meg any more: code at 0x1000 isn't the Linux kernel nowadays, but rather the "gunzip" part of the gzip program. The following additional steps are needed to uncompress the real kernel and execute it:
Code at 0x1000 is compressed/head.S, and is in charge of "gunzipping" the kernel: it calls the function decompress_kernel, defined in compressed/misc.c, which in turns calls inflate which writes its output starting at address 0x100000 (one meg). High memory can now be accessed, because the processor is definitely out of its limited boot environment -- the "real" mode.
After decompression, head.S jumps to the real beginning of the kernel. The relevant code is in ../kernel/head.S, outside of the boot directory.
Boot is over now, and head.S (i.e., the code found at 0x100000 that used to be at 0x1000 before introducing compressed boots) can complete processor initialization and call start_kernel(). Everything is written in C from now on.
The various data movements that are performed at system boot are depicted in Figure 1.
The boot steps shown up to now rely on the assumption that the compressed kernel can fit in half a meg of space. While the assumption holds most of the times, a system stuffed of device drivers might not fit any more. This oversizing may happen for example to kernels used in installation disks: these kernels can easily get bigger than the available space, and some new machinery is needed to fix the problem. This something is called bzImage, and has been introduced in kernel version 1.3.73.
A bzImage is generated by issuing make bzImage from the toplevel Linux source directory. This kind of kernel image boots similarly to the zImage, with a few changes:
When the system is loaded to address 0x10000, a little helper routine is called after loading each 64k data block. The helper routing moves the data block to high memory by using a special BIOS call. Only not-so-old BIOS'es implement the functionality, and that's why make boot still builds the conventional zImage as I write this article -- but this might change in the near future.
setup.S doesn't move the system back to 0x1000 (4k), but jumps instead directly to address 0x100000 (one meg) after entering protected mode. "One meg" is where data has been moved by the BIOS in the previous step.
The decompressor found at one-meg writes the uncompressed kernel image in low memory until it gets exhausted, and then in high memory after the compressed image. The two pieces are then reassembled to address 0x100000 (one meg). Several memory moves are needed to perform the task correctly, but I won't detail the issue any deeper.
The rule for building the big compressed image can be read from Makefile: it affects several files in arch/i386/boot. One good point of bzImage is that when kernel/head.S gets called it won't notice the extra work, and everything will go on as usual.