The kernel support library, libkern.a, supplies a variety of functions and other definitions that are primarily of use in OS kernels. (In contrast, the other parts of the OSKit are more generic components useful in a variety of environments including, but not limited to, OS kernels.) The kernel support library contains all the code necessary to create a minimal working “kernel” that boots and sets up the machine for a generic “OS-friendly” environment. For example, on the x86, the kernel support library provides code to get into protected mode, set up default descriptor tables, etc. The library also includes a remote debugging stub, providing convenient source-level debugging of the kernel over a serial line using GDB’s serial-line remote debugging protocol. As always, all components of this library are optional and replaceable, so although some pieces may be unusable in some environments, others should still work fine.
This library contains a much higher percentage of machine-dependent code than the other libraries in the toolkit, primarily because this library deals with heavily machine-dependent facilities such as page tables, interrupt vector tables, trap handling, etc. The library attempts to hide some machine-dependent details from the OS by providing generic, machine-independent interfaces to machine-dependent library code. For example, regardless of the architecture and boot loading mechanism in use, the kernel startup code included in the library always sets up a generic C-compatible execution environment and starts the kernel by calling the well-known main routine, just as in ordinary C programs. However, the library makes no attempt to provide a complete architecture-independence layer, since such a layer would have to make too many assumptions about the OS that is using it. For example, although the library provides page table management routines, these routines have fairly low-level, architecture-specific interfaces.
The functionality provided by the kernel support library is divided into two main classes: the generic support code, and the base environment. The generic support contains simple routines and definitions that are almost completely independent of the particular OS environment in which they are used: for example, the generic support includes symbolic definitions for bits in processor registers and page tables, C wrapper functions to access special-purpose processor registers, etc. The generic support code should be usable in any OS that needs it.
The base environment code, on the other hand, is somewhat less generic in that it is designed to create, and function in, a well-defined default or “base” kernel execution environment. Out of necessity, this code makes more assumptions about how it is used, and therefore it is more likely that parts of it will not be usable to a particular client OS. For example, on the x86 architecture, the base environment code sets up a default global descriptor table containing a “standard” set of basic, flat-model segment descriptors, as well as a few extra slots reserved for use by the client OS. This “base GDT” is likely to be sufficient for many kernels, but may not be usable to kernels that make more exotic uses of the processor’s GDT. In order to allow piecemeal replacement of the base environment as necessary, the assumptions made by the code and the intermodule dependencies are clearly documented in the sections covering the base environment code.
Following is a brief summary of the main facilities provided by the library, indexed by the section numbers of the sections describing each facility:
This section includes machine-independent types, constants, macros, and functions that every supported architecture provides. These are used extensively within the OSKit itself as well as by the applications built on the OSKit.
#include <oskit/machine/page.h>
This file provides of following symbols, which define the architectural page size of the architecture for which the OSKit is configured:
In addition, the following macros are provided for convenience in performing page-related manipulations of addresses:
Note that many modern architectures support multiple page sizes. On such architectures, the page size defined in this file is the minimum architectural page size, i.e., the finest granularity over which the MMU has control. Since there seems to be no sufficiently generic and useful way that this header file could provide symbols indicating which “other” page sizes the architecture supports, making good use of larger pages probably must be done in machine-dependent code.
Some operating systems on some architectures do not actually support the minimum architectural page size in software; instead, they aggregate multiple architectural pages together into larger “logical pages” managed by the OS software. On such operating systems, it would be inappropriate for general OS or application code to use the PAGE_SIZE value provided by oskit/page.h, since this value would be smaller (more fine-grained) than the OS software actually supports, and therefore inappropriate. However, this is purely a high-level OS issue; like other parts of the toolkit, no one is required to use this header file if it is inappropriate in a particular situation.
This file was originally derived from Mach’s vm_param.h.
#include <oskit/machine/spin_lock.h>
This file provides the architecture-dependent definition of the spin_lock_t “spin lock” data type and associated manipulation macros. This facility provides a basic locking mechanism which can be used with preemptive threading or on a multi-processor.
This header file is taken from CMU’s Mach kernel.
#include <oskit/queue.h>
struct | queue_entry *next; | /* next element | */ | |
struct | queue_entry *prev; | /* previous element | */ | |
typedef struct queue_entry *queue_t;
typedef struct queue_entry queue_head_t;
typedef struct queue_entry queue_chain_t;
typedef struct queue_entry *queue_entry_t;
Macros and structures for implementation of a “queue” data type. The implementation uses a doubly-linked list and supports operations to insert and delete anywhere in the list.
This header file is taken from CMU’s Mach kernel.
#include <oskit/debug.h>
This file contains simple macros and functions to assist in debugging. Many of these facilities are intended to be used to “annotate” programs permanently or semi-permanently in ways that reflect the code’s proper or desired behavior. These facilities typically change their behavior depending on whether the preprocessor symbol DEBUG is defined: if it is defined, then extra code is introduced to check invariants and such; when DEBUG is not defined, all of this debugging code is “compiled out” so that it does not result in any size increase or efficiency loss in the resulting compiled code.
The following macros and functions are intended to be used as permanent- or semi-permanent annotations to be sprinkled throughout ordinary code to increase its robustness and clarify its invariants and assumptions to human readers:
Assertions are typically used to codify assumptions made by a code sequence, e.g., about the parameters to a function or the conditions on entry to or exit from a loop. By placing explicit assert statements in well-chosen locations to verify that the code’s invariants indeed hold, a thicker “safety net” is woven into the code, which tends to make bugs manifest themselves earlier and in much more obvious ways, rather than allowing incorrect results to “trickle” through the program’s execution for a long time, sometimes resulting in completely baffling behavior. Assertions can also act as a form of documentation, clearly describing to human readers the exact requirements and assumptions in a piece of code.
The following macros and functions are primarily intended to be used as temporary scaffolding during debugging, and removed from production code:
If DEBUG is not defined, the here macro is not defined at all; this makes it obvious when you’ve accidentally left invocations of this macro in a piece of code after it has been debugged.
As with here, if DEBUG is not defined, the debugmsg macro is not defined at all, in order to make it obvious if any invocations are accidentally left in production code.
Note that only panic and dump_stack_trace are real functions; the others are simply macros.
Functions to implements a simple “global critical region.” These functions are used throughout the OSKit to ensure proper serialization for various “touchy” but non-performance critical activities such as panicing, rebooting, debugging, etc. This critical region can safely be entered recursively; the only requirement is that enters match exactly with leaves.
The implementation of this module is machine-dependent, and generally disables interrupts and, on multiprocessors, grabs a recursive spin lock.
This section covers useful macros, definitions, and routines that are specific to the Intel x86 processor architecture but that are independent of the interrupt control, bus structure, and other ancillary functions traditionally associated with a “PC.” Those facilities are covered in section 15.4.
#include <oskit/x86/asm.h>
This file contains convenience macros useful when writing x86 assembly language code in AT&T/GAS syntax. This header file is directly derived from Mach, and similar headers are used in various BSD kernels.
Symbol name extension: The following macros allow assembly language code to be written that coexists with C code compiled for either ELF or a.out format. In a.out format, by convention an underscore (_) is prefixed to each public symbol referenced or defined by the C compiler; however, the underscore prefix is not used in ELF format.
Alignment: The following macros relate to alignment of code and data:
XXX S_ARG, B_ARG, frame stuff, . . .
XXX need to make the macros more easily overridable, using ifdefs.
XXX need to clean out old trash still in the header file
XXX IODELAY macro
#include <oskit/x86/eflags.h>
XXX
This header file can be used in assembly language code as well as C. The flags defined here correspond the the ones in the processor databooks.
#include <oskit/x86/proc_reg.h>
XXX
This header file contains the definitions for the processor’s control registers (CR0, CR4). It also contains macros for getting and setting the processor registers and flags. There is also a macro for reading the processor’s cycle counter (on Pentium and above processors).
This header file is taken from CMU’s Mach kernel.
#include <oskit/x86/debug_reg.h>
This provides the definitions for the processor’s built-in debug registers. There are also inline functions that allow the hardware-assisted breakpoints to be set.
DR0 through DR3 are the breakpoint address registers; DR6 is the status register, and DR7 is the control register.
#include <oskit/x86/fp_reg.h>
XXX
This file contains the structure definition for saving the floating-point state and then restoring it. It also contains definitions for the x87 control and status registers.
This header file is taken from CMU’s Mach kernel.
#include <oskit/x86/far_ptr.h>
This contains struct definitions for creating “far pointers.” Far pointers on the x86 are those that take an explicit segment in addition to the offset value.
#include <oskit/x86/pio.h>
These are macros for accessing IO-space directly on the x86. These instructions will generate traps if executed in user-mode without permissions (either IOPL in the eflags register or access via the io-bitmap in the tss).
The above macros have versions that begin with i16_, which are defined to be the same. It may be desirable to use the i16_ versions in 16-bit code in place of the normal macros for clarity.
This header file is taken from CMU’s Mach kernel.
#include <oskit/x86/seg.h>
XXX
This header file is based on a file in CMU’s Mach kernel.
#include <oskit/x86/gate_init.h>
This file contains the C structures and assembly-language macro definitions used to build x86 gate descriptor tables suitable for use by gate_init (see Section 15.5.10).
The assembly-language macros are designed to be used while writing trap entrypoint routines. See oskit/libkern/x86/base_trap_inittab.S for example code that uses this facility.
#include <oskit/x86/trap.h>
XXX
This contains the definitions for the trap numbers returned by the processor when something goes ‘wrong’. These can be used to determine the cause of the trap.
This header file is taken from CMU’s Mach kernel.
XXX
This header file is derived from Mach’s intel/pmap.h.
#include <oskit/x86/tss.h>
struct x86_tss { int back_link; /* previous task's segment, if nested */ int esp0; /* initial stack pointer ... */ int ss0; /* and segment for ring 0 */ int esp1; /* initial stack pointer ... */ int ss1; /* and segment for ring 1 */ int esp2; /* initial stack pointer ... */ int ss2; /* and segment for ring 2 */ int cr3; /* CR3 - page table physical address */ int eip; /* eip */ int eflags; /* eflags */ int eax; /* eax */ int ecx; /* ecx */ int edx; /* edx */ int ebx; /* ebx */ int esp; /* current stack pointer (ring 3) */ int ebp; /* ebp */ int esi; /* esi */ int edi; /* edi */ int es; /* es */ int cs; /* cs */ int ss; /* current stack segment (ring 3) */ int ds; /* ds */ int fs; /* fs */ int gs; /* gs */ int ldt; /* local descriptor table segment */ unsigned short trace_trap; /* trap on switch to task */ unsigned short io_bit_map_offset; /* offset to IO perm bitmap */ }; |
XXX
XXX only the 32-bit version
This contains the definition of a 32-bit tss. The tss used by the 80286 is incompatible with this.
This header file is taken from CMU’s Mach kernel.
XXX
This section covers useful macros, definitions, and routines for “PC”-specific features.
#include <oskit/x86/pc/irq_list.h>
XXX
Many of the interrupt vectors have pre-defined uses. The rest of them can be assigned to ISA, PCI, or other devices. This file contains the ‘defined’ interrupt usages.
#include <oskit/x86/pc/pic.h>
XXX
This contains definitions for the 8259(A) Programmable Interrupt Controller (PIC). In addition to numerous constants, it also contains the prototypes for several functions and macros.
#include <oskit/x86/pc/keyboard.h>
XXX
This header file contains the register definitions for the PC keyboard. The port addresses are defined, along with the status and control bits. This would be used by a keyboard device driver, or someone manipulating they keyboard directly. (ie, to turn on and off the keyboard LEDs).
This header file is taken from CMU’s Mach kernel.
#include <oskit/x86/pc/rtc.h>
This file is taken from FreeBSD (XXX cite?) and contains definitions for the standard NVRAM, or Real Time Clock, register locations.
#include <oskit/x86/cpuid.h>
unsigned | stepping : 4; | /* Stepping ID | */ | |
unsigned | model : 4; | /* Model | */ | |
unsigned | family : 4; | /* Family | */ | |
unsigned | type : 2; | /* Processor type | */ | |
unsigned | feature_flags; | /* Features supported | */ | |
char | vendor_id[12]; | /* Vendor ID string | */ | |
unsigned | char cache_config[16]; | /* Cache information | */ | |
This structure is used to hold identification information about x86 processors, such as information returned by the CPUID instruction. The cpuid toolkit function, described below, fills in an instance of this structure with information about the current processor.
Note that it is expected that the cpu_info structure will continue to grow in the future as new x86-architecture processors are released, so client code should not depend on this structure in ways that will break if the structure’s size changes.
The family field describes the processor family:
The type field is one of the following:
The feature_flags field is a bit field containing the following bits:
The cpuid.h header file also contains symbolic definitions for other constants such as the cache configuration descriptor values; see the header file and the Intel documentation for details on these.
This function identifies the CPU on which it is running using Intel’s recommended CPU identification procedure, and fills in the supplied structure with the information found.
Note that since the cpuid function is 32-bit code, it wouldn’t run on anything less than an 80386 in the first place; therefore it doesn’t bother to check for earlier processors.
#include <oskit/x86/cpuid.h>
void cpu_info_format(struct cpu_info *info, void (*formatter)(void *data, const char *fmt, ...), void *data);
This function takes the information in a cpu_info structure and formats it as human-readable text. The formatter should be a pointer to a printf-like function to be called to format the output data. The formatter function may be called multiple times to output all the relevant information.
Determine the minimum (least-common-denominator) feature set of the two provided structures and return that. The new feature set is returned in id1.
Typically used on SMP systems to determine the basic feature set common across all processors in the system regardless of type.
This function is merely a convenient front-end to cpu_info_format; it simply formats the CPU information and outputs it to the console using printf.
This 16-bit function switches the processor into protected mode by turning on the Protection Enable (PE) bit in CR0. The instruction that sets the PE bit is followed immediately by a jump instruction to flush the prefetch buffer, as recommended by Intel documentation.
The function also initializes the CS register with the appropriate new protected-mode code segment, whose selector is specified in the prot_cs parameter. The prot_cs must evaluate to a constant, as it is used as an immediate operand in an inline assembly language code fragment.
This routine does not perform any of the other steps in Intel’s recommended mode switching procedure, such as setting up the GDT or reinitializing the data segment registers; these steps must be performed separately. The overall mode switching sequence is necessarily much more dependent on various OS-specific factors such as the layout of the GDT; therefore the OSKit does not attempt to provide a “generic” function to perform the entire switch. Instead, the full switching sequence is provided as part of the base environment setup code; see Section 15.10 for more details.
This 16-bit function switches the processor out of protected mode and back into real mode by turning off the Protection Enable (PE) bit in CR0. The instruction that clears the PE bit is followed immediately by a jump instruction to flush the prefetch buffer, as recommended by Intel documentation. At the same time, this function also initializes the CS register with the appropriate real-mode code segment, specified by the real_cs parameter.
This routine does not perform any of the other steps in Intel’s recommended mode switching procedure, such as reinitializing the data segment registers; these steps must be performed separately. See Section 15.10 for information on the full mode switch implementation provided by the base environment.
Loads the processor page directory using pdir and turns on paging.
The caller must already have created and initialized an appropriate initial page directory as described in Intel documentation. The OSKit provides convenient facilities that can be used to create x86 page directories and page tables; for more information, see Section 15.9.
This function assumes that pdir equivalently maps the physical memory that contains the currently executing code, the currently loaded GDT and IDT.
Turns paging off and flushes the TLB.
This function assumes that the currently loaded page directory equivalently maps the physical memory that contains the currently executing code, the currently loaded GDT and IDT.
#include <oskit/x86/gate_init.h>
void gate_init(struct x86_gate *dest, const struct gate_init_entry *src, unsigned entry_cs);
Install entries in a processor descriptor table from the specified array of gate descriptors (see Section 15.3.9). Typically used to initialize the processor IDT with trap and interrupt vectors (see Section 15.7.4).
The base environment code for the x86 architecture is designed to assist the OS developer in dealing with much of the “x86 grunge” that OS developers typically would rather not worry about. The OSKit provides easy-to-use primitives to set up and maintain various common flavors of x86 kernel environments without unnecessarily constraining the OS implementation. The base environment support on the x86 architecture is divided into the three main categories: segmentation, paging, and trap handling. The base environment support code in each category is largely orthogonal and easily separable, although it is also designed to work well together.
The x86 architecture supports a very complex virtual memory model involving both segmentation and paging; one of the goals of the OSKit’s base environment support for the x86 is to smooth over some of this complexity, hiding the details that the OS doesn’t want to deal with while still allowing the OS full freedom to use the processor’s virtual memory mechanisms as it sees fit. This section describes the memory models supported and assumed by the base environment.
First, here is a summary of several important terms that are used heavily used in the following text; for full details on virtual, linear, and physical addresses on the x86 architecture, see the appropriate processor manuals.
The OSKit provides a standard mechanism, defined in base_vm.h (see Section 15.6.2), which is used throughout the base environment to maintain considerable independence from the memory model in effect. These facilities allow the base environment support code to avoid various assumptions about the relationships between kernel virtual addresses, linear addresses, and physical addresses. Client OS code can use these facilities as well if desired.
Of course, it is impractical for the base environment code to avoid assumptions about the memory model completely. In particular, the code assumes that, for “relevant” code and data (e.g., the functions implementing the base environment and the data structures they manipulate), kernel virtual addresses can be converted to and from linear or physical addresses by adding or subtracting an offset stored in a global variable. However, the code does not assume that these offsets are always the same (the client OS is allowed to change them dynamically), or that all available physical memory is mapped into the kernel’s virtual address space, or that all linear memory is accessible through the kernel’s data segment descriptors. Detailed information about the memory model assumptions made by particular parts of the base environment support are documented in the appropriate API sections.
If the OSKit’s startup code is being used to start the OS, then the specific memory model in effect initially depends on the startup environment, described in later the appropriate sections. For example, for kernels booted from a MultiBoot boot loader, in the initial memory environment virtual addresses, linear addresses, and physical addresses are all exactly equal (the offsets are zero). On the other hand, for kernels loaded from DOS, linear addresses and physical addresses will still be equal but kernel virtual addresses will be at some offset depending on where in physical memory the kernel was loaded. Regardless of the initial memory setup, the client OS is free to change the memory model later as necessary.
XXX example memory maps
XXX explain how to change memory models at run-time
#include <oskit/machine/base_vm.h>
This header file provides generic virtual memory-related definitions commonly used throughout the base environment, which apply to both segmentation and paging. In particular, this file defines a set of macros and global variables which allow the rest of the base environment code in the toolkit (and the client OS, if it chooses) to maintain independence from the memory model in effect. These facilities allow code to avoid various assumptions about the relationships between kernel virtual addresses, linear addresses, and physical addresses.
The following variable and associated macros are provided to convert between linear and kernel virtual addresses.
Similarly, the following variable and associated macros convert between physical and kernel virtual addresses. (Conversions between linear and physical addresses can be done by combining the two sets of macros.)
XXX real_cs
Note that there is nothing in this header file that defines or relates to “user-mode” address spaces. This is because the base environment code in the OSKit is not concerned with user mode in any way; in fact, it doesn’t even care whether or not the OS kernel implements user address spaces at all. For example, boot loaders or unprotected real-time kernels built using the OSKit probably do not need any notion of user mode at all.
This function provides a single entrypoint to initialize and activate all of the processor structures necessary for ordinary execution. This includes identifying the CPU, and initializing and activating the base GDT, IDT, and TSS, and reloading all segment registers as recommended by Intel. The call returns with the CS segment set to KERNEL_CS (the default kernel code segment; see 15.7.1 for details), DS, ES, and SS set to KERNEL_DS (the default kernel data segment), and FS and GS set to 0. After the base_cpu_setup call completes, a full working kernel environment is in place: segment registers can be loaded, interrupts and traps can be fielded by the OS, privilege level changes can occur, etc.
This function does not initialize or activate the processor’s paging mechanism, since unlike the other mechanisms, paging is optional on the x86 and not needed in some environments (e.g., boot loaders or embedded kernels).
The base_cpu_setup function is actually just a simple wrapper that calls base_cpu_init followed by base_cpu_load.
Note that it is permissible to call this function (and/or the more primitive functions it is built on) more than once. This is particularly useful when reconfiguring the kernel memory map. For example, a typical MultiBoot (or other 32-bit) kernel generally starts out with paging disabled, so it must run in the low range of linear/physical memory. However, after enabling page translation, the OS may later want to relocate itself to run at a higher address in linear memory so that application programs can use the low part (e.g., v86-mode programs). An easy way to do this with the OSKit is to call base_cpu_setup once at the very beginning, to initialize the basic unpaged kernel environment, and then later, after paging is enabled and appropriate mappings have been established in high linear address space, modify the linear_base_va variable (Section 15.6.2) to reflect the kernel’s new linear address base, and finally call base_cpu_setup again to reinitialize and reload the processor tables according to the new memory map.
This function initializes all of the critical data structures used by the base environment, including base_cpuid, base_idt, base_gdt, and base_tss, but does not actually activate them or otherwise modify the processor’s execution state. The base_cpu_load function must be called later to initialize the processor with these structures. Separate initialization and activation functions are provided to allow the OS to customize the processor data structures if necessary before activating them.
This function loads the critical base environment data structures (in particular, the GDT, IDT, and TSS) into the processor, and reinitializes all segment registers from the new GDT as recommended in Intel processor documentation. The structures must already have been set up by a call to base_cpu_init and/or custom initialization code in the client OS.
This function returns with the CS segment set to KERNEL_CS (the default kernel code segment; see Section 15.7.1 for details), DS, ES, and SS set to KERNEL_DS (the default kernel data segment), and FS and GS set to 0. After the base_cpu_load call completes, a full working kernel environment is in place: segment registers can be loaded, interrupts and traps can be fielded by the OS, privilege level changes can occur, etc.
This is a global variable that is filled in by base_cpu_init with information about the processor on which base_cpu_init was called. (Alternatively, it can also be initialized manually by the OS simply by calling cpuid(&base_cpuid)). This structure is used by other parts of the kernel support library to determine whether or not certain processor features are available, such as 4MB superpages. See 15.5.1 for details on the contents of this structure.
Note that in a multiprocessor system, this variable will reflect the boot processor. This is generally not a problem, since most SMPs use identical processors, or at least processors in the same generation, so that they appear equivalent to OS software. (For example, it is very unlikely that you’d find an SMP that mixes 486 and Pentium processors), However, if this ever turns out to be a problem, the OS can always override the cpuid or base_cpu_init function, or just modify the contents of the base_cpuid variable after calling base_cpu_init so that it reflects the least common denominator of all the processors.
#include <oskit/machine/base_stack.h>
Definitions related to the default kernel stack.
This header file can be used in assembly language code as well as C.
Although most modern operating systems use a simple “flat” address space model, the x86 enforces a segmentation model which cannot be disabled directly; instead, it must be set up to emulate a flat address space model if that is what the OS desires. The base environment code provides functionality to set up a simple flat-model processor environment suitable for many types of kernels, both “micro” and “macro.” For example, it provides a default global descriptor table (GDT) containing various flat-model segments for the kernel’s use, as well as a default task state segment (TSS).
Furthermore, even though this base environment is often sufficient, the client OS is not limited to using it exactly as provided by default: the client kernel is given the flexibility to tweak various parameters, such as virtual and linear memory layout, as well as the freedom to operate completely outside of the base environment when necessary. For example, although the base environment provides a default TSS, the OS is free to create its own TSS structures and use them when running applications that need special facilities such as v86 mode or I/O ports. Alternatively, the OS could use the default processor data structures only during startup, and switch to its own complete, customized set after initialization.
The base environment code in the OSKit generally assumes that it is running in a simple flat model, in which only one code segment and one data segment are used for all kernel code and data, respectively, and that the code and data segments are synonymous (they each map to the same range of linear addresses). The OS is free to make more exotic uses of segmentation if it so desires, as long as the OSKit code is run in a consistent environment.
XXX diagram of function call tree?
The base segmentation environment provided by the OSKit is described in more detail in the following API sections.
This variable is used in the base environment as the default global descriptor table. The default base_gdt definition contains GDTSZ selector slots, including the Intel-reserved, permanently unused slot 0.
The following symbols are defined in base_gdt.h to be segment selectors for the descriptors in the base GDT. These selectors can be converted to indices into the GDT descriptor array base_gdt by dividing by 8 (the processor reserves the low three bits of all selectors for other information).
If the client OS wants to make use of the base GDT but needs more selector slots for its own purposes, it can define its own instance of the base_gdt variable so that it has room for more than GDTSZ elements; base_gdt_init will initialize only the first “standard” segment descriptors, leaving the rest for the client OS’s use.
On multiprocessor systems, the client OS may want each processor to have its own GDT. In this case, the OS can create a separate clone of the base GDT for each additional processor besides the boot processor, and leave the boot processor using the base GDT. Alternatively, the OS could use the base GDT only during initialization, and switch all processors to custom GDTs later; this approach provides the most flexibility to the OS, since the custom GDTs can be arranged in whatever way is most convenient.
This function initializes the standard descriptors in the base GDT as described in Section 15.7.1.
For all of the standard descriptors except LINEAR_CS and LINEAR_DS, the kvtolin macro is used to compute the linear address to plug into the offset field of the descriptor: for BASE_TSS, this is kvtolin(&base_tss); for the kernel code and data segments, it is kvtolin(0) (i.e., the linear address corresponding to the beginning of kernel virtual address space). LINEAR_CS and LINEAR_DS are always given an offset of 0.
A 16-bit version of this function, i16_base_gdt_init, is also provided so that the GDT can be initialized properly before the processor has been switched to protected mode. (Switching to protected mode on the x86 according to Intel’s recommended procedure requires a functional GDT to be already initialized and activated.)
This function loads the base GDT into the processor’s GDTR, and then reinitializes all segment registers from the descriptors in the newly loaded GDT. It returns with the CS segment set to KERNEL_CS (the default kernel code segment; see Section 15.7.1 for details), DS, ES, and SS set to KERNEL_DS (the default kernel data segment), and FS and GS set to 0.
This global variable is used in the base environment as the default interrupt descriptor table. The default definition of base_idt in the library contains the architecturally-defined maximum of 256 interrupt vectors (IDTSZ).2
The base_idt.h header file does not define any symbols representing interrupt vector numbers. The lowest 32 vectors are the processor trap vectors defined by Intel; since these are not specific to the base environment, they are defined in the generic header file x86/trap.h (see Section 15.3.10). Standard hardware interrupt vectors are PC-specific, and therefore are defined separately in x86/pc/irq_list.h (see Section 15.4.1). For the same reason, there is no base_idt_init function, only separate functions to initialize the trap vectors in the base IDT (base_trap_init, Section 15.8.2), and hardware interrupt vectors in the IDT (base_irq_init, Section 15.12.3).
This function loads the base_idt into the processor, so that subsequent traps and hardware interrupts will vector through it. It uses the kvtolin macro to compute the proper linear address of the IDT to be loaded into the processor.
The base_tss variable provides a default task state segment that the OS can use for privilege level switching if it does not otherwise use the x86’s task switching mechanisms. The x86 architecture requires every protected-mode OS to have at least one TSS even if no task switching is done; however, many x86 kernels do not use the processor’s task switching features because it is faster to context switch manually. Even if special TSS segments are used sometimes (e.g., to take advantage of the I/O bitmap feature when running MS-DOS programs), the OS can still use a common TSS for all tasks that do not need to use these special features; this is the strategy taken by the Mach kernel, for example. The base_tss provided by the toolkit serves in this role as a generic “default” TSS.
The base_tss is a minimal TSS, in that it contains no I/O bitmap or interrupt redirection map. XXX The toolkit also supports an alternate default TSS with a full I/O permission bitmap, but it isn’t fully integrated or documented yet.
The base_tss_init function initializes the base_tss to a valid minimal state. It sets the I/O permission bitmap offset to point past the end of the TSS, so that it will be interpreted by the processor as empty (no permissions for any I/O ports). It also initializes the ring 0 stack segment selector (ss0) to KERNEL_DS, and the ring 0 stack pointer (esp0) to the current stack pointer value at the time of the function call, to provide a minimal working context for trap handling. Once the OS kernel sets up a “real” kernel stack, it should reinitialize base_tss.esp0 to point to that.
This function activates the base_tss in the processor using the LTR instruction, after clear the busy bit in the BASE_TSS segment descriptor to ensure that a spurious trap isn’t generated.
The first 32 vectors in the IDT are used to handle processor exceptions (“traps”). In the base OSKit environment, these vectors are initialized from the base_trap_inittab array (15.8.3) using the base_trap_init function (15.8.2). By default, each exception vector in the processor IDT is set to point to a common assembly language stub that saves a standard trap frame (15.8.1), and calls a designated high-level handler specified in the base_trap_handlers table (15.8.4). Initially, all the entries in this table point to base_trap_default_handler (15.8.5). Custom trap handlers can be installed by changing the appropriate entry in the table. The default action for all traps can be changed by overriding base_trap_default_handler.
This affords client OSes with a variety of choices for modifying the behavior of trap handling. By using the base trap environment unchanged (i.e., the client OS is not expecting or handling traps), all traps will produce a trap dump and panic. This behavior is sufficient for most simple OSKit applications. By setting entries base_trap_handlers, the client can provide its own C language trap handlers while still using the default trap_state structure. The OSKit remote GDB debugging package (15.17.5) does this. Finally, the client OS can override base_trap_inittab to allow for different high-level handlers for every exception type and/or to permit the use of a different trap state format.
#include <oskit/x86/base_trap.h>
struct trap_state { /* Saved segment registers */ unsigned int gs; unsigned int fs; unsigned int es; unsigned int ds; /* PUSHA register state frame */ unsigned int edi; unsigned int esi; unsigned int ebp; unsigned int cr2; /* we save cr2 over esp for page faults */ unsigned int ebx; unsigned int edx; unsigned int ecx; unsigned int eax; /* Processor trap number, 0-31. */ unsigned int trapno; /* Error code pushed by the processor, 0 if none. */ unsigned int err; /* Processor state frame */ unsigned int eip; unsigned int cs; unsigned int eflags; unsigned int esp; unsigned int ss; /* Virtual 8086 segment registers */ unsigned int v86_es; unsigned int v86_ds; unsigned int v86_fs; unsigned int v86_gs; }; |
This structure defines the saved state frame pushed on the stack by the default trap entrypoints provided by the base environment (see Section 15.8.3). It is also used by the trap_dump routine, which is used in the default environment to dump the saved register state and panic if an unexpected trap occurs; and by gdb_trap, the default trap handler for remote GDB debugging.
This client OS is not obligated to use this structure as the saved state frame for traps it handles; if this structure is not used, then the OS must also override (or not use) the dependent routines mentioned above.
The structure elements from err down corresponds to the basic trap frames pushed on the stack by the x86 processor. (For traps in which the processor does not push an error code, the default trap entrypoint code sets err to zero.) The structure elements from esp down are only pushed by traps from lower privilege (rings 1-3), and the structure elements from v86_es down are only pushed by traps from v86 mode.
The rest of the state frame is pushed manually by the default trap entrypoint code. The saved integer register state is organized in a format compatible with the processor’s PUSHA instruction. However, in the slot that would otherwise hold the pushed ESP (which is useless since it is the trap handler’s stack pointer rather than the trapping code’s stack pointer), the default trap handler saves the CR2 register (page fault linear address) during page faults.
This trap state structure is borrowed from Mach.
This function initializes the processor trap vectors in the base IDT to the default trap entrypoints defined in base_trap_inittab.
This gate initialization table (see Section 15.3.9) encapsulates the base environment’s default trap entrypoint code. This module provides IDT entrypoints for all of the processor-defined trap vectors; each entrypoint pushes a standard state frame on the stack (see Section 15.8.1), and then calls the C function pointed to by the corresponding entry in base_trap_handlers array (see Section 15.8.4). Through these entrypoints, the OSKit provides the client OS with a convenient, uniform method of handling all processor traps in ordinary high-level C code.
If a trap occurs and the trap entrypoint code finds that the corresponding entry in base_trap_handlers is null, or if it points to a handler routine but the handler returns a nonzero value indicating failure, the entrypoint code calls trap_dump_panic (see Section 15.8.7) to dump the register state to the console and panic the kernel. This behavior is typically appropriate in kernels that do not expect traps to occur during proper operation (e.g., boot loaders or embedded operating systems), where a trap probably indicates a serious software bug.
On the other hand, if a trap handler is present and returns success (zero), the entrypoint code restores the saved state and resumes execution of the trapping code. The trap handler may change the contents of the trap_state structure passed by the entrypoint code; in this case, final contents of the structure on return from the trap handler will be the state restored.
All of the IDT entries initialized by the base_trap_inittab are trap gates rather than interrupt gates; therefore, if hardware interrupts are enabled when a trap occurs, then interrupts will still be enabled during the trap handler unless the trap handler explicitly disables them. If the OS wants interrupts to be disabled during trap handling, it can change the processor trap vectors in the IDT (vectors 0-31) into interrupt gates, or it can simply use its own trap entrypoint code instead.
#include <oskit/x86/base_trap.h>
void (*base_trap_handlers[BASE_TRAP_COUNT]) (struct trap_state *ts );
Contains a function pointer for every hardware trap vector. By default, all entries in this table point to base_trap_default_handler, which will simply dump the register state to the console and panic. The client OS can set entries in this table to point to its own trap handler function(s), or to alternative trap handlers supplied by the OSKit, such as the remote GDB debugging trap handler, gdb_trap (see Section 15.17.5).
The trap handler returns zero (success) to resume execution, or nonzero (failure) to cause the entrypoint code to dump the register state and panic the kernel.
This routine is the default handler for all traps in the base environment. It simply displays displays the trap state information and then calls panic.
It is expected that the client OS will override entries in the base_trap_handlers array for traps it cares about. Alternatively, the client OS may override the base_trap_default_handler entrypoint entirely.
This function dumps the contents of the specified trap state frame to the console using the printf function, in a simple human-readable form. The function is smart enough to determine whether the trap occurred from supervisor mode, user mode, or v86 mode, and interpret the saved state accordingly. For example, for traps from rings 1-3 or from v86 mode, the the original stack pointer is part of the saved state frame; however, for traps from ring 0, the original stack pointer is simply the end of the stack frame pushed by the processor, since no stack switch occurs in this case.
In addition, for traps from ring 0, this routine also provides a hex dump of the top of the kernel stack as it appeared when the trap occurred; this stack dump can aid in tracking down the cause of a kernel bug. trap_dump does not attempt to dump the stack for traps from user or v86 mode, because there seems to be no sufficiently generic way for it to access the appropriate user stack; in addition, in this case the trap might have been caused by a user-stack-related exception, in which case attempting to dump the user stack could lead to a recursive trap.
This function simply calls trap_dump (Section 15.8.6) to dump the specified trap state frame, and then calls panic (Section 14.8.3). It is invoked by the default trap entrypoint code (Section 15.8.3) if a trap occurs when there is no interrupt handler, or if there is an interrupt handler but it returns a failure indication.
XXX diagram of function call tree?
XXX Although a “base” x86 paging environment is defined, it is not automatically initialized by base_cpu_init, and paging is not activated by base_cpu_load. This is because unlike segmentation, paging is an optional feature on the x86 architecture, and many simple “kernels” such as boot loaders would prefer to ignore it completely. Therefore, client kernels that do want the base paging environment must call the functions to initialize and activate it manually, after the basic CPU segmentation environment is set up.
XXX describe assumptions made about use of page tables, e.g. 4MB pages whenever possible, always modify/unmap _exactly_ the region that was mapped.
XXX assumes that mappings are only changed or unmapped with the same size and offset as the original mapping.
XXX does not attempt to support page table sharing in any way, since this code has no clue about the relationship between address spaces; it only knows about page directories and page tables.
This function can be used to set up a minimal paging environment. It first allocates and clears an initial page directory using ptab_alloc (see Section 15.9.7), sets base_pdir_pa to point to it (see Section 15.9.2), then direct-maps all known physical memory into this address space starting at linear address 0, allocating additional page tables as needed. Finally, this function enables the processor’s paging mechanism, using the base page directory as the initial page directory.
The global variable phys_mem_max (see Section 15.11.2) is assumed to indicate the top of physical memory; all memory from 0 up to at least this address is mapped. The function actually rounds phys_mem_max up to the next 4MB superpage boundary, so that on Pentium and higher processors, all physical memory can be mapped using 4MB superpages even if known physical memory does not end exactly on a 4MB boundary. Note that phys_mem_max does not necessarily need to reflect all physical memory in the machine; for example, it is perfectly reasonable for the client OS to set it to some artificially lower value so that only that part of physical memory is direct-mapped.
On Pentium and higher processors, this function sets the PSE (page size extensions) bit in CR4 in addition to the PG (paging) bit, so that the 4MB page mappings used to map physical memory will work properly.
This variable is initialized by base_paging_init (see Section 15.9.1) to contain the physical address of the base page directory. This is the value that should be loaded into the processor’s page directory base register (CR3) in order to run in the linear address space defined by this page directory. (The base page directory is automatically activated in this way during initialization; the client OS only needs to load the CR3 register itself if it wants to switch among multiple linear address spaces.) The pdir_find_pde function (Section 15.9.3) and other related functions can be used to manipulate the page directory and its associated page tables.
Initially, the base page directory and its page tables directly map physical memory starting at linear address 0. The client OS is free to change the mappings after initialization, for example by adding new mappings outside of the physical address range, or by relocating the physical memory mappings to a different location in the linear address space as described in Section 15.6.3.
Most “real” operating systems will need to create other, separate page directories and associated page tables to represent different address spaces or protection domains. However, the base page directory may still be useful, e.g., as a template for initializing the common kernel portion of other page directories, or as a “kernel-only” address space for use by kernel tasks, etc.
#include <oskit/x86/base_paging.h>
pd_entry_t *pdir_find_pde(oskit_addr_t pdir_pa, oskit_addr_t la);
This primitive macro uses the appropriate bits in linear address la (bits 22-31) to look up a particular entry in the specified page directory. Note that this function takes the physical address of a page directory, but returns a kernel virtual address (i.e., an ordinary pointer to the selected page directory entry).
Returns a pointer to the selected page directory entry.
#include <oskit/x86/base_paging.h>
pd_entry_t *ptab_find_pte(oskit_addr_t ptab_pa, oskit_addr_t la);
This macro uses the appropriate bits in la (bits 12-21) to look up a particular entry in the specified page table. This macro is just like pdir_find_pde, except that it selects an entry based on the page table index bits in the linear address rather than the page directory index bits (bits 22-31). Note that this function takes the physical address of a page table, but returns a kernel virtual address (an ordinary pointer).
Returns a pointer to the selected page table entry.
#include <oskit/x86/base_paging.h>
pt_entry_t *pdir_find_pte(oskit_addr_t pdir_pa, oskit_addr_t la);
This function is a combination of pdir_find_pde and ptab_find_pte: it descends through both levels of the x86 page table hierarchy and finds the page table entry for the specified linear address.
This function assumes that if the page directory entry selected by bits 22-31 of la is valid (the INTEL_PDE_VALID bit is set), then that entry actually refers to a page table, and is not a 4MB page mapping. The caller must ensure that this is the case.
Returns a pointer to the selected page table entry, or NULL if there is no page table for this linear address.
This function is a simple extension of pdir_find_pte: instead of returning the address of the selected page table entry, it returns the contents of the page table entry: i.e., the physical page frame in bits 12-31 and the associated INTEL_PTE_* flags in bits 0-11. If there is no page table in the page directory for the specified linear address, then this function returns 0, the same as if there was a page table but the selected page table entry was zero (invalid).
As with pdir_find_pte, this function assumes that if the page directory entry selected by bits 22-31 of la is valid (the INTEL_PDE_VALID bit is set), then that entry actually refers to a page table, and is not a 4MB page mapping.
Returns the selected page table entry, or zero if there is no page table for this linear address. Also returns zero if the selected page table entry exists but is zero.
All of the following page mapping routines call this function to allocate new page tables as needed to create page mappings. It attempts to allocate a single page of physical memory, and if successful, returns 0 with the physical address of that page in *out_ptab_pa. The newly allocated page is cleared to all zeros by this function. If this function is unsuccessful, it returns nonzero.
The default implementation of this function assumes that the OSKit’s minimal C library (libc) and list-based memory manager (liblmm) are being used to manage physical memory, and allocates page table pages from the malloc_lmm memory pool (see Section 14.5.1). However, in more complete OS environments, e.g., in which low physical memory conditions should trigger a page-out rather than failing immediately, this routine can be overridden to provide the desired behavior.
Returns zero if the allocation was successful, or nonzero on failure.
The page mapping and unmapping functions described in the following sections call this routine to free a page table that is no longer needed; thus, this function is the partner of ptab_alloc (see Section 15.9.7). The default implementation again assumes that the malloc_lmm memory pool is being used to manage physical memory. If the client OS overrides ptab_alloc to use a different allocation mechanism, it should also override ptab_free correspondingly.
#include <oskit/x86/base_paging.h>
int pdir_map_page(oskit_addr_t pdir_pa, oskit_addr_t la, pt_entry_t mapping);
This function creates a single 4KB page mapping in the linear address space represented by the specified page directory. If the page table covering the specified linear address does not exist (i.e., the selected page directory entry is invalid), then a new page table is allocated using ptab_alloc and inserted into the page directory before the actual page mapping is inserted into the page table. Any new page tables created by this function are mapped into the page directory with permissions INTEL_PTE_USER | INTEL_PTE_WRITE: full permissions are granted at the page directory level, although the specified mapping value, which is inserted into the selected page table entry, may restrict permissions at the individual page granularity.
This function assumes that if the page directory entry selected by bits 22-31 of la is valid (the INTEL_PDE_VALID bit is set), then that entry actually refers to a page table, and is not a 4MB page mapping. In other words, the caller should not attempt to create a 4KB page mapping in a part of the linear address space already covered by a valid 4MB superpage mapping. The caller must first unmap the 4MB superpage mapping, then map the 4KB page (which will cause a page table to be allocated). If the caller follows the guidelines described in Section 15.9, then this requirement should not be a problem.
If all goes well and the mapping is successful, this function returns zero. If this function needed to allocate a new page table but the ptab_alloc function failed (returned nonzero), then this function passes back the return value from ptab_alloc.
This function invalidates a single 4KB page mapping in the linear address space represented by the specified page directory. The la parameter should fall in a page previous mapped with pdir_map_page, otherwise the result of the call is undefined. XXX Is this overly restrictive?
This function assumes that if the page directory entry selected by bits 22-31 of la is valid (the INTEL_PDE_VALID bit is set), then that entry actually refers to a page table, and is not a 4MB page mapping. In other words, the caller should not attempt to destroy a 4KB page mapping in a part of the linear address space covered by a valid 4MB superpage mapping. Use pmap_unmap_range to remove a 4MB superpage mapping.
#include <oskit/x86/base_paging.h>
int pdir_map_range(oskit_addr_t pdir_pa, oskit_addr_t la, oskit_addr_t pa, oskit_size_t size, pt_entry_t mapping_bits);
This function maps a range of linear addresses in the linear address space represented by the specified page directory onto a contiguous range of physical addresses. The linear (source) address, physical (destination) address, and mapping size must be multiples of the 4KB architectural page size, but other than that no restrictions are imposed on the location or size of the mapping range. If the processor description in the global base_cpuid variable (see Section 15.6.6) indicates that page size extensions are available, and the physical and linear addresses are properly aligned, then this function maps as much of the range as possible using 4MB superpage mappings instead of 4KB page mappings. Where 4KB page mappings are needed, this function allocates new page tables as necessary using ptab_alloc. Any new page tables created by this function are mapped into the page directory with permissions INTEL_PTE_USER | INTEL_PTE_WRITE: full permissions are granted at the page directory level, although the mapping_bits may specify more restricted permissions for the actual page mappings.
This function assumes that no valid mappings already exist in the specified linear address range; if any mappings do exist, this function may not work properly. If the caller follows the guidelines described in Section 15.9, always unmapping previous mappings before creating new ones, then this requirement should not be a problem.
If all goes well and the mapping is successful, this function returns zero. If this function needed to allocate a new page table but the ptab_alloc function failed (returned nonzero), then this function passes back the return value from ptab_alloc.
#include <oskit/x86/base_paging.h>
void pdir_prot_range(oskit_addr_t pdir_pa, oskit_addr_t la, oskit_size_t size, pt_entry_t new_mapping_bits);
This function can be used to modify the permissions and other attribute bits associated with a mapping range previously created with pdir_map_range. The la and size parameters must be exactly the same as those passed to the pdir_map_range used to create the mapping.
#include <oskit/x86/base_paging.h>
void pdir_unmap_range(oskit_addr_t pdir_pa, oskit_addr_t la, oskit_size_t size);
This function removes a mapping range previously created using pdir_map_range. The la and size parameters must be exactly the same as those passed to the pdir_map_range used to create the mapping.
#include <oskit/x86/base_paging.h>
void pdir_clean_range(oskit_addr_t pdir_pa, oskit_addr_t la, oskit_size_t size);
This function scans the portion of the given page directory covering the given address range, looking for associated page table pages that are unnecessary and frees them. “Unnecessary” page table pages are those which contain only entries for which INTEL_PTE_VALID is not set. These pages are freed with ptab_free and the corresponding page directory entries marked as invalid.
This function is primarily intended for debugging purposes: it dumps the mappings described by the specified page directory and all associated page tables in a reasonably compact, human-readable form, using printf. 4MB superpage as well as 4KB page mappings are handled properly, and contiguous ranges of identical mappings referring to successive physical pages or superpages are collapsed into a single line for display purposes. The permissions and other page directory/page table entry flags are expanded out as human-readable flag names.
This is primarily a helper function for pdir_dump, but it can also be used independently, to dump the contents of an individual page table. For output purposes, the page table is assumed to reside at base_la in “some” linear address space: in other words, this parameter provides the topmost ten bits in the linear addresses dumped by this routine. Contiguous ranges of identical mappings referring to successive physical pages are collapsed into a single line for display purposes. The permissions and other page directory/page table entry flags are expanded out as human-readable flag names.
The physical memory address space on PCs is divided into distinct regions which have certain attributes. The lowest 1MB of physical memory is “special” in that only it can be accessed from real mode. The lowest 16MB of physical memory is special in that only it can be accessed by the built-in DMA controller. On some PCs, there may be an additional boundary imposed by the motherboard. Memory above this boundary (e.g., 64MB) may not be cacheable by the L2 cache. The base environment uses the OSKit LMM library 25 to accommodate these differences. Physical memory is managed by a single LMM malloc_lmm with separate regions for each “type” of memory. Hence, LMM allocation requests can be made with appropriate flag values to obtain memory with the desired characteristics.
#include <oskit/x86/pc/phys_lmm.h>
There are three priority values assigned to regions of physical memory as they are made available at boot time (via lmm_add_region). In increasing order of priority (i.e., increasing preference for allocation):
These priorities prevent the simple memory allocation interfaces from handing out the more precious low-address memory. To enable savvy applications to explicitly allocate memory of a given type, the following flag values are assigned at boot time and can be passed to LMM allocation routines:
Thus, if neither flag is set, memory can be allocated from any of the three regions with preference given to LMM_PRI_HIGH, followed by LMM_PRI_16MB and LMM_PRI_1MB. If just LMMF_16MB is set, memory can be allocated from either LMM_PRI_16MB or LMM_PRI_1MB in that order. If both flags are set, memory can only be allocated from the LMM_PRI_1MB region.
This variable records the highest physical memory address; i.e., the end address of the highest free block added to malloc_lmm. This is the highest address that the kernel should ever have to deal with.
Not all addresses between 0 and phys_mem_max are necessarily available for allocation, there may be holes in the available physical memory space.
This routine sets up malloc_lmm with three physical memory regions, one for each of the memory types described in phys_lmm.h. No actual memory is added to those regions. You can then call phys_lmm_add() to add memory to those regions. In the base environment, base_multiboot_init_mem handles this chore.
Add a chunk of physical memory to the appropriate region(s) on the malloc_lmm. The provided memory block may be arbitrarily aligned and may cross region boundaries (e.g., the 16MB boundary); it will be shrunken and split apart as necessary. If the address of the end of the block is greater than the current value in phys_max_mem, this address is recorded.
Note that phys_lmm_add takes a physical address, not a virtual address as the underlying LMM routines do. This routine will perform the conversion as needed with phystokv.
In the base environment, each hardware interrupt vector in the processor IDT points to a small assembly language stub that saves a standard trap frame (15.8.1), disables and acknowledges the hardware interrupt, and calls a designated high-level handler routine specified in the base_irq_handlers table (15.12.2). Initially, all the entries in this table point to base_irq_default_handler (15.12.5). Custom interrupt handlers can be installed by changing the appropriate entry in the table. The default action for all interrupts can be changed by overriding base_irq_default_handler.
The base environment also includes support for a single “software interrupt.” A software interrupt is delivered after all pending hardware interrupts have been processed but before returning from the interrupt context. A software interrupt can be posted at any time with base_irq_softint_request (15.12.7) but will only be triggered upon return from a hardware interrupt; i.e., processing of a software interrupt requested from a non-interrupt context is deferred until a hardware interrupt occurs. The software interrupt handler is contained in the function pointer base_irq_softint_handler (15.12.8), and initially points to the function base_irq_softint_default_handler, but can be replaced by a custom version provided by the kernel.
#include <oskit/x86/pc/base_irq.h>
#include <oskit/x86/pc/base_irq.h>
void (*base_irq_handlers[BASE_IRQ_COUNT]) (struct trap_state *ts );
Contains a function pointer for every hardware interrupt vector. By default, all entries in this table point to base_irq_default_handler. Custom interrupt handlers can be installed by changing the appropriate table entry.
Interrupt handlers can freely examine and modify the processor state (15.8.1) of the interrupted activity, e.g., to implement threads and preemption. On entry, the processor’s IDT interrupt vector number is in ts->trapno and the hardware IRQ number that caused the interrupt is in ts->err.
Initializes the system to properly handle hardware interrupts. It loads the appropriate entries in the base IDT (15.7.4) with the gate descriptor information from base_irq_inittab, programs the PICs to the standard vector base addresses (see Section 15.12.1), and disables all interrupt request lines.
Processor interrupts must be disabled when this routine is called.
NOTE: This routine no longer enables interrupts!
This gate initialization table (15.3.9) encapsulates the base environment’s default interrupt entrypoint code. This module provides IDT entrypoints for all the standard PC hardware interrupt vectors; each entrypoint pushes a standard state frame on the stack (15.8.1), disables and acknowledges the hardware interrupt, and then calls the C handler function pointed to by the appropriate entry of the base_irq_handlers array (15.12.2). Upon return from the handler, the interrupt code checks for a pending software interrupt and dispatches to function pointer contained in base_irq_softint_handler.
This routine is the default handler for all interrupts in the base environment. It simply displays a warning message and returns.
It is expected that the client OS will override this default behavior for all interrupts it cares about, leaving this routine to be called only for unexpected interrupts.
Hardware interrupt nesting counter, used to ensure that the software interrupt handler isn’t called until all outstanding hardware interrupts have been processed. In addition, this variable also acts as the software interrupt pending flag: if the high bit is clear, a software interrupt is pending.
This routine requests a software interrupt and is typically called from a hardware interrupt handler to schedule lower priority processing.
After requesting a software interrupt, the function pointed to by base_irq_softint_handler will be called when all hardware interrupt handlers have completed processing and base_irq_nest is zero. If an interrupt is scheduled from a non-interrupt context, the handler will not be called until the next hardware interrupt occurs and has been processed.
Only a single software interrupt may be pending at a time.
Pointer to a software interrupt handler called by the interrupt entry/exit stub code when a software interrupt has been requested and needs to be run. The default value of this pointer is the function base_irq_softint_default_handler, which simply returns; to use software interrupts, the kernel must override it.
The handler is free to examine and modify the processor state in state.
The base console environment allows “console” input and output using either the display and keyboard or a serial line. Additionally it allows a remote kernel debugging with GDB over a serial line. Selection of the display or serial port as console and which serial ports to use for the console and GDB are controlled by command line options or environment variables as described in this section.
The base console environment uses a simple polled interface for serial port input and output as well as for keyboard input. The video display output interface is a simple, dumb text terminal. See the appropriate sections for details.
In the base interface, all input via stdin (e.g., getchar, scanf) and all output via stdout or stderr (e.g., putchar, printf) use the console.
For simplicity, a set of vanilla console functions is provided that direct input and output to/from the appropriate device. For example, console_putchar will invoke com_cons_putchar if the console device is a serial port, or direct_cons_putchar if the console device is a display. Other vanilla console I/O routines include console_getchar, console_puts, and console_putbytes (a raw block output function that does not append a newline). All behave as expected. These routines are provided to so that higher level I/O code does not need to be concerned with which type of device is currenty the console. Both the minimal C library (section § 14) and the FreeBSD C library (Section 21) take advantage of this redirection.
#include <oskit/x86/pc/base_console.h>
The following variable are used in the base_console code:
Refer to Section 15.13.2 for more information on command line options and environment variables. See Section 15.17 for more on remote GDB.
This function parses the multiboot command line and optionally initializes the serial lines.
Command line options recognized by the base_console code include:
Environment variables recognized include:
A POSIX termios structures with values suitable for a basic cooked-mode console tty. Used in the base environment when initializing a serial-port console in com_cons_init.
A POSIX termios structures with values suitable for a basic raw-mode console tty. Used in the base environment when initializing a serial-port for remote GDB debuging in com_cons_init.
Read a character from the PC keyboard. If none is available, this routine loops polling the keyboard status register until a character is available.
Supports only a subset of the available key presses. In particular, only the shifted and unshifted printable ASCII characters along with Escape, Backspace, Tab, and Carriage return. It does not support the remaining control characters or multi-character (function) keys.
Returns the character read.
Outputs the indicated character on the video console. Handles “\n” (newline), “\r” (carriage return), “\b” (backspace), and “\t” (tab) in addition to printable ASCII characters. Tabs are expanded to spaces (with stops are every 8 columns) and lines are automatically wrapped at 80 characters. Newline implies a carriage return.
Quick poll for an available input character. Returns a character or -1 if no character was available.
Due to the large delay between when a character is typed and when the scan code arrives at the keyboard controller (4-5 ms), there are a variety of situations in which this routine may return -1 even though a character has been typed:
In other words, this routine never delays in an attempt to wait for the next scan code to arrive when one is not currently available. Hence the utility of this routine is questionable.
Returns a character if available, -1 otherwise.
This routine must be called once to initialize a COM port for use by other com_cons routines. The supplied termios structure indicates the baud rate and other settings. If com_params is zero, a default of 9600 baud, 8 bit characters, no parity, and 1 stop bit is used.
Read a character from the indicated serial port. If none is available, this routine loops polling the status register until a character is available.
Returns the character read.
Outputs the indicated character on the specified serial port.
Waits until the transmit FIFOs are empty on the serial port specified. This is useful as it allows the programmer to know that the message sent out has been received. This is necessary before resetting the UART or changing settings if it is desirable for any data already “sent” to actually be transmitted.
Special function to enable receive character interrupts on a serial port.
Since the base COM console operates by polling, there is no need to handle serial interrupts in order to do basic I/O. However, if you want to be notified up when a character is received, call this function immediately after com_cons_init, and make sure the appropriate IDT entry is initialized properly.
For example, the serial debugging code for the PC COM port uses this so that the program can be woken up when the user presses the interrupt character (^C) from the remote debugger.
MultiBoot is a standardized interface between boot loaders and 32-bit operating systems on x86 PC platforms, which attempts to solve the traditional problem that every single operating system tends to come with its own boot loader or set of boot loaders which are completely incompatible with boot loaders written for any other operating system. The MultiBoot standard allows any MultiBoot-compliant operating system to be loaded from any MultiBoot-supporting boot loader. MultiBoot is also designed to provide advanced features needed by many modern operating systems, such as direct 32-bit protected-mode startup, and support for boot modules, which are arbitrary files loaded by the boot loader into physical memory along with the kernel and passed to the kernel on startup. These boot modules may be dynamically loadable device drivers, application program executables, files on an initial file system, or anything else the OS may need before it has full device access. The MultiBoot standard is already supported by several boot loaders and operating systems, and is gradually becoming more widespread. For details on the MultiBoot standard see Section 15.14.12.
The MultiBoot standard is separate from and largely independent of the OSKit. However, if MultiBoot is used, the toolkit can leverage it to provide a powerful, flexible, and extremely convenient method of booting custom operating systems that use the OSKit. The toolkit provides startup code which allows MultiBoot-compliant OS kernels to be built easily, and which handles the details of finding and managing physical memory on startup, interpreting the command line passed by the boot loader, finding and using boot modules, etc. If you use the OSKit’s MultiBoot startup support, your kernel automatically inherits a complete, full-featured 32-bit protected-mode startup environment and the ability to use various existing boot loaders, without being constrained by the limitations of traditional OS-specific boot loaders.
The MultiBoot startup code in the OSKit has two components. The first component is contained in the object file multiboot.o, installed by the toolkit in the prefix /lib/oskit/ directory. This object file contains the actual MultiBoot header and entrypoint; it must be linked into the kernel as the very first object file, so that its contents will be at the very beginning of the resulting executable. (This object file takes the place of the crt0.o or crt1.o normally used when linking ordinary applications in a Unix-like system.) The second component is contained in the libkern.a library; it contains the rest of the MultiBoot startup code as well as various utility routines for the use of the client OS.
XXX diagram of MultiBoot kernel executable image
The toolkit’s MultiBoot startup code will work when using either ELF or a.out format. ELF is the format recommended for kernel images by the MultiBoot standard; however, the a.out format is also supported through the use of some special header information embedded in the multiboot.o code linked at the very beginning of the kernel’s text segment. This information allows the MultiBoot boot loader to determine the location and sizes of the kernel’s text, data, and bss sections in the kernel executable without knowing the details of the particular a.out flavor in use (e.g., Linux, NetBSD, FreeBSD, Mach, VSTa, etc.), all of which are otherwise mutually incompatible.
After the MultiBoot boot loader loads the kernel executable image, it searches through the beginning of the image for the MultiBoot header which provides important information about the OS being loaded. The boot loader performs its activities, then shuts itself down and jumps to the OS kernel entrypoint defined in the kernel’s MultiBoot header. In one processor register the boot loader passes to the kernel the address of a MultiBoot information structure, containing various information passed from the boot loader to the OS, organized in a standardized format defined by the MultiBoot specification.
In the OSKit’s MultiBoot startup code, the kernel entrypoint is a short code fragment in multiboot.o which sets up the initial stack and performs other minimal initialization so that ordinary 32-bit C code can be run safely.3 This code fragment then calls the C function multiboot_main, with a pointer to the MultiBoot information structure as its argument. Normally, the multiboot_main function comes from libkern.a; it performs other high-level initialization to create a convenient, stable 32-bit environment, and then calls the familiar main routine, which the client OS must provide.
Once the OS kernel receives control in its main routine, the processor has been set up in the base environment defined earlier in Section 15.6. The base_gdt, base_idt, and base_tss have been set up and activated, so that segmentation operations work and traps can be handled. Paging is disabled, and all kernel code and data segment descriptors are set up with an offset of zero, so that virtual addresses, linear addresses, and physical addresses are all the same. The client OS is free to change this memory layout later, e.g., by enabling paging and reorganizing the linear address space as described in Section 15.6.3.
As part of the initialization performed by multiboot_main, the OSKit’s MultiBoot startup code uses information passed to the OS by the boot loader, describing the location and amount of physical memory available, to set up the malloc_lmm memory pool (see Section 14.5.1). This allows the OS kernel to allocate and manage physical memory using the normal C-language memory allocation mechanisms, as well as directly using the underlying LMM memory manager library functions. The physical memory placed on the malloc_lmm pool during initialization is guaranteed not to contain any of the data structures passed by the boot loader which the OS may need to use, such as the command line or the boot modules; this way, the kernel can freely allocate and use memory right from the start without worrying about accidentally “stepping on” boot loader data that it will need to access later on. In addition, the physical memory placed on the malloc_lmm is divided into the three separate regions defined in phys_lmm.h (see Section 15.11.1): one for low memory below 1MB, one for “DMA” memory below 16MB, and one for all physical memory above this line. This division allows the kernel to allocate “special” memory when needed for device access or for calls to real-mode BIOS routines, simply by specifying the appropriate flags in the LMM allocation calls.
The MultiBoot specification allows an arbitrary ASCII string to be passed from the boot loader to the OS as a “command line” for the OS to interpret as it sees fit. As passed from the boot loader to the OS, this is a single null-terminated ASCII string. However, the default MultiBoot initialization code provided by the OSKit performs some preprocessing of the command line before the actual OS receives control in its main routine. In particular, it parses the single command line string into an array of individual argument strings so that the arguments can be passed to the OS through the normal C-language argc/argv parameters to main. In addition, any command-line arguments containing an equals sign (‘=’) are added to the environ array rather than the argv array, effectively providing the OS with a minimal initial environment that can be specified by the user (through the boot loader) and examined by the OS using the normal getenv mechanism (see Section 14.4.17).
Note that this command-line preprocessing mechanism matches the kernel command-line conventions established by Linux, although it provides more convenience and flexibility to the OS by providing this information to the OS through standard C-language facilities, and by not restricting the “environment variables” to be comma-separated lists of numeric constants, as Linux does. This mechanism also provides much more flexibility than traditional BSD/Mach command-line mechanisms, in which the boot loader itself does most of the command-line parsing, and basically only passes a single fixed “flags” word to the OS.
Since MultiBoot kernels initially run in physical memory, with paging disabled and segmentation effectively “neutralized,” the kernel must be linked at an address within the range of physical memory present on typical PCs. Normally the best place to link the kernel is at 0x100000, or 1MB, which is the beginning of extended memory just beyond the real-mode ROM BIOS. Since the processor is already in 32-bit protected mode when the MultiBoot boot loader starts the OS, running above the 1MB “boundary” is not a problem. By linking at 1MB, the kernel has plenty of “room to grow,” having essentially all extended memory available to it in one contiguous chunk.
In some cases, it may be preferable to link the kernel at a lower address, below the 1MB boundary, for example if the kernel needs to run on machines without any extended memory, or if the kernel contains code that needs to run in real mode. This is also allowed by the MultiBoot standard. However, note that the kernel should generally leave at least the first 0x500 bytes of physical memory untouched, since this area contains important BIOS data structures that will be needed if the kernel ever makes calls to the BIOS, or if it wants to glean information about the machine from this area such as hard disk configuration data.
#include <oskit/x86/multiboot.h>
This header file is not specific to the MultiBoot startup code provided by the OSKit; it merely contains generic symbolic structure and constant definitions corresponding to the data structures specified in the MultiBoot specification. The following C structures are defined:
For more information on these structures and the associated constants, see the multiboot.h header file and the MultiBoot specification.
XXX should move this to x86/pc/multiboot.h?
The first thing that multiboot_main does on entry from the minimal startup code in multiboot.o is copy the MultiBoot information structure passed by the boot loader into a global variable in the kernel’s bss segment. Copying the information structure this way allows it to be accessed more conveniently by the kernel, and makes it unnecessary for the memory initialization code (base_multiboot_init_mem; see Section 15.14.9) to carefully “step over” the information structure when determining what physical memory is available for general use.
After the OS has received control in its main routine, it is free to examine the boot_info structure and use it to locate other data passed by the boot loader, such as the boot modules. The client OS must not attempt to access the original copy of the information structure passed by the boot loader, since that copy of the structure may be overwritten as memory is dynamically allocated and used. However, this should not be a problem, since a pointer to the original copy of the multiboot_info structure is never even passed to the OS by the MultiBoot startup code; it is only accessible to the OS if it overrides the multiboot_main function.
This is the first C-language function to run, invoked by the minimal startup code fragment in multiboot.o. The default implementation merely copies the MultiBoot information structure passed by the boot loader into the global variable boot_info (see Section 15.14.7), and then calls the following routines to set up the base environment and start the OS:
If the client OS does not wish some or all of the above to be performed, it may override the multiboot_main function with a version that does what it needs, or, alternatively, it may instead override the specific functions of interest called by multiboot_main.
This function had better never return.
This function finds all physical memory available for general use and adds it to the malloc_lmm pool, as described in Section 15.14.3. It is normally called automatically during initialization by multiboot_main (see Section 15.14.8).
This function uses the lower and upper memory size fields in the MultiBoot information structure to determine the total amount of physical memory available; it then adds all of this memory to the malloc_lmm pool except for the following “special” areas:
This function uses phys_lmm_init to initialize the malloc_lmm, and phys_lmm_add to add available physical memory to it (see Section 15.11.1); as a consequence, this causes the physical memory found to be split up automatically according to the three main functional “classes” of PC memory: low 1MB memory accessible to real-mode software, low 16MB memory accessible to the built-in DMA controller, and “all other” memory. This division allows the OS to allocate “special” memory when needed for device access or for calls to real-mode BIOS routines, simply by specifying the appropriate flags in the LMM allocation calls.
XXX currently doesn’t use the memory range array.
This function breaks up the kernel command line string passed by the boot loader into independent C-language-compatible argument strings. Option strings are separated by any normal whitespace characters (spaces, tabs, newlines, etc.). In addition, strings containing an equals sign (‘=’) are added to the environ array rather than the argv array, effectively providing the OS with a minimal initial environment that can be specified by the user (through the boot loader) and examined by the OS using the normal getenv mechanism (see Section 14.4.17).
XXX example.
XXX currently no quoting support.
XXX currently just uses “kernel” as argv[0].
#include <oskit/x86/pc/base_multiboot.h>
struct multiboot_module *base_multiboot_find(const char *string);
This is not an initialization function, but rather a utility function for the use of the client OS. Given a particular string, it searches the array of boot modules passed by the boot loader for a boot module with a matching string. This function can be easily used by the OS to locate specific boot modules by name.
If multiple boot modules have matching strings, then the first one found is returned. If any boot modules have no strings attached (no pun intended), then those boot modules will never be “found” by this function, although they can still be found by hunting through the boot module array manually.
If successful, returns a pointer to the multiboot_module entry matched; from this structure, the actual boot module data can be found using the mod_start and mod_end elements, which contain the start and ending physical addresses of the boot module data, respectively.
If no matching boot module can be found, this function returns NULL.
Excerpt from "MultiBoot Standard" Version 0.6 March 29, 1996 _________________________________________________________________ (This contains the essential MultiBoot specification, omitting background and related info found in ftp://flux.cs.utah.edu/flux/multiboot/.) Contents * Terminology * Scope and Requirements * Details * Authors The following items are not part of the standards document, but are included for prospective OS and bootloader writers. * Example OS Code * Example Bootloader Code _________________________________________________________________ Terminology Throughout this document, the term "boot loader" means whatever program or set of programs loads the image of the final operating system to be run on the machine. The boot loader may itself consist of several stages, but that is an implementation detail not relevant to this standard. Only the "final" stage of the boot loader - the stage that eventually transfers control to the OS - needs to follow the rules specified in this document in order to be "MultiBoot compliant"; earlier boot loader stages can be designed in whatever way is most convenient. The term "OS image" is used to refer to the initial binary image that the boot loader loads into memory and transfers control to to start the OS. The OS image is typically an executable containing the OS kernel. The term "boot module" refers to other auxiliary files that the boot loader loads into memory along with the OS image, but does not interpret in any way other than passing their locations to the OS when it is invoked. _________________________________________________________________ Scope and Requirements Architectures This standard is primarily targetted at PC's, since they are the most common and have the largest variety of OS's and boot loaders. However, to the extent that certain other architectures may need a boot standard and do not have one already, a variation of this standard, stripped of the x86-specific details, could be adopted for them as well. Operating systems This standard is targetted toward free 32-bit operating systems that can be fairly easily modified to support the standard without going through lots of bureaucratic rigmarole. The particular free OS's that this standard is being primarily designed for are Linux, FreeBSD, NetBSD, Mach, and VSTa. It is hoped that other emerging free OS's will adopt it from the start, and thus immediately be able to take advantage of existing boot loaders. It would be nice if commercial operating system vendors eventually adopted this standard as well, but that's probably a pipe dream. Boot sources It should be possible to write compliant boot loaders that load the OS image from a variety of sources, including floppy disk, hard disk, and across a network. Disk-based boot loaders may use a variety of techniques to find the relevant OS image and boot module data on disk, such as by interpretation of specific file systems (e.g. the BSD/Mach boot loader), using precalculated "block lists" (e.g. LILO), loading from a special "boot partition" (e.g. OS/2), or even loading from within another operating system (e.g. the VSTa boot code, which loads from DOS). Similarly, network-based boot loaders could use a variety of network hardware and protocols. It is hoped that boot loaders will be created that support multiple loading mechanisms, increasing their portability, robustness, and user-friendliness. Boot-time configuration It is often necessary for one reason or another for the user to be able to provide some configuration information to the OS dynamically at boot time. While this standard should not dictate how this configuration information is obtained by the boot loader, it should provide a standard means for the boot loader to pass such information to the OS. Convenience to the OS OS images should be easy to generate. Ideally, an OS image should simply be an ordinary 32-bit executable file in whatever file format the OS normally uses. It should be possible to 'nm' or disassemble OS images just like normal executables. Specialized tools should not be needed to create OS images in a "special" file format. If this means shifting some work from the OS to the boot loader, that is probably appropriate, because all the memory consumed by the boot loader will typically be made available again after the boot process is created, whereas every bit of code in the OS image typically has to remain in memory forever. The OS should not have to worry about getting into 32-bit mode initially, because mode switching code generally needs to be in the boot loader anyway in order to load OS data above the 1MB boundary, and forcing the OS to do this makes creation of OS images much more difficult. Unfortunately, there is a horrendous variety of executable file formats even among free Unix-like PC-based OS's - generally a different format for each OS. Most of the relevant free OS's use some variant of a.out format, but some are moving to ELF. It is highly desirable for boot loaders not to have to be able to interpret all the different types of executable file formats in existence in order to load the OS image - otherwise the boot loader effectively becomes OS-specific again. This standard adopts a compromise solution to this problem. MultiBoot compliant boot images always either (a) are in ELF format, or (b) contain a "magic MultiBoot header", described below, which allows the boot loader to load the image without having to understand numerous a.out variants or other executable formats. This magic header does not need to be at the very beginning of the executable file, so kernel images can still conform to the local a.out format variant in addition to being MultiBoot compliant. Boot modules Many modern operating system kernels, such as those of VSTa and Mach, do not by themselves contain enough mechanism to get the system fully operational: they require the presence of additional software modules at boot time in order to access devices, mount file systems, etc. While these additional modules could be embedded in the main OS image along with the kernel itself, and the resulting image be split apart manually by the OS when it receives control, it is often more flexible, more space-efficient, and more convenient to the OS and user if the boot loader can load these additional modules independently in the first place. Thus, this standard should provide a standard method for a boot loader to indicate to the OS what auxiliary boot modules were loaded, and where they can be found. Boot loaders don't have to support multiple boot modules, but they are strongly encouraged to, because some OS's will be unable to boot without them. _________________________________________________________________ Details There are three main aspects of the boot-loader/OS image interface this standard must specify: * The format of the OS image as seen by the boot loader. * The state of the machine when the boot loader starts the OS. * The format of the information passed by the boot loader to the OS. OS Image Format An OS image is generally just an ordinary 32-bit executable file in the standard format for that particular OS, except that it may be linked at a non-default load address to avoid loading on top of the PC's I/O region or other reserved areas, and of course it can't use shared libraries or other fancy features. Initially, only images in a.out format are supported; ELF support will probably later be specified in the standard. Unfortunately, the exact meaning of the text, data, bss, and entry fields of a.out headers tends to vary widely between different executable flavors, and it is sometimes very difficult to distinguish one flavor from another (e.g. Linux ZMAGIC executables and Mach ZMAGIC executables). Furthermore, there is no simple, reliable way of determining at what address in memory the text segment is supposed to start. Therefore, this standard requires that an additional header, known as a 'multiboot_header', appear somewhere near the beginning of the executable file. In general it should come "as early as possible", and is typically embedded in the beginning of the text segment after the "real" executable header. It _must_ be contained completely within the first 8192 bytes of the executable file, and must be longword (32-bit) aligned. These rules allow the boot loader to find and synchronize with the text segment in the a.out file without knowing beforehand the details of the a.out variant. The layout of the header is as follows: +-------------------+ 0 | magic: 0x1BADB002 | (required) 4 | flags | (required) 8 | checksum | (required) +-------------------+ 8 | header_addr | (present if flags[16] is set) 12 | load_addr | (present if flags[16] is set) 16 | load_end_addr | (present if flags[16] is set) 20 | bss_end_addr | (present if flags[16] is set) 24 | entry_addr | (present if flags[16] is set) +-------------------+ All fields are in little-endian byte order, of course. The first field is the magic number identifying the header, which must be the hex value 0x1BADB002. The flags field specifies features that the OS image requests or requires of the boot loader. Bits 0-15 indicate requirements; if the boot loader sees any of these bits set but doesn't understand the flag or can't fulfill the requirements it indicates for some reason, it must notify the user and fail to load the OS image. Bits 16-31 indicate optional features; if any bits in this range are set but the boot loader doesn't understand them, it can simply ignore them and proceed as usual. Naturally, all as-yet-undefined bits in the flags word must be set to zero in OS images. This way, the flags fields serves for version control as well as simple feature selection. If bit 0 in the flags word is set, then all boot modules loaded along with the OS must be aligned on page (4KB) boundaries. Some OS's expect to be able to map the pages containing boot modules directly into a paged address space during startup, and thus need the boot modules to be page-aligned. If bit 1 in the flags word is set, then information on available memory via at least the 'mem_*' fields of the multiboot_info structure defined below must be included. If the bootloader is capable of passing a memory map (the 'mmap_*' fields) and one exists, then it must be included as well. If bit 16 in the flags word is set, then the fields at offsets 8-24 in the multiboot_header are valid, and the boot loader should use them instead of the fields in the actual executable header to calculate where to load the OS image. This information does not need to be provided if the kernel image is in ELF format, but it should be provided if the images is in a.out format or in some other format. Compliant boot loaders must be able to load images that either are in ELF format or contain the load address information embedded in the multiboot_header; they may also directly support other executable formats, such as particular a.out variants, but are not required to. All of the address fields enabled by flag bit 16 are physical addresses. The meaning of each is as follows: * header_addr -- Contains the address corresponding to the beginning of the multiboot_header - the physical memory location at which the magic value is supposed to be loaded. This field serves to "synchronize" the mapping between OS image offsets and physical memory addresses. * load_addr -- Contains the physical address of the beginning of the text segment. The offset in the OS image file at which to start loading is defined by the offset at which the header was found, minus (header_addr - load_addr). load_addr must be less than or equal to header_addr. * load_end_addr -- Contains the physical address of the end of the data segment. (load_end_addr - load_addr) specifies how much data to load. This implies that the text and data segments must be consecutive in the OS image; this is true for existing a.out executable formats. * bss_end_addr -- Contains the physical address of the end of the bss segment. The boot loader initializes this area to zero, and reserves the memory it occupies to avoid placing boot modules and other data relevant to the OS in that area. * entry -- The physical address to which the boot loader should jump in order to start running the OS. The checksum is a 32-bit unsigned value which, when added to the other required fields, must have a 32-bit unsigned sum of zero. Machine State When the boot loader invokes the 32-bit operating system, the machine must have the following state: * CS must be a 32-bit read/execute code segment with an offset of 0 and a limit of 0xffffffff. * DS, ES, FS, GS, and SS must be a 32-bit read/write data segment with an offset of 0 and a limit of 0xffffffff. * The address 20 line must be usable for standard linear 32-bit addressing of memory (in standard PC hardware, it is wired to 0 at bootup, forcing addresses in the 1-2 MB range to be mapped to the 0-1 MB range, 3-4 is mapped to 2-3, etc.). * Paging must be turned off. * The processor interrupt flag must be turned off. * EAX must contain the magic value 0x2BADB002; the presence of this value indicates to the OS that it was loaded by a MultiBoot-compliant boot loader (e.g. as opposed to another type of boot loader that the OS can also be loaded from). * EBX must contain the 32-bit physical address of the multiboot_info structure provided by the boot loader (see below). All other processor registers and flag bits are undefined. This includes, in particular: * ESP: the 32-bit OS must create its own stack as soon as it needs one. * GDTR: Even though the segment registers are set up as described above, the GDTR may be invalid, so the OS must not load any segment registers (even just reloading the same values!) until it sets up its own GDT. * IDTR: The OS must leave interrupts disabled until it sets up its own IDT. However, other machine state should be left by the boot loader in "normal working order", i.e. as initialized by the BIOS (or DOS, if that's what the boot loader runs from). In other words, the OS should be able to make BIOS calls and such after being loaded, as long as it does not overwrite the BIOS data structures before doing so. Also, the boot loader must leave the PIC programmed with the normal BIOS/DOS values, even if it changed them during the switch to 32-bit mode. Boot Information Format Upon entry to the OS, the EBX register contains the physical address of a 'multiboot_info' data structure, through which the boot loader communicates vital information to the OS. The OS can use or ignore any parts of the structure as it chooses; all information passed by the boot loader is advisory only. The multiboot_info structure and its related substructures may be placed anywhere in memory by the boot loader (with the exception of the memory reserved for the kernel and boot modules, of course). It is the OS's responsibility to avoid overwriting this memory until it is done using it. The format of the multiboot_info structure (as defined so far) follows: +-------------------+ 0 | flags | (required) +-------------------+ 4 | mem_lower | (present if flags[0] is set) 8 | mem_upper | (present if flags[0] is set) +-------------------+ 12 | boot_device | (present if flags[1] is set) +-------------------+ 16 | cmdline | (present if flags[2] is set) +-------------------+ 20 | mods_count | (present if flags[3] is set) 24 | mods_addr | (present if flags[3] is set) +-------------------+ 28 - 40 | syms | (present if flags[4] or flags[5] is set) +-------------------+ 44 | mmap_length | (present if flags[6] is set) 48 | mmap_addr | (present if flags[6] is set) +-------------------+ The first longword indicates the presence and validity of other fields in the multiboot_info structure. All as-yet-undefined bits must be set to zero by the boot loader. Any set bits that the OS does not understand should be ignored. Thus, the flags field also functions as a version indicator, allowing the multiboot_info structure to be expanded in the future without breaking anything. If bit 0 in the multiboot_info.flags word is set, then the 'mem_*' fields are valid. 'mem_lower' and 'mem_upper' indicate the amount of lower and upper memory, respectively, in kilobytes. Lower memory starts at address 0, and upper memory starts at address 1 megabyte. The maximum possible value for lower memory is 640 kilobytes. The value returned for upper memory is maximally the address of the first upper memory hole minus 1 megabyte. It is not guaranteed to be this value. If bit 1 in the multiboot_info.flags word is set, then the 'boot_device' field is valid, and indicates which BIOS disk device the boot loader loaded the OS from. If the OS was not loaded from a BIOS disk, then this field must not be present (bit 3 must be clear). The OS may use this field as a hint for determining its own "root" device, but is not required to. The boot_device field is layed out in four one-byte subfields as follows: +-------+-------+-------+-------+ | drive | part1 | part2 | part3 | +-------+-------+-------+-------+ The first byte contains the BIOS drive number as understood by the BIOS INT 0x13 low-level disk interface: e.g. 0x00 for the first floppy disk or 0x80 for the first hard disk. The three remaining bytes specify the boot partition. 'part1' specifies the "top-level" partition number, 'part2' specifies a "sub-partition" in the top-level partition, etc. Partition numbers always start from zero. Unused partition bytes must be set to 0xFF. For example, if the disk is partitioned using a simple one-level DOS partitioning scheme, then 'part1' contains the DOS partition number, and 'part2' and 'part3' are both zero. As another example, if a disk is partitioned first into DOS partitions, and then one of those DOS partitions is subdivided into several BSD partitions using BSD's "disklabel" strategy, then 'part1' contains the DOS partition number, 'part2' contains the BSD sub-partition within that DOS partition, and 'part3' is 0xFF. DOS extended partitions are indicated as partition numbers starting from 4 and increasing, rather than as nested sub-partitions, even though the underlying disk layout of extended partitions is hierarchical in nature. For example, if the boot loader boots from the second extended partition on a disk partitioned in conventional DOS style, then 'part1' will be 5, and 'part2' and 'part3' will both be 0xFF. If bit 2 of the flags longword is set, the 'cmdline' field is valid, and contains the physical address of the the command line to be passed to the kernel. The command line is a normal C-style null-terminated string. If bit 3 of the flags is set, then the 'mods' fields indicate to the kernel what boot modules were loaded along with the kernel image, and where they can be found. 'mods_count' contains the number of modules loaded; 'mods_addr' contains the physical address of the first module structure. 'mods_count' may be zero, indicating no boot modules were loaded, even if bit 1 of 'flags' is set. Each module structure is formatted as follows: +-------------------+ 0 | mod_start | 4 | mod_end | +-------------------+ 8 | string | +-------------------+ 12 | reserved (0) | +-------------------+ The first two fields contain the start and end addresses of the boot module itself. The 'string' field provides an arbitrary string to be associated with that particular boot module; it is a null-terminated ASCII string, just like the kernel command line. The 'string' field may be 0 if there is no string associated with the module. Typically the string might be a command line (e.g. if the OS treats boot modules as executable programs), or a pathname (e.g. if the OS treats boot modules as files in a file system), but its exact use is specific to the OS. The 'reserved' field must be set to 0 by the boot loader and ignored by the OS. NOTE: Bits 4 & 5 are mutually exclusive. If bit 4 in the multiboot_info.flags word is set, then the following fields in the multiboot_info structure starting at byte 28 are valid: +-------------------+ 28 | tabsize | 32 | strsize | 36 | addr | 40 | reserved (0) | +-------------------+ These indicate where the symbol table from an a.out kernel image can be found. 'addr' is the physical address of the size (4-byte unsigned long) of an array of a.out-format 'nlist' structures, followed immediately by the array itself, then the size (4-byte unsigned long) of a set of null-terminated ASCII strings (plus sizeof(unsigned long) in this case), and finally the set of strings itself. 'tabsize' is equal to it's size parameter (found at the beginning of the symbol section), and 'strsize' is equal to it's size parameter (found at the beginning of the string section) of the following string table to which the symbol table refers. Note that 'tabsize' may be 0, indicating no symbols, even if bit 4 in the flags word is set. If bit 5 in the multiboot_info.flags word is set, then the following fields in the multiboot_info structure starting at byte 28 are valid: +-------------------+ 28 | num | 32 | size | 36 | addr | 40 | shndx | +-------------------+ These indicate where the section header table from an ELF kernel is, the size of each entry, number of entries, and the string table used as the index of names. They correspond to the 'shdr_*' entries ('shdr_num', etc.) in the Executable and Linkable Format (ELF) specification in the program header. All sections are loaded, and the physical address fields of the elf section header then refer to where the sections are in memory (refer to the i386 ELF documentation for details as to how to read the section header(s)). Note that 'shdr_num' may be 0, indicating no symbols, even if bit 5 in the flags word is set. If bit 6 in the multiboot_info.flags word is set, then the 'mmap_*' fields are valid, and indicate the address and length of a buffer containing a memory map of the machine provided by the BIOS. 'mmap_addr' is the address, and 'mmap_length' is the total size of the buffer. The buffer consists of one or more of the following size/structure pairs ('size' is really used for skipping to the next pair): +-------------------+ -4 | size | +-------------------+ 0 | BaseAddrLow | 4 | BaseAddrHigh | 8 | LengthLow | 12 | LengthHigh | 16 | Type | +-------------------+ where 'size' is the size of the associated structure in bytes, which can be greater than the minimum of 20 bytes. 'BaseAddrLow' is the lower 32 bits of the starting address, and 'BaseAddrHigh' is the upper 32 bits, for a total of a 64-bit starting address. 'LengthLow' is the lower 32 bits of the size of the memory region in bytes, and 'LengthHigh' is the upper 32 bits, for a total of a 64-bit length. 'Type' is the variety of address range represented, where a value of 1 indicates available RAM, and all other values currently indicated a reserved area. The map provided is guaranteed to list all standard RAM that should be available for normal use. __________________________________________________________________________ Authors Bryan Ford Flux Research Group Dept. of Computer Science University of Utah Salt Lake City, UT 84112 multiboot@flux.cs.utah.edu baford@cs.utah.edu Erich Stefan Boleyn 924 S.W. 16th Ave, #202 Portland, OR, USA 97205 (503) 226-0741 erich@uruk.org We would also like to thank the many other people have provided comments, ideas, information, and other forms of support for our work. __________________________________________________________________________ Example OS code can be found in the OSKit in the "kern/x86" directory and in the oskit/x86/multiboot.h file. __________________________________________________________________________ Example Bootloader Code (from Erich Boleyn) - The GRUB bootloader project (http://www.uruk.org/grub) will be fully Multiboot-compliant, supporting all required and optional features present in this standard. A final release has not been made, but the GRUB beta release (which is quite stable) is available from ftp://ftp.uruk.org/public/grub/. |
The BIOS startup code is written and functional but not yet documented or integrated into the OSKit source tree.
The DOS startup code is written and functional but not yet documented or integrated into the OSKit source tree.
In addition to the libkern functionality described above which is intended to facilitate implementing kernels, the library also provides complete, easy-to-use functionality to facilitate debugging kernels. The OSKit does not itself contain a complete kernel debugger (at least, not yet), but it contains extensive support for remote debugging using GDB, the GNU debugger. This remote debugging support allows you to run the debugger on one machine, and run the actual OS kernel being debugged on a different machine. The two machines can be of different architectures. A small “debugging stub” is linked into the OS kernel; this piece of code handles debugging-related traps and interrupts and communicates with the remote debugger, acting as a “slave” that simply interprets and obeys the debugger’s commands.
This section describes remote debugging in general, applicable to any mechanism for communicating with the remote kernel (e.g., serial line or ethernet). The next section (15.18) describes kernel debugging support specific to the serial line mechanism (currently the only one implemented).
XXX diagram
One of the main advantages of remote debugging is that you can use a complete, full-featured source-level debugger, since it can run on a stable, well-established operating system such as Unix; a debugger running on the same machine as the kernel being debugged would necessarily have to be much smaller and simpler because of the lack of a stable underlying OS it can rely on. Another advantage is that remote debugging is less invasive: since most of the debugging code is on a different machine, and the remote debugging stub linked into the OS is much smaller than even a simple stand-alone debugger, there is much less that can “go wrong” with the debugging code when Strange Things start to happen due to subtle kernel bugs. The main disadvantage of remote debugging, of course, is that it requires at least two machines with an appropriate connection between them.
The GNU debugger, GDB, supports a variety of remote debugging protocols. The most common and well-supported is the serial-line protocol, which operates over an arbitrary serial line (typically null-modem) connection operating at any speed supported by the two machines involved. The serial-line debugging protocol supports a multitude of features such as multiple threads, signals, and data compression. GDB also supports an Ethernet-based remote debugging protocol and a variety of existing vendor- and OS-specific protocols.
Ths OS kit’s GDB support has been tested with GDB versions 4.15 and 4.16; probably a version >= 4.15 is required.
The GDB remote debugging support provided by the OSKit is broken into two components: the protocol-independent component and the protocol-specific component. The protocol-independent component encapsulates all the processor architecture-specific code to handle processor traps and convert them into the “signals” understood by GDB, to convert saved state frames to and from GDB’s standard representation for a given architecture, and to perform “safe” memory reads and writes on behalf of the remote user so that faulting accesses will terminate cleanly without causing recursive traps.
The protocol-specific component of the toolkit’s remote GDB support encapsulates the code necessary to talk to the remote debugger using the appropriate protocol. Although this code is specific to a particular protocol, it is architecture-neutral. The OSKit currently supports only the standard serial-line protocol, although support for other protocols is planned (particularly the remote Ethernet debugging protocol) and should be easy to add.
If you are using the base environment’s default trap handler, then activating the kernel debugger is extremely easy: it is simply necessary to call an appropriate initialization routine near the beginning of your kernel code; all subsequent traps that occur will be dispatched to the remote debugger. For example, on a PC, to activate serial-line debugging over COM1 using default serial parameters, simply make the call ‘gdb_pc_com_init(1, 0)’. Some example kernels are provided with the OSKit that demonstrate how to initialize and use the remote debugging facilities; see Section 1.6.1 for more information.
If you want a trap to occur immediately after initialization of the debugging mechanism, to transfer control to the remote debugger from the start and give you the opportunity to set breakpoints and such, simply invoke the gdb_breakpoint macro immediately after the call to initialize the remote debugger (see Section 15.17.11).
If your kernel uses its own trap entrypoint mechanisms or its own serial line communication code (e.g., “real” interrupt-driven serial device drivers instead of the simple polling code used by default by the toolkit), then you will have to write a small amount of “glue” code to interface the generic remote debugging support code in the toolkit with your specific OS mechanisms. However, this glue code should generally be extremely small and simple, and you can use the default implementations in the OSKit as templates to work from or use as examples.
Although the OSKit’s remote debugging support code is most directly and obviously useful for debugging the OS kernel itself, most of the code does not assume that the kernel is the entity being debugged. In fact, it is quite straightforward to adapt the mechanism to allow remote debugging of other entities, such as user-level programs running on top of the kernel. To make the debugging stub operate on a different address space than the kernel’s, it is simply necessary to override the gdb_copyin and gdb_copyout routines with alternate versions that transfer data to or from the appropriate address space. Operating systems that support a notion of user-level address spaces generally have some kind of “copyin” and “copyout” routines anyway to provide safe access to user address spaces; the replacement gdb_copyin and gdb_copyout routines can call those standard user space access routines. In addition, the trap handling mechanism may need to be set up so that only traps occurring in a particular context (e.g., within a particular user process or thread) will be dispatched to the remote debugger.
This structure represents the processor register state for the target architecture in the form in which GDB expects it. GDB uses a standard internal data structure for each processor architecture to represent the register state of a program being debugged, and most of GDB’s architecture-neutral remote debugging protocols use this standard structure. The gdb_state structure defined by the OSKit is defined to match GDB’s corresponding register state structure for each supported architecture.
This function is intended to be installed as the kernel trap handler for all traps by setting each of the entries in the base_trap_handlers array to point to it (see Section 15.8.4), when remote GDB debugging is desired. (Alternatively, the client OS can use its own trap handlers which chain to gdb_trap when appropriate.) This function converts the contents of the trap_state structure saved by the base trap entrypoint code into the gdb_state structure used by GDB. It also converts the architecture-specific processor trap vector number into a suitable machine-independent signal number which can be interpreted by the remote debugger.
After converting the register state and trap vector appropriately, this function calls the appropriate protocol-specific GDB stub through the gdb_signal function pointer variable (see Section 15.17.9). Finally, it converts the final register state, possibly modified by the remote debugger, back into the original trap_state format and returns an appropriate success or failure code as described below.
On architectures that don’t provide a way for the kernel to “validate” memory accesses before performing them, such as the x86, this function also provides support for “recovering” from faulting memory accesses during calls to gdb_copyin or gdb_copyout (see Sections 15.17.6 and 15.17.7). This is typically implemented using a “recovery pointer” which is set before a “safe” memory access and cleared afterwards; gdb_trap checks this recovery pointer, and if set, modifies the trap state appropriately and returns from the trap without invoking the protocol-specific GDB stub.
If the client OS uses its own trap entrypoint code which saves register state in a different format when handling traps, then the client OS will also need to override the gdb_trap function with a version that understands its custom saved state format.
The gdb_trap function returns success (zero) when the remote debugger instructs the local stub to resume execution at the place it was stopped and “consume” the trap that caused the debugger to be invoked; this is the normal case.
This function returns failure (nonzero) if the remote debugger passed the same or a different signal back to the local GDB stub, instructing the local kernel to handle the trap (signal) itself. If the default trap entrypoint mechanism provided by the base environment in use, then this simply causes the kernel to panic with a register dump, since the default trap code does not know how to “handle” signals by itself. However, if the client OS uses its own trap entrypoint mechanism or interposes its own trap handler over gdb_trap, then it may wish to interpret a nonzero return code from gdb_trap as a request for the trap to be handled using the “normal” mechanism, (e.g., dispatched to the application being debugged).
The protocol-specific local GDB stub calls this function in order to read data in the address space of the program being debugged. The default implementation of this function provided by libkern assumes that the kernel itself is the program being debugged; thus, it acts basically like an ordinary memcpy. However, the client can override this function with a version that accesses a different address space, such as a user process’s address space, in order to support remote debugging of entities other than the kernel.
If a fault occurs while trying to read the specified data, this function catches the fault cleanly and returns an error code rather than allowing a recursive trap to be dispatched to the debugger. This way, if the user of the debugger accidentally attempts to follow an invalid pointer or display unmapped or nonexistent memory, it will merely cause the debugger to report an error rather than making everything go haywire.
Returns zero if the transfer completed successfully, or nonzero if some or all of the source region is not accessible.
#include <oskit/gdb.h>
int gdb_copyout(const void *src_buf, oskit_addr_t dest_va, oskit_size_t size);
The protocol-specific local GDB stub calls this function in order to write data into the address space of the program being debugged. The default implementation of this function provided by libkern assumes that the kernel itself is the program being debugged; thus, it acts basically like an ordinary memcpy. However, the client can override this function with a version that accesses a different address space, such as a user process’s address space, in order to support remote debugging of entities other than the kernel.
If a fault occurs while trying to write the specified data, this function catches the fault cleanly and returns an error code rather than allowing a recursive trap to be dispatched to the debugger. This way, if the user of the debugger accidentally attempts to write to unmapped or nonexistent memory, it will merely cause the debugger to report an error rather than making everything go haywire.
Returns zero if the transfer completed successfully, or nonzero if some or all of the destination region is not writable.
#include <oskit/gdb.h>
extern void (*gdb_signal)(int *inout_signo, struct gdb_state *inout_gdb_state );
Before gdb_trap is called for the first time, this function pointer must be initialized to point to an appropriate GDB debugging stub, such as gdb_serial_signal (see Section 15.18.2). This function is called to notify the remote debugger that a relevant processor trap or interrupt has occurred, and to wait for further instructions from the remote debugger. When the function returns, execution will be resumed as described in Section 15.17.5.
This architecture-specific function merely modifies the specified processor state structure to enable or disable single-stepping according to the trace_enable parameter. On architectures that have some kind of trace flag, this function simply sets or clears that flag as appropriate. On other architectures, this behavior is achieved through other means. This function is called by machine-independent remote debugging stubs such as gdb_serial_signal before resuming execution of the subject program, according to whether the remote debugger requested that the program “continue” or “step” one instruction.
This is simply an architecture-specific macro which causes an instruction causing a breakpoint trap to be emitted at the corresponding location in the current function. This macro can be used to set “manual breakpoints” in program code, as well as to give control to the debugger at the very beginning of program execution as described in Section 15.17.2.
The GDB serial-line debugging protocol is probably the most powerful and commonly-used remote debugging protocol supported by GDB; this is the only protocol for which the OSKit currently has direct support. The GDB serial-line debugging stub supplied with the OSKit is fully architecture-independent, and supports most of the major features of the GDB serial-line protocol.
For technical information on the remote serial-line GDB debugging protocol, or information on how to run and use the remote debugger itself, consult the appropriate sections of the GDB manual. This section merely describes how remote serial-line debugging is supported by the OSKit.
Note that source code for several example serial-line debugging stubs are supplied in the GDB distribution (gdb/*-stub.c); in fact, this code was used as a template and example for the OSKit’s serial-line debugging stub. However, these stubs are highly machine-dependent and make many more assumptions about how they are used. For example, they assume that they have exclusive control of the processor’s trap vector table, and are therefore only generally usable in an embedded environment where traps are never supposed to occur during normal operation and therefore all traps can be fielded directly by the debugger. In contrast, the serial-line debugging stub provided in the OSKit is much more generic and cleanly decomposed, and therefore should be usable in a much wider range of environments.
If the machine on which the kernel is being debugged is truly “remote,” e.g., in a different room or a completely different building, and you don’t have easy access to the machine’s “real” console, it is possible to make the kernel use the remote debugger as its “console” for printing status messages and such. To do this, simply write your kernel’s “console” output functions (e.g., putchar and puts, if you’re using the OSKit’s minimal C library for console output routines such as printf) so that they call gdb_serial_putchar and gdb_serial_puts, described in Sections 15.18.5 and 15.18.6, respectively. The OSKit base console environment (section 15.13) does this as necessary.
This mechanism only works for console output: console input cannot be obtained from the remote debugger’s console because the GDB serial-line debugging protocol does not currently support it. However, console input can be obtained “outside the protocol” as described in section 15.18.4.
#include <oskit/gdb_serial.h>
void gdb_serial_signal([in/out] int *signo, [in/out] struct gdb_state *state);
This is the main trap/signal handler routine in the serial-line debugging stub; it should be called whenever a relevant processor trap occurs. This function notifies the remote debugger about the event that caused the processor to stop, and then waits for instructions from the remote debugger. The remote debugger may then cause the stub to perform various actions, such as examine memory, modify the register state, or kill the program being debugged. Eventually, the remote debugger will probably instruct the stub to resume execution, in which case this function returns with the signal number and trap state modified appropriately.
If this function receives a “kill” (‘k’) command from the remote debugger, then it breaks the remote debugging connection and then calls panic to reboot the machine. XXX may not be appropriate when debugging a user task; should call an intermediate function.
This function sends a message to the remote debugger indicating that the program being debugged is terminating. This message causes the debugger to display an appropriate message on the debugger’s console along with the exit_code, and causes it to break the connection (i.e., stop listening for further messages on the serial port). If no remote debugging connection is currently active, this function does nothing.
The client OS should typically call this function just before it reboots for any reason, so that the debugger does not hang indefinitely waiting for a response from a kernel that is no longer running. Alternatively, if the remote debugging facility is being used to debug a user-mode process running under the kernel, then this function should be called when that process terminates.
Note that despite its name, this function does return. It does not by itself cause the machine to “exit” or reboot or hang or whatever; it merely notifies the debugger that the subject program is about to terminate.
Unfortunately, the GDB protocol doesn’t support console input. However, we can simulate it with a rather horrible kludge: when the kernel first does a read from the console we take a breakpoint, allowing the user to fill an input buffer with a command such as:
|
The supplied characters will be returned from successive calls to gdb_serial_getchar, until inbuf is emptied, at which point we hit a breakpoint again.
Returns the next available character in the inbuf array.
If a remote debugging connection is currently active, this function sends the specified character to the remote debugger in a special “output” (‘O’) message which causes that character to be sent to the debugger’s standard output. This allows the serial line used for remote debugging to double as a remote serial console, as described in Section 15.18.1.
Note that using gdb_serial_putchar by itself to print messages can be very inefficient, because a separate message is used for each character, and each of these messages must be acknowledged by the remote debugger before the next character can be sent. When possible, it is much faster to print strings of text using gdb_serial_puts (see Section 15.18.6). If you are using the implementation of printf in the OSKit’s minimal C library (see Section 14.6), you can make this happen automatically by overriding puts with a version that calls gdb_serial_puts directly instead of calling putchar successively on each character.
If this function is called while no remote debugging connection is active, but the gdb_serial_send and gdb_serial_receive pointers are initialized to point to serial-line communication functions, then this function simply sends the specified character out the serial port using gdb_serial_send. This way, if the kernel attempts to print any messages before a connection has been established or after the connection has been dropped (e.g., by calling gdb_serial_exit), they won’t confuse the debugger or cause the kernel to hang as they otherwise would, and they may be seen by the remote user if the serial port is being monitored at the time.
If the gdb_serial_send and gdb_serial_receive pointers are uninitialized (still NULL) when this function is called, it does nothing.
If a remote debugging connection is currently active, this function sends the specified string, followed by a newline character, to the remote debugger in a special “output” (‘O’) message which causes the line to be sent to the debugger’s standard output. This allows the serial line used for remote debugging to double as a remote serial console, as described in Section 15.18.1.
If this function is called while no remote debugging connection is active, but the gdb_serial_send and gdb_serial_receive pointers are initialized to point to serial-line communication functions, then this function simply sends the specified line out the serial port using gdb_serial_send. This way, if the kernel attempts to print any messages before a connection has been established or after the connection has been dropped (e.g., by calling gdb_serial_exit), they won’t confuse the debugger or cause the kernel to hang as they otherwise would, and they may be seen by the remote user if the serial port is being monitored at the time.
If the gdb_serial_send and gdb_serial_receive pointers are uninitialized (still NULL) when this function is called, it does nothing.
Before the remote serial-line debugging stub can be used, this global variable must be initialized to point to a function to call to read a character from the serial port. The function should not return until a character has been received; the GDB stub has no notion of timeouts or interruptions.
Calling functions in the GDB serial-line debugging stub before this variable is initialized (i.e., while it is still null) is guaranteed to be harmless.
Returns the character received.
Before the remote serial-line debugging stub can be used, this global variable must be initialized to point to a function to call to send out a character on the serial port.
Calling functions in the GDB serial-line debugging stub before this variable is initialized (i.e., while it is still null) is guaranteed to be harmless.
Returns the character received.
This is a simple “wrapper” function which ties together all of the OSKit’s remote debugging facilities to automatically create a complete remote debugging environment for a specific, typical configuration: namely, remote serial-line debugging on a PC through a COM port. This function can be used as-is if this configuration happens to suit your purposes, or it can be used as an example for setting up the debugging facilities for other configurations.
Specifically, this function does the following:
Kernel annotations are “markers” that can be placed in code or static data. Annotations are static and are collected into a special section of the object/executable file. How this section is created is object-file format specific and is normally handled by the default startup files (e.g, crt0.o).
Annotations are organized in tables which is sorted by a key value (typically the address being marked) at boot time via anno_init.
The basic annotation structures look like:
struct | anno_entry *start; | /* first entry | */ | |
struct | anno_entry *end; | /* last entry | */ | |
oskit_addr_t | val1; | /* lookup value | */ | |
oskit_addr_t | val2; | /* context dependent value | */ | |
oskit_addr_t | val3; | /* context dependent value | */ | |
struct | anno_table *table; | /* associated anno_table | */ | |
Annotation tables contain a pointer to the first and last entries they contain. All entries in a table are contiguous and sorted by the key value.
Annotation entries specify the table they belong to, the key value used for lookups, and two uninterpreted values.
Though annotations can be structured arbitrarily, the OSKit supports two common kernel annotation uses: “trap tables” and “interrupt tables.” These are described in the following anno_trap and anno_intr sections.
Currently, annotation support only works with the ELF binary file format (i.e., it does not work with a.out).
Contains architecture-dependent, assembly-code callable macros for placing annotations in kernel text and initialized data. No C-callable versions are included since most C compilers may reorder code making exact placement of annotations impossible.
ANNO_ENTRY creates a generic annotation entry associated with the indicated table and filled with the specified values.
ANNO_TEXT records an annotation in the given table for the current point in the text (code) segment. The current value of the program counter is placed in val1. The specified values for val2 and val3 are recorded.
Dumps, using printf, all registered annotation tables and entries. Should not be called before anno_init.
#include <oskit/anno.h>
struct anno_entry *anno_find_exact(struct anno_table *tab, oskit_addr_t val1);
Find an entry in the specified annotation table whose val1 field exactly matches the specified value.
Returns a pointer to the anno_entry matching the value, or zero if no entry matches.
#include <oskit/anno.h>
struct anno_entry *anno_find_lower(struct anno_table *tab, oskit_addr_t val1);
Find an entry in the specified annotation table with the largest val1 field less than or equal to the specified value. If no entry has a lower or equal value, returns zero.
Returns a pointer to the appropriate anno_entry, or zero if no lower entry is found.
This routine should be called once at program startup; it sorts all of the annotation entries appropriately and initializes the annotation tables they reside in.
The interrupt annotation table anno_intr contains entries which associate a handler function with ranges of addresses in the kernel. Each entry defines an action to be performed if an asynchronous exception occurs between the address associated with this entry and that associated with the following entry. When an interrupt occurs, the default OSKit interrupt handler (in base_irq_inittab.S) uses anno_find_lower to locate the correct annotation entry based on the instruction pointer at the time of the interrupt. This handler function is invoked prior to calling the standard interrupt handling function.
ANNO_INTR is a macro in oskit/x86/anno.h. It records an annotation in anno_intr for the current point in the code segment. The given routine and val3 arguments are stored in the entry’s val2 and val3 fields respectively. To disable interrupt redirection for a piece of code, place an ANNO_INTR(0,0) call before it.
The annotation interrupt handler function is called in the context of the interrupted thread with a pointer to the associated annotation entry and a pointer to the architecture-specific trap state for the thread. On return from the annotation handler, the actual interrupt handler is called.
An example of interrupt annotation usage in a kernel is an efficient alternative to using “spls” (i.e., raising processor priority) to protect critical sections from interrupts. By marking the critical section with an annotation entry, the kernel detects when an interrupt occurs within it and invokes an associated roll-back or roll-forward routine to back out of or complete the critical section before invoking the interrupt handler.
The trap annotation table anno_trap contains entries which associate a handler function with specific addresses in the kernel. These addresses correspond to points where synchronous exceptions are expected to occur. When such an exception occurs, the default OSKit trap handler (in base_trap_inittab.S) uses anno_find_exact to locate an annotation entry based on the instruction pointer at the time of the fault. This handler function is invoked instead of the standard kernel trap handler in those instances.
ANNO_TRAP is a macro in oskit/x86/anno.h. It records an annotation in anno_trap for the current point in the code segment. The given routine and val3 arguments are stored in the entry’s val2 and val3 fields respectively.
The annotation trap handler function is called in the context of the faulting thread with a pointer to the matching annotation entry and a pointer to the architecture-specific trap state for the thread. If the handler returns zero, the OSKit trap handler will restore state from the trap state structure and resume execution. If it returns non-zero, it is considered a failed fault and the kernel will panic.
An example of trap annotation usage is a kernel copyin routine which must read data in the user’s address space. Associating an annotation entry with the instruction which moves data from the user’s address space enables the kernel to catch any access violation caused and reflect it to the user.
The Boot Module (BMOD) filesystem is derived from the trivial memory-based filesystem, memfs (Chapter 38), exporting the OSKit filesystem interface. The initial contents of the BMOD filesystem are loaded from the Multiboot boot image. This allows an OSKit kernel to load a filesystem at boot time, possibly not even requiring an actual disk-based filesystem.
XXX multiboot strings are parsed to create hierarchical filesystem. XXX new BMODs may be added, multiboot BMODs may be modified or destroyed. XXX no guarantee on alignment of multiboot created BMOD files.
Initialize the BMOD filesystem. Create the supporting memfs, and load it with the contents of the multiboot BMODs.
Returns a handle to the root of the BMOD filesystem
The signal handling facilities allow the client OS to provide compatibility with POSIX style signal handling semantics. The support provided is extremely basic and is intended to be used in conjunction with the OSKit’s FreeBSD C library (see Section 21) by arranging for unexpected hardware traps to be converted into an appropriate signal and delivered through the C library. By default, the FreeBSD C library will not arrange for signals to be delivered unless the oskit_init_libc initialization routine is called (see Section 14.7.1). The exception are kernels linked with the POSIX threads module, which will always call the initialization routine.
#include <oskit/c/signal.h>
void oskit_sendsig_init(int (*deliver_function)(int signo, int code, struct trap_state *ts));
Initialize the kernel signal delivery mechanism, providing an upper level signal delivery routine. This delivery function will usually be an entrypoint in the C library that provides the appropriate signal semantics to the application. This entrypoint is responsible for converting the machine dependent trap state information into a suitable signal context structure, as defined by the API of the library in use. Since a pointer to the trap state structure is passed along, the callee is free to modify the machine state in way it wishes.
#include <oskit/c/signal.h>
#include <oskit/x86/base_trap.h>
Propagate a machine trap to the signal handling entrypoint provided to oskit_sendsig_init() above. This routine is intended to be called by modules that have replaced a particular trap handler, and wish to propagate the trap to the application in the form of a signal. If the C library has not called oskit_sendsig_init(), the routine returns without doing anything.
Returns non-zero if a C library handler has not been installed, and thus the signal could not be propagated.
#include <oskit/c/signal.h>
#include <oskit/x86/base_trap.h>
Convert the machine dependent trap state structure state (see Section 15.8.1) into a signal code, and pass that, along with the trap state, to the C library via oskit_sendsig above.
This routine is provided as a default trap handler that can be plugged into the base_trap_handlers array (see Section 15.8.4). Unexpected hardware traps are thus converted into signals and delivered to the application through the C library.