5.2. Low-Level Processor InitializationAs shown in Figure 52, BootX launches the kernel by calling the _start symbol in the kernel. In a multiprocessor system, the kernel begins execution on one processor that was chosen by Open Firmware. For the purposes of kernel startup, we consider this the master processor and the rest, if any, as slave processors. Figure 52. Low-level processor initialization
We will use the terms CPU and processor interchangeably unless the terms have specific meanings in some context. In Mach parlance, a processor is typically a hardware-independent entity, whereas a CPU represents the underlying hardware entity. 5.2.1. Per-Processor Data_start() first initializes a pointer to the current per-processor data area. The kernel maintains a table of such per-processor data structures. The tablePerProcTableis an array of per_proc_entry structures. A per_proc_entry structure consists of a per_proc_info structure, which holds data for one processor. The per_proc_info structure for the master processor is specially labeled as BootProcInfo. These structures reside in aligned memory. Note that a thread's machine-specific context includes a pointer to the current per_proc_info structure. Figure 53 shows an excerpt from the declaration of the per_proc_info structure. Figure 53. The kernel's per-processor data table
The pf member of the per_proc_info structure is a structure of type procFeatures. It holds per-processor features such as the reported processor type, which processor facilities are available, various cache sizes, supported power-saving modes, and the maximum physical address supported. // osfmk/ppc/exception.h struct procFeatures { unsigned int Available; /* 0x000 */ #define pfFloat 0x80000000 #define pfFloatb 0 #define pfAltivec 0x40000000 #define pfAltivecb 1 ... #define pfValid 0x00000001 #define pfValidb 31 unsigned short rptdProc; /* 0x004 */ unsigned short lineSize; /* 0x006 */ unsigned int l1iSize; /* 0x008 */ unsigned int l1dSize; /* 0x00C */ ... unsigned int pfPowerTune0; /* 0x080 */ unsigned int pfPowerTune1; /* 0x084 */ unsigned int rsrvd88[6]; /* 0x088 */ }; ... typedef struct procFeatures procFeatures; 5.2.2. Reset TypesSeveral types of processor initializations can be performed by Mac OS X. The kernel distinguishes between these by setting or clearing certain bits of the Condition Register (CR). For example, if it is the first processor coming up in a given context, the CR bit specified by the bootCPU variable is set. If it is the first time that particular processor is being initialized, the CR bit specified by the firstInit variable is set. The logical AND of bootCPU and firstInit is called firstBoot. It will be nonzero if it is the first processor starting up during kernel initialization (as opposed to a processor waking up from sleep, say). If the processor indeed is in a first-ever initialization, _start() performs one-time general low-level initialization before control flows to the allstart label in osfmk/ppc/start.s. As Figure 52 shows, other code paths also lead to this point in the code, depending on the type of reset the processor is going through. Unlike in the case when BootX directly calls _start(), other reset operations are handled by a designated reset handler. Recall from Table 51 that 0x0100 is the vector offset for the system reset exception, which could be a result of a hard or soft processor reset. A structure variable called ResetHandler, which is of type resethandler_t, resides in memory at offset 0xF0just before the 0x0100 exception handler. // osfmk/ppc/exception.h typedef struct resethandler { unsigned int type; vm_offset_t call_paddr; vm_offset_t arg__paddr; } resethandler_t; ... extern resethandler_t ResetHandler; ... #define RESET_HANDLER_NULL 0x0 #define RESET_HANDLER_START 0x1 #define RESET_HANDLER_BUPOR 0x2 #define RESET_HANDLER_IGNORE 0x3 ... // osfmk/ppc/lowmem_vectors.s . = 0xf0 .globl EXT(ResetHandler) EXT(ResetHandler): .long 0x0 .long 0x0 .long 0x0 . = 0x100 .L_handler100: mtsprg 2,r13 /* Save R13 */ mtsprg 3,r11 /* Save R11 */ /* * Examine the ResetHandler structure * and take appropriate action. */ ... When the 0x0100 handler runs to handle a reset exception, it examines the ResetHandler structure to determine the type of reset. Note that the 0x0100 handler will never be run because of a true hard resetsuch a reset will be seen only by Open Firmware. For other types of resets, namely start, BUPOR,[1] and ignore, the kernel will set up the ResetHandler structure appropriately before a reset exception is generated.
A RESET_HANDLER_START is generated when the system is waking up from sleep. In this case, the 0x0100 handler clears the reset type by setting it to RESET_HANDLER_NULL, loads the arg__paddr field of the ResetHandler structure to GPR3, loads the call_paddr field to LR, and finally branches through LR to call the function pointed to by call_paddr. The cpu_start() [osfmk/ppc/cpu.c] and cpu_sleep() [osfmk/ppc/cpu.c] functions use this mechanism by setting ResetHandler fields. Specifically, they set call_paddr to point to _start_cpu() [osfmk/ppc/start.s]. _start_cpu() clears the bootCPU and firstInit fields, sets the current per-processor data pointer, sets the processor's Timebase Register using values from another processor, and branches to the allstart label. In doing so, it bypasses some initial instructions that only the boot processor executes. A RESET_HANDLER_BUPOR is used to bring up a processor when starting directly from a power-on reset (POR). For example, the startCPU() method of the platform-dependent processor driver can generate a soft reset. In the specific case of the 970FX, the startCPU() method implemented in the MacRISC4CPU class (which inherits from the IOCPU class) performs a reset by strobing the processor's reset line. The 0x0100 handler calls resetPOR() [osfmk/ppc/start.s] to handle this type of reset. resetPOR() sets the type field of ResetHandler to RESET_HANDLER_NULL, ensures that the processor is in 32-bit mode, loads GPR3 with a pointer to the boot arguments structure, and branches to _start().
In a multiprocessor system, each CPU's Processor ID Register (PIR) is set to a unique value during a POR. Finally, if the reset type is RESET_HANDLER_IGNORE, the kernel ignores the reset. This is used for software debouncingfor example, when a nonmaskable interrupt (NMI) is used to enter a debugger.
Both ResetHandler and the exception routines reside in physically addressed memory. The kernel uses special machine-dependent routinesimplemented in osfmk/ppc/machine_routines_asm.sto read from and write to such locations. These routines handle the necessary preprocessing and postprocessing while performing I/O to physical addresses. For example, on the 970FX, this preprocessing makes the floating-point and vector-processing units unavailable, delays recognition of external exceptions and decrementer exception conditions, and disables data translation. Postprocessing reverses the changes made by preprocessing. 5.2.3. Processor TypesThe initial kernel code in osfmk/ppc/start.s uses a table of processor typesprocessor_typesthat maps specific processor types to their relevant features. The table contains entries for numerous PowerPC processor models: 750CX (version 2.x), 750 (generic), 750FX (version 1.x and generic), 7400 (versions 2.0 through 2.7 and generic), 7410 (version 1.1 and generic), 7450 (versions 1.xx, 2.0, and 2.1), 7455 (versions 1.xx, 2.0, and 2.1), 7457, 7447A, 970, and 970FX.[2] The entries in this table are ordered: A more specific entry appears before a less restrictive entry. Figure 54 shows an annotated version of the table entry for the 970FX processor.
Figure 54. The entry for the PowerPC 970FX in the processor-type table
The kernel uses the contents of the current CPU's Processor Version Register (PVR) to find a matching entry in processor_table by looping through the table and examining the ptFilter and ptVersion fields of each candidate entry. Once a matching entry is found, a pointer to ptInitRout(), the processor-specific initialization routine, is also saved. At this point, if the master processor is booting for the first time, a variety of processor features and capabilities are set in the CPU capabilities vector, which is an integer variable called _cpu_capabilities [osfmk/ppc/commpage/commpage.c] and whose bits represent CPU capabilities. Since the processors in a multiprocessor system have identical features, this step is bypassed for a secondary processorthe master's feature information is simply copied for the others. 5.2.4. Memory PatchingAlthough a given version of Mac OS X uses the same kernel executable regardless of the computer model, the kernel may alter itself at boot time, based on the underlying hardware. During an initial boot, the master processor consults one or more patch tables built into the kernel and examines their entries to determine whether any of them are applicable. Figure 55 shows the structure of a patch-table entry. Figure 55. Data structure and related definitions of a patch-table entry
The kernel's patch table is defined in osfmk/ppc/ppc_init.c. Figure 56 shows an annotated excerpt from this table. Figure 56. The kernel's patch table
As the kernel examines each entry in the patch table, it checks the entry's type. If the type is PATCH_FEATURE, the kernel compares the patch value with the ptPatch field from the current processor's processor_types table entry. If there is a match, the kernel applies the patch by writing the patch data to the location specified by the patch address. If the entry is of type PATCH_PROCESSOR instead, the kernel compares it with the ptRptdProc field (processor type reported) of its processor_types table entry to check for a potential match. Let us look at specific examples. The first patch entry shown in Figure 56 has a patch value PatchExt32. This value appears as the ptPatch value in the processor_types entries of all 32-bit processors that Mac OS X supports. Therefore, it will match on all 32-bit processors but will not match on 64-bit processors such as the 970 and the 970FX. The address to patch, extPatch32, is in the osfmk/ppc/lowmem_vectors.s file: .L_exception_entry: ... .globl EXT(extPatch32) LEXT(extPatch32) b extEntry64 ... /* 32-bit context saving */ ... /* 64-bit context saving */ extEntry64: ... Since the patch value will not match on 64-bit processors, the code fragment will remain as shown on these processors. On a 32-bit processor, however, the instruction that branches to extEntry64 will be replaced by the patch entry's data, 0x60000000, which is the PowerPC no-op instruction (nop). Patch entry 1 in Figure 56 will match on a 970 or a 970FX, causing the instruction at address extPatchMCK to be turned into a no-op instruction. By default, the instruction at extPatchMCK is a branch that bypasses 64-bit-specific code in the Machine Check Exception (MCE) handler [osfmk/ppc/lowmem_vectors.s]. Patch entry N in Figure 56 replaces an eieio instruction with an lwsync instruction on matching systems.
5.2.5. Processor-Specific InitializationThe ptInitRout field of a processor_types table entry, if valid, points to a function for model-specific initialization of the processor. This field points to init970() [osfmk/ppc/start.s] for both the 970 and the 970FX. init970() clears the "deep nap" bit of HID0[3] during all types of processor initialization: during the boot processor's first initialization, when a slave processor is started, or when a processor wakes up from sleep. In the case of the boot processor initializing for the first time, init970() synthesizes a dummy L2 cache register (L2CR), with its value set to the actual L2 cache size on the 970FX (512KB).
At this point, the kernel sets the valid bit (pfValid) in the Available field of the processor features member (pF) of the per_proc_info structure. Next, the kernel performs initialization based on whether the processor is 32-bit or 64-bit. For example, on a 32-bit processor, the BAT registers are cleared and the contents of the HID0 register are adjusted to clear any sleep-related bits. Thereafter, the code branches to startcommon() [osfmk/ppc/start.s]. On a 64-bit processor, the kernel sets the value of HID0 appropriately, prepares a machine status value in the SRR1 register, and loads the continuation point (the startcommon() routine) in SRR0. It then executes an rfid instruction, which results in the machine state in SRR1 to be restored to the Machine State Register (MSR). Execution then continues in startcommon(). 5.2.6. Other Early InitializationThe kernel checks whether the floating-point facility is available on the processor, and if so, it loads FPR0 with a known floating-point initialization value, which is then copied to the rest of the FPRs. Floating-point is then turned off for the time being. The initialization value is defined in osfmk/ppc/aligned_data.s: .globl EXT(FloatInit) .align 3 EXT(FloatInit): .long 0xC24BC195 /* Initial value */ .long 0x87859393 /* of floating-point registers */ .long 0xE681A2C8 /* and others */ .long 0x8599855A After booting, the value can be seen in an FPR as long as that FPR has not been used. For example, you can debug a simple program using GDB and view the contents of the FPRs. $ cat test.c main() { } $ gcc -g -o test test.c $ gdb ./test ... (gdb) break main Breakpoint 1 at 0x2d34: file test.c, line 1. (gdb) run ... Breakpoint 1, main () at test.c:1 1 main() { } (gdb) info all-registers ... f14 -238423838475.15292 (raw 0xc24bc19587859393) f15 -238423838475.15292 (raw 0xc24bc19587859393) f16 -238423838475.15292 (raw 0xc24bc19587859393) ... Similarly, the kernel checks whether AltiVec is availableif it is, the kernel sets the VRSAVE register to zero, indicating that no VRs have been used yet. It sets the non-Java (NJ) bit and clears the saturate (SAT) bit in the VSCRwe discussed these bits in Chapter 3. A special vector initialization value is loaded into VR0, which is then copied to the other VRs. AltiVec is then turned off for the time being. The initialization value, labeled QNaNbarbarian, is defined in osfmk/ppc/aligned_data.s. It is a sequence of long integers, each with a value of 0x7FFFDEAD. Again, you can potentially see this value in untouched VRs while debugging a program. (gdb) info all-registers ... v0 { uint128 = 0x7fffdead7fffdead7fffdead7fffdead, v4_float = {nan(0x7fdead), nan(0x7fdead), nan(0x7fdead), nan(0x7fdead)}, v4_int32 = {2147475117, 2147475117, 2147475117, 2147475117}, v8_int16 = {32767, -8531, 32767, -8531, 32767, -8531, 32767, -8531}, v16_int8 = "\177??\177??\177??\177??" } (raw 0x7fffdead7fffdead7fffdead7fffdead) ...
The kernel then initializes all caches by calling cacheInit() [osfmk/ppc/machine_routines_asm.s], which first ensures that a variety of features are turned off.[4] For example, it turns off data and instruction address translation, external interrupts, floating-point, and AltiVec. It also initializes various caches via steps such as the following:
On 64-bit processors, cache management is rather different than on 32-bit processors. For example, the L2 cache cannot be disabled on the 970FX. Consequently, the kernel performs a different set of operations to initialize the caches.
Figure 57. Flushing the L2 cache on the PowerPC 970FX
After cache initialization, the kernel configures and starts address translation, unless the underlying processor is the boot processor, in which case it is not yet time for virtual memory. Address translation is configured by calling hw_setup_trans() [osfmk/ppc/hw_vm.s], which first marks the segment registers (SRs) and segment table entries (STEs) invalid by appropriately setting the validSegs field of the per-processor structure (struct per_proc_info). It further sets the structure's ppInvSeg field to 1 to force the complete invalidation of SRs and the segment lookaside buffer (SLB). It also sets the ppCurSeg field to 0 to specify that the current segment is a kernel segment. If running on 32-bit hardware, the kernel invalidates BAT mappings by loading the data and instruction BAT register with zeros. Next, it retrieves the base address (hash_table_base) and size (hash_table_size) of the page hash table and loads it into the SDR1 register. Note that these variables are not initialized yet during the initial boot of the master processor. However, they are defined for a slave processor, which is the scenario in which the kernel calls hw_setup_trans() at this point. The kernel then loads each SR with the predefined invalid segment value of 1. If running on 64-bit hardware, the setup is somewhat simpler, since there are no BAT registers. The kernel sets up the page hash table as in the 32-bit case and invalidates all SLB entries via the slbia instruction. It also ensures that 64-bit mode is turned off. After configuring address translation, the kernel starts address translation by calling hw_start_trans() [osfmk/ppc/hw_vm.s], which sets the data relocate (DR) and instruction relocate (IR) bits of the MSR to enable data and instruction address translation, respectively. At this point, the kernel is almost ready to continue execution in higher-level code; the boot processor will execute the ppc_init() [osfmk/ppc/ppc_init.c] function, whereas a nonboot processor will execute the ppc_init_cpu() [osfmk/ppc/ppc_init.c] function. Both these functions do not return. Before calling either of these functions, the kernel fabricates a C language call frame. It initializes GPR1 with a pointer to an interrupt stack, stores a zero as a null frame backpointer on the stack, and loads GPR3 with a pointer to the boot arguments. Thereafter, it calls ppc_init() or ppc_init_cpu() as appropriate. The kernel guards the calls to these functions by placing a breakpoint trapa tw instructionafter each call. Consequently, in the unlikely case that either call returns, a trap will be generated. |