You might wonder, why boot2 comes after boot0, and not boot1. Actually, there is a 512-byte file called boot1 in the directory /boot as well. It is used for booting from a floppy. When booting from a floppy, boot1 plays the same role as boot0 for a harddisk: it locates boot2 and runs it.
You may have realized that a file /boot/mbr exists as well. It is a simplified version of boot0. The code in mbr does not provide a menu for the user, it just blindly boots the partition marked active.
The code implementing boot2 resides in sys/boot/i386/boot2/, and the executable itself is in /boot. The files boot0 and boot2 that are in /boot are not used by the bootstrap, but by utilities such as boot0cfg. The actual position for boot0 is in the MBR. For boot2 it is the beginning of a bootable FreeBSD slice. These locations are not under the filesystem's control, so they are invisible to commands like ls.
The main task for boot2 is to load the file /boot/loader, which is the third stage in the bootstrapping
procedure. The code in boot2 cannot use any services like open()
and read()
, since the kernel
is not yet loaded. It must scan the harddisk, knowing about the filesystem structure,
find the file /boot/loader, read it into memory using a BIOS
service, and then pass the execution to the loader's entry point.
Besides that, boot2 prompts for user input so the loader can be booted from different disk, unit, slice and partition.
The boot2 binary is created in special way:
sys/boot/i386/boot2/Makefile: boot2: boot2.ldr boot2.bin ${BTX}/btx/btx btxld -v -E ${ORG2} -f bin -b ${BTX}/btx/btx -l boot2.ldr \ -o boot2.ld -P 1 boot2.bin
This Makefile snippet shows that btxld(8) is used to link the binary. BTX, which stands for BooT eXtender, is a piece of code that provides a protected mode environment for the program, called the client, that it is linked with. So boot2 is a BTX client, i.e., it uses the service provided by BTX.
The btxld utility is the linker. It links two binaries together. The difference between btxld(8) and ld(1) is that ld usually links object files into a shared object or executable, while btxld links an object file with the BTX, producing the binary file suitable to be put on the beginning of the partition for the system boot.
boot0 passes the execution to BTX's entry point. BTX then switches the processor to protected mode, and prepares a simple environment before calling the client. This includes:
virtual v86 mode. That means, the BTX is a v86 monitor. Real mode instructions like pushf, popf, cli, sti, if called by the client, will work.
Interrupt Descriptor Table (IDT) is set up so all hardware interrupts are routed to the default BIOS's handlers, and interrupt 0x30 is set up to be the syscall gate.
Two system calls: exec
and exit
, are defined:
sys/boot/i386/btx/lib/btxsys.s: .set INT_SYS,0x30 # Interrupt number # # System call: exit # __exit: xorl %eax,%eax # BTX system int $INT_SYS # call 0x0 # # System call: exec # __exec: movl $0x1,%eax # BTX system int $INT_SYS # call 0x1
BTX creates a Global Descriptor Table (GDT):
sys/boot/i386/btx/btx/btx.s: gdt: .word 0x0,0x0,0x0,0x0 # Null entry .word 0xffff,0x0,0x9a00,0xcf # SEL_SCODE .word 0xffff,0x0,0x9200,0xcf # SEL_SDATA .word 0xffff,0x0,0x9a00,0x0 # SEL_RCODE .word 0xffff,0x0,0x9200,0x0 # SEL_RDATA .word 0xffff,MEM_USR,0xfa00,0xcf# SEL_UCODE .word 0xffff,MEM_USR,0xf200,0xcf# SEL_UDATA .word _TSSLM,MEM_TSS,0x8900,0x0 # SEL_TSS
The client's code and data start from address MEM_USR (0xa000), and a selector (SEL_UCODE) points to the client's code segment. The SEL_UCODE descriptor has Descriptor Privilege Level (DPL) 3, which is the lowest privilege level. But the INT 0x30 instruction handler resides in a segment pointed to by the SEL_SCODE (supervisor code) selector, as shown from the code that creates an IDT:
mov $SEL_SCODE,%dh # Segment selector init.2: shr %bx # Handle this int? jnc init.3 # No mov %ax,(%di) # Set handler offset mov %dh,0x2(%di) # and selector mov %dl,0x5(%di) # Set P:DPL:type add $0x4,%ax # Next handler
So, when the client calls __exec()
, the code will be
executed with the highest privileges. This allows the kernel to change the protected mode
data structures, such as page tables, GDT, IDT, etc later, if needed.
boot2 defines an important structure, struct bootinfo. This structure is initialized by boot2 and passed to the loader, and then further to the kernel. Some nodes of this structures are set by boot2, the rest by the loader. This structure, among other information, contains the kernel filename, BIOS harddisk geometry, BIOS drive number for boot device, physical memory available, envp pointer etc. The definition for it is:
/usr/include/machine/bootinfo.h: struct bootinfo { u_int32_t bi_version; u_int32_t bi_kernelname; /* represents a char * */ u_int32_t bi_nfs_diskless; /* struct nfs_diskless * */ /* End of fields that are always present. */ #define bi_endcommon bi_n_bios_used u_int32_t bi_n_bios_used; u_int32_t bi_bios_geom[N_BIOS_GEOM]; u_int32_t bi_size; u_int8_t bi_memsizes_valid; u_int8_t bi_bios_dev; /* bootdev BIOS unit number */ u_int8_t bi_pad[2]; u_int32_t bi_basemem; u_int32_t bi_extmem; u_int32_t bi_symtab; /* struct symtab * */ u_int32_t bi_esymtab; /* struct symtab * */ /* Items below only from advanced bootloader */ u_int32_t bi_kernend; /* end of kernel space */ u_int32_t bi_envp; /* environment */ u_int32_t bi_modulep; /* preloaded modules */ };
boot2 enters into an infinite loop waiting for user input,
then calls load()
. If the user does not press anything, the
loop breaks by a timeout, so load()
will load the default
file (/boot/loader). Functions ino_t
lookup(char *filename)
and int xfsread(ino_t inode, void
*buf, size_t nbyte)
are used to read the content of a file into memory. /boot/loader is an ELF binary, but where the ELF header is
prepended with a.out's struct exec structure. load()
scans the loader's ELF header, loading the content of /boot/loader into memory, and passing the execution to the loader's
entry:
sys/boot/i386/boot2/boot2.c: __exec((caddr_t)addr, RB_BOOTINFO | (opts & RBX_MASK), MAKEBOOTDEV(dev_maj[dsk.type], 0, dsk.slice, dsk.unit, dsk.part), 0, 0, 0, VTOP(&bootinfo));