Chapter 6. The xnu Kernel
We saw in previous chapters that the Mac OS X kernel environment consists of Mach and BSD derivatives, the I/O Kit driver framework, in-kernel libraries, loadable I/O Kit drivers, and other loadable extensions. Although the Darwin xnu package contains only about half the code that potentially runs in the kernel environment, we will consider xnu to be the kernel. In this chapter, we will look at several abstractions and mechanisms in xnu, while deferring subsystem-specific details to later chapters.
6.1. xnu Source
In Chapter 5, we visited various parts of the kernel as we traced the execution of kernel code during system startup. Let us now take a brief tour of the xnu kernel source to better understand how the source is organized. Since the xnu package contains close to 3000 files, it is impractical to visit each file. We will only look at major directories in the xnu source tree to enumerate the components implemented within.
In this section, file and directory names are listed relative to the top-level directory in the xnu source archive. For example, given that the xnu-<version>.tar.gz Darwin package will unpack into a top-level directory called xnu-<version>, we refer to a file xnu-<version>/foo/bar as foo/bar.
At the topmost level, xnu contains the directories listed in Table 61. Besides these, there exist a few other miscellaneous files and directories that are not important in the current discussion. We will look at some of them later in this chapter in the context of kernel compilation (Section 6.10).
Table 61. Primary Components of the xnu Kernel Source Directory
Component
bsd/
The BSD kernel
config/
Lists of per-subsystem exported functions, property list files for pseudo-extensions
iokit/
The I/O Kit kernel runtime
libkern/
The kernel library
libsa/
The stand-alone library
osfmk/
The Mach kernel
pexpert/
The Platform Expert
Table 62 lists some contents of the bsd/ directory. Section 2.4.2 provides an overview of the functionality implemented in the kernel's BSD portion.
Table 62. Primary Contents of the bsd/ Directory Directory
Description
bsd/bsm/
Basic Security Module (BSM) headers used by the kernel's auditing mechanism. BSM is both a security audit format and an API used to track security-related events in the operating system.
bsd/crypto/
Various cipher and hash implementations: AES (Rijndael), Blowfish, CAST-128, DES, MD5, RC4, SHA-1, and SHA-2.
bsd/dev/memdev.c
A RAM disk driver (for /dev/mdX devices).
bsd/dev/ppc/
BSD drivers for entities such as /dev/console, /dev/mem, /dev/kmem, /dev/null, /dev/zero, and a BSD driver wrapper for the NVRAM. The latter calls Platform Expert functions to perform the actual work. The BSD device switch tables for block and character devices are also initialized here. Also present are some machine-dependent functions used in the BSD subsystem, such as unix_syscall(), unix_syscall_return(), ppc_gettimeofday(), and signal-handling functions.
bsd/dev/random/
An implementation of the Yarrow[a] pseudorandom number generator (PRNG) and the /dev/random device.
bsd/dev/unix_startup.c
Functions that initialize various BSD-related data structures during system startup.
bsd/dev/vn/
The vnode disk driver, which provides block and character interfaces to a vnode, allowing files to be treated as disks. The /usr/libexec/vndevice utility is used to control the driver.
bsd/hfs/
The HFS and HFS Plus file systems.
bsd/isofs/
The ISO 9660 file system for read-only optical discs.
bsd/kern/
The core of xnu's BSD component. It contains implementations of asynchronous I/O calls, the kauth mechanism, the audit mechanism, process-related system calls, sysctl calls, POSIX IPC, System V IPC, the unified buffer cache, sockets, mbufs, and various other system calls.
bsd/libkern/
Utility routines such as bcd(), bcmp(), inet_ntoa(), rindex(), and strtol().
bsd/miscfs/
Miscellaneous file systems: the dead file system for vnodes whose underlying file system has been dissociated (deadfs), the device file system (devfs), the file descriptor file system (fdesc), the fifo file system (fifofs), the null mount file system (nullfs), the file system for device-special files (specfs), the in-memory synthfs used for synthesizing mount points, the union mount file system (union), and the volume ID file system (volfs).
bsd/net/
Networking: the Berkeley packet filter (BPF), bridging, data link interface layer (DLIL), Ethernet, ARP, PPP, routing, IEEE 802.1q (VLAN), IEEE 802.3ad (Link Aggregation), etc.
bsd/netat/
AppleTalk Networking.
bsd/netinet/
IPv4 Networking: BOOTP, DHCP, ICMP, TCP, UDP, IP, the "dummynet" bandwidth limiter, and divert sockets.
bsd/netinet6/
IPv6 Networking.
bsd/netkey/
PF_KEY Key Management API (RFC 2367).
bsd/nfs/
NFS client and the kernel portion of the NFS server.
bsd/ufs/
An implementation of UFS based on the fast file system (ffs).
bsd/uxkern/
A Mach exception handler that translates Mach exceptions into Unix signals.
bsd/vfs/
The BSD virtual file system layer.
bsd/vm/
Vnode pager (swap to/from vnodes, demand paging from files), shared memory server calls.
[a] Yarrow gets its name from a flowering plant with distinctive flat flower heads and lacy leaves. In China, its stalks have been used as a randomizer in divination since the second millennium B.C.
Table 63 lists some contents of the iokit/ directory. Section 2.4.3 provides an overview of the I/O Kit's functionality.
Table 63. Primary Contents of the iokit/ Directory Directory
Description
iokit/Drivers/platform/
Implementations of I/O Kit classes listed in the KernelConfigTables arraye.g., AppleCPU, AppleNMI, and AppleNVRAM. As we will see in Chapter 10, the I/O Catalog is initialized with the contents of this array.
iokit/Families/IONVRAM/
Subclass of the NVRAM controller classsimply calls the Platform Expert to register the NVRAM controller, which publishes the "IONVRAM" resource in the I/O Kit.
iokit/Families/IOSystemManagement/
Watchdog timer.
iokit/IOKit/
I/O Kit header files.
iokit/Kernel/
Implementations of core I/O Kit classes and utility functions.
iokit/KernelConfigTables.cpp
Declarations of the list of "fake" kernel extensions and the KernelConfigTables array.
iokit/bsddev/
Support functions for BSDe.g., the di_root_image() netboot hook called by BSD to mount a disk image as the root device, and several other functions used by BSD while searching for a root device.
Table 64 lists some contents of the libkern/ directory. Section 2.4.4 provides an overview of libkern's functionality.
Table 64. Primary Contents of the libkern/ Directory Directory
Description
libkern/c++/
Implementations of various libkern classes (see Table 65).
libkern/gen/
High-level-language wrappers around assembly functions for atomic operations, miscellaneous debugging functions.
libkern/kmod/
Start and stop routines for the kernel's C++ and C language runtime environments.
libkern/libkern/
libkern header files.
libkern/mach-o/
A header describing the format of Mach-O files (loader.h), and another header containing definitions for accessing Mach-O headers (mach_header.h).
libkern/ppc/
Implementations of PowerPC-specific bcmp(), memcmp(), strlen(), and atomic increment/decrement functions.
libkern/stdio/
An implementation of scanf().
libkern/uuid/
Routines for parsing and generating universally unique identifiers (UUIDs) based on the first Ethernet device's hardware address and the current time.
libkern is part of the Kernel framework (Kernel.framework), which is exposed to the developer. Its headers are located in /System/Library/Frameworks/Kernel.framework/Headers/libkern/. Table 65 shows the important classes contained in this library.
Table 65. libkern Classes and Routines Base and Abstract Classes
OSObject
The abstract base class for the Mac OS X kernel. It derives from the true root class OSMetaClassBase. It implements basic functionality such as allocation primitives, reference counting, and type-safe object casting.
OSMetaClass
A peer class to OSObject. It derives from the true root class OSMetaClassBase. An instance of this class represents one class that is known by the I/O Kit's RTTI system.
OSCollection
The abstract superclass for all collections.
OSIterator
The abstract superclass for iterator classes.
Collection Classes
OSArray
A class for maintaining lists of object references.
OSDictionary
A class for maintaining dictionaries of object references.
OSOrderedSet
A class for maintaining and sorting sets of OSMetaClassBase-derived objects.
OSSet
A class for storing OSMetaClassBase-derived objects.
OSCollectionIterator
A class that provides a mechanism to iterate over OSCollection-derived collections.
Container Classes
OSBoolean
A class for Boolean values.
OSData
A class for managing byte arrays.
OSNumber
A class for numeric values.
OSString
A class for managing strings.
OSSymbol
A class for representing unique string values.
OSSerialize
A class used by the container classes to serialize their instance data.
OSUnserializeXML
A class that recreates a container object from its serialized instance data in an XML buffer.
Table 66 lists some contents of the libsa/ directory. Section 2.4.5 provides an overview of libsa's functionality.
Table 66. Primary Contents of the libsa/Directory File
Description
libsa/bootstrap.cpp
Constructor and destructor functions for libsa.
libsa/bsearch.c, libsa/dgraph.c, libsa/sort.c
Functions for binary searching, directed graphs, and heap sortused for supporting kernel extension loading.
libsa/c++rem3.c
Symbol remangler for code compiled with version 2.95 of the GNU C++ compilerinvoked during symbol table parsing when a Mach-O object file (typically a kernel extension) is mapped.
libsa/catalogue.cpp
I/O Catalog routines, such as those for accessing and manipulating kernel extension dictionaries, accessing mkext caches, and recording boot-time kernel extensions into dictionaries.
libsa/kext.cpp, libsa/kld_patch.c, libsa/kmod.cpp, libsa/load.c
The core of libsa's functionality: routines for resolving kernel extension dependencies, retrieving kernel extension versions, loading kernel extensions, patching vtables, etc.
libsa/malloc.c
Simple implementations of malloc() and realloc().
libsa/mkext.c
Routines for LZSS compression/decompression, and for computing 32-bit Adler checksums.
libsa/strrchr.c, libsa/strstr.c
String functions.
libsa/vers_rsrc.c
Routines for parsing and generating version strings.
Recall from Chapter 2 that the libsa stand-alone library is used only for loading kernel extensions during system startup. In a typical booting scenario, when the kernel extension daemon (kextd) is started, it sends a kIOCatalogRemoveKernelLinker message to the I/O Catalog in the kernel. This message notifies the I/O Catalog that kextd is ready to handle the loading of kernel extensions from user space. Moreover, the message triggers the I/O Catalog to invoke destructors for the kernel's __KLD segment and to deallocate it. The __KLD segment contains libsa's code. The kernel's __LINKEDIT segment is also deallocated.
Section 2.4.1 provides an overview of the functionality implemented in the Mach portion of xnu. Table 67 lists the important components of the osfmk/ directory.
Table 67. Primary Contents of the osfmk/Directory Directory or File
Description
osfmk/UserNotification/
Kernel portion of the Kernel User Notification Center (KUNC) mechanism, which can be used by software running in the kernel to execute user-space programs and to display notices or alert messages. The /usr/libexec/kuncd daemon is the user-space agent that processes such requests from the kernel.
osfmk/console/i386/
VGA text console, x86 serial console.
osfmk/console/iso_font.c
Data for the ISO Latin-1 font.
osfmk/console/panic_dialog.c
Panic user-interface routines, including routines for drawing, managing, and testing the panic dialog.
osfmk/console/panic_image.c
Pixel data for the default panic imagean 8-bit, 472x255 image.
osfmk/console/panic_ui/
Panic image files and utilities to convert them into a kernel-usable format.
osfmk/console/ppc/
Fast video scrolling, PowerPC serial console.
osfmk/console/rendered_numbers.c
Pixel data for hexadecimal digits 0 through F and the colon character.
osfmk/console/video_console.c
Hardware-independent portion of the video console.
osfmk/ddb/
Built-in kernel debugger.
osfmk/default_pager/
Default pager, including the back-end for managing swap files.
osfmk/device/
Mach support for the I/O Kit, including device representation through Mach ports. The I/O Kit master port is also set here.
osfmk/ipc/
The core of Mach's IPC facility implementation.
osfmk/kdp/
A kernel debugging protocol called KDP that uses a TFTP-like UDP-based transfer mechanism.
osfmk/kern/
The core Mach kernel: implementations of abstractions such as processors, processor sets, tasks, threads, memory allocation, and timers. IPC interfaces are also implemented here.
osfmk/mach/
Mach headers and MIG definition files.
osfmk/mach-o/
Functions for accessing Mach-O headers.
osfmk/mach_debug/
Mach debugging headers and MIG definition files.
osfmk/machine/
Headers that are wrappers for machine-dependent headers.
osfmk/ppc/
PowerPC-specific code: machine startup, exception vectors, trap handling, low-level context-switching code, low-level memory management, diagnostic calls, Classic support functions, machine-dependent debugger components, virtual machine monitor, kernel components for Apple's CHUD Tools, etc.
osfmk/profiling/
Kernel profiling support, which must be explicitly compiled in. The kgmon utility is used to control the profiling mechanism: It can stop or start the collection of kernel profiling data, dump the contents of the profile buffers, reset all the profile buffers, and retrieve specific named values from the kernel.
osfmk/sys/
Miscellaneous headers.
osfmk/vm/
Mach virtual memory subsystem, including the in-kernel shared memory server.
Section 2.4.6 provides an overview of the functionality of the Platform Expert. Table 68 lists the important components of the pexpert/ directory.
Table 68. Primary Contents of the pexpert/ Directory Directory or File
Description
pexpert/gen/bootargs.c
Boot-argument parsing routines.
pexpert/gen/device_tree.c
Routines for accessing device tree entries and their properties.
pexpert/gen/pe_gen.c
Miscellaneous functions, including an 8-bit color lookup table used during bootstrapping.
pexpert/i386/
Machine identification, debugging output support, keyboard driver, generic interrupt handler, polled-mode serial port driver, and other platform-dependent routines such as for reading the timestamp counter, setting and clearing interrupts, generating a fake device tree, etc.
pexpert/pexpert/
Miscellaneous platform headers, including those containing image data for the rotating gearwheel image shown at startup to indicate boot progress.
pexpert/ppc/
Machine identification, debugging output support, clock speed determination by running timed loops, timebase value retrieval, and other platform functions.
6.2. Mach
Let us briefly review our discussion of Mach from Chapters 1 and 2. Mach was designed as a communications-oriented operating system kernel with full multiprocessing support. Various types of operating systems could be built upon Mach. It aimed to be a microkernel in which traditional operating system services such as file systems, I/O, memory managers, networking stacks, and even operating system personalities were meant to reside in user space, with a clean logical and modular separation between them and the kernel. In practice, releases of Mach prior to release 3 had monolithic implementations. Release 3a project started at Carnegie Mellon University and continued by the Open Software Foundationwas the first true microkernel version of Mach: BSD ran as a user-space task in this version.
The Mach portions of xnu were originally based on Open Group's Mach Mk 7.3 system, which in turn was based on Mach 3. xnu's Mach contains enhancements from MkLinux and work done on Mach at the University of Utah. Examples of the latter include the migrating thread model, wherein the thread abstraction is further decoupled into an execution context and a schedulable thread of control with an associated chain of contexts.
xnu Is Not a Microkernel
All kernel components reside in a single kernel address space in Mac OS X. Although the kernel is modular and extensible, it is still monolithic. Nevertheless, note that the kernel closely works with a few user-space daemons such as dynamic_pager, kextd, and kuncd.
In this chapter, we will discuss basic Mach concepts and programming abstractions. We will look at some of these concepts in more detail in the next three chapters in the context of process management, memory management, and interprocess communication (IPC).
In this book, Mach-related programming examples are presented to demonstrate the internal working of certain aspects of Mac OS X. However, Apple does not support the direct use of most Mach-level APIs by third-party programs. Consequently, you are advised against using such APIs in software you distribute.
6.2.1. Kernel Fundamentals
Mach provides a virtual machine interface to higher layers by abstracting system hardwarea scenario that is common among many operating systems. The core Mach kernel is designed to be simple and extensible: It provides an IPC mechanism that is the building block for many services offered by the kernel. In particular, Mach's IPC features are unified with its virtual memory subsystem, which leads to various optimizations and simplifications.
The 4.4BSD virtual memory system was based on the Mach 2.0 virtual memory system, with updates from newer versions of Mach.
Mach has five basic abstractions from a programmer's standpoint:
Task
Thread
Port
Message
Memory object
Besides providing the basic kernel abstractions, Mach represents various other hardware and software resources as port objects, allowing manipulation of such resources through its IPC mechanism. For example, Mach represents the overall computer system as a host object, a single physical CPU as a processor object, and one or more groups of CPUs in a multiprocessor system as processor set objects.
6.2.1.1. Tasks and Threads
Mach divides the traditional Unix abstraction of a process into two parts: a task and a thread. As we will see in Chapter 7, the terms thread and process have context-specific connotations in the Mac OS X user space, depending on the environment. Within the kernel, a BSD process, which is analogous to a traditional Unix process, is a data structure with a one-to-one mapping with a Mach task. A Mach task has the following key features.
It is an execution environment and a static entity. A task does not executethat is, it performs no computationby itself. It provides a framework within which other entities (threads) execute.
It is the basic unit of resource allocation and can be thought of as a resource container. A task contains a collection of resources such as access to processors, paged virtual address space (virtual memory), IPC space, exception handlers, credentials, file descriptors, protection state, signal management state, and statistics. Note that a task's resources include Unix items too, which on Mac OS X are contained in a task through its one-to-one association with a BSD process structure.
It represents the protection boundary of a program. One task cannot access another task's resources unless the former has obtained explicit access using some well-defined interface.
A thread is the actual executing entity in Machit is a point of control flow in a task. It has the following features.
It executes within the context of a task, representing an independent program countera stream of instructionswithin the task. A thread is also the fundamental schedulable entity, with associated scheduling priority and attributes. Each thread is scheduled preemptively and independently of other threads, whether they are in the same task or in any other task.
The code that a thread executes resides within the address space of its task.
Each task may contain zero or more threads, but each thread belongs to exactly one task. A task with no threadsalthough legitimatecannot run.
All threads within a task share all the task's resources. In particular, since all threads share the same memory, a thread can overwrite another thread's memory within the same task, without requiring any additional privileges. Since there may be several concurrently executing threads in one task, threads within a task must cooperate.
A thread may have its own exception handlers.
Each thread has its own computation state, which includes processor registers, a program counter, and a stack. Note that while a thread's stack is designated as private, it resides in the same address space as other threads within the same task. As noted earlier, threads within a task can access each other's stacks if they choose to.
A thread uses a kernel stack for handling system calls. A kernel stack's size is 16KB.
To sum up, a task is passive, owns resources, and is a basic unit of protection. Threads within a task are active, execute instructions, and are basic units of control flow.
A single-threaded traditional Unix process is analogous to a Mach task with only one thread, whereas a multithreaded Unix process is analogous to a Mach task with many threads.
A task is considerably more expensive to create or destroy than a thread.
Whereas every thread has a containing task, a Mach task is not related to its creating task, unlike Unix processes. However, the kernel maintains process-level parent-child relationships in the BSD process structures. Nevertheless, we may consider a task that creates another task to be the parent task and the newly created task to be the child task. During creation, the child inherits certain aspects of the parent, such as registered ports, exception and bootstrap ports, audit and security tokens, shared mapping regions, and the processor set. Note that if the parent's processor set has been marked inactive, the child is assigned to the default processor set.
The Kernel Task
As we saw in our discussion of kernel startup in Chapter 5, the kernel uses the task and thread abstractions to divide its functionality into various execution flows. The kernel uses a single taskthe kernel taskwith multiple threads that perform kernel operations such as scheduling, thread reaping, callout management, paging, and Unix exception handling. Thus, xnu is a monolithic kernel containing markedly different components such as Mach, BSD, and the I/O Kit, all running as groups of threads in a single task in the same address space.
Once a task is created, anyone with a valid task identifier (and thus the appropriate rights to a Mach IPC port) can perform operations on the task. A task can send its identifier to other tasks in an IPC message, if it so desires.
6.2.1.2. Ports
A Mach port is a multifaceted abstraction. It is a kernel-protected unidirectional IPC channel, a capability, and a name. Traditionally in Mach, a port is implemented as a message queue with a finite length.
Besides Mach ports, Mac OS X provides many other types of IPC mechanisms, both within the kernel and in user space. Examples of such mechanisms include POSIX and System V IPC, multiple notification mechanisms, descriptor passing, and Apple Events. We will examine several IPC mechanisms in Chapter 9.
The port abstraction, along with associated operations (the most fundamental being send and receive), is the basis for communication in Mach. A port has kernel-managed capabilitiesor rightsassociated with it. A task must hold the appropriate rights to manipulate a port. For example, rights determine which task can send messages to a given port or which task may receive messages destined for it. Several tasks can have send rights to a particular port, but only one task can hold receive rights to a given port.
In the object-oriented sense, a port is an object reference. Various abstractions in Mach, including data structures and services, are represented by ports. In this sense, a port acts as a protected access provider to a system resource. You access objects such as tasks, threads, or memory objects[1] through their respective ports. For example, each task has a task port that represents that task in calls to the kernel. Similarly, a thread's point of control is accessible to user programs through a thread port. Any such access requires a port capability, which is the right to send or receive messages to that port, or rather, to the object the port represents. In particular, you perform operations on an object by sending messages to one of its ports.[2] The object holding receive rights to the port can then receive the message, process it, and possibly perform an operation requested in the message. The following are two examples of this mechanism.
[1] With the exception of virtual memory, all Mach system resources are accessed through ports.
[2] Objects may have multiple ports representing different types of functionality or access level. For example, a privileged resource may have a control port accessible only to the superuser and an information port accessible to all users.
A window manager can represent each window it manages by a port. Its client tasks can perform window operations by sending messages to the appropriate window ports. The window manager task receives and processes these operations.
Each task, and each thread within the task, has an exception port. An error handler can register one of its ports as a thread's exception port. When an exception occurs, a message will be sent to this port. The handler can receive and process this message. Similarly, a debugger can register one of its ports as the task's exception port. Thereafter, unless a thread has explicitly registered its own thread exception port, exceptions in all of the task's threads will be communicated to the debugger.
Since a port is a per-task resource, all threads within a task automatically have access to the task's ports. A task can allow other tasks to access one or more of its ports. It does so by passing port rights in IPC messages to other tasks. Moreover, a thread can access a port only if the port is known to the containing taskthere is no global, system-wide port namespace.
Several ports may be grouped together in a port set. All ports in a set share the same queue. Although there still is a single receiver, each message contains an identifier for the specific port within the port set on which the message was received. This functionality is similar to the Unix select() system call.
Network-Transparent Ports
Mach ports were designed to be network transparent, allowing tasks on network-connected machines to communicate with each other without worrying about where other tasks were located. A network message server (netmsgserver) was typically used in such scenarios as a trusted intermediary. Tasks could advertise their services by checking in with the netmsgserver. A check-in operation registered a unique name with the netmsgserver. Other tasks, including tasks on other machines, could look up service names on the netmsgserver, which itself used a port available to all tasks. This way, the netmsgserver could propagate port rights across networks. Mac OS X does not support this distributed IPC feature of Mach, and as such does not have any internal or external network message servers. Distributed IPC is, however, possible on Mac OS X using higher-level mechanisms such as the Cocoa API's Distributed Objects feature.
Note that a port can be used to send messages in only one direction. Therefore, unlike a BSD socket, a port does not represent an end point of a bidirectional communication channel. If a request message is sent on a certain port and the sender needs to receive a reply, another port must be used for the reply.
As we will see in Chapter 9, a task's IPC space includes mappings from port names to the kernel's internal port objects, along with rights for these names. A Mach port's name is an integerconceptually similar to a Unix file descriptor. However, Mach ports differ from file descriptors in several ways. For example, a file descriptor may be duplicated multiple times, with each descriptor being a different number referring to the same open file. If multiple port rights are similarly opened for a particular port, the port names will coalesce into a single name, which would be reference-counted for the number of rights it represents. Moreover, other than certain standard ports such as registered, bootstrap, and exception ports, Mach ports are not inherited implicitly across the fork() system call.
6.2.1.3. Messages
Mach IPC messages are data objects that threads exchange with each other to communicate. Typical intertask communication in Mach, including between the kernel and user tasks, occurs using messages. A message may contain actual inline data or a pointer to out-of-line (OOL) data. OOL data transfer is an optimization for large transfers, wherein the kernel allocates a memory region for the message in the receiver's virtual address space, without making a physical copy of the message. The shared memory pages are marked copy-on-write (COW).
A message may contain arbitrary program data, copies of memory ranges, exceptions, notifications, port capabilities, and so on. In particular, the only way to transfer port capabilities from one task to another is through messages.
Mach messages are transferred asynchronously. Even though only one task can hold receive rights to a port, multiple threads within a task may attempt to receive messages on a port. In such a case, only one of the threads will succeed in receiving a given message.
6.2.1.4. Virtual Memory and Memory Objects
Mach's virtual memory (VM) system can be cleanly separated into machine-independent and machine-dependent parts. For example, address maps, memory objects, share maps, and resident memory are machine-independent, whereas the physical map (pmap) is machine-dependent. We will discuss VM-related abstractions in detail in Chapter 8.
Features of Mach's VM design include the following.
Mach provides per-task protected address spaces, with a sparse memory layout. A task's address space description is a linear list of memory regions (vm_map_t), where each region points to a memory object (vm_object_t).
The machine-dependent address mappings are contained in a pmap object (pmap_t).
A task can allocate or deallocate regions of virtual memory both within its own address space and in other tasks' address spaces.
Tasks can specify protection and inheritance properties of memory on a per-page basis. Memory pages can be unshared between tasks or shared using either copy-on-write or read-write mode. Each group of pagesa memory regionhas two protection values: a current and a maximum. The current protection corresponds to the actual hardware protection being used for the pages, whereas the maximum protection is the highest (most permissive) value that current protection may ever achieve. The maximum protection is an absolute upper limit in that it cannot be elevated (made more permissive), but only lowered (made more restrictive). Therefore, the maximum protection represents the most access that can be had to a memory region.
A memory object is a container for data (including file data) that is mapped into the address space of a task. It serves as a channel for providing memory to tasks. Mach traditionally allows a memory object to be managed by a user-mode external memory manager, wherein the handling of page faults and page-out data requests can be performed in user space. An external pager can also be used to implement networked virtual memory. This external memory management (EMM) feature of Mach is not used in Mac OS X. xnu provides basic paging services in the kernel through three pagers: the default (anonymous) pager, the vnode pager, and the device pager.
The default pager handles anonymous memorythat is, memory with no explicitly designated pager. It is implemented in the Mach portion of the kernel. With help from the dynamic_pager user-space application,[3] which manages on-disk backing-store (or swap) files, the default pager pages to swap files on a normal file system.
[3] The dynamic_pager application is not involved in actual paging operationsit only creates or deletes swap files based on various criteria.
Swap files reside under the /var/vm/ directory by default. The files are named swapfileN, where N is the swap file's number. The first swap file is called swapfile0.
The vnode pager is used for memory-mapped files. Since the Mac OS X VFS is in the BSD portion of the kernel, the vnode pager is implemented in the BSD layer.
The device pager is used for non-general-purpose memory. It is implemented in the Mach layer but used by the I/O Kit.
6.2.2. Exception Handling
A Mach exception is a synchronous interruption of a program's execution that occurs due to the program itself. The causes for exceptions can be erroneous conditions such as executing an illegal instruction, dividing by zero, or accessing invalid memory. Exceptions can also be caused deliberately, such as during debugging, when a debugger breakpoint is hit.
xnu's Mach implementation associates an array of exception ports with each task and another with each thread within a task. Each such array has as many slots as there are exception types defined for the implementation, with slot 0 being invalid. All of a thread's exception ports are set to the null port (IP_NULL) when the thread is created, whereas a task's exception ports are inherited from those of the parent task. The kernel allows a programmer to get or set individual exception ports for both tasks and threads. Consequently, a program can have multiple exception handlers. A single handler may also handle multiple exception types. Typical preparation for exception handling by a program involves allocation of one or more ports to which the kernel will send exception notification messages. The port can then be registered as an exception port for one or more types of exceptions for either a thread or a task. The exception handler code typically runs in a dedicated thread, waiting for notification messages from the kernel.
Exception handling in Mach can be viewed as a metaoperation consisting of several suboperations. The thread that causes an exception is called the victim thread, whereas the thread that runs the exception handler is called the handler thread. When a victim causes (raises) an exception, the kernel suspends the victim thread and sends a message to the appropriate exception port, which may be either a thread exception port (more specific) or a task exception port (if the thread has not set an exception port). Upon receiving (catching) the message, the handler thread processes the exceptionan operation that may involve fixing the victim's state, arranging for it to be terminated, logging an error, and so on. The handler replies to the message, indicating whether the exception was processed successfully (cleared). Finally, the kernel either resumes the victim or terminates it.
A thread exception port is typically relevant for error handling. Each thread may have its own exception handlers that process exceptions corresponding to errors that affect only individual threads. A task exception port is typically relevant for debugging. A debugger can attach to a task by registering one of its own ports as the debugged task's exception port. Since a task inherits its exception ports from the creating task, the debugger will also be able to control child processes of the debugged program. Moreover, exception notifications for all threads that have no registered exception port will be sent to the task exception port. Recall that a thread is created with null exception ports and, correspondingly, with no default handlers. Therefore, this works well in the general case. Even when a thread does have valid exception ports, the corresponding exception handlers can forward exceptions to the task exception port.
6.3. A Flavor of the Mach APIs
Let us look at a few simple examples of using the Mach APIs. These will serve as a prelude to more complex or subsystem-specific examples that we will see later in this chapter and in subsequent chapters.
Documentation for most Mach calls exported by the xnu kernel is available in the osfmk/man/ directory within the xnu package. You might find it useful to test API-based examples while referring to the API documentation.
6.3.1. Displaying Host Information
The host_info() Mach call retrieves information about a host, such as the type and number of processors installed, the number of processors currently available, and the memory size. As is the case with many Mach "info" calls, host_info() takes a flavor argument, which specifies the kind of information to be retrieved. For example, host_info() accepts the HOST_BASIC_INFO, HOST_SCHED_INFO, and HOST_PRIORITY_INFO flavors as arguments to return basic, scheduler-related, and scheduler-priority-related information, respectively, from the kernel. Besides host_info(), other calls such as host_kernel_version(), host_get_boot_info(), and host_page_size() can be used to retrieve miscellaneous information. Figure 61 shows an example of using the host_info() call.
Figure 61. Retrieving basic host information using Mach calls
// host_basic_info.c
#include <stdio.h>
#include <stdlib.h>
#include <mach/mach.h>
#define EXIT_ON_MACH_ERROR(msg, retval) \
if (kr != KERN_SUCCESS) { mach_error(msg ":" , kr); exit((retval)); }
int
main()
{
kern_return_t kr; // the standard return type for Mach calls
host_name_port_t myhost;
kernel_version_t kversion;
host_basic_info_data_t hinfo;
mach_msg_type_number_t count;
char *cpu_type_name, *cpu_subtype_name;
vm_size_t page_size;
// get send rights to the name port for the current host
myhost = mach_host_self();
kr = host_kernel_version(myhost, kversion);
EXIT_ON_MACH_ERROR("host_kernel_version", kr);
count = HOST_BASIC_INFO_COUNT; // size of the buffer
kr = host_info(myhost, // the host name port
HOST_BASIC_INFO, // flavor
(host_info_t)&hinfo, // out structure
&count); // in/out size
EXIT_ON_MACH_ERROR("host_info", kr);
kr = host_page_size(myhost, &page_size);
EXIT_ON_MACH_ERROR("host_page_size", kr);
printf("%s\n", kversion);
// the slot_name() library function converts the specified
// cpu_type/cpu_subtype pair to a human-readable form
slot_name(hinfo.cpu_type, hinfo.cpu_subtype, &cpu_type_name,
&cpu_subtype_name);
printf("cpu %s (%s, type=0x%x subtype=0x%x "
"threadtype=0x%x)\n", cpu_type_name, cpu_subtype_name,
hinfo.cpu_type, hinfo.cpu_subtype, hinfo.cpu_threadtype);
printf("max_cpus %d\n", hinfo.max_cpus);
printf("avail_cpus %d\n", hinfo.avail_cpus);
printf("physical_cpu %d\n", hinfo.physical_cpu);
printf("physical_cpu_max %d\n", hinfo.physical_cpu_max);
printf("logical_cpu %d\n", hinfo.logical_cpu);
printf("logical_cpu_max %d\n", hinfo.logical_cpu_max);
printf("memory_size %u MB\n", (hinfo.memory_size >> 20));
printf("max_mem %llu MB\n", (hinfo.max_mem >> 20));
printf("page_size %u bytes\n", page_size);
exit(0);
}
$ gcc -Wall -o host_basic_info host_basic_info.c
$ ./host_basic_info # Power Mac G5 Quad 2.5GHz
Darwin Kernel Version 8.5.0: ... root:xnu-792.6.61.obj~1/RELEASE_PPC
cpu ppc970 (PowerPC 970, type=0x12 subtype=0x64 threadtype=0x0)
max_cpus 4
avail_cpus 4
physical_cpu 4
physical_cpu_max 4
logical_cpu 4
logical_cpu_max 4
memory_size 2048 MB
max_mem 4096 MB
page_size 4096 bytes
$ ./host_basic_info # iMac Core Duo 1.83GHz
Darwin Kernel Version 8.5.1: ... root:xnu-792.8.36.obj~1/RELEASE_I386
cpu i486 (Intel 80486, type=0x7, subtype=0x4, threadtype=0x0)
max_cpus 2
avail_cpus 2
...
page_size 4096 bytes
Note in Figure 61 that as discussed in Chapter 5, the memory_size value reported by Mach is pinned to 2GB on a machine with more than 2GB of physical memory.
6.3.2. Accessing the Kernel's Clock Services
The kernel provides clock services with different clock types such as system, calendar, and real time. Accessing these services involves acquiring send rights to their ports and sending messages requesting the clocks' attributes or functionality. Figure 62 shows a program that retrieves the attributes and current time values from the kernel's various clocks.
Figure 62. Retrieving clock attributes and time values in Mach
// host_clock.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <mach/mach.h>
#include <mach/clock.h>
#define OUT_ON_MACH_ERROR(msg, retval) \
if (kr != KERN_SUCCESS) { mach_error(msg ":" , kr); goto out; }
int
main()
{
kern_return_t kr;
host_name_port_t myhost;
clock_serv_t clk_system, clk_calendar, clk_realtime;
natural_t attribute[4];
mach_msg_type_number_t count;
mach_timespec_t timespec;
struct timeval t;
myhost = mach_host_self();
// Get a send right to the system clock's name port
kr = host_get_clock_service(myhost, SYSTEM_CLOCK,
(clock_serv_t *)&clk_system);
OUT_ON_MACH_ERROR("host_get_clock_service", kr);
// Get a send right to the calendar clock's name port
kr = host_get_clock_service(myhost, CALENDAR_CLOCK,
(clock_serv_t *)&clk_calendar);
OUT_ON_MACH_ERROR("host_get_clock_service", kr);
// Get a send right to the real-time clock's name port
kr = host_get_clock_service(myhost, REALTIME_CLOCK,
(clock_serv_t *)&clk_realtime);
OUT_ON_MACH_ERROR("host_get_clock_service", kr);
//// System clock
count = sizeof(attribute)/sizeof(natural_t);
// Get the clock's resolution in nanoseconds
kr = clock_get_attributes(clk_system, CLOCK_GET_TIME_RES,
(clock_attr_t)attribute, &count);
OUT_ON_MACH_ERROR("clock_get_attributes", kr);
// Get the current time
kr = clock_get_time(clk_system, ×pec);
OUT_ON_MACH_ERROR("clock_get_time", kr);
printf("System clock : %u s + %u ns (res %u ns)\n",
timespec.tv_sec, timespec.tv_nsec, attribute[0]);
//// Real-time clock
count = sizeof(attribute)/sizeof(natural_t);
kr = clock_get_attributes(clk_realtime, CLOCK_GET_TIME_RES,
(clock_attr_t)&attribute, &count);
OUT_ON_MACH_ERROR("clock_get_attributes", kr);
kr = clock_get_time(clk_realtime, ×pec);
OUT_ON_MACH_ERROR("clock_get_time", kr);
printf("Realtime clock: %u s + %u ns (res %u ns)\n",
timespec.tv_sec, timespec.tv_nsec, attribute[0]);
//// Calendar clock
count = sizeof(attribute)/sizeof(natural_t);
kr = clock_get_attributes(clk_calendar, CLOCK_GET_TIME_RES,
(clock_attr_t)&attribute, &count);
OUT_ON_MACH_ERROR("clock_get_attributes", kr);
kr = clock_get_time(clk_calendar, ×pec);
gettimeofday(&t, NULL);
OUT_ON_MACH_ERROR("clock_get_time", kr);
printf("Calendar clock: %u s + %u ns (res %u ns)\n",
timespec.tv_sec, timespec.tv_nsec, attribute[0]);
printf("gettimeofday : %ld s + %d us\n", t.tv_sec, t.tv_usec);
out:
// Should deallocate ports here for cleanliness
mach_port_deallocate(mach_task_self(), myhost);
mach_port_deallocate(mach_task_self(), clk_calendar);
mach_port_deallocate(mach_task_self(), clk_system);
mach_port_deallocate(mach_task_self(), clk_realtime);
exit(0);
}
$ gcc -Wall -o host_clock host_clock.c
$ ./host_clock
System clock : 134439 s + 840456243 ns (res 10000000 ns)
Realtime clock: 134439 s + 841218705 ns (res 10000000 ns)
Calendar clock: 1104235237 s + 61156000 ns (res 10000000 ns)
gettimeofday : 1104235237 s + 61191 us
6.3.3. Using a Clock Service to Ring an Alarm
Having seen how to acquire send rights to a clock service's port, we can use these rights to request the service to ring an alarm at a specified time. The clock will notify us by sending an IPC message when the alarm is fired. The program shown in Figure 63 sets an alarm to ring after 2.5 seconds and waits for the alarm message to arrive on a port it allocated.
Figure 63. Setting an alarm using Mach calls
// host_alarm.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <mach/mach.h>
#include <mach/clock.h>
#define OUT_ON_MACH_ERROR(msg, retval) \
if (kr != KERN_SUCCESS) { mach_error(msg ":" , kr); goto out; }
// Structure for the IPC message we will receive from the clock
typedef struct msg_format_recv_s {
mach_msg_header_t header;
int data;
mach_msg_trailer_t trailer;
} msg_format_recv_t;
int
main()
{
kern_return_t kr;
clock_serv_t clk_system;
mach_timespec_t alarm_time;
clock_reply_t alarm_port;
struct timeval t1, t2;
msg_format_recv_t message;
mach_port_t mytask;
// The C library optimized this call by returning the task port's value
// that it caches in the mach_task_self_ variable
mytask = mach_task_self();
kr = host_get_clock_service(mach_host_self(), SYSTEM_CLOCK,
(clock_serv_t *)&clk_system);
OUT_ON_MACH_ERROR("host_get_clock_service", kr);
// Let us set the alarm to ring after 2.5 seconds
alarm_time.tv_sec = 2;
alarm_time.tv_nsec = 50000000;
// Allocate a port (specifically, get receive right for the new port)
// We will use this port to receive the alarm message from the clock
kr = mach_port_allocate(
mytask, // the task acquiring the port right
MACH_PORT_RIGHT_RECEIVE, // type of right
&alarm_port); // task's name for the port right
OUT_ON_MACH_ERROR("mach_port_allocate", kr);
gettimeofday(&t1, NULL);
// Set the alarm
kr = clock_alarm(clk_system, // the clock to use
TIME_RELATIVE, // how to interpret alarm time
alarm_time, // the alarm time
alarm_port); // this port will receive the alarm message
OUT_ON_MACH_ERROR("clock_alarm", kr);
printf("Current time %ld s + %d us\n"
"Setting alarm to ring after %d s + %d ns\n",
t1.tv_sec, t1.tv_usec, alarm_time.tv_sec, alarm_time.tv_nsec);
// Wait to receive the alarm message (we will block here)
kr = mach_msg(&(message.header), // the message buffer
MACH_RCV_MSG, // message option bits
0, // send size (we are receiving, so 0)
message.header.msgh_size,// receive limit
alarm_port, // receive right
MACH_MSG_TIMEOUT_NONE, // no timeout
MACH_PORT_NULL); // no timeout notification port
// We should have received an alarm message at this point
gettimeofday(&t2, NULL);
OUT_ON_MACH_ERROR("mach_msg", kr);
if (t2.tv_usec < t1.tv_usec) {
t1.tv_sec += 1;
t1.tv_usec -= 1000000;
}
printf("\nCurrent time %ld s + %d us\n", t2.tv_sec, t2.tv_usec);
printf("Alarm rang after %ld s + %d us\n", (t2.tv_sec - t1.tv_sec),
(t2.tv_usec - t1.tv_usec));
out:
mach_port_deallocate(mytask, clk_system);
// Release user reference for the receive right we created
mach_port_deallocate(mytask, alarm_port);
exit(0);
}
$ gcc -Wall -o host_alarm host_alarm.c
$ ./host_alarm
Current time 1104236281 s + 361257 us
Setting alarm to ring after 2 s + 50000000 ns
Current time 1104236283 s + 412115 us
Alarm rang after 2 s + 50858 us
6.3.4. Displaying Host Statistics
The host_statistics() call can be used to retrieve statistical information about processor and virtual memory usage on a system-wide basis. Figure 64 shows an example of using this call.
Figure 64. Using Mach calls to retrieve scheduling and virtual memory statistics
// host_statistics.c
#include <stdio.h>
#include <stdlib.h>
#include <mach/mach.h>
// Wrapper function with error checking
kern_return_t
do_host_statistics(host_name_port_t host,
host_flavor_t flavor,
host_info_t info,
mach_msg_type_number_t *count)
{
kern_return_t kr;
kr = host_statistics(host, // control port for the host
flavor, // type of statistics desired
(host_info_t)info, // out buffer
count); // in/out size of buffer
if (kr != KERN_SUCCESS) {
(void)mach_port_deallocate(mach_task_self(), host);
mach_error("host_info:", kr);
exit(1);
}
return kr;
}
int
main()
{
kern_return_t kr;
host_name_port_t host;
mach_msg_type_number_t count;
vm_size_t page_size;
host_load_info_data_t load_info;
host_cpu_load_info_data_t cpu_load_info;
vm_statistics_data_t vm_stat;
host = mach_host_self();
count = HOST_LOAD_INFO_COUNT;
// Get system loading statistics
kr = do_host_statistics(host, HOST_LOAD_INFO, (host_info_t)&load_info,
&count);
count = HOST_VM_INFO_COUNT;
// Get virtual memory statistics
kr = do_host_statistics(host, HOST_VM_INFO, (host_info_t)&vm_stat, &count);
count = HOST_CPU_LOAD_INFO_COUNT;
// Get CPU load statistics
kr = do_host_statistics(host, HOST_CPU_LOAD_INFO,
(host_info_t)&cpu_load_info, &count);
kr = host_page_size(host, &page_size);
printf("Host statistics:\n");
// (average # of runnable processes) / (# of CPUs)
printf("Host load statistics\n");
printf(" time period (sec) %5s%10s%10s\n", "5", "30", "60");
printf(" load average %10u%10u%10u\n", load_info.avenrun[0],
load_info.avenrun[1], load_info.avenrun[2]);
printf(" Mach factor %10u%10u%10u\n", load_info.mach_factor[0],
load_info.mach_factor[1], load_info.mach_factor[2]);
printf("\n");
printf("Cumulative CPU load statistics\n");
printf(" User state ticks = %u\n",
cpu_load_info.cpu_ticks[CPU_STATE_USER]);
printf(" System state ticks = %u\n",
cpu_load_info.cpu_ticks[CPU_STATE_SYSTEM]);
printf(" Nice state ticks = %u\n",
cpu_load_info.cpu_ticks[CPU_STATE_NICE]);
printf(" Idle state ticks = %u\n",
cpu_load_info.cpu_ticks[CPU_STATE_IDLE]);
printf("\n");
printf("Virtual memory statistics\n");
printf(" page size = %u bytes\n", page_size);
printf(" pages free = %u\n", vm_stat.free_count);
printf(" pages active = %u\n", vm_stat.active_count);
printf(" pages inactive = %u\n", vm_stat.inactive_count);
printf(" pages wired down = %u\n", vm_stat.wire_count);
printf(" zero fill pages = %u\n", vm_stat.zero_fill_count);
printf(" pages reactivated = %u\n", vm_stat.reactivations);
printf(" pageins = %u\n", vm_stat.pageins);
printf(" pageouts = %u\n", vm_stat.pageouts);
printf(" translation faults = %u\n", vm_stat.faults);
printf(" copy-on-write faults = %u\n", vm_stat.cow_faults);
printf(" object cache lookups = %u\n", vm_stat.lookups);
printf(" object cache hits = %u (hit rate %2.2f %%)\n", vm_stat.hits,
100 * (double)vm_stat.hits/(double)vm_stat.lookups);
exit(0);
}
$ gcc -Wall -o host_statistics host_statistics.c
$ ./host_statistics
Host statistics:
Host load statistics
time period (sec) 5 30 60
load average 276 233 70
Mach factor 1685 1589 1609
Cumulative CPU load statistics
User state ticks = 109098
System state ticks = 41056
Nice state ticks = 535
Idle state ticks = 1974855
Virtual memory statistics
page size = 4096 bytes
pages free = 434154
pages active = 70311
pages inactive = 236301
pages wired down = 45666
zero fill pages = 2266643
pages reactivated = 0
pageins = 55952
pageouts = 0
translation faults = 4549671
copy-on-write faults = 83912
object cache lookups = 36028
object cache hits = 19120 (hit rate 53.07 %)
6.4. Entering the Kernel
On a typical operating system, user processes are logically insulated from the kernel's memory by using different processor execution modes. The Mac OS X kernel executes in a higher-privileged mode (PowerPC OEA) than any user program (PowerPC UISA and VEA). Each user processthat is, each Mach taskhas its own virtual address space. Similarly, the kernel has its own, distinct virtual address space that does not occupy a subrange of the maximum possible address space of a user process. Specifically, the Mac OS X kernel has a private 32-bit (4GB) virtual address space, and so does each 32-bit user process. Similarly, a 64-bit user process also gets a private virtual address space that is not subdivided into kernel and user parts.
Although the Mac OS X user and kernel virtual address spaces are not subdivisions of a single virtual address space, the amounts of virtual memory usable within both are restricted due to conventional mappings. For example, kernel addresses in the 32-bit kernel virtual address space lie between 0x1000 and 0xDFFFFFFF (3.5GB). Similarly, the amount of virtual memory a 32-bit user process can use is significantly less than 4GB, since various system libraries are mapped by default into each user address space. We will see specific examples of such mappings in Chapter 8.
We will refer to the kernel virtual address space simply as the kernel space. Moreover, even though each user process has its own address space, we will often use the phrase the user space when the specific process is not relevant. In this sense, we can think of all user processes as residing in the user space. The following are some important characteristics of the kernel and user spaces.
The kernel space is inaccessible to user tasks. The kernel enforces this protection by using memory management hardware to create a boundary between kernel-level and user-level code.
The user space is fully accessible to the kernel.
The kernel normally prevents one user task from modifying, or even accessing, another task's memory. However, such protection is usually subject to task and system ownership. For example, there exist kernel-provided mechanisms through which a task T1 can access the address space of another task T2 if T1 is running with root privileges, or if T1 and T2 are both owned by the same user. Tasks can also explicitly share memory with other tasks.
The user space cannot directly access hardware. However, it is possible to have user-space device drivers that access hardware after mediation by the kernel.
Since the kernel mediates access to physical resources, a user program must exchange information with the kernel to avail the kernel's services. Typical user-space execution requires exchange of both control information and data. In such an exchange between a Mach task and the kernel, a thread within the task transitions to kernel space from user space, transferring control to the kernel. After handling the user thread's request, the kernel returns control back to the thread, allowing it to continue normal execution. At other times, the kernel can acquire control even though the current thread was not involved in the reason for the transferin fact, the transfer is often not explicitly requested by the programmer. We refer to execution within the kernel and user spaces as being in the kernel mode and the user mode, respectively.
A Modal Dialog
Technically, even the Mac OS X kernel mode can be thought of as consisting of two submodes. The first mode refers to the environment in which the kernel's own threads runthat is, the kernel task and its resources. The kernel task is a proper Mach task (it is the first Mach task to be created) that runs several dozen kernel threads on a typical system.
The second mode refers to threads running in the kernel after they enter the kernel from user space through a system callthat is, threads trap from user space into the kernel. Kernel subsystems that need to be aware of the two modes may handle them differently.
6.4.1. Types of Control Transfer
Although such transfers of control are traditionally divided into categories based on the events that caused them, at the PowerPC processor level, all categories are handled by the same exception mechanism. Examples of events that can cause the processor to change execution mode include the following:
External signals, such as from the interrupt controller hardware
Abnormal conditions encountered while executing an instruction
Expected system events such as rescheduling and page faults
Trace exceptions caused by deliberate enabling of single-stepping (setting the SE bit of the MSR) or branch-tracing (setting the BE bit of the MSR)
Conditions internal to the processor, such as the detection of a parity error in the L1 D-cache
Execution of the system call instruction
Nevertheless, it is still useful to categorize control transfers in Mac OS X based on the events causing them. Let us look at some broad categories.
6.4.1.1. External Hardware Interrupts
An external hardware interrupt is a transfer of control into the kernel that is typically initiated by a hardware device to indicate an event. Such interrupts are signaled to the processor by the assertion of the processor's external interrupt input signal, which causes an external interrupt exception in the processor. External interrupts are asynchronous, and their occurrence is typically unrelated to the currently executing thread. Note that external interrupts can be masked.
An example of an external interrupt is a storage device controller causing an interrupt to signal the completion of an I/O request. On certain processors, such as the 970FX, a thermal exceptionused to notify the processor of an abnormal conditionis signaled by the assertion of the thermal interrupt input signal. In this case, even though the abnormal condition is internal to the processor, the source of the interrupt is external.
6.4.1.2. Processor Traps
A processor trap is a transfer of control into the kernel that is initiated by the processor itself because of some event that needs attention. Processor traps may be synchronous or asynchronous. Although the conditions that cause traps could all be termed abnormal in that they are all exceptional (hence the exception), it is helpful to subclassify them as expected (such as page faults) or unexpected (such as a hardware failure). Other examples of reasons for traps include divide-by-zero errors, completion of a traced instruction, illegal access to memory, and the execution of an illegal instruction.
6.4.1.3. Software Traps
The Mac OS X kernel implements a mechanism called asynchronous system traps (ASTs), wherein one or more reason bits can be set by software for a processor or a thread. Each bit represents a particular software trap. When a processor is about to return from an interrupt context, including returns from system calls, it checks for these bits, and takes a trap if it finds one. The latter operation involves executing the corresponding interrupt-handling code. A thread checks for such traps in many cases when it is about to change its execution state, such as from being suspended to running. The kernel's clock interrupt handler also periodically checks for ASTs. We categorize ASTs on Mac OS X as software traps because they are both initiated and handled by software. Some AST implementations may use hardware support.
6.4.1.4. System Calls
The PowerPC system call instruction is used by programs to generate a system call exception, which causes the processor to prepare and execute the system call handler in the kernel. The system call exception is synchronous. Hundreds of system calls constitute a well-defined set of interfaces serving as entry points into the kernel for user programs.
POSIX
A standard set of system calls, along with their behavior, error handling, return values, and so on, is defined by a Portable Operating System Interface (POSIX) standard, which defines an interface and not its implementation. Mac OS X provides a large subset of the POSIX API.
The name POSIX was suggested by Richard Stallman. POSIX documentation suggests that the word should be pronounced pahz-icks, as in positive, and not poh-six or other variations.
To sum up, a hardware interrupt from an external device generates an external interrupt exception, a system call generates a system call exception, and other situations result in a variety of exceptions.
6.4.2. Implementing System Entry Mechanisms
PowerPC exceptions are the fundamental vehicles for propagating any kind of interrupts (other than ASTs), whether hardware- or software-generated. Before we discuss how some of these exceptions are processed, let us look at the key components of the overall PowerPC exception-processing mechanism on Mac OS X. These include the following, some of which we have come across in earlier chapters:
The kernel's exception vectors that reside in a designated memory area starting at physical memory address 0x0
PowerPC exception-handling registers
The rfid (64-bit) and rfi (32-bit) system linkage instructions, which are used for returning from interrupts
The sc system linkage instruction, which is used to cause a system call exception
Machine-dependent thread state, including memory areas called exception save areas, which are used for saving various types of context during exception processing
A system linkage instruction connects user-mode and supervisor-mode software. For example, by using a system linkage instruction (such as sc), a program can call on the operating system to perform a service. Conversely, after performing the service, the operating system can return to user-mode software by using another system linkage instruction (such as rfid).
6.4.2.1. Exceptions and Exception Vectors
The __VECTORS segment of the kernel executable (Figure 65) contains the kernel's exception vectors. As we saw in Chapter 4, BootX copies these to their designated location (starting at 0x0) before transferring control to the kernel. These vectors are implemented in osfmk/ppc/lowmem_vectors.s.
Figure 65. The Mach-O segment containing the exception vectors in the kernel executable
$ otool -l /mach_kernel
...
Load command 2
cmd LC_SEGMENT
cmdsize 124
segname __VECTORS
vmaddr 0x00000000
vmsize 0x00007000
fileoff 3624960
filesize 28672
maxprot 0x00000007
initprot 0x00000003
nsects 1
flags 0x0
Section
sectname __interrupts
segname __VECTORS
addr 0x00000000
size 0x00007000
offset 3624960
align 2^12 (4096)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
...
Table 51 lists various PowerPC processor exceptions and some of their details. Recall that most exceptions are subject to one or more conditions; for example, most exceptions can occur only when no higher-priority exception exists. Similarly, exceptions caused by failed effective-to-virtual address translations can occur only if address translation is enabled. Moreover, depending on a system's specific hardware, or whether the kernel is being debugged, some exceptions listed in Table 51 may be inconsequential. Figure 66 shows an excerpt from lowmem_vectors.s. For example, when there is a system call exception, the processor executes the code starting at the label .L_handlerC00 (vector offset 0xC00).
Figure 66. The kernel's exception vectors
; osfmk/ppc/lowmem_vectors.s
...
#define VECTOR_SEGMENT .section __VECTORS, __interrupts
VECTOR_SEGMENT
.globl EXT(lowGlo)
EXT(lowGlo):
.globl EXT(ExceptionVectorsStart)
EXT(ExceptionVectorsStart):
baseR:
...
. = 0x100 ; T_RESET
.globl EXT(ResetHandler)
.L_handler100:
...
. = 0x200 ; T_MACHINE_CHECK
.L_handler200:
...
. = 0x300 ; T_DATA_ACCESS
.L_handler300:
...
. = 0xC00 ; T_SYSTEM_CALL
.L_handlerC00:
...
The exception vectors for the x86 version of Darwin are implemented in osfmk/i386/locore.s.
Exception Vectors in Early UNIX
The concept of exception vectors in early UNIX was very similar to the one being discussed here, although there were far fewer vectors. The UNIX trap vectors were defined in an assembly file called low.s or l.s, representing that the vectors resided in low memory. Figure 67 shows an excerpt from the low.s file in the Third Edition UNIX source.
Figure 67. Trap vectors in Third Edition UNIX
/ PDP-11 Research UNIX V3 (Third Edition), circa 1973
/ ken/low.s
/ low core
...
.globl start
. = 0^.
4
br 1f
/ trap vectors
trap; br7+0 / bus error
trap; br7+1 / illegal instruction
trap; br7+2 / bpt-trace trap
trap; br7+3 / iot trap
trap; br7+4 / power fail
trap; br7+5 / emulator trap
trap; br7+6 / system entry
. = 040^.
1: jmp start
. = 060^.
klin; br4
klou; br4
...
6.4.2.2. Exception-Handling Registers
The Machine Status Save/Restore Register 0 (SRR0) is a special branch-processing register in the PowerPC architecture. It is used to save machine status on interrupts and to restore machine status on return from interrupts. When an interrupt occurs, SRR0 is set to the current or next instruction address, depending on the nature of the interrupt. For example, if an interrupt is being caused due to an illegal instruction exception, then SRR0 will contain the address of the current instruction (the one that failed to execute).
SRR1 is used for a related purpose: It is loaded with interrupt-specific information when an interrupt occurs. It also mirrors certain bits of the Machine State Register (MSR) in the case of an interrupt.
The special-purpose registers SPRG0, SPRG1, SPRG2, and SPRG3 are used as support registers (in an implementation-dependent manner) in various stages of exception processing. For example, the Mac OS X kernel uses SPRG2 and SPRG3 to save interrupt-time general-purpose registers GPR13 and GPR11, respectively, in the implementation of the low-level exception vectors. Furthermore, it uses SPRG0 to hold a pointer to the per_proc structure.
6.4.2.3. System Linkage Instructions
System Call
When a system call is invoked from user space, GPR0 is loaded with the system call number, and the sc instruction is executed. The effective address of the instruction following the system call instruction is placed in SRR0, certain bit ranges of the MSR are placed into the corresponding bits of SRR1, certain bits of the SRR1 are cleared, and a system call exception is generated. The processor fetches the next instruction from the well-defined effective address of the system call exception handler.
Return from Interrupt
rfid (return-from-interrupt-double-word) is a privileged, context-altering, and context-synchronizing instruction used to continue execution after an interrupt. Upon its execution, among other things, the next instruction is fetched from the address specified by SRR0. rfid's 32-bit counterpart is the rfi instruction.
A context-altering instruction is one that alters the context in which instructions are executed, data is accessed, or data and instruction addresses are interpreted in general. A context-synchronizing instruction is one that ensures that any address translations associated with instructions following it will be discarded if the translations were performed using the old contents of the page table entry (PTE).
6.4.2.4. Machine-Dependent Thread State
We will examine the in-kernel thread data structure [osfmk/kern/thread.h] and related structures in Chapter 7. Each thread contains a machine-dependent state, represented by a machine_thread structure [osfmk/ppc/thread.h].
Figure 68 shows a portion of the machine_thread structure. Its fields include the following.
The kernel and user save area pointers (pcb and upcb, respectively) refer to saved kernel-state and user-state contexts. The contents of a save area in xnu are analogous to those of the process control block (PCB) in a traditional BSD kernel.
The current, deferred, and normal facility context structures (curctx, deferctx, and facctx, respectively) encapsulate the contexts for the floating-point and AltiVec facilities. Note that the save area holds only a normal context that does not include floating-point or vector contexts.
The vmmCEntry and vmmControl pointers point to data structures related to the kernel's virtual machine monitor (VMM) facility, which allows a user program to create, manipulate, and run virtual machine (VM) instances. A VMM instance includes a processor state and an address space. The VMM facility and its use are discussed in Section 6.9.
The kernel stack pointer (ksp) either points to the top of the thread's kernel stack or is zero.
The machine_thread structure also contains several data structures related to the kernel's support for the Blue Boxthat is, the Classic environment.
Figure 68. Structure for a thread's machine-dependent state
// osfmk/kern/thread.h
struct thread {
...
struct machine_thread machine;
...
};
// osfmk/ppc/thread.h
struct facility_context {
savearea_fpu *FPUsave; // FP save area
savearea *FPUlevel; // FP context level
unsigned int FPUcpu; // last processor to enable FP
unsigned int FPUsync; // synchronization lock
savearea_vec *VMXsave; // VMX save area
savearea *VMXlevel; // VMX context level
unsigned int VMXcpu; // last processor to enable VMX
unsigned int VMXsync; // synchronization lock
struct thread_activation *facAct; // context's activation
};
typedef struct facility_context facility_context;
...
struct machine_thread {
savearea *pcb; // the "normal" save area
savearea *upcb; // the "normal" user save area
facility_context *curctx; // current facility context pointer
facility_context *deferctx; // deferred facility context pointer
facility_context facctx; // "normal" facility context structure
struct vmmCntrlEntry *vmmCEntry; // pointer to current emulation context
struct vmmCntrlTable *vmmControl; // pointer to VMM control table
...
unsigned int ksp; // top of stack or zero
unsigned int preemption_count;
struct per_proc_info *PerProc; // current per-processor data
...
};
6.4.2.5. Exception Save Areas
Save areas are fundamental to xnu's exception processing. Important characteristics of the kernel's save area management include the following.
Save areas are stored in pages, with each page logically divided into an integral number of save area slots. Consequently, a save area never spans a page boundary.
The kernel accesses save areas using both virtual and physical addressing. A low-level interrupt vector refers to a save area using its physical address, as exceptions (including PTE misses) must not occur at that level. Certain queuing operations during save area management are also performed using physical addresses.
Save areas can be permanent, or they can be dynamically allocated. Permanent save areas are allocated at boot time and are necessary so that interrupts can be taken. The initial save areas are allocated from physical memory. The number of initial save areas is defined in osfmk/ppc/savearea.h, as are other save area management parameters. Eight "back-pocket" save areas are also allocated at boot time for use in emergencies.
Save areas are managed using two global free lists: the save area free list, and the save area free pool. Each processor additionally has a local list. The pool contains entire pages, with each slot within a page being marked free or otherwise. The free list gets its save areas from pool pages. The free list can be grown or shrunk as necessary. An unused save area from the free list is returned to its pool page. If all slots in a pool page are marked free, it is taken off the free pool list and entered into a pending release queue.
We can write a simple program as follows to display some save-area-related sizes used by the kernel.
$ cat savearea_sizes.c
// savearea_sizes.c
#include <stdio.h>
#include <stdlib.h>
#define XNU_KERNEL_PRIVATE
#define __APPLE_API_PRIVATE
#define MACH_KERNEL_PRIVATE
#include <osfmk/ppc/savearea.h>
int
main(void)
{
printf("size of a save area structure in bytes = %ld\n", sizeof(savearea));
printf("# of save areas per page = %ld\n", sac_cnt);
printf("# of save areas to make at boot time = %ld\n", InitialSaveAreas);
printf("# of save areas for an initial target = %ld\n", InitialSaveTarget);
exit(0);
}
$ gcc -I /work/xnu -Wall -o savearea_sizes savearea_sizes.c
$ ./savearea_sizes
size of a save area structure in bytes = 640
# of save areas per page = 6
# of save areas to make at boot time = 48
# of save areas for an initial target = 24
Structure declarations for the various save area types are also contained in osfmk/ppc/savearea.h.
// osfmk/ppc/savearea.h
#ifdef MACH_KERNEL_PRIVATE
typedef struct savearea_comm {
// ... fields common to all save areas
// ... fields used to manage individual contexts
} savearea_comm;
#endif
#ifdef BSD_KERNEL_PRIVATE
typedef struct savearea_comm {
unsigned int save_000[24];
} savearea_comm;
#endif
typedef struct savearea {
savearea_comm save_hdr;
// general context: exception data, all GPRs, SRR0, SRR1, XER, LR, CTR,
// DAR, CR, DSISR, VRSAVE, VSCR, FPSCR, Performance Monitoring Counters,
// MMCR0, MMCR1, MMCR2, and so on
...
} savearea;
typedef struct savearea_fpu {
savearea_comm save_hdr;
...
// floating-point context that is, all FPRs
} savearea_fpu;
typedef struct savearea_vec {
savearea_comm save_hdr;
...
save_vrvalid; // valid VRs in saved context
// vector context that is, all VRs
} savearea_vec;
...
When a new thread is created, a save area is allocated for it by machine_thread_create() [osfmk/ppc/pcb.c]. The save area is populated with the thread's initial context. Thereafter, a user thread begins life with a taken interruptthat is, it looks from an observer's standpoint that the thread is in the kernel because of an interrupt. It returns to user space through thread_return() [osfmk/ppc/hw_exception.s], retrieving its context from the save area. In the case of kernel threads, machine_stack_attach() [osfmk/ppc/pcb.c] is called to attach a kernel stack to a thread and initialize its state, including the address where the thread will continue execution.
// osfmk/ppc/pcb.c
kern_return_t
machine_thread_create(thread_t thread, task_t task)
{
savearea *sv; // pointer to newly allocated save area
...
sv = save_alloc(); // allocate a save area
bzero((char *)((unsigned int)sv // clear the save area
+ sizeof(savearea_comm)),
(sizeof(savearea) - sizeof(savearea_comm)));
sv->save_hdr.save_prev = 0; // clear the back pointer
...
sv->save_hdr.save_act = thread; // set who owns it
thread->machine.pcb = sv; // point to the save area
// initialize facility context
thread->machine.curctx = &thread->machine.facctx;
// initialize facility context pointer to activation
thread->machine.facctx.facAct = thread;
...
thread->machine.upcb = sv; // set user pcb
...
sv->save_fpscr = 0; // clear all floating-point exceptions
sv->save_vrsave = 0; // set the vector save state
...
return KERN_SUCCESS;
}
What's in a Context?
When a thread executes, its execution environment is described by a context, which in turn relates to the thread's memory state and its execution state. The memory state refers to the thread's address space, as defined by the virtual-to-real address mappings that have been set up for it. The execution state's contents depend on whether the thread is running as part of a user task, running as part of the kernel task to perform some kernel operation, or running as part of the kernel task to service an interrupt.[4]
[4] All threads in the kernel are created within the kernel task.
6.5. Exception Processing
Figure 69 shows a high-level view of exception processing. Recall from earlier discussion that the vectors reside in physical memory starting at location 0x0. Consider the example of an instruction access exception, which is caused when the effective address of an instruction fails to translate to a virtual address. As listed in Table 51, the vector offset for this exception is 0x400. Consequently, the processor executes the code at physical address 0x400 to handle this exception. Most exception handlers simply save GPR13 and GPR11, set an interrupt code in GPR11, and jump to .L_exception_entry() [osfmk/ppc/lowmem_vectors.s] for further processing. For some exceptions, such as system reset (0x100), system call (0xC00), and trace (0xD00), the first-level exception handlers perform more work. Nevertheless, there exists a code path from each handler to .L_exception_entry().
Figure 69. A high-level view of exception processing in the xnu kernel
[View full size image]
Figure 610 shows the code structure of .L_exception_entry(). It first saves a variety of contextan operation whose implementation is different for 32-bit and 64-bit processors. The address labeled as extPatch32 contains an unconditional branch instruction to 64-bit-specific code. This branch must not be taken on a 32-bit processorinstead, execution should continue with the 32-bit code that follows this instruction. As we saw in Chapter 5, the kernel performs boot-time memory patching of instructions and data. In this case, the kernel would replace the unconditional branch with a no-op instruction at boot time on a 32-bit processor.
Figure 610. Common code for exception processing
; osfmk/ppc/lowmem_vectors.s
; Caller's GPR13 is saved in SPRG2
; Caller's GPR11 is saved in SPRG3
; Exception code is in GPR11
; All other registers are live
; Interrupts are off
; VM is off
; In 64-bit mode, if supported
;
.L_exception_entry:
.globl EXT(extPatch32)
LEXT(extPatch32)
b extEntry64 ; Patched to a no-op if 32-bit
...
; 32-bit context saving
...
b xcpCommon ; Join common interrupt processing
; 64-bit context saving
extEntry64:
...
b xcpCommon ; Join common interrupt processing
; All of the context is now saved
; We will now get a fresh save area
; Thereafter, we can take an interrupt
xcpCommon:
...
; Save some more aspects of the context, such as some
; floating-point and vector status
...
; Done saving all of the context
; Start filtering the interrupts
Redrive:
...
; Use the exception code to retrieve the next-level exception
; handler from xcpTable
...
; Load the handler in CTR
...
bctr ; Go process the exception
Once .L_exception_entry() has saved all the context, it refers to an exception vector filter table called xcpTable, which too is defined in osfmk/ppc/lowmem_vectors.s and resides in low memory (the first 32KB of physical memory). The common exception-handling code in .L_exception_entry() uses the incoming exception code to look up the handler in the filter table, after which it branches to the handler. Table 69 lists the exception handlers corresponding to the various exception codes set by the exception vectors. For example, the codes T_INTERRUPT (vector offset 0x500), T_DECREMENTER (vector offset 0x900), T_SYSTEM_MANAGEMENT (vector offset 0x1400), and T_THERMAL (vector offset 0x1700) are all channeled to code labeled as PassUpRupt, which leads to a higher-level interrupt handler. Similarly, traps (various exception codes) and system calls (the T_SYSTEM_CALL exception code) are channeled to the PassUpTrap and xcpSyscall labels, respectively. Figure 611 depicts a simplified view of the processing of traps, interrupts, and system calls.
Table 69. Table-Driven Exception Filtering Exception Code
Handler
Notes
T_IN_VAIN
EatRupt
Restore state, return from interrupt
T_RESET
PassUpTrap
Handled by thandler() [osfmk/ppc/hw_exception.s]
T_MACHINE_CHECK
MachineCheck
MachineCheck() implemented in osfmk/ppc/lowmem_vectors.s
T_DATA_ACCESS
EXT(handlePF)
handlePF() implemented in osfmk/ppc/hw_vm.s
T_INSTRUCTION_ACCESS
EXT(handlePF)
T_INTERRUPT
PassUpRupt
Handled by ihandler() [osfmk/ppc/hw_exception.s]
T_ALIGNMENT
EXT(AlignAssist)
AlignAssist() implemented in osfmk/ppc/Emulate.s
T_PROGRAM
EXT(Emulate)
Emulate() implemented in osfmk/ppc/Emulate.s
T_FP_UNAVAILABLE
PassUpFPU
Handled by fpu_switch() [osfmk/ppc/cswtch.s]
T_DECREMENTER
PassUpRupt
T_IO_ERROR
PassUpTrap
T_RESERVED
PassUpTrap
T_SYSTEM_CALL
xcpSyscall
Handled locally in the case of a "CutTrace" system call, by FirmwareCall() [osfmk/ppc/Firmware.s] in the case of other firmware calls, and by shandler() [bsd/dev/ppc/systemcalls.c] in the case of normal system calls
T_trACE
PassUpTrap
T_FP_ASSIST
PassUpTrap
T_PERF_MON
PassUpTrap
T_VMX
PassUpVMX
Handled by vec_switch() [osfmk/ppc/cswtch.s]
T_INVALID_EXCP0
PassUpTrap
T_INVALID_EXCP1
PassUpTrap
T_INVALID_EXCP2
PassUpTrap
T_INSTRUCTION_BKPT
PassUpTrap
T_SYSTEM_MANAGEMENT
PassUpRupt
T_ALTIVEC_ASSIST
EXT(AltivecAssist)
AltivecAssist() implemented in osfmk/ppc/AltiAssist.s
T_THERMAL
PassUpRupt
T_INVALID_EXCP5
PassUpTrap
T_INVALID_EXCP6
PassUpTrap
T_INVALID_EXCP7
PassUpTrap
T_INVALID_EXCP8
PassUpTrap
T_INVALID_EXCP9
PassUpTrap
T_INVALID_EXCP10
PassUpTrap
T_INVALID_EXCP11
PassUpTrap
T_INVALID_EXCP12
PassUpTrap
T_INVALID_EXCP13
PassUpTrap
T_RUNMODE_TRACE
PassUpTrap
T_SIGP
PassUpRupt
T_PREEMPT
PassUpTrap
T_CSWITCH
conswtch
conswtch() implemented in osfmk/ppc/lowmem_vectors.s
T_SHUTDOWN
PassUpRupt
T_CHOKE
PassUpAbend
Handled by chandler() [osfmk/ppc/hw_exception.s]
T_DATA_SEGMENT
EXT(handleDSeg)
handleDSeg() implemented in osfmk/ppc/hw_vm.s
T_INSTRUCTION_SEGMENT
EXT(handleISeg)
handleISeg() implemented in osfmk/ppc/hw_vm.s
T_SOFT_PATCH
WhoaBaby
WhoaBaby() implemented in osfmk/ppc/lowmem_vectors.ssimply an infinite loop
T_MAINTENANCE
WhoaBaby
T_INSTRUMENTATION
WhoaBaby
T_ARCHDEP0
WhoaBaby
T_HDEC
EatRupt
Figure 611. Processing of traps, interrupts, and system calls
[View full size image]
Table 69 lists some handlers with the EXT() macro, which is defined in osfmk/ppc/asm.h. This macro simply adds an underscore prefix to its argument, allowing assembly code to refer to the corresponding external symbol while maintaining the C language name (without the underscore) for visual consistency.
Examples of exception handlers[5] shown in Figure 611 and Table 69 include PassUpTrap, PassUpRupt, EatRupt, xcpSyscall, and WhoaBaby. Let us briefly look at each of these.
[5] In many cases, the handlers are labels in assembly code.
PassUpTrap loads the address of thandler() [osfmk/ppc/hw_exception.s] in GPR20 and branches to PassUp. When thandler() executes, virtual memory is turned on, but interrupts are turned off.
PassUpRupt loads the address of ihandler() [osfmk/ppc/hw_exception.s] in GPR20 and branches to PassUp. As is the case with thandler(), ihandler() also executes with virtual memory turned on and interrupts turned off.
EatRupt is the main place for returning from an interrupt. It is also used if the interrupt has already been handled and nothing further needs to be done. For example, if it is found during page-fault processing that the exception has already been handled (say, due to another fault for the same page), the page-fault handler will return a T_IN_VAIN, which is handled by EatRupt. Other examples of this situation include software assistance of floating-point and VMX.
xcpSyscall handles system calls depending on their types, such as whether they are normal or special system calls. In the case of normal system calls, xcpSyscall loads the address of shandler() [osfmk/ppc/hw_exception.s] in GPR20 and branches to PassUp. We will discuss system call types and their processing in Section 6.6.
WhoaBaby is for exceptions that must not occur during the normal functioning of the operating system. Its handler is extremely simplemerely an infinite loop.
; osfmk/ppc/lowmem_vectors.s
WhoaBaby: b . ; open the hood and wait for help
PassUp [osfmk/ppc/lowmem_vectors.s] places the exception code in GPR3 and the next-level exception handler's address in SRR0. It also switches segment registers between the kernel and the user. It finally executes the rfid instruction (rfi on 32-bit processors) to launch the exception handler.
6.5.1. Hardware Interrupts
A true hardware interrupt can only occur because of a device that has a physical connectionan interrupt linefrom itself to the system's interrupt controller. Such a connection may involve a device controller. A PCI device is a good example: An interrupt line connects a PCI device slot to the PCI controller, which connects it to the interrupt controller. When the system boots, Open Firmware assigns one or more interrupts to a PCI device. The interrupts used by the device are listed in an array called IOInterruptSpecifiers in the device's I/O Registry node. When the device causes a hardware interrupt, it is signaled to the processor by setting an interrupt identifier bit and asserting the processor's external interrupt input signal, causing an external interrupt exception (vector offset 0x500, exception code T_INTERRUPT). As we saw earlier, processing of this exception will eventually lead to the ihandler() function. Moreover, as shown in Figure 611, other exception codes such as T_DECREMENTER and T_SHUTDOWN also lead to ihandler().
Not all devices cause true hardware interrupts. A USB device, for example, generates an "interrupt" by sending a message on the USB bus without involving the system's interrupt controller.
ihandler() ensures the integrity of the interrupt stack, marks it busy, and calls the higher-level interrupt() function [osfmk/ppc/interrupt.c], which disables preemption and performs different operations depending on the specific exception code it is called with.
// osfmk/ppc/interrupt.c
struct savearea *
interrupt(int type, struct savearea *ssp, unsigned int dsisr, unsigned int dar)
{
...
disable_preemption();
...
switch (type) {
case T_DECREMENTER:
...
break;
case T_INTERRUPT:
...
break;
...
default:
#if MACH_KDP || MACH_KDB
if (!Call_Debugger(type, ssp))
#endif
unresolved_kernel_trap(type, ssp, dsisr, dar, NULL);
break;
}
enable_preemption();
return ssp;
}
In the case of a T_DECREMENTER exception code, interrupt() calls rtclock_intr() [osfmk/ppc/rtclock.c]the real-time clock device interrupt function. interrupt() also checks whether the current thread has its quick-activation single-shot timer set; if so, it checks whether the timer has expired, in which case it is cleared. The kernel's virtual machine monitor facility uses this timer.
In the case of a T_INTERRUPT exception code, interrupt() increments the count of incoming interrupts and calls the platform-specific interrupt handler function referred to in the per-processor structure. The type of this handler function (IOInterruptHandler) is defined in iokit/IOKit/IOInterrupts.h.
typedef void (* IOInterruptHandler)(void *target,
void *refCon,
void *nub,
int source);
In the case of a T_SHUTDOWN exception code, which is generated by a special system call (a so-called firmware callsee Section 6.8.8.1), interrupt() calls cpu_doshutdown() [osfmk/ppc/cpu.c].
If an invalid exception code is sent to ihandler() for processing, it will either panic the system or drop into the debugger if one is available. The panic will be accompanied by an "Unresolved kernel trap . . ." message.
6.5.2. Miscellaneous Traps
The low-level trap handlerthandler() [osfmk/ppc/hw_exception.s]performs different operations depending on the specific trap. A system call exception may end up in the trap handler if there is nothing for the system call to do except generate a trap. The trap handler may jump to the interrupt handler if it finds that it is running on an interrupt stack. thandler() eventually calls the higher-level TRap() function [osfmk/ppc/trap.c] to process traps.
// osfmk/ppc/trap.c
struct savearea *
trap(int trapno, struct savearea *ssp, unsigned int dsisr, addr64_t dar)
{
int exception;
...
exception = 0;
...
if (/* kernel mode */) {
// Handle traps originating from the kernel first
// Examples of such traps are T_PREEMPT, T_PERF_MON, T_RESET
// Various traps should never be seen here
// Panic if any of these traps are encountered
...
} else {
/* user mode */
// Handle user mode traps
...
}
// The 'exception' variable may have been set during trap processing
if (exception) {
doexception(exception, code, subcode);
}
...
if (/* user mode */) {
// If an AST is needed, call ast_taken()
// Repeat until an AST is not needed
}
return ssp;
}
There are several criteria for the invalidity of a trap's occurrence. For example, T_IN_VAIN should never be seen by trap() because it should have been disposed of by EatRupt in osfmk/ppc/lowmem_vectors.s. Note that TRap() determines whether a trap originated from the user mode or kernel mode by looking at the contents of the SRR1 in the save area. It uses the USER_MODE() macro [osfmk/ppc/proc_reg.h] for this purpose.
// osfmk/ppc/proc_reg.h
#define ENDIAN_MASK(val,size) (1 << ((size-1) - val))
...
#define MASK32(PART) ENDIAN_MASK(PART ## _BIT, 32)
...
#define MASK(PART) MASK32(PART)
...
#define MSR_PR_BIT 17
...
#define USER_MODE(msr) (msr & MASK(MSR_PR) ? TRUE : FALSE)
The Choker
In the case of fatal errors, an exception of type T_CHOKE is generated. This exception results in chandler() [osfmk/ppc/hw_exception.s]the choke handlerbeing called. The choke handler "chokes" the system by going into an infinite loop that increments GPR31 by 1 in each iteration. If thandler() or ihandler() detects that the kernel or interrupt stack, respectively, is invalid, it invokes a special firmware system call for choking the system by setting the external interrupt value to T_CHOKE.
6.5.3. System Calls
The remaining type of exception is for system calls. As noted earlier, system calls are well-defined entry points into the kernel typically used by user-level programs. The next section covers the details of the Mac OS X system call mechanism
6.6. System Call Processing
In traditional UNIX, a system call is one of a well-defined set of functions that allow a user process to interact with the kernel. A user process invokes a system call to request the kernel to perform one or more operations on its behalf. The kernel may perform the requested operations after validating input parameters to the system call, if any, and perhaps several other checks. A system call may involve exchange of datatypically at least a return valuebetween the kernel and the user process.
Our definition of a Mac OS X system call is a function callable through the sc instruction. Note that it is legal to use the sc instruction from within the kernel. It is also possible to directlyfrom within the kernelcall an internal function that implements a system call. Nevertheless, a typical invocation of a system call is from user space.
Since the Mac OS X kernel is an amalgamation of entities with quite different personalities and characteristics, it is interesting to ask which portions of xnu these system calls provide entry to: BSD, Mach, the I/O Kit, or something else? The answer is: all of them, and more.
Based on how they are processed, Mac OS X system calls can be categorized as ultra-fast traps, firmware calls, and normal system calls. Figure 612 shows the key code paths involved in system call processing. The figure should be followed beginning at the "Start" label.
Figure 612. Details of system call processing in Mac OS X
[View full size image]
We can also categorize Mac OS X system calls based on what they do, that is, based on their flavors. The following categorization also captureslargelythe division based on the kernel subsystems that these system calls provide access to.
BSD system calls are the Unix system calls, although several system calls in this category either have Mac OS Xspecific nuances or are seen only on Mac OS X. The BSD system calls substantially contribute to Mac OS X's POSIX compatibility at the system call level.
Mach system callsor Mach trapsserve as the building blocks for exporting a variety of Mach functionality through kernel calls, which are invoked from user space via Mach IPC. In particular, unlike BSD, Mach's kernel functionality is often accessed through kernel-user IPC, rather than having separate system calls for individual pieces of functionality.
I/O Kit traps constitute a subset of the Mach traps.
PowerPC-only special system calls include special-purpose calls for diagnostics, for performance monitoring, for access to the kernel's virtual machine monitor, and for calls related to the Blue Box.
PowerPC-only ultra-fast traps are system calls that perform very little work, do not have the context save/restore overhead of regular system calls, and return very quickly. Another type of a fast system call is a fastpath call, which is conceptually similar to an ultra-fast call but performs somewhat more work. These calls also do not return as rapidly as the ultra-fast calls.
Certain system calls can be optimized using the commpage feature on Mac OS X. The commpage area of memory contains frequently used code and data. These entities are grouped together as a set of consecutive pages that are mapped by the kernel into the address space of each process. The gettimeofday() system call is optimized in this mannerwhen a user process executes gettimeofday(), its commpage implementation is attempted first, failing which the actual system call is executed. We will see details of this mechanism later in this chapter.
As shown in Figure 612, the details of how each system call category is handled in the kernel differ. Nevertheless, all system calls are invoked from user space via the same basic mechanism. Each category uses one or more unique ranges of system call numbers. In a typical invocation of any type of system call, the calling entity in user space places the system call number in GPR0 and executes the sc instruction. These statements must be qualified with the following points to avoid confusion.
User programs normally do not call a system call directlya library stub invokes the sc instruction.
Some program invocations of system calls may never transition to the kernel because they are handled by a library entirely in user space. The commpage-optimized calls are examples of this.
Regardless of the programmer-visible invocation mechanism, a system call that does transition to the kernel always involves execution of the sc instruction.
The kernel's hardware vector for the system call exception maps the system call number in GPR0 to an index into the first-level dispatch table containing handlers for various types of system calls. It then branches to the call handler. Figure 613 shows details of this mapping.
Figure 613. Mapping an incoming system call number to an index in the first-level system call dispatch table
[View full size image]
The first-level dispatch tablescTablealso resides in low memory. As Figure 613 shows, it can map ultra-fast system calls to their individual handlers, route all non-ultra-fast valid system calls to a normal dispatcher, and if an impossible index is encountered, it can send the call to WhoaBaby. The dispatcher for normal system calls sets the exception code in GPR11 to T_SYSTEM_CALL. Such calls, including BSD and Mach system calls, are next processed by .L_exception_entry(), the exception-handling code common to most exceptions. As shown in Figure 611, .L_exception_entry() branches to xcpSyscall to handle the T_SYSTEM_CALL exception code. xcpSyscall hands over the processing of most system calls to shandler() [osfmk/ppc/hw_exception.s].
User-Level System Call Emulation
Remnants of the Mach multiserver emulation facility can be seen in the xnu kernel. A typical multiserver configuration consisted of the Mach kernel along with one or more servers, as well as an emulation library that intercepted system calls for emulated processes, redirecting them to the appropriate emulation services. A user-process address space consisted of user code representing the application and the emulation library. xnu includes some of this code inherited from Mach, such as the task_set_emulation() and task_set_emulation_vector() functions. However, the code is not functional in xnu.
6.7. System Call Categories
Let us now look at details and examples of the various system call categories, beginning with the most staple variety from a developer's standpoint: the BSD system calls.
6.7.1. BSD System Calls
shandler() calls unix_syscall() [bsd/dev/ppc/systemcalls.c] to handle BSD system calls. unix_syscall() receives as its argument a pointer to a save areathe process control block. Before we discuss unix_syscall()'s operation, let us look at some relevant data structures and mechanisms.
6.7.1.1. Data Structures
BSD system calls on Mac OS X have numbers that start from zero and go as high as the highest numbered BSD system call. These numbers are defined in <sys/syscall.h>.
// <sys/syscall.h>
#ifdef __APPLE_API_PRIVATE
#define SYS_syscall 0
#define SYS_exit 1
#define SYS_fork 2
#define SYS_read 3
#define SYS_write 4
#define SYS_open 5
#define SYS_close 6
...
#define SYS_MAXSYSCALL 370
#endif
Several system call numbers are reserved or simply unused. In some cases, they may represent calls that have been obsoleted and removed, creating holes in the sequence of implemented system calls.
Note that the zeroth system callsyscall()is the indirect system call: It allows another system call to be invoked given the latter's number, which is provided as the first argument to syscall(), followed by the actual arguments required by the target system call. The indirect system call has traditionally been used to allow testingsay, from a high-level language like Cof new system calls that do not have stubs in the C library.
// Normal invocation of system call number SYS_foo
ret = foo(arg1, arg2, ..., argN);
// Indirect invocation of foo using the indirect system call
ret = syscall(SYS_foo, arg1, arg2, ..., argN);
The syscall.h file is generated during kernel compilation by the bsd/kern/makesyscalls.sh shell script,[6] which processes the system call master file bsd/kern/syscalls.master. The master file contains a line for each system call number, with the following entities in each column within the line (in this order):
[6] The script makes heavy use of the awk and sed Unix utilities.
The system call number
The type of cancellation supported by the system call in the case of a thread cancellation: one of PRE (can be canceled on entry itself), POST (can be canceled only after the call is run), or NONE (not a cancellation point)
The type of funnel[7] to be taken before executing the system call: one of KERN (the kernel funnel) or NONE
[7] Beginning with Mac OS X 10.4, the network funnel is not used.
The files to which an entry for the system call will be added: either ALL or a combination of T (bsd/kern/init_sysent.cthe system call table), N (bsd/kern/syscalls.cthe table of system call names), H (bsd/sys/syscall.hsystem call numbers), and P (bsd/sys/sysproto.hsystem call prototypes)
The system call function's prototype
Comments that will be copied to output files
; bsd/kern/syscalls.master
;
; Call# Cancel Funnel Files { Name and Args } { Comments }
;
...
0 NONE NONE ALL { int nosys(void); } { indirect syscall }
1 NONE KERN ALL { void exit(int rval); }
2 NONE KERN ALL { int fork(void); }
...
368 NONE NONE ALL { int nosys(void); }
369 NONE NONE ALL { int nosys(void); }
The file bsd/kern/syscalls.c contains an array of stringssyscallnames[]that contains each system call's textual name.
// bsd/kern/syscalls.c
const char *syscallnames[] = {
"syscall", /* 0 = syscall indirect syscall */
"exit", /* 1 = exit */
"fork", /* 2 = fork */
...
"#368", /* 368 = */
"#369", /* 369 = */
};
We can examine the contents of the syscallnames[] arrayand for that matter, other kernel data structuresfrom user space by reading from the kernel memory device /dev/kmem.[8]
[8] The /dev/kmem and /dev/mem devices have been removed from the x86 version of Mac OS X. A simple kernel extension can be written to provide /dev/kmem's functionality, allowing experiments such as this one. This book's accompanying web site provides information about writing such a driver.
Running nm on the kernel binary gives us the address of the symbol syscallnames, which we can dereference to access the array.
$ nm /mach_kernel | grep syscallnames
0037f3ac D _syscallnames
$ sudo dd if=/dev/kmem of=/dev/stdout bs=1 count=4 iseek=0x37f3ac | od -x
...
0000000 0032 a8b4
0000004
$ sudo dd if=/dev/kmem of=/dev/stdout bs=1 count=1024 iseek=0x32a8b4 | strings
syscall
exit
fork
...
The file bsd/kern/init_sysent.c contains the system call switch table, sysent[], which is an array of sysent structures, containing one structure for each system call number. This file is generated from the master file during kernel compilation.
// bsd/kern/init_sysent.c
#ifdef __ppc__
#define AC(name) (sizeof(struct name) / sizeof(uint64_t))
#else
#define AC(name) (sizeof(struct name) / sizeof(register_t))
#endif
__private_extern__ struct sysent sysent[] = {
{
0,
_SYSCALL_CANCEL_NONE,
NO_FUNNEL,
(sy_call_t *)nosys,
NULL,
NULL,
_SYSCALL_RET_INT_T
}, /* 0 = nosys indirect syscall */
{
AC(exit_args),
_SYSCALL_CANCEL_NONE,
KERNEL_FUNNEL,
(sy_call_t *)exit,
munge_w,
munge_d,
_SYSCALL_RET_NONE
}, /* 1 = exit */
...
{
0,
_SYSCALL_CANCEL_NONE,
NO_FUNNEL,
(sy_call_t *)nosys,
NULL,
NULL,
_SYSCALL_RET_INT_T
}, /* 369 = nosys */
};
int nsysent = sizeof(sysent) / sizeof(sysent[0]);
The sysent structure is declared in bsd/sys/sysent.h.
// bsd/sys/sysent.h
typedef int32_t sy_call_t(struct proc *, void *, int *);
typedef void sy_munge_t(const void *, void *);
extern struct sysent {
int16_t sy_narg; // number of arguments
int8_t sy_cancel; // how to cancel, if at all
int8_t sy_funnel; // funnel type, if any, to take upon entry
sy_call_t *sy_call; // implementing function
sy_munge_t *sy_arg_munge32; // arguments munger for 32-bit process
sy_munge_t *sy_arg_munge64; // arguments munger for 64-bit process
int32_t sy_return_type; // return type
} sysent[];
The sysent structure's fields have the following meanings.
sy_narg is the number of argumentsat most eighttaken by the system call. In the case of the indirect system call, the number of arguments is limited to seven since the first argument is dedicated for the target system call's number.
As we saw earlier, a system call specifies whether it can be canceled before execution, after execution, or not at all. The sy_cancel field holds the cancellation type, which is one of _SYSCALL_CANCEL_PRE, _SYSCALL_CANCEL_POST, or _SYSCALL_CANCEL_NONE (corresponding to the PRE, POST, and NONE cancellation specifiers, respectively, in the master file). This feature is used in the implementation of the pthread_cancel(3) library call, which in turn invokes the __pthread_markcancel() [bsd/kern/kern_sig.c] system call to cancel a thread's execution. Most system calls cannot be canceled. Examples of those that can be canceled include read(), write(), open(), close(), recvmsg(), sendmsg(), and select().
The sy_funnel field may contain a funnel type that causes the system call's processing to take (lock) the corresponding funnel before the system call is executed, and drop (unlock) the funnel after it has executed. The possible values for this argument in Mac OS X 10.4 are NO_FUNNEL and KERNEL_FUNNEL (corresponding to the KERN and NONE funnel specifiers, respectively, in the master file).
The sy_call field points to the kernel function that implements the system call.
The sy_arg_munge32 and sy_arg_munge64 fields point to functions that are used for munging[9] system call arguments for 32-bit and 64-bit processes, respectively. We will discuss munging in Section 6.7.1.2.
[9] Munging a data structure means rewriting or transforming it in some way.
The sy_return_type field contains one of the following to represent the system call's return type: _SYSCALL_RET_NONE, _SYSCALL_RET_INT_T, _SYSCALL_RET_UINT_T, _SYSCALL_RET_OFF_T, _SYSCALL_RET_ADDR_T, _SYSCALL_RET_SIZE_T, and _SYSCALL_RET_SSIZE_T.
Recall that unix_syscall() receives a pointer to the process control block, which is a savearea structure. The system call's arguments are received as saved registers GPR3 through GPR10 in the save area. In the case of an indirect system call, the actual system call arguments start with GPR4, since GPR3 is used for the system call number. unix_syscall() copies these arguments to the uu_arg field within the uthread structure before passing them to the call handler.
// bsd/sys/user.h
struct uthread {
int *uu_ar0; // address of user's saved GPR0
u_int64_t uu_arg[8]; // arguments to current system call
int *uu_ap; // pointer to argument list
int uu_rval[2]; // system call return values
...
};
As we will see in Chapter 7, an xnu thread structure contains a pointer to the thread's user structure, roughly analogous to the user area in BSD. Execution within the xnu kernel refers to several structures, such as the Mach task structure, the Mach thread structure, the BSD process structure, and the BSD uthread structure. The latter contains several fields used during system call processing.
The U-Area
Historically, the UNIX kernel maintained an entry for every process in a process table, which always remained in memory. Each process was also allocated a user structureor a u-areathat was an extension of the process structure. The u-area contained process-related information that needed to be accessible to the kernel only when the process was executing. Even though the kernel would not swap out a process structure, it could swap out the associated u-area. Over time, the criticality of memory as a resource has gradually lessened, but operating systems have become more complex. Correspondingly, the process structure has grown in size and the u-area has become less important, with much of its information being moved into the process structure.
6.7.1.2. Argument Munging
Note that uu_arg is an array of 64-bit unsigned integerseach element represents a 64-bit register. This is problematic since a parameter passed in a register from 32-bit user space will not map as is to the uu_arg array. For example, a long long parameter will be passed in a single GPR in a 64-bit program, but in two GPRs in a 32-bit program.
unix_syscall() addresses the issue arising from the difference depicted in Figure 614 by calling the system call's specified argument munger, which copies arguments from the save area to the uu_arg array while adjusting for the differences.
Figure 614. Passing a long long parameter in 32-bit and 64-bit ABIs
$ cat foo.c
extern void bar(long long arg);
void
foo(void)
{
bar((long long)1);
}
$ gcc -static -S foo.c
$ cat foo.s
...
li r3,0
li r4,1
bl _bar
...
$ gcc -arch ppc64 -static -S foo.c
$ cat foo.s
...
li r3,1
bl _bar
...
The munger functions are implemented in bsd/dev/ppc/munge.s. Each function takes two arguments: a pointer to the beginning of the system call parameters within the save area and a pointer to the uu_arg array. A munger function is named munge_<encoding>, where <encoding> is a string that encodes the number and types of system call parameters. <encoding> is a combination of one or more of the d, l, s, and w characters. The characters mean the following:
d represents a 32-bit integer, a 64-bit pointer, or a 64-bit long when the calling process is 64-bitthat is, in each case, the parameter was passed in a 64-bit GPR. Such an argument is munged by copying two words from input to output.
l represents a 64-bit long long passed in two GPRs. Such an argument is munged by skipping a word of input (the upper 32 bits of the first GPR), copying a word of input to output (the lower 32 bits of the first GPR), skipping another word of input, and copying another word from input to output.
s represents a 32-bit signed value. Such an argument is munged by skipping a word of input, loading and sign-extending the next word of input to yield two words, and copying the two words to output.
w represents a 32-bit unsigned value. Such an argument is munged by skipping a word of input, copying a zero word to output, and copying a word from input to output.
Moreover, multiple munger functions are aliased to a common implementation if each function, except one, is a prefix of another. For example, munger_w, munger_ww, munger_www, and munger_wwww are aliased to the same implementationconsequently, four arguments are munged in each case, regardless of the actual number of arguments. Similarly, munger_wwwww, munger_wwwwww, munger_wwwwwww, and munger_wwwwwwww are aliased to the same implementation, whose operation is shown in Figure 615.
Figure 615. An example of system call argument munging
[View full size image]
Consider the example of the read() system call. It takes three arguments: a file descriptor, a pointer to a buffer, and the number of bytes to read.
ssize_t
read(int d, void *buf, size_t nbytes);
The 32-bit and 64-bit mungers for the read() system call are munge_www() and munge_ddd(), respectively.
6.7.1.3. Kernel Processing of BSD System Calls
Figure 616 shows pseudocode depicting the working of unix_syscall(), which, as we saw earlier, is called by shandler() to process BSD system calls.
Figure 616. Details of the final dispatching of BSD system calls
// bsd/dev/ppc/systemcalls.c
void
unix_syscall(struct savearea *regs)
{
thread_t thread_act;
struct uthread *uthread;
struct proc *proc;
struct sysent *callp;
int error;
unsigned short code;
...
// Determine if this is a direct or indirect system call (the "flavor").
// Set the 'code' variable to either GPR3 or GPR0, depending on flavor.
...
// If kdebug tracing is enabled, log an entry indicating that a BSD
// system call is starting, unless this system call is kdebug_trace().
...
// Retrieve the current thread and the corresponding uthread structure.
thread_act = current_thread();
uthread = get_bsdthread_info(thread_act);
...
// Ensure that the current task has a non-NULL proc structure associated
// with it; if not, terminate the current task.
...
// uu_ar0 is the address of user's saved GPR0.
uthread->uu_ar0 = (int *)regs;
// Use the system call number to retrieve the corresponding sysent
// structure. If system call number is too large, use the number 63, which
// is an internal reserved number for a nosys().
//
// In early UNIX, the sysent array had space for 64 system calls. The last
// entry (that is, sysent[63]) was a special system call.
callp = (code >= nsysent) ? &sysent[63] : &sysent[code];
if (callp->sy_narg != 0) { // if the call takes one or more arguments
void *regsp;
sy_munge_t *mungerp;
if (/* this is a 64-bit process */) {
if (/* this is a 64-bit unsafe call */) {
// Turn it into a nosys() -- use system call #63 and bail out.
...
}
// 64-bit argument munger
mungerp = callp->sy_arg_munge64;
} else { /* 32-bit process */
// 32-bit argument munger
mungerp = callp->sy_arg_munge32;
}
// Set regsp to point to either the saved GPR3 in the save area (for a
// direct system call), or to the saved GPR4 (for an indirect system
// call). An indirect system call can take at most 7 arguments.
...
// Call the argument munger.
(*mungerp)(regsp, (void *)&uthread->uu_arg[0]);
}
// Evaluate call for cancellation, and cancel, if required and possible.
...
// Take the kernel funnel if the call requires so.
...
// Assume there will be no error.
error = 0;
// Increment saved SRR0 by one instruction.
regs->save_srr0 += 4;
// Test if this is a kernel trace point -- that is, if system call tracing
// through ktrace(2) is enabled for this process. If so, write a trace
// record for this system call.
...
// If auditing is enabled, set up an audit record for the system call.
...
// Call the system call's specific handler.
error = (*(callp->sy_call))(proc, (void *)uthread->uu_arg,
&(uthread->uu_rval[0]));
// If auditing is enabled, commit the audit record.
...
// Handle return value(s)
...
// If this is a ktrace(2) trace point, write a trace record for the
// return of this system call.
...
// Drop the funnel if one was taken.
...
// If kdebug tracing is enabled, log an entry indicating that a BSD
// system call is ending, unless this system call is kdebug_trace().
...
thread_exception_return();
/* NOTREACHED */
}
unix_syscall() potentially performs several types of tracing or logging: kdebug tracing, ktrace(2) tracing, and audit logging. We will discuss kdebug and kTRace(2) later in this chapter.
Arguments are passed packaged into a structure to the call-specific handler. Let us consider the example of the socketpair(2) system call, which takes four arguments: three integers and a pointer to a buffer for holding two integers.
int socketpair(int domain, int type, int protocol, int *rsv);
The bsd/sys/sysproto.h file, which, as noted earlier, is generated by bsd/kern/makesyscalls.sh, contains argument structure declarations for all BSD system calls. Note also the use of left and right padding in the declaration of the socketpair_args structure.
// bsd/sys/sysproto.h
#ifdef __ppc__
#define PAD_(t) (sizeof(uint64_t) <= sizeof(t) \
? 0 : sizeof(uint64_t) - sizeof(t))
#else
...
#endif
#if BYTE_ORDER == LITTLE_ENDIAN
...
#else
#define PADL_(t) PAD_(t)
#define PADR_(t) 0
#endif
...
struct socketpair_args {
char domain_l_[PADL_(int)]; int domain; char domain_r_[PADR_(int)];
char type_l_[PADL_(int)]; int type; char type_r_[PADR_(int)];
char protocol_l_[PADL_(int)]; int protocol; char protocol_r_[PADR_(int)];
char rsv_l_[PADL_(user_addr_t)]; user_addr_t rsv; \
char rsv_r_[PADR_(user_addr_t)];
};
...
The system call handler function for socketpair(2) retrieves its arguments as fields of the incoming socket_args structure.
// bsd/kern/uipc_syscalls.c
// Create a pair of connected sockets
int
socketpair(struct proc *p,
struct socketpair_args *uap,
__unused register_t *retval)
{
struct fileproc *fp1, *fp2;
struct socket *so1, *so2;
int fd, error, sv[2];
...
error = socreate(uap->domain, &so1, uap->type, &uap->protocol);
...
error = socreate(uap->domain, &so2, uap->type, &uap->protocol);
...
error = falloc(p, &fp1, &fd);
...
sv[0] = fd;
error = falloc(p, &fp2, &fd);
...
sv[1] = fd;
...
error = copyout((caddr_t)sv, uap->rsv, 2 * sizeof(int));
...
return (error);
}
Note that before calling the system call handler, unix_syscall() sets the error status to zero, assuming that there will be no error. Recall that the saved SRR0 register contains the address of the instruction immediately following the system call instruction. This is where execution would resume after returning to user space from the system call. As we will shortly see, a standard user-space library stub for a BSD system call invokes the cerror() library function to set the errno variablethis should be done only if there is an error. unix_syscall() increments the saved SRR0 by one instruction, so that the call to cerror() will be skipped if there is no error. If the system call handler indeed does return an error, the SRR0 value is decremented by one instruction.
After returning from the handler, unix_syscall() examines the error variable to take the appropriate action.
If error is ERESTART, this is a restartable system call that needs to be restarted. unix_syscall() decrements SRR0 by 8 bytes (two instructions) to cause execution to resume at the original system call instruction.
If error is EJUSTRETURN, this system call wants to be returned to user space without any further processing of return values.
If error is nonzero, the system call returned an error, which unix_syscall() copies to the saved GPR3 in the process control block. It also decrements SRR0 by one instruction to cause the cerror() routine to be executed upon return to user space.
If error is 0, the system call returned success. unix_syscall() copies the return values from the uthread structure to the saved GPR3 and GPR4 in the process control block. Table 610 shows how the return values are handled.
Table 610. Handling of BSD System Call Return Values Call Return Type
Source for GPR3
Source for GPR4
Erroneous return
The error variable
Nothing
_SYSCALL_RET_INT_T
uu_rval[0]
uu_rval[1]
_SYSCALL_RET_UINT_T
uu_rval[0]
uu_rval[1]
_SYSCALL_RET_OFF_T (32-bit process)
uu_rval[0]
uu_rval[1]
_SYSCALL_RET_OFF_T (64-bit process)
uu_rval[0] and uu_rval[1] as a single u_int64_t value
The value 0
_SYSCALL_RET_ADDR_T
uu_rval[0] and uu_rval[1] as a single user_addr_t value
The value 0
_SYSCALL_RET_SIZE_T
uu_rval[0] and uu_rval[1] as a single user_addr_t value
The value 0
_SYSCALL_RET_SSIZE_T
uu_rval[0] and uu_rval[1] as a single user_addr_t value
The value 0
_SYSCALL_RET_NONE
Nothing
Nothing
Finally, to return to user mode, unix_syscall() calls thread_exception_return() [osfmk/ppc/hw_exception.s], which checks for outstanding ASTs. If any ASTs are found, ast_taken() is called. After ast_taken() returns, thread_exception_return() checks for outstanding ASTs one more time (and so on). It then jumps to .L_thread_syscall_return() [osfmk/ppc/hw_exception.s], which branches to chkfac() [osfmk/ppc/hw_exception.s], which branches to exception_exit() [osfmk/ppc/lowmem_vectors.s]. Some of the context is restored during these calls. exception_exit() eventually branches to EatRupt [ofsmk/ppc/lowmem_vectors.s], which releases the save area, performs the remaining context restoration and state cleanup, and finally executes the rfid (rfi for 32-bit) instruction to return from the interrupt.
Looking Back at System Calls
The system call mechanism in early UNIX operated similarly in concept to the one we have discussed here: It allowed a user program to call on the kernel by executing the trap instruction in user mode. The low-order byte of the instruction word encoded the system call number. Therefore, in theory, there could be up to 256 system calls. Their handler functions in the kernel were contained in a sysent table whose first entry was the indirect system call. First Edition UNIX (circa November 1971) had fewer than 35 documented system calls. Figure 617 shows a code excerpt from Third Edition UNIX (circa February 1973)note that the system call numbers for various system calls are identical to those in Mac OS X.
Figure 617. System call data structures in Third Edition UNIX
/* Third Edition UNIX */
/* ken/trap.c */
...
struct {
int count;
int (*call)();
} sysent[64];
...
/* ken/sysent.c */
int sysent[]
{
0, &nullsys, /* 0 = indir */
0, &rexit, /* 1 = exit */
0, &fork, /* 2 = fork */
2, &read, /* 3 = read */
2, &write, /* 4 = write */
2, &open, /* 5 = open */
...
0, &nosys, /* 62 = x */
0, &prproc /* 63 = special */
...
6.7.1.4. User Processing of BSD System Calls
A typical BSD system call stub in the C library is constructed using a set of macros, some of which are shown in Figure 618. The figure also shows an assembly-language fragment for the the exit() system call. Note that the assembly code is shown with a static call to cerror() for simplicity, as the invocation is somewhat more complicated in the case of dynamic linking.
Figure 618. Creating a user-space system call stub
$ cat testsyscall.h
// for system call numbers
#include <sys/syscall.h>
// taken from <architecture/ppc/mode_independent_asm.h>
#define MI_ENTRY_POINT(name) \
.globl name @\
.text @\
.align 2 @\
name:
#if defined(__DYNAMIC__)
#define MI_BRANCH_EXTERNAL(var) \
MI_GET_ADDRESS(r12,var) @\
mtctr r12 @\
bctr
#else /* ! __DYNAMIC__ */
#define MI_BRANCH_EXTERNAL(var) \
b var
#endif
// taken from Libc/ppc/sys/SYS.h
#define kernel_trap_args_0
#define kernel_trap_args_1
#define kernel_trap_args_2
#define kernel_trap_args_3
#define kernel_trap_args_4
#define kernel_trap_args_5
#define kernel_trap_args_6
#define kernel_trap_args_7
#define SYSCALL(name, nargs) \
.globl cerror @\
MI_ENTRY_POINT(_##name) @\
kernel_trap_args_##nargs @\
li r0,SYS_##name @\
sc @\
b 1f @\
blr @\
1: MI_BRANCH_EXTERNAL(cerror)
// let us define the stub for SYS_exit
SYSCALL(exit, 1)
$ gcc -static -E testsyscall.h | tr '@' '\n'
...
; indented and annotated for clarity
.globl cerror
.globl _exit
.text
.align 2
_exit:
li r0,1 ; load system call number in r0
sc ; execute the sc instruction
b 1f ; jump over blr, to the cerror call
blr ; return
1: b cerror ; call cerror, which will also return to the user
The f in the unconditional branch instruction to 1f in Figure 618 specifies the directionforward, in this case. If you have another label named 1 before the branch instruction, you can jump to it using 1b as the operand.
Figure 618 also shows the placement of the call to cerror() in the case of an error. When the sc instruction is executed, the processor places the effective address of the instruction following the sc instruction in SRR0. Therefore, the stub is set to call the cerror() function by default after the system call returns. cerror() copies the system call's return value (contained in GPR3) to the errno variable, calls cthread_set_errno_self() to set the per-thread errno value for the current thread, and sets both GPR3 and GPR4 to -1, thereby causing the calling program to receive return values of -1 whether the expected return value is one word (in GPR3) or two words (in GPR3 and GPR4).
Let us now look at an example of directly invoking a system call using the sc instruction. Although doing so is useful for demonstration, a nonexperimental user program should not use the sc instruction directly. The only API-compliant and future-proof way to invoke system calls under Mac OS X is through user libraries. Almost all supported system calls have stubs in the system library (libSystem), of which the standard C library is a subset.
As we noted in Chapter 2, the primary reason system calls must not be invoked directly in user programsespecially shipping productsis that the interfaces between system shared libraries and the kernel are private to Apple and are subject to change. Moreover, user programs are allowed to link with system libraries (including libSystem) only dynamically. This allows Apple flexibility in modifying and extending its private interfaces without affecting user programs.
With that caveat, let us use the sc instruction to invoke a simple BSD system callsay, getpid(). Figure 619 shows a program that uses both the library stub and our custom stub to call getpid(). We need an extra instructionsay, a no-opimmediately following the sc instruction, otherwise the program will behave incorrectly.
Figure 619. Directly invoking a BSD system call
// getpid_demo.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/syscall.h>
pid_t
my_getpid(void)
{
int syscallnum = SYS_getpid;
__asm__ volatile(
"lwz r0,%0\n"
"sc\n"
"nop\n" // The kernel will arrange for this to be skipped
:
: "g" (syscallnum)
);
// GPR3 already has the right return value
// Compiler warning here because of the lack of a return statement
}
int
main(void)
{
printf("my pid is %d\n", getpid());
printf("my pid is %d\n", my_getpid());
return 0;
}
$ gcc -Wall -o getpid_demo getpid_demo.c
getpid_demo.c: In function 'my_getpid':
getpid_demo.c:24: warning: control reaches end of non-void function
$ ./getpid_demo
my pid is 2345
my pid is 2345
$
Note that since user programs on Mac OS X can only be dynamically linked with Apple-provided libraries, one would expect a user program not to have any sc instructions at allit should only have dynamically resolved symbols to system call stubs. However, dynamically linked 32-bit C and C++ programs do have a couple of embedded sc instructions that come from the language runtime startup codespecifically, the __dyld_init_check() function.
; dyld.s in the source for the C startup code
/*
* At this point the dynamic linker initialization was not run so print a
* message on stderr and exit non-zero. Since we can't use any libraries the
* raw system call interfaces must be used.
*
* write(stderr, error_message, sizeof(error_message));
*/
li r5,78
lis r4,hi16(error_message)
ori r4,r4,lo16(error_message)
li r3,2
li r0,4 ; write() is system call number 4
sc
nop ; return here on error
/*
* _exit(59);
*/
li r3,59
li r0,1 ; exit() is system call number 1
sc
trap ; this call to _exit() should not fall through
trap
6.7.2. Mach Traps
Although Mach traps are similar to traditional system calls in that they are entry points into the kernel, they are different in that Mach kernel services are typically not offered directly through these traps. Instead, certain Mach traps are IPC entry points through which user-space clientssuch as the system libraryaccess kernel services by exchanging IPC messages with the server that implements those services, just as if the server were in user space.
There are almost ten times as many BSD system calls as there are Mach traps.
Consider an example of a simple Mach trapsay, task_self_trap(), which returns send rights to the task's kernel port. The documented mach_task_self() library function is redefined in <mach/mach_init.h> to be the value of the environment variable mach_task_self_, which is populated by the system library during the initialization of a user process. Specifically, the library stub for the fork() system call[10] sets up the child process by calling several initialization routines, including one that initializes Mach in the process. This latter step caches the return value of task_self_trap() in the mach_task_self_ variable.
[10] We will see how fork() is implemented in Chapter 7.
// <mach/mach_init.h>
extern mach_port_t mach_task_self_;
#define mach_task_self() mach_task_self_
...
The program shown in Figure 620 uses several apparently different ways of retrieving the same informationthe current task's self port.
Figure 620. Multiple ways of retrieving a Mach task's self port
// mach_task_self.c
#include <stdio.h>
#include <mach/mach.h>
#include <mach/mach_traps.h>
int
main(void)
{
printf("%#x\n", mach_task_self());
#undef mach_task_self
printf("%#x\n", mach_task_self());
printf("%#x\n", task_self_trap());
printf("%#x\n", mach_task_self_);
return 0;
}
$ gcc -Wall -o mach_task_self mach_task_self.c
$ ./mach_task_self
0x807
0x807
0x807
0x807
The value returned by task_self_trap() is not a unique identifier like a Unix process ID. In fact, its value will be the same for all tasks, even on different machines, provided the machines are running identical kernels.
An example of a complex Mach trap is mach_msg_overwrite_trap() [osfmk/ipc/mach_msg.c], which is used for sending and receiving IPC messages. Its implementation contains over a thousand lines of C code. mach_msg_trap() is a simplified wrapper around mach_msg_overwrite_trap(). The C library provides the mach_msg() and mach_msg_overwrite() documented functions that use these traps but also can restart message sending or receiving in the case of interruptions. User programs access kernel services by performing IPC with the kernel using these "msg" traps. The paradigm used is essentially client server, wherein the clients (programs) request information from the server (the kernel) by sending messages, and usuallybut not alwaysreceiving replies. Consider the example of Mach's virtual memory services. As we will see in Chapter 8, a user program can allocate a region of virtual memory using the Mach vm_allocate() function. Now, although vm_allocate() is implemented in the kernel, it is not exported by the kernel as a regular system call. It is available as a remote procedure in the "Kernel Server" and is callable by user clients. The vm_allocate() function that user programs call lives in the C library, representing the client end of the remote procedure call. Various other Mach services, such as those that allow the manipulation of tasks, threads, processors, and ports, are provided similarly.
Mach Interface Generator (MIG)
Implementations of Mach services commonly use the Mach Interface Generator (MIG), which simplifies the task of creating Mach clients and servers by subsuming a considerable portion of frequently used IPC code. MIG accepts a definition file that describes IPC-related interfaces using a predefined syntax. Running the MIG program/usr/bin/migon a definition file generates a C header, a client (user) interface module, and a server interface module. We will see an example of using MIG in Chapter 9. MIG definition files for various kernel services are located in the /usr/include/mach/ directory. A MIG definition file conventionally has a .def extension.
Mach traps are maintained in an array of structures called mach_trap_table, which is similar to BSD's sysent table. Each element of this array is a structure of type mach_trap_t, which is declared in osfmk/kern/syscall_sw.h. Figure 621 shows the MACH_TRAP() macro.
Figure 621. Mach trap table data structures and definitions
// osfmk/kern/syscall_sw.h
typedef void mach_munge_t(const void *, void *);
typedef struct {
int mach_trap_arg_count;
int (* mach_trap_function)(void);
#if defined(__i386__)
boolean_t mach_trap_stack;
#else
mach_munge_t *mach_trap_arg_munge32;
mach_munge_t *mach_trap_arg_munge64;
#endif
#if !MACH_ASSERT
int mach_trap_unused;
#else
const char * mach_trap_name;
#endif
} mach_trap_t;
#define MACH_TRAP_TABLE_COUNT 128
extern mach_trap_t mach_trap_table[];
extern int mach_trap_count;
...
#if !MACH_ASSERT
#define MACH_TRAP(name, arg_count, munge32, munge64) \
{ (arg_count), (int (*)(void)) (name), (munge32), (munge64), 0 }
#else
#define MACH_TRAP(name, arg_count, munge32, munge64) \
{ (arg_count), (int (*)(void)) (name), (munge32), (munge64), #name }
#endif
...
The MACH_ASSERT compile-time configuration option controls the ASSERT() and assert() macros and is used while compiling debug versions of the kernel.
The MACH_TRAP() macro shown in Figure 621 is used to populate the Mach trap table in osfmk/kern/syscall_sw.cFigure 622 shows how this is done. Mach traps on Mac OS X have numbers that start from -10, decrease monotonically, and go as high in absolute value as the highest numbered Mach trap. Numbers 0 tHRough -9 are reserved for Unix system calls and are unused. Note also that the argument munger functions are the same as those used in BSD system call processing.
Figure 622. Mach trap table initialization
// osfmk/kern/syscall_sw.c
mach_trap_t mach_trap_table[MACH_TRAP_TABLE_COUNT] = {
MACH_TRAP(kern_invalid, 0, NULL, NULL), /* Unix */ /* 0 */
MACH_TRAP(kern_invalid, 0, NULL, NULL), /* Unix */ /* -1 */
... ... ...
MACH_TRAP(kern_invalid, 0, NULL, NULL), /* Unix */ /* -9 */
MACH_TRAP(kern_invalid, 0, NULL, NULL), /* -10 */
... ...
MACH_TRAP(kern_invalid, 0, NULL, NULL), /* -25 */
MACH_TRAP(mach_reply_port, 0, NULL, NULL), /* -26 */
MACH_TRAP(thread_self_trap, 0, NULL, NULL), /* -27 */
... ...
MACH_TRAP(mach_msg_trap, 7, munge_wwwwwww, munge_ddddddd), /* -31 */
... ...
MACH_TRAP(task_for_pid, 3, munge_www, munge_ddd), /* -46 */
MACH_TRAP(pid_for_task, 2, munge_ww, munge_dd), /* -47 */
... ...
MACH_TRAP(kern_invalid, 0, NULL, NULL), /* -127 */
};
int mach_trap_count = (sizeof(mach_trap_table) / \
sizeof(mach_trap_table[0]));
...
kern_return_t
kern_invalid(void)
{
if (kern_invalid_debug)
Debugger("kern_invalid mach_trap");
return KERN_INVALID_ARGUMENT;
}
...
The assembly stubs for Mach traps are defined in osfmk/mach/syscall_sw.h using the machine-dependent kernel_trap() macro defined in osfmk/mach/ppc/syscall_sw.h. Table 611 enumerates the key files used in the implementation of these traps.
Table 611. Implementing Mach Traps in xnu File
Contents
osfmk/kern/syscall_sw.h
Declaration of the trap table structure
osfmk/kern/syscall_sw.c
Population of the trap table; definitions of default error functions
osfmk/mach/mach_interface.h
Master header file that includes headers for the various Mach APIsspecifically the kernel RPC functions corresponding to these APIs (the headers are generated from MIG definition files)
osfmk/mach/mach_traps.h
Prototypes for traps as seen from user space, including declaration of each trap's argument structure
osfmk/mach/syscall_sw.h
Instantiation of traps by defining assembly stubs, using the machine-dependent kernel_trap() macro (note that some traps may have different versions for the 32-bit and 64-bit system libraries, whereas some traps may not be available in one of the two libraries)
osfmk/mach/ppc/syscall_sw.h
PowerPC definitions of the kernel_trap() macro and associated macros; definitions of other PowerPC-only system calls
The kernel_trap() macro takes three arguments for a trap: its name, its index in the trap table, and its argument count.
// osfmk/mach/syscall_sw.h
kernel_trap(mach_reply_port, -26, 0);
kernel_trap(thread_self_trap, -27, 0);
...
kernel_trap(task_for_pid, -45, 3);
kernel_trap(pid_for_task, -46, 2);
...
Let us look at a specific example, say, pid_for_task(), and see how its stub is instantiated. pid_for_task() attempts to find the BSD process ID for the given Mach task. It takes two arguments: the port for a task and a pointer to an integer for holding the returned process ID. Figure 623 shows the implementation of this trap.
Figure 623. Setting up the pid_for_task() Mach trap
// osfmk/mach/syscall_sw.h
kernel_trap(pid_for_task, -46, 2);
...
// osfmk/mach/ppc_syscall_sw.h
#include <mach/machine/asm.h>
#define kernel_trap(trap_name, trap_number, trap_args) \
ENTRY(trap_name, TAG_NO_FRAME_USED) @\
li r0, trap_number @\
sc @\
blr
...
// osfmk/ppc/asm.h
// included from <mach/machine/asm.h>
#define TAG_NO_FRAME_USED 0x00000000
#define EXT(x) _ ## x
#define LEXT(x) _ ## x ## :
#define FALIGN 4
#define MCOUNT
#define Entry(x,tag) .text@.align FALIGN@ .globl EXT(x)@ LEXT(x)
#define ENTRY(x,tag) Entry(x,tag)@MCOUNT
...
// osfmk/mach/mach_traps.h
#ifndef KERNEL
extern kern_return_t pid_for_task(mach_port_name_t t, int *x);
...
#else /* KERNEL */
...
struct pid_for_task_args {
PAD_ARG_(mach_port_name_t, t);
PAD_ARG_(user_addr_t, pid);
};
extern kern_return_t pid_for_task(struct pid_for_task_args *args);
...
// bsd/vm/vm_unix.c
kern_return_t
pid_for_task(struct pid_for_task_args *args)
{
mach_port_name_t t = args->t;
user_addr_t pid_addr = args->pid;
...
}
Using the information shown in Figure 623, the trap definition for pid_for_task() will have the following assembly stub:
.text
.align 4
.globl _pid_for_task
_pid_for_task:
li r0,-46
sc
blr
Let us test the assembly stub by changing the stub's function name from _pid_for_task to _my_pid_for_task, placing it in a file called my_pid_for_task.S, and using it in a C program. Moreover, we can call the regular pid_for_task() to verify the operation of our stub, as shown in Figure 624.
Figure 624. Testing the pid_for_task() Mach trap
// traptest.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <mach/mach.h>
#include <mach/mach_error.h>
extern kern_return_t my_pid_for_task(mach_port_t, int *);
int
main(void)
{
pid_t pid;
kern_return_t kr;
mach_port_t myTask;
myTask = mach_task_self();
// call the regular trap
kr = pid_for_task(myTask, (int *)&pid);
if (kr != KERN_SUCCESS)
mach_error("pid_for_task:", kr);
else
printf("pid_for_task says %d\n", pid);
// call our version of the trap
kr = my_pid_for_task(myTask, (int *)&pid);
if (kr != KERN_SUCCESS)
mach_error("my_pid_for_task:", kr);
else
printf("my_pid_for_task says %d\n", pid);
exit(0);
}
$ gcc -Wall -o traptest traptest.c my_pid_for_task.S
$ ./traptest
pid_for_task says 20040
my_pid_for_task says 20040
In general, handling of Mach traps follows a similar path in the kernel as BSD system calls. shandler() identifies Mach traps by virtue of their call numbers being negative. It looks up the trap handler in mach_trap_table and performs the call.
Mach traps in Mac OS X support up to eight parameters that are passed in GPRs 3 through 10. Nevertheless, mach_msg_overwrite_TRap() takes nine parameters, but the ninth parameter is not used in practice. In the trap's processing, a zero is passed as the ninth parameter.
6.7.3. I/O Kit Traps
Trap numbers 100 through 107 in the Mach trap table are reserved for I/O Kit traps. In Mac OS X 10.4, only one I/O Kit trap is implemented (but not used): iokit_user_client_trap() [iokit/Kernel/IOUserClient.cpp]. The I/O Kit framework (IOKit.framework) implements the user-space stub for this trap.
6.7.4. PowerPC-Only System Calls
The Mac OS X kernel maintains yet another system call table called PPCcalls, which contains a few special PowerPC-only system calls. PPCcalls is defined in osfmk/ppc/PPCcalls.h. Each of its entries is a pointer to a function that takes one argument (a pointer to a save area) and returns an integer.
// osfmk/ppc/PPCcalls.h
typedef int (*PPCcallEnt)(struct savearea *save);
#define PPCcall(rout) rout
#define dis (PPCcallEnt)0
PPCcallEnt PPCcalls[] = {
PPCcall(diagCall), // 0x6000
PPCcall(vmm_get_version), // 0x6001
PPCcall(vmm_get_features), // 0x6002
... // ...
PPCcall(dis),
...
};
...
Call numbers for the PowerPC system calls begin at 0x6000 and can go up to 0x6FFFthat is, there can be at most 4096 such calls. The assembly stubs for these calls are instantiated in osfmk/mach/ppc/syscall_sw.h.
// osfmk/mach/ppc/syscall_sw.h
#define ppc_trap(trap_name,trap_number) \
ENTRY(trap_name, TAG_NO_FRAME_USED) @\
li r0, trap_number @\
sc @\
blr
...
ppc_trap(diagCall, 0x6000);
ppc_trap(vmm_get_version, 0x6001);
ppc_trap(vmm_get_features, 0x6002);
...
Note that the ppc_trap() macro is similar to the kernel_trap() macro used for defining assembly stubs for Mach traps. shandler() passes most of these calls to ppscall() [osfmk/hw_exception.s], which looks up the appropriate handler in the PPCcalls table.
Depending on their purpose, these calls can be categorized as follows:
Calls that are used for low-level performance monitoring, diagnostics, and power management (Table 612)
Table 612. PowerPC-Only Calls for Performance Monitoring, Diagnostics, and Power Management Call Number
Call Name
Purpose
0x6000
diagCall
Calls the routines implemented in the kernel's built-in diagnostics facility (see Section 6.8.8.2)
0x6009
CHUDCall
Acts as a hook for the Computer Hardware Understanding Development (CHUD) interfacedisabled to begin with, but is set to a private system call callback function when such a callback is registered by CHUD
0x600A
ppcNull
Does nothing and simply returns (a null system call); used for performance testing
0x600B
perfmon_control
Allows manipulation of the PowerPC performance-monitoring facility
0x600C
ppcNullinst
Does nothing but forces various timestamps to be returned (an instrumented null system call); used for performance testing
0x600D
pmsCntrl
Controls the Power Management Stepper
Calls that allow a user program to instantiate and control a virtual machine using the kernel's virtual machine monitor (VMM) facility (Table 613)
Table 613. PowerPC-Only Calls for the Virtual Machine Monitor Call Number
Call Name
Purpose
0x6001
vmm_get_version
Retrieves the VMM facility's version
0x6002
vmm_get_features
Retrieves the VMM facility's supported features
0x6003
vmm_init_context
Initializes a new VMM context
0x6004
vmm_dispatch
Used as an indirect system call for dispatching various VMM system callsis also an ultra-fast trap (see Section 6.7.5)
0x6008
vmm_stop_vm
Stops a running virtual machine
Calls that provide kernel assistance to the Blue Box (Classic) environment (Table 614)
Table 614. PowerPC-Only Calls for the Blue Box Call Number
Call Name
Purpose
0x6005
bb_enable_bluebox
Enables a thread for use in the Blue Box virtual machine
0x6006
bb_disable_bluebox
Disables a thread for use in the Blue Box virtual machine
0x6007
bb_settaskenv
Sets the Blue Box per-thread task environment data
6.7.5. Ultra-Fast Traps
Certain traps are handled entirely by the low-level exception handlers in osfmk/ppc/lowmem_vectors.s, without saving or restoring much (or any) state. Such traps also return from the system call interrupt very rapidly. These are the ultra-fast traps (UFTs). As shown in Figure 613, these calls have dedicated handlers in the scTable, from where the exception vector at 0xC00 loads them. Table 615 lists the ultra-fast traps.
Table 615. Ultra-Fast Traps Call Number
Association
Purpose
0xFFFF_FFFE
Blue Box only
Determines whether the given Blue Box task is preemptive, and also loads GPR0 with the shadowed task environment (MkIsPreemptiveTaskEnv)
0xFFFF_FFFF
Blue Box only
Determines whether the given Blue Box task is preemptive (MkIsPreemptiveTask)
0x8000_0000
CutTrace firmware call
Used for low-level tracing (see Section 6.8.9.2)
0x6004
vmm_dispatch
Treats certain calls (those belonging to a specific range of selectors supported by this dispatcher call) as ultra-fast trapseventually handled by vmm_ufp() [osfmk/ppc/vmachmon_asm.s]
0x7FF2
User only
Returns the pthread_self valuei.e., the thread-specific pointer (Thread Info UFT)
0x7FF3
User only
Returns floating-point and AltiVec facility statusi.e., if they are being used by the current thread (Facility Status UFT)
0x7FF4
Kernel only
Loads the Machine State Registernot used on 64-bit hardware (Load MSR UFT)
A comm area (see Section 6.7.6) routine uses the Thread Info UFT for retrieving the thread-specific (self) pointer, which is also called the per-thread cookie. The pthread_self(3) library function retrieves this value. The following assembly stub, which directly uses the UFT, retrieves the same value as the pthread_self() function in a user program.
; my_pthread_self.S
.text
.globl _my_pthread_self
_my_pthread_self:
li r0,0x7FF2
sc
blr
Note that on certain PowerPC processorsfor example, the 970 and the 970FXthe special-purpose register SPRG3, which Mac OS X uses to hold the per-thread cookie, can be read from user space.
; my_pthread_self_970.S
.text
.globl _my_pthread_self_970
_my_pthread_self_970:
mfspr r3,259 ; 259 is user SPRG3
blr
Let us test our versions of pthread_self() by using them in a 32-bit program on both a G4 and a G5, as shown in Figure 625.
Figure 625. Testing the Thread Info UFT
$ cat main.c
#include <stdio.h>
#include <pthread.h>
extern pthread_t my_pthread_self();
extern pthread_t my_pthread_self_970();
int
main(void)
{
printf("library: %p\n", pthread_self()); // call library function
printf("UFT : %p\n", my_pthread_self()); // use 0x7FF2 UFT
printf("SPRG3 : %p\n", my_pthread_self_970()); // read from SPRG3
return 0;
}
$ machine
ppc970
$ gcc -Wall -o my_pthread_self main.c my_pthread_self.S my_pthread_self_970.S
$ ./my_pthread_self
library: 0xa000ef98
UFT : 0xa000ef98
SPRG3 : 0xa000ef98
$ machine
ppc7450
$ ./my_pthread_self
library: 0xa000ef98
UFT : 0xa000ef98
zsh: illegal hardware instruction ./f
The Facility Status UFT can be used to determine which processor facilitiessuch as floating-point and AltiVecare being used by the current thread. The following function, which directly uses the UFT, will return with a word whose bits specify the processor facilities in use.
; my_facstat.S
.text
.globl _my_facstat
_my_facstat:
li r0,0x7FF3
sc
blr
The program in Figure 626 initializes a vector variable only if you run it with one or more arguments on the command line. Therefore, it should report that AltiVec is being used only if you run it with an argument.
Figure 626. Testing the Facility Status UFT
// isvector.c
#include <stdio.h>
// defined in osfmk/ppc/thread_act.h
#define vectorUsed 0x20000000
#define floatUsed 0x40000000
#define runningVM 0x80000000
extern int my_facstat(void);
int
main(int argc, char **argv)
{
int facstat;
vector signed int c;
if (argc > 1)
c = (vector signed int){ 1, 2, 3, 4 };
facstat = my_facstat();
printf("%s\n", (facstat & vectorUsed) ? \
"vector used" : "vector not used");
return 0;
}
$ gcc -Wall -o isvector isvector.c my_facstat.S
$ ./isvector
vector not used
$ ./isvector usevector
vector used
6.7.5.1. Fast Traps
A few other traps that need somewhat more processing than ultra-fast traps, or are not as beneficial to handle so urgently, are handled by shandler() in osfmk/ppc/hw_exception.s. These are called fast traps, or fastpath calls. Table 616 lists the fastpath calls. Figure 612 shows the handling of both ultra-fast and fast traps.
Table 616. Fastpath System Calls Call Number
Call Name
Purpose
0x7FF1
CthreadSetSelf
Sets a thread's identifier. This call is used by the Pthread library to implement pthread_set_self(), which is used during thread creation.
0x7FF5
Null fastpath
Does nothing. It branches straight to exception_exit() in lowmem_vectors.s.
0x7FFA
Blue Box interrupt notification
Results in the invocation of syscall_notify_interrupt() [osfmk/ppc/PseudoKernel.c], which queues an interrupt for the Blue Box and sets an asynchronous procedure call (APC) AST. The Blue Box interrupt handlerbbsetRupt() [osfmk/ppc/PseudoKernel.c]runs asynchronously to handle the interrupt.
6.7.5.2. Blue Box Calls
The Mac OS X kernel includes support code for the Blue Box virtualizer that provides the Classic runtime environment. The support is implemented as a small layer of software called the PseudoKernel, whose functionality is exported via a set of fast/ultra-fast system calls. We came across these calls in Tables 614, 615, and 616.
The truBlueEnvironment program, which resides within the Resources subdirectory of the Classic application package (Classic Startup.app), directly uses the 0x6005 (bb_enable_bluebox), 0x6006 (bb_disable_bluebox), 0x6007 (bb_settaskenv), and 0x7FFA (interrupt notification) system calls.
A specially designated threadthe Blue threadruns Mac OS while handling Blue Box interrupts, traps, and system calls. Other threads can only issue system calls. The bb_enable_bluebox() [osfmk/ppc/PseudoKernel.c] PowerPC-only system call is used to enable the support code in the kernel. It receives three arguments from the user-space caller: a task identifier, a pointer to the trap table (TWI_TableStart), and a pointer to a descriptor table (Desc_TableStart). bb_enable_bluebox() passes these arguments in a call to enable_bluebox() [osfmk/ppc/PseudoKernel.c], which aligns the passed-in descriptor address to a page, wires the page, and maps it into the kernel. The page holds a BlueThreadTrapDescriptor structure (BTTD_t), which is declared in osfmk/ppc/PseudoKernel.h. Thereafter, enable_bluebox() initializes several Blue Boxrelated fields of the thread's machine-specific state (the machine_thread structure). Figure 627 shows pseudocode depicting the operation of enable_bluebox().
Figure 627. Enabling the kernel's Blue Box support
// osfmk/ppc/thread.h
struct machine_thread {
...
// Points to Blue Box Trap descriptor area in kernel (page aligned)
unsigned int bbDescAddr;
// Points to Blue Box Trap descriptor area in user (page aligned)
unsigned int bbUserDA;
unsigned int bbTableStart;// Points to Blue Box Trap dispatch area in user
unsigned int emPendRupts; // Number of pending emulated interruptions
unsigned int bbTaskID; // Opaque task ID for Blue Box threads
unsigned int bbTaskEnv; // Opaque task data reference for Blue Box threads
unsigned int specFlags; // Special flags
...
unsigned int bbTrap; // Blue Box trap vector
unsigned int bbSysCall; // Blue Box syscall vector
unsigned int bbInterrupt; // Blue Box interrupt vector
unsigned int bbPending; // Blue Box pending interrupt vector
...
};
// osfmk/ppc/PseudoKernel.c
kern_return_t
enable_bluebox(host_t host, void *taskID, void *TWI_TableStart,
char *Desc_TableStart)
{
thread_t th;
vm_offset_t kerndescaddr, origdescoffset;
kern_return_t ret;
ppnum_t physdescpage;
BTTD_t *bttd;
th = current_thread(); // Get our thread.
// Ensure descriptor is non-NULL.
// Get page offset of the descriptor in 'origdescoffset'.
// Now align descriptor to a page.
// Kernel wire the descriptor in the user's map.
// Map the descriptor's physical page into the kernel's virtual address
// space, calling the resultant address 'kerndescaddr'. Set the 'bttd'
// pointer to 'kerndescaddr'.
// Set the thread's Blue Box machine state.
// Kernel address of the table
th->machine.bbDescAddr = (unsigned int)kerndescaddr + origdescoffset;
// User address of the table
th->machine.bbUserDA = (unsigned int)Desc_TableStart;
// Address of the trap table
th->machine.bbTableStart = (unsigned int)TWI_TableStart;
...
// Remember trap vector.
th->machine.bbTrap = bttd->TrapVector;
// Remember syscall vector.
th->machine.bbSysCall = bttd->SysCallVector;
// Remember interrupt vector.
th->machine.bbPending = bttd->PendingIntVector;
// Ensure Mach system calls are enabled and we are not marked preemptive.
th->machine.specFlags &= ~(bbNoMachSC | bbPreemptive);
// Set that we are the Classic thread.
th->machine.specFlags |= bbThread;
...
}
Once the Blue Box trap and system call tables are established, the PseudoKernel can be invoked[11] while changing Blue Box interruption state atomically. Both thandler() and shandler() check for the Blue Box during trap and system call processing, respectively.
[11] The PseudoKernel can be invoked both from PowerPC (native) and 68K (system) contexts.
thandler() checks the specFlags field of the current activation's machine_thread structure to see if the bbThread bit is set. If the bit is set, thandler() calls checkassist() [osfmk/ppc/hw_exception.s], which checks whether all the following conditions hold true.
The SRR1_PRG_TRAP_BIT bit[12] of SRR1 specifies that this is a trap.
[12] The kernel uses bit 24 of SRR1 for this purpose. This reserved bit can be implementation-defined.
The trapped address is in user space.
This is not an ASTthat is, the trap type is not a T_AST.
The trap number is not out of rangethat is, it is not more than a predefined maximum.
If all of these conditions are satisfied, checkassist() branches to atomic_switch_trap() [osfmk/ppc/atomic_switch.s], which loads the trap table (the bbTrap field of the machine_thread structure) in GPR5 and jumps to .L_CallPseudoKernel() [osfmk/ppc/atomic_switch.s].
shandler() checks whether system calls are being redirected to the Blue Box by examining the value of the bbNoMachSC bit of the specFlags field. If this bit is set, shandler() calls atomic_switch_syscall() [osfmk/ppc/atomic_switch.s], which loads the system call table (the bbSysCall field of the machine_thread structure) in GPR5 and falls through to .L_CallPseudoKernel().
In both cases, .L_CallPseudoKernel()among other thingsstores the vector contained in GPR5 in the saved SRR0 as the instruction at which execution will resume. Thereafter, it jumps to fastexit() [osfmk/ppc/hw_exception.s], which jumps to exception_exit() [osfmk/ppc/lowmem_vectors.s], thus causing a return to the caller.
A particular Blue Box trap value (bbMaxTrap) is used to simulate a return-from-interrupt from the PseudoKernel to user context. Returning Blue Box traps and system calls use this trap, which results in the invocation of .L_ExitPseudoKernel() [osfmk/ppc/atomic_switch.s].
6.7.6. The Commpage
The kernel reserves the last eight pages of every address space for the kernel-user comm areaalso referred to as the commpage. Besides being wired in kernel memory, these pages are mapped (shared and read-only) into the address space of every process. Their contents include code and data that are frequently accessed systemwide. The following are examples of commpage contents:
Specifications of processor features available on the machine, such as whether the processor is 64-bit, what the cache-line size is, and whether AltiVec is present
Frequently used routines, such as functions for copying, moving, and zeroing memory; for using spinlocks; for flushing the data cache and invalidating the instruction cache; and for retrieving the per-thread cookie
Various time-related values maintained by the kernel, allowing the current seconds and microseconds to be retrieved by user programs without making system calls
There are separate comm areas for 32-bit and 64-bit address spaces, although they are conceptually similar. We will discuss only the 32-bit comm area in this section.
Using the end of the address space for the comm area has an important benefit: It is possible to access both code and data in the comm area from anywhere in the address space, without involving the dynamic link editor or requiring complex address calculations. Absolute unconditional branch instructions, such as ba, bca, and bla, can branch to a location in the comm area from anywhere because they have enough bits in their target address encoding fields to allow them to reach the comm area pages using a sign-extended target address specification. Similarly, absolute loads and stores can comfortably access the comm area. Consequently, accessing the comm area is both efficient and convenient.
The comm area is populated during kernel initialization in a processor-specific and platform-specific manner. commpage_populate() [osfmk/ppc/commpage/commpage.c] performs this initialization. In fact, functionality contained in the comm area can be considered as processor capabilitiesa software extension to the native instruction set. Various comm-area-related constants are defined in osfmk/ppc/cpu_capabilities.h.
// osfmk/ppc/cpu_capabilities.h
// Start at page -8, ie 0xFFFF8000
#define _COMM_PAGE_BASE_ADDRESS (-8*4096)
// Reserved length of entire comm area
#define _COMM_PAGE_AREA_LENGTH (7*4096)
// Mac OS X uses two pages so far
#define _COMM_PAGE_AREA_USED (2*4096)
// The Objective-C runtime fixed address page to optimize message dispatch
#define OBJC_PAGE_BASE_ADDRESS (-20*4096)
// Data in the comm page
...
// Code in the comm page (routines)
...
// Used by gettimeofday()
#define _COMM_PAGE_GETTIMEOFDAY \
(_COMM_PAGE_BASE_ADDRESS+0x2e0)
...
The comm area's actual maximum length is seven pages (not eight) since Mach's virtual memory subsystem does not map the last page of an address space.
Each routine in the commpage is described by a commpage_descriptor structure, which is declared in osfmk/ppc/commpage/commpage.h.
// osfmk/ppc/cpu_capabilities.h
typedef struct commpage_descriptor {
short code_offset; // offset to code from this descriptor
short code_length; // length in bytes
short commpage_address; // put at this address
short special; // special handling bits for DCBA, SYNC, etc.
long musthave; // _cpu_capability bits we must have
long canthave; // _cpu_capability bits we cannot have
} commpage_descriptor;
Implementations of the comm area routines are in the osfmk/ppc/commpage/ directory. Let us look at the example of gettimeofday(), which is both a system call and a comm area routine. It is substantially more expensive to retrieve the current time using the system call. Besides a regular system call stub for gettimeofday(), the C library contains the following entry point for calling the comm area version of gettimeofday().
.globl __commpage_gettimeofday
.text
.align 2
__commpage_gettimeofday:
ba __COMM_PAGE_GETTIMEOFDAY
Note that _COMM_PAGE_GETTIMEOFDAY is a leaf procedure that must be jumped to, instead of being called as a returning function.
Note that comm area contents are not guaranteed to be available on all machines. Moreover, in the particular case of gettimeofday(), the time values are updated asynchronously by the kernel and read atomically from user space, leading to occasional failures in reading. The C library falls back to the system call version in the case of failure.
// <darwin>/<Libc>/sys/gettimeofday.c
int
gettimeofday(struct timeval *tp, struct timezone *tzp)
{
...
#if defined(__ppc__) || defined(__ppc64__)
{
...
// first try commpage
if (__commpage_gettimeofday(tp)) {
// if it fails, try the system call
if (__ppc_gettimeofday(tp,tzp)) {
return (-1);
}
}
}
#else
if (syscall(SYS_gettimeofday, tp, tzp) < 0) {
return -1;
}
#endif
...
}
Since the comm area is readable from within every process, let us write a program to display the information contained in it. Since the comm area API is private, you must include the required headers from the kernel source tree rather than a standard header directory. The program shown in Figure 628 displays the data and routine descriptors contained in the 32-bit comm area.
Figure 628. Displaying the contents of the comm area
// commpage32.c
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#define PRIVATE
#define KERNEL_PRIVATE
#include <machine/cpu_capabilities.h>
#include <machine/commpage.h>
#define WSPACE_FMT_SZ "24"
#define WSPACE_FMT "%-" WSPACE_FMT_SZ "s = "
#define CP_CAST_TO_U_INT32(x) (u_int32_t)(*(u_int32_t *)(x))
#define ADDR2DESC(x) (commpage_descriptor *)&(CP_CAST_TO_U_INT32(x))
#define CP_PRINT_U_INT8_BOOL(label, item) \
printf(WSPACE_FMT "%s\n", label, \
((u_int8_t)(*(u_int8_t *)(item))) ? "yes" : "no")
#define CP_PRINT_U_INT16(label, item) \
printf(WSPACE_FMT "%hd\n", label, (u_int16_t)(*(u_int16_t *)(item)))
#define CP_PRINT_U_INT32(label, item) \
printf(WSPACE_FMT "%u\n", label, (u_int32_t)(*(u_int32_t *)(item)))
#define CP_PRINT_U_INT64(label, item) \
printf(WSPACE_FMT "%#llx\n", label, (u_int64_t)(*(u_int64_t *)(item)))
#define CP_PRINT_D_FLOAT(label, item) \
printf(WSPACE_FMT "%lf\n", label, (double)(*(double *)(item)))
const char *
cpuCapStrings[] = {
#if defined (__ppc__)
"kHasAltivec", // << 0
"k64Bit", // << 1
"kCache32", // << 2
"kCache64", // << 3
"kCache128", // << 4
"kDcbaRecommended", // << 5
"kDcbaAvailable", // << 6
"kDataStreamsRecommended", // << 7
"kDataStreamsAvailable", // << 8
"kDcbtStreamsRecommended", // << 9
"kDcbtStreamsAvailable", // << 10
"kFastThreadLocalStorage", // << 11
#else /* __i386__ */
"kHasMMX", // << 0
"kHasSSE", // << 1
"kHasSSE2", // << 2
"kHasSSE3", // << 3
"kCache32", // << 4
"kCache64", // << 5
"kCache128", // << 6
"kFastThreadLocalStorage", // << 7
"NULL", // << 8
"NULL", // << 9
"NULL", // << 10
"NULL", // << 11
#endif
NULL, // << 12
NULL, // << 13
NULL, // << 14
"kUP", // << 15
NULL, // << 16
NULL, // << 17
NULL, // << 18
NULL, // << 19
NULL, // << 20
NULL, // << 21
NULL, // << 22
NULL, // << 23
NULL, // << 24
NULL, // << 25
NULL, // << 26
"kHasGraphicsOps", // << 27
"kHasStfiwx", // << 28
"kHasFsqrt", // << 29
NULL, // << 30
NULL, // << 31
};
void print_bits32(u_int32_t);
void print_cpu_capabilities(u_int32_t);
void print_commpage_descriptor(const char *, u_int32_t);
void
print_bits32(u_int32_t u)
{
u_int32_t i;
for (i = 32; i--; putchar(u & 1 << i ? '1' : '0'));
}
void
print_cpu_capabilities(u_int32_t cap)
{
int i;
printf(WSPACE_FMT, "cpu capabilities (bits)");
print_bits32(cap);
printf("\n");
for (i = 0; i < 31; i++)
if (cpuCapStrings[i] && (cap & (1 << i)))
printf("%-" WSPACE_FMT_SZ "s + %s\n", " ", cpuCapStrings[i]);
}
void
print_commpage_descriptor(const char *label, u_int32_t addr)
{
commpage_descriptor *d = ADDR2DESC(addr);
printf("%s @ %08x\n", label, addr);
#if defined (__ppc__)
printf(" code_offset = %hd\n", d->code_offset);
printf(" code_length = %hd\n", d->code_length);
printf(" commpage_address = %hx\n", d->commpage_address);
printf(" special = %#hx\n", d->special);
#else /* __i386__ */
printf(" code_address = %p\n", d->code_address);
printf(" code_length = %ld\n", d->code_length);
printf(" commpage_address = %#lx\n", d->commpage_address);
#endif
printf(" musthave = %#lx\n", d->musthave);
printf(" canthave = %#lx\n", d->canthave);
}
int
main(void)
{
u_int32_t u;
printf(WSPACE_FMT "%#08x\n", "base address", _COMM_PAGE_BASE_ADDRESS);
printf(WSPACE_FMT "%s\n", "signature", (char *)_COMM_PAGE_BASE_ADDRESS);
CP_PRINT_U_INT16("version", _COMM_PAGE_VERSION);
u = CP_CAST_TO_U_INT32(_COMM_PAGE_CPU_CAPABILITIES);
printf(WSPACE_FMT "%u\n", "number of processors",
(u & kNumCPUs) >> kNumCPUsShift);
print_cpu_capabilities(u);
CP_PRINT_U_INT16("cache line size", _COMM_PAGE_CACHE_LINESIZE);
#if defined (__ppc__)
CP_PRINT_U_INT8_BOOL("AltiVec available?", _COMM_PAGE_ALTIVEC);
CP_PRINT_U_INT8_BOOL("64-bit processor?", _COMM_PAGE_64_BIT);
#endif
CP_PRINT_D_FLOAT("two52 (2^52)", _COMM_PAGE_2_TO_52);
CP_PRINT_D_FLOAT("ten6 (10^6)", _COMM_PAGE_10_TO_6);
CP_PRINT_U_INT64("timebase", _COMM_PAGE_TIMEBASE);
CP_PRINT_U_INT32("timestamp (s)", _COMM_PAGE_TIMESTAMP);
CP_PRINT_U_INT32("timestamp (us)", _COMM_PAGE_TIMESTAMP + 0x04);
CP_PRINT_U_INT64("seconds per tick", _COMM_PAGE_SEC_PER_TICK);
printf("\n");
printf(WSPACE_FMT "%s", "descriptors", "\n");
// example descriptor
print_commpage_descriptor(" mach_absolute_time()",
_COMM_PAGE_ABSOLUTE_TIME);
exit(0);
}
$ gcc -Wall -I /path/to/xnu/osfmk/ -o commpage32 commpage32.c
$ ./commpage32
base address = 0xffff8000
signature = commpage 32-bit
version = 2
number of processors = 2
cpu capabilities (bits) = 00111000000000100000011100010011
+ kHasAltivec
+ k64Bit
+ kCache128
+ kDataStreamsAvailable
+ kDcbtStreamsRecommended
+ kDcbtStreamsAvailable
+ kFastThreadLocalStorage
+ kHasGraphicsOps
+ kHasStfiwx
+ kHasFsqrt
cache line size = 128
AltiVec available? = yes
64-bit processor? = yes
two52 (2^52) = 4503599627370496.000000
ten6 (10^6) = 1000000.000000
timebase = 0x18f0d27c48c
timestamp (s) = 1104103731
timestamp (us) = 876851
seconds per tick = 0x3e601b8f3f3f8d9b
descriptors =
mach_absolute_time() @ ffff8200
code_offset = 31884
code_length = 17126
commpage_address = 7883
special = 0x22
musthave = 0x4e800020
canthave = 0
6.8. Kernel Support for Debugging, Diagnostics, and Tracing
In this section, we will look at various facilities in the Mac OS X kernel for both kernel-level and application-level debugging, diagnostics, and tracing. Note that we will not discuss how to actually use the kernel debuggers herewe visit that topic in Chapter 10, in the context of creating kernel extensions.
6.8.1. GDB (Network-Based or FireWire-Based Debugging)
The most convenient way to perform kernel-level debugging on Mac OS X is through the GNU debugger, GDB, which supports either a network-based or FireWire-based kernel debugging configuration, both of which require two machines.
The standard Mac OS X kernel includes support for two-machine network-based debugging using GDB.
In the network-based configuration, GDB running on the debugging machine communicates over Ethernet with a stub in the target machine's kernel. This remote debugger protocol is called the Kernel Debugging Protocol (KDP). It uses a variant of TFTP over UDP as the core transfer protocol. The default debugger-side UDP port number is 41139. The following are examples of requests in the KDP protocol:
Connection-oriented requests (KDP_CONNECT, KDP_DISCONNECT)
Requests for obtaining client information (KDP_HOSTINFO, KDP_VERSION, KDP_MAXBYTES)
Requests for obtaining executable image information (KDP_LOAD, KDP_IMAGEPATH)
Requests for accessing memory (KDP_READMEM, KDP_WRITEMEM)
Requests for accessing registers (KDP_READREGS, KDP_WRITEREGS)
Requests for manipulating breakpoints (KDP_BREAKPOINT_SET, KDP_BREAKPOINT_REMOVE)
Each KDP requestand the corresponding replyhas its own packet format. Note that the target-side in-kernel KDP implementation does not use the kernel's networking stack but has its own minimal UDP/IP implementation.
Two fundamental functions used by the KDP implementation are for sending and receiving protocol packets. A network driver that supports kernel debugging must provide polled-mode implementations of these two functions: sendPacket(), the transmit handler, and receivePacket(), the receive handler. These functions are used only when the kernel debugger is active.
Since a network driver must explicitly support KDP, remote debugging is possible only when using network interfaces driven by such drivers. In particular, the AirPort driver does not support KDP. Hence, remote debugging cannot be done over a wireless network.
In the FireWire debugging configuration, KDP is used over a FireWire cable courtesy of a kernel extension (AppleFireWireKDP.kext) on the target machine and a translator program (FireWireKDPProxy) on the debugger machine. The translator routes data between the FireWire connection and UDP port 41139 on the debugger systemthat is, it acts as a local proxy for the target machine. GDB still performs network-based debugging, except that it communicates with localhost instead of directly communicating with the shim on the target machine.
6.8.2. KDB (Serial-Line-Based Debugging)
Although GDB is typically sufficient as a kernel debugger, the Mac OS X kernel also supports a built-in kernel debugger called KDB, which is more suitableand in some cases may be the only optionfor debugging low-level kernel components. Since remote debugging with GDB uses network or FireWire hardware, it cannot be used for kernel debugging before the required hardware is operational. For example, debugging the built-in Ethernet hardware used by GDB or low-level hardware interrupt handlers requires the use of the built-in debugger.
KDB's nature and functionality are roughly similar to that of the kdb debugger in BSD variants.
KDB also requires two machines for debugging, although the entire debugger is built into the kernel. You interact with KDB over a serial line, which means that both the target machine and the debugging machine must have serial ports. Whereas the debugging machine can have any type of serial port, including those provided by USB-based or PCI-based serial port adapters, the target machine must have a built-in hardware serial porttypically on the main logic board. Xserve is an example of a system model that has such a real serial port.
Recall the discussion of kprintf() initialization from Chapter 5. Serial output from kprintf() is disabled unless DB_KPRT is set in the debug boot argument. When debug output to a serial port is enabled, kprintf() requires a serial device that can be directly addressed, since scc_putc() performs polled I/Oit directly reads and writes serial chip registers. This allows debugging to work even with interrupts disabled. Figure 629 shows an excerpt from the initialization of the kprintf() function.
Figure 629. Initialization of the kprintf() function
// pexpert/ppc/pe_kprintf.c
void
PE_init_kprintf(boolean_t vm_initialized)
{
...
if ((scc = PE_find_scc())) { // See if we can find the serial port
scc = io_map_spec(scc, 0x1000); // Map it in
initialize_serial((void *)scc); // Start the serial driver
PE_kputc = serial_putc;
simple_lock_init(&kprintf_lock, 0);
} else
PE_kputc = cnputc;
...
}
void
serial_putc(char c)
{
(void)scc_putc(0, 1, c);
if (c == '\n')
(void)scc_putc(0, 1, '\r');
}
void
kprintf(const char *fmt, ...)
{
...
if (!disableSerialOutput) {
va_start(listp, fmt);
_doprnt(fmt, &listp, PE_kputc, 16);
va_end(listp);
}
...
}
Moreover, unlike GDB-based remote debugging, which works with the default Mac OS X kernel, using KDB requires building a custom kernel with the DEBUG configuration (see Section 6.10 for a discussion of kernel compilation).
6.8.3. CHUD Support
The Computer Hardware Understanding Development (CHUD) Tools software is a suite of graphical and command-line programs for measuring and optimizing software performance on Mac OS X.[13] It is also used for benchmarking and analyzing various aspects of system hardware. Besides user-space programs, the CHUD Tools suite employs kernel extensions (CHUDProf.kext and CHUDUtils.kext), which export a variety of functions to the user space through I/O Kit user clients.[14] Finally, the kernel implements several functions and callback hooks for use by CHUD software. The bsd/dev/ppc/chud/ and osfmk/ppc/chud/ directories contain the implementations of these functions and hooks. Many of the functions are analogous to functions in the Mach API. The CHUD-related functions and hooks in the kernel can be categorized as described in the following subsections.
[13] We enumerated the individual programs in the CHUD Tools suite in Chapter 2.
[14] An I/O Kit user client is an in-kernel object that allows a user-space application to communicate with a device that the user client represents. We will look at user clients in Chapter 10.
6.8.3.1. Task-Related Functions
Examples include chudxnu_current_task(), chudxnu_task_read(), chudxnu_task_write(), chudxnu_pid_for_task(), chudxnu_task_for_pid(), chudxnu_current_pid(), and chudxnu_is_64bit_task().
6.8.3.2. Thread-Related Functions
Examples include chudxnu_bind_thread(), chudxnu_unbind_thread(), chudxnu_thread_get_state(), chudxnu_thread_set_state(), chudxnu_thread_user_state_available(), chudxnu_thread_get_callstack(), chudxnu_thread_get_callstack64(), chudxnu_current_thread(), chudxnu_task_for_thread(), chudxnu_all_threads(), chudxnu_thread_info(), and chudxnu_thread_last_context_switch().
6.8.3.3. Memory-Related Functions
Examples include chudxnu_avail_memory_size(), chudxnu_phys_memory_size(), chudxnu_io_map(), and chudxnu_phys_addr_wimg().
6.8.3.4. CPU-Related Functions
These include functions for the following operations:
Retrieving the numbers of available and physical CPUs
Retrieving the current CPU's index
Starting and stopping CPUs
Enabling, disabling, and querying napping on a CPU
Enabling and disabling interrupts
Checking whether the current CPU is running in an interrupt context (determined by checking whether the pointer to the interrupt stack is NULL or not)
Generating a fake I/O interrupt
Reading and writing special-purpose registers
Flushing and enabling CPU caches
Acquiring and releasing the performance-monitoring facility
Performing SCOM facility reads and writes
Retrieving a pointer to and the size of the branch-tracing buffer
Retrieving and clearing interrupt counters
6.8.3.5. Callback-Related Functions
The kernel supports registration of CHUD callbacks in several kernel subsystems, such as those listed here.
Per-CPU timer callback The CPU's per_proc_info structure's pp_chud field, which is a pointer to a chudcpu_data_t structure, is used to hold timer-related data structures for this callback.
System-wide trap callback The perfTrapHook function pointer points to the callback. It is invoked from TRap() [osfmk/ppc/trap.c] during trap processing.
System-wide interrupt callback The perfIntHook function pointer points to the callback. It is invoked from interrupt() [osfmk/ppc/interrupt.c] during interrupt processing.
System-wide AST callback The perfASTHook function pointer points to the callback. It is invoked from trap() [osfmk/ppc/trap.c] during trap processing.
System-wide CPU signal callback The perfCpuSigHook function pointer points to the callback. It is invoked from cpu_signal_handler() [osfmk/ppc/cpu.c] during the processing of inter-CPU signals.
System-wide kdebug callback The kdebug_chudhook function pointer points to the callback. It is invoked from kernel_debug() [bsd/kern/kdebug.c] and its variants, which are used (as part of KERNEL_DEBUG macros) throughout the kernel for fine-grained tracing of kernel events.
System-wide system call callback The PowerPC-only system call 0x6009 becomes alivecalling it from user space invokes the callback in the kernel.
Timer callbacks Multiple timers can be allocated, and thread-based callouts can be established to run the CHUD timer callbacks.
Figure 630 shows how the kernel invokes CHUD system-wide hooks for traps and ASTs.
Figure 630. Invocation of CHUD system-wide hooks for traps and ASTs
/ osfmk/ppc/trap.c
struct savearea *
trap(int trapno, struct savearea *ssp, unsigned int dsisr, addr64_t dar)
{
...
ast_t *myast;
...
myast = ast_pending();
if (perfASTHook) {
if (*myast & AST_PPC_CHUD_ALL) {
perfASTHook(trapno, ssp, dsisr, (unsigned int)dar);
}
} else {
*myast &= ~AST_PPC_CHUD_ALL;
}
if (perfTrapHook) {
if (perfTrapHook(trapno, ssp, dsisr, (unsigned int)dar) ==
KERN_SUCCESS)
return ssp; // if it succeeds, we are done...
}
...
}
Let us look at the operation of the CHUD system call hook. We noted in Table 612 that the PowerPC-only system call 0x6009 (CHUDCall) is disabled by default. Figure 631 shows that invoking a disabled PowerPC-only system call results in an erroneous (nonzero) return.
Figure 631. Invoking a disabled PowerPC-only system call
// CHUDCall.c
#include <stdio.h>
int
CHUDCall(void)
{
int ret;
__asm__ volatile(
"li r0,0x6009\n"
"sc\n"
"mr %0,r3\n"
: "=r" (ret) // output
: // no input
);
return ret;
}
int
main(void)
{
int ret = CHUDCall();
printf("%d\n", ret);
return ret;
}
$ gcc -Wall -o CHUDCall CHUDCall.c
$ ./CHUDCall
1
Let us now see what is involved in registering a CHUD system call callback with the kernel. If successfully registered, the callback will be invoked in the kernel when the 0x6009 system call is executed. We can carry out such an experiment by using the following steps.
Create a trivial loadable kernel extension that has only the start and stop entry points. We will discuss kernel extensions in Chapter 10. You can create a trivial kernel extension using the Generic Kernel Extension template in Xcode.
Implement a wrapper function that first checks whether the callback function pointer is non-NULL, and if so, it copies the save area that the 0x6009 system call receives as an argument to a thread state structure. It then invokes the callback with a pointer to the thread state structure as an argument.
Implement a function that copies information from a save area to a thread state structure.
Implement a function that sets PPCcalls[9]the table entry for system call 0x6009to point to the wrapper. Call this function from the kernel extension's start routine.
Implement a function that disables the 0x6009 system call by setting PPCcalls[9] to NULL. Call this function from the kernel extension's stop routine.
Figure 632 shows most of the code for implementing these steps. Note that the code, including the portion that is not shown, is largely identical to the code in osfmk/ppc/chud/chud_osfmk_callback.c. To create a working kernel extension for this experiment, you need to provide the missing code.
Figure 632. Implementing a kernel extension to register a PowerPC-only system call
// CHUDSyscallExtension.c
#include <sys/systm.h>
#include <mach/mach_types.h>
#define XNU_KERNEL_PRIVATE
#define __APPLE_API_PRIVATE
#define MACH_KERNEL_PRIVATE
// Either include the appropriate headers or provide structure declarations
// for the following:
//
// struct savearea
// struct ppc_thread_state
// struct ppc_thread_state64
// PowerPC-only system call table (from osfmk/ppc/PPCcalls.h)
typedef int (* PPCcallEnt)(struct savearea *save);
extern PPCcallEnt PPCcalls[];
// The callback function's prototype
typedef kern_return_t (* ppc_syscall_callback_func_t) \
(thread_flavor_t flavor, thread_state_t tstate, \
mach_msg_type_number_t count);
// Pointer for referring to the incoming callback function
static ppc_syscall_callback_func_t callback_func = NULL;
// Identical to chudxnu_copy_savearea_to_threadstate(), which is implemented
// in osfmk/ppc/chud/chud_osfmk_callbacks.c
kern_return_t
ppc_copy_savearea_to_threadstate(thread_flavor_t flavor,
thread_state_t tstate,
mach_msg_type_number_t *count,
struct savearea *sv)
{
...
}
// PPCcalls[9] will point to this when a callback is registered
kern_return_t
callback_wrapper(struct savearea *ssp)
{
if (ssp) {
if (callback_func) {
struct my_ppc_thread_state64 state;
mach_msg_type_number_t count = PPC_THREAD_STATE64_COUNT;
ppc_copy_savearea_to_threadstate(PPC_THREAD_STATE64,
(thread_state_t)&state,
&count, ssp);
ssp->save_r3 = (callback_func)(PPC_THREAD_STATE64,
(thread_state_t)&state, count);
} else {
ssp->save_r3 = KERN_FAILURE;
}
}
return 1; // Check for ASTs
}
// Example callback function
kern_return_t
callback_func_example(thread_flavor_t flavor,
thread_state_t tstate,
mach_msg_type_number_t count)
{
printf("Hello, CHUD!\n");
return KERN_SUCCESS;
}
/ Callback registration
kern_return_t
ppc_syscall_callback_enter(ppc_syscall_callback_func_t func)
{
callback_func = func;
PPCcalls[9] = callback_wrapper;
__asm__ volatile("eieio");
__asm__ volatile("sync");
return KERN_SUCCESS;
}
// Callback cancellation
kern_return_t
ppc_syscall_callback_cancel(void)
{
callback_func = NULL;
PPCcalls[9] = NULL;
__asm__ volatile("eieio");
__asm__ volatile("sync");
return KERN_SUCCESS;
}
kern_return_t
PPCSysCallKEXT_start(kmod_info_t *ki, void *d)
{
ppc_syscall_callback_enter(callback_func_example);
printf("PPCSysCallKEXT_start\n");
return KERN_SUCCESS;
}
kern_return_t
PPCSysCallKEXT_stop(kmod_info_t *ki, void *d)
{
ppc_syscall_callback_cancel();
printf("PPCSysCallKEXT_stop\n");
return KERN_SUCCESS;
}
If you run the program from Figure 631 after the kernel extension shown in Figure 632 is loaded, you should get a zero return from the system call, and the "Hello, CHUD!" message should appear in the system log.
6.8.4. Kernel Profiling (kgmon and gprof)
The Mac OS X kernel can be compiled with support for profiling its own code. Such a compilation is achieved by selecting the PROFILE configuration before initiating a kernel build. Doing so enables several aspects of the kernel profiling mechanism, such as the following.
The kernel is compiled with the -pg GCC option, thereby generating extra code to write profiling information for subsequent analysis.
BSD initialization during system startup calls kmstartup() [bsd/kern/subr_prof.c] to initialize profiling data structures that reside in a global gmonparam structure [bsd/sys/gmon.h] called _gmonparam. Moreover, kmstartup() allocates kernel memory for holding profile data.
When running on the master processor, the kernel invokes bsd_hardclock() [bsd/kern/kern_clock.c] every time hertz_tick() [osfmk/kern/mach_clock.c] is called from the real-time clock interrupt handler. When kernel profiling is enabled, bsd_hardclock() updates the information in _gmonparam.
When kernel profiling is enabled, the code for servicing the KERN_PROF sysctl is included in kern_sysctl() [bsd/kern/kern_sysctl.c]the dispatcher for kernel-related sysctl calls.
The kgmon command-line program is used to enable or disable profiling, reset in-kernel profile buffers, and dump the contents of the profile buffers to a gmon.out file. kgmon communicates with the kernel primarily using management information base (MIB) names of the format CTL_KERNKERN_PROF<terminal name>, where <terminal name> can be one of the following:
GPROF_STATE enable or disable profiling
GPROF_COUNT retrieve buffer containing profile tick counts
GPROF_FROMS retrieve buffer containing "from" hash buckets
GPROF_TOS retrieve buffer containing "to" (destination) structures
GPROF_GMONPARAM retrieve the state of kernel profiling
Once profiling data is dumped to the gmon.out file, the standard gprof command-line program can be used to display the execution profile. Figure 633 shows an example of using kgmon and gprof.
Figure 633. Using kgmon and gprof for kernel profiling
$ uname -v # This kernel was compiled with profiling support
Darwin Kernel Version.../BUILD/obj/PROFILE_PPC
$ kgmon # Profiling should be disabled to begin with
kgmon: kernel profiling is off.
$ sudo kgmon -b # Resume the collection of profile data
kgmon: kernel profiling is running.
... # Wait for the data of interest
$ sudo kgmon -h # Stop the collection of profile data
kgmon: kernel profiling is off.
$ ls # No output files yet
$ kgmon -p # Dump the contents of the profile buffers
kgmon: kernel profiling is off.
$ ls # We should have a gmon.out file now
gmon.out
$ gprof /mach_kernel.profile gmon.out
...
granularity: each sample hit covers 4 byte(s) for 0.03% of 34.23 seconds
called/total parents
index %time self descendents called+self name index
called/total children
<spontaneous>
[1] 98.8 33.81 0.00 _machine_idle_ret [1]
-----------------------------------------------
<spontaneous>
[2] 0.6 0.22 0.00 _ml_set_interrupts_enabled [2]
...
0.00 0.00 6/117 _thread_setstatus [818]
0.00 0.00 6/6 _thread_userstack [1392]
0.00 0.00 6/6 _thread_entrypoint [1388]
0.00 0.00 3/203 _current_map [725]
0.00 0.00 3/3 _swap_task_map [1516]
0.00 0.00 3/3037 _pmap_switch [436]
...
Index by function name
[1149] _BTFlushPath [257] _fdesc_readdir [1029] _psignal_lock
...
[782] __ZN18IOMemoryDescr [697] _ipc_kobject_destro[1516] _swap_task_map
...
[27] _devfs_make_link [436] _pmap_switch [1213] _wait_queue_member
...
$
6.8.4.1. Per-Process Profiling (profil(2))
The xnu kernel implements the profil() system call that allows a user process to gather its own CPU-usage statistics by profiling the program counter.
int
profil(char *samples, size_t size, u_long offset, u_int scale)
samples is a buffer whose length is size bytes. It is divided into sequential bins of 16 bits each. offset specifies the lowest program counter (PC) value at which the kernel will sample the PC upon every clock tickit is the starting address of the program region to be sampled. For each sampled PC, the kernel increments the value in the bin whose number is computed based on the scale argument. To compute the bin number, that is, the index in the samples array, the kernel subtracts offset from the sampled PC and multiplies the result by scale. If the resultant index is within the bounds of the samples array, the corresponding bin's value is incremented; otherwise, profiling is turned off.
scale is passed as an unsigned integer but represents 16 bits' worth of fraction, with the value 1 being in the middle. Therefore, a scale value of 0x10000 results in a one-to-one mapping from PC value to bins, whereas a higher value results in a many-to-one mapping. A scale value of 0 or 1 turns off profiling.
// bsd/kern/subr_prof.c
#define PC_TO_INDEX(pc, prof) \
((int)(((u_quad_t)((pc) - (prof)->pr_off) * \
(u_quad_t)((prof)->pr_scale)) >> 16) & ~1)
Enabling profiling sets the P_PROF bit in the p_flag field of the BSD process structure. Moreover, the pr_scale field of the profiling substructure within the process structure is set to the scale value. Thereafter, as shown in Figure 634, every time there is a BSD-level clock tick (100 times a second), bsd_hardclock() checks whether the process is running in user mode and has a nonzero scale value. If so, it sets a flag (P_OWEUPC) in the process, indicating that a call to addupc_task() [bsd/kern/subr_prof.c] is owed to the task the next time there is an AST. It then calls astbsd_on() to generate an AST_BSD, which is serviced by bsd_ast(), which in turn calls addupc_task(). The latter updates the profiling buffer, turning off profiling in the case of an error.
Figure 634. Implementation of the profil() system call
// bsd/kern/kern_clock.c
void
bsd_hardclock(boolean_t usermode, caddr_t pc, int numticks)
{
register struct proc *p;
...
p = (struct proc *)current_proc();
...
if (usermode) {
if (p->p_stats && p->p_stats->p_prof.pr_scale) {
// Owe process an addupc_task() call at next AST
p->p_flag |= P_OWEUPC;
astbsd_on();
}
...
}
...
}
// bsd/kern/kern_sig.c
// called when there is an AST_BSD
void
bsd_ast(thread_t thr_act)
{
...
if ((p->p_flag & P_OWEUPC) && (p->p_flag & P_PROFIL) {
pc = get_useraddr();
addupc_task(p, pc, 1);
p->p_flag &= ~P_OWEUPC;
}
...
}
// bsd/kern/subr_prof.c
void
addupc_task(register struct proc *p, user_addr_t pc, u_int ticks)
{
...
// 64-bit or 32-bit profiling statistics collection
if (/* 64-bit process */) {
// calculate offset in profile buffer using PC_TO_INDEX()
// if target location lies within the buffer, copyin() existing
// count value from that location into the kernel
// increment the count by ticks
// copyout() the updated information to user buffer
// if there is any error, turn off profiling
} else {
// do 32-bit counterpart
}
}
Another system calladd_profil()can be used to profile multiple, noncontiguous areas of program memory. Multiple calls to add_profil() can follow a single call to profil(). Note that calling profil() removes any buffers that were allocated by one or more previous calls to add_profil().
6.8.4.2. Mach Task and Thread Sampling
Mach 3 provides calls for sampling tasks and threads. The mach_sample_thread() call periodically samples the program counter of the specified thread, saves the sampled values in buffers, and sends the buffers to the specified reply port when the buffers become full. The mach_sample_task() call performs a similar function, but for all threads of a given task. The sampled values are not tagged by thread, which means that samples for various threads will typically be intermingled.
The Mac OS X kernel includes code for Mach-based task and thread sampling. The code is conditionally compiledif MACH_PROF is defined during kernel compilation. However, note that the code is not functional.
kern_return_t
task_sample(task_t sample_task, mach_port_make_send_t reply_port);
kern_return_t
thread_sample(thread_act_t sample_thread, mach_port_make_send_t reply_port);
kern_return_t
receive_samples(mach_port_t sample_port, sample_array_t samples,
mach_msg_type_number_t sample_count);
The receive_samples() call is used to receive a message containing sampled values. It is invoked by prof_server()a MIG-generated library function that simplifies the work involved in processing the incoming IPC message.[15] We will discuss this style of IPC in Chapter 9 when we look at Mach exception handling.
[15] The prof_server() function does not exist in the default Mac OS X system library.
The osfmk/mach/prof.defs file contains the MIG definitions for this profiling interface.
6.8.5. Per-Process Kernel Tracing (kTRace(2) and kdump)
The ktrace() system call can be used on Mac OS X to enable or disable tracing of selected operations in one or more processes.
int
ktrace(const char *tracefile, // pathname of file in which to save trace records
int ops, // ktrace operation
int trpoints, // trace points of interest (what to trace)
int pid); // primary process of interest
// a negative pid specifies a process group
The ops argument to ktrace() can be one of the following:
KtrOP_SET enable trace points specified in the trpoints argument
KtrOP_CLEAR disable trace points specified in trpoints
KtrOP_CLEARFILE stop all tracing
KTRFLAG_DESCEND also apply tracing change to all children of the process specified by the pid argument
The ktrace command uses the ktrace() system call, allowing trace data to be logged to a specified file (ktrace.out by default). The kdump command displays the data in human-readable format. Only the superuser may trace setuid and setgid processes, or another user's processes.
Categories of operations to tracetrace pointsare specified by logically OR'ing respective bits and passing the resultant value as the TRpoints argument. Each selection causes the corresponding types of events to be generated at one or more locations in the kernel. The following bit values can be used to specify operation types when calling ktrace().
KTRFAC_SYSCALL TRaces BSD system calls. When this bit is set, ktrsyscall() is called by unix_syscall() before the system call handler is invoked. kTRsyscall() writes a "system call" trace record (struct ktr_syscall).
Ktr_SYSRET traces return from BSD system calls. When this bit is set, kTRsysret() is called by unix_syscall() after the system call handler has returned and return values have been processed. ktrsysret() writes a "return from system call" trace record (struct ktr_sysret).
KtrFAC_NAMEI traces name lookup operations. When this bit is set, kTRnamei() is called by namei() [bsd/vfs/vfs_lookup], sem_open() [bsd/kern/posix_sem.c], and shm_open() [bsd/kern/posix_shm.c]. kTRnamei() writes a stringthe relevant pathnameas the trace data.
KtrFAC_GENIO traces a variety of I/O operations. When this bit is set, ktrgenio() is called by recvit() and sendit() [both in bsd/kern/uipc_syscalls.c], and dofileread(), dofilewrite(), rd_uio(), and wr_uio() [all in bsd/kern/sys_generic.c]. ktrgenio() writes a "generic process I/O" trace record (struct ktr_genio).
KtrFAC_PSIG traces posted signals. When this bit is set, kTRpsig() is called by postsig() [bsd/kern/kern_sig.c]. ktrpsig() writes a "processed signal" trace record (struct ktr_psig).
KtrFAC_CSW TRaces context switches. When this bit is set, ktrcsw() is called by _sleep_continue() and _sleep() [both in bsd/kern/kern_synch.c]. ktrcsw() writes a "context switch" trace record (struct ktr_csw).
All ktrace event-logging functions are implemented in bsd/kern/kern_ktrace.c.
The p_traceflag field of the process structure holds the operation-related bits. This field also contains other relevant flags, such as the following.
KTRFAC_ACTIVE specifies that ktrace logging is in progress.
KtrFAC_ROOT specifies that the tracing status for that process was previously established by the superuser, and only the superuser can now change it further.
KTRFAC_INHERIT specifies that the child process arising out of a fork() system call will inherit the parent's p_traceflag field.
Note that a record resulting from some type of tracing event consists of a generic header (struct ktr_header) followed by an event-specific structure.
// bsd/sys/ktrace.h
struct ktr_header {
int ktr_len; // length of buffer that follows this header
short ktr_type; // trace record type
pid_t ktr_pid; // process ID generating the record
char ktr_comm[MAXCOMLEN+1]; // command name generating the record
struct timeval ktr_time; // record generation timestamp (microsecond)
caddr_t ktr_buf; // buffer
}
Event-specific structures can vary in length even for a given event type. For example, the ktr_syscall structure contains a system call number, the number of arguments passed to that system call, and an array of 64-bit unsigned integers containing the arguments.
struct ktr_syscall {
short ktr_code; // system call number
short ktr_narg; // number of arguments
u_int64_t ktr_args[1]; // a 64-bit "GPR" for each argument
};
6.8.6. Auditing Support
We briefly looked at the user-space aspects of the auditing system in Chapter 2. The Mac OS X kernel supports auditing of system events using the Basic Security Module (BSM) format for audit records. Figure 635 shows the key interactions between the user and kernel components of the auditing system.
Figure 635. The Mac OS X auditing system
[View full size image]
During the initialization of the kernel's BSD portion, audit_init() [bsd/kern/kern_audit.c] is called to initialize the auditing system. Besides allocating the relevant data structures and initializing various parameters, audit_init() also calls kau_init() [bsd/kern/kern_bsm_audit.c] to initialize the BSM audit subsystem. In particular, kau_init() sets up the initial event-to-class mappings for BSD system calls, Mach system calls, and several types of open events.
Initially, BSD and Mach system call events map to the null audit class (AU_NULL). Note that at this point, auditing is initialized but not started in the kernel. When the user-space audit daemon (auditd) starts up, it establishes a log file to which the kernel will write audit recordsauditing in the kernel begins as a side effect of this operation. Let us first look at auditd's operation while referring to Figure 635.
auditd is a simple daemon whose responsibilities are limited to managing audit log files and enabling or disabling auditing. It is not involved in the actual writing of the audit records to disk.[16] The following are auditd's primary initialization steps.
[16] In this sense, the audit daemon can be likened to the dynamic_pager program, which manages swap files. We will see the operation of dynamic_pager in Chapter 8.
It installs a signal handler for the SIGTERM and SIGCHLD signals. The handler sends a Mach IPC message to auditd's main server loop. The reason these signals are not handled in the signal handler itself is that auditd may not be at a clean point while it is in the signal handler.
It allocates a Mach port for the aforementioned signal "reflection," and anotherthe audit control portfor communication with user programs (such as the audit utility) and the kernel.
It registers the control port with the kernel as a special host-level port by calling host_set_audit_control_port(). Thereafter, clients of auditd can communicate with it by retrieving the control port via host_get_audit_control_port(). Note that it places both ports in a single port set,[17] allowing it to later wait for incoming messages on either port by using the port set alone.
[17] We will discuss the port set concept in Chapter 9.
It reads the audit control file (/etc/security/audit_control), which contains system-wide audit parameters. Once it has determined the pathname of the directory for storing audit log files (/var/audit/ by default), auditd generates the pathname for the audit log file itself and invokes the auditctl() system call to convey the pathname to the kernel. auditctl()'s kernel implementation creates a kernel thread running audit_worker() [bsd/kern/kern_audit.c]unless the thread already exists,[18] in which case it is woken up.
[18] If you enable, disable, and reenable auditing on a system, the audit_worker kernel thread will exist during the reenabling and, therefore, will be woken up.
Depending on the flags passed to the open() system call, it is treated as different types of open events. For example, if there are multiple invocations of the open() system call with the flags argument being O_RDONLY, O_WRONLY, and O_RDWR, the corresponding open events are AUE_OPEN_R, AUE_OPEN_W, and AUE_OPEN_RW, respectively.
It generates an "audit startup" audit record indicating its own startup. Note that this event is "artificial" in that it is programmatically generated by auditd. It uses functions in the BSM library (libbsm.dylib) for this purpose, which in turn use the audit() system call, a function that allows user-space applications to explicitly submit audit records to the kernel for inclusion in the audit log.
It parses the audit event file (/etc/security/audit_event), which contains descriptions of auditable events on the system, mapping an audit event number to a name (such as AUE_OPEN), a description (such as "open(2)attr only", and a class name (such as "fa"). The classes are described in /etc/security/audit_classinformation in this file must conform with the audit class information known to the kernel. For each event line in audit_event, auditd registers the class mapping by invoking the A_SETCLASS command through the auditon() system call.
It finally enters its server loop, waiting for messages to arrive on either of the control or signal ports.
The in-kernel audit record structure (struct kaudit_record) for a thread resides in the thread's corresponding utHRead structure (struct uthread).
// bsd/sys/user.h
struct uthread {
...
struct kaudit_record *uu_ar;
...
};
// bsd/bsm/audit_kernel.h
struct kaudit_record {
struct audit_record k_ar; // standard audit record
u_int32_t k_ar_commit; // AR_COMMIT_KERNEL, AR_COMMIT_USER
void *k_udata; // opaque user data, if any
u_int k_ulen; // user data length
struct uthread *k_uthread; // thread that we are auditing
TAILQ_ENTRY(kaudit_record) k_q; // queue metadata
};
The kernel allocates and initializes a new audit record for a thread by calling audit_new() [bsd/kern/kern_audit.c] when the first auditable event occurs. This is typically when either a BSD or Mach system call is invoked. However, this can also occur during the audit() system call, which, as we saw earlier, is used by a user program to submit an audit record. Since the audit() system call itself is not audited, it is possible for a uthread structure's uu_ar field to be NULL if no auditable event has occurred so far for that thread. Various portions of the kernel code use audit macros to add information to a thread's existing audit record. These macros resolve to conditional code that operates only if auditing is currently enabled. Figure 636 shows an example of audit macros.
Figure 636. Audit macros in the kernel and how they are used
// bsd/bsm/audit_kernel.h
#define AUDIT_ARG(op, args...) do { \
if (audit_enabled) \
audit_arg_ ## op (args); \
} while (0)
#define AUDIT_SYSCALL_ENTER(args...) do { \
if (audit_enabled) { \
audit_syscall_enter(args); \
} \
} while (0)
// Additional check for uu_ar since it is possible that an audit record
// was begun before auditing was disabled
#define AUDIT_SYSCALL_EXIT(error, proc, uthread) do { \
if (audit_enabled || (uthread->uu_ar != NULL)) { \
audit_syscall_exit(error, proc, uthread); \
} \
} while (0)
// bsd/dev/ppc/systemcalls.c
void
unix_syscall(struct savearea *regs)
{
...
AUDIT_SYSCALL_ENTER(code, proc, uthread);
// call the system call handler
error = (*(callp->sy_call))(proc, (void *uthread->uu_arg,
&(uthread->uu_rval[0]));
AUDIT_SYSCALL_EXIT(error, proc, uthread);
...
}
// bsd/vfs/vfs_syscalls.c
static int
open1(...)
{
...
AUDIT_ARG(fflags, oflags);
AUDIT_ARG(mode, vap->va_mode);
...
}
When audit_syscall_enter() [bsd/kern/kern_audit.c] is called, the current uthread structure's uu_ar field will be NULL. If the event corresponding to the current system call and its arguments is auditable, audit_syscall_enter() allocates an audit record and sets uu_ar to point to it. As long as the system call remains in the kernel, any audit_arg_xxx() functions that may be called by kernel code will append information to the thread's audit record. When the system call finishes, audit_syscall_exit() [bsd/kern/kern_audit.c] commits the record by calling audit_commit() [bsd/kern/kern_audit.c] and sets the uu_ar field to NULL. audit_commit() inserts the record in a queue of audit records that are ready to be written to disk. The queue is serviced by the audit worker thread, which, as we saw earlier, is created when auditd first specifies a log file pathname to the kernel. The audit worker thread writes audit records to the log file by calling audit_write() [bsd/kern/kern_audit.c], which directly writes to the log file's vnode (the audit_vp variable) through the vn_rdwr() kernel function. Auditing is disabled by passing a NULL log file pathname, which results in the audit_vp variable being set to NULL. The worker thread checks for a valid audit_vp in each iteration of its loopif audit_vp is NULL, it sets audit_enabled to 0, causing the various audit logger functions to be ineffective.
Other responsibilities of the worker thread include sending trigger messages to auditd if the log file becomes full or if the amount of free disk space on the volume containing the log file falls below a configured threshold.
6.8.7. Fine-Grained Kernel Event Tracing (kdebug)
The Mac OS X kernel provides a fine-grained kernel-tracing facility called kdebug, which can be enabled or disabled on a per-process basis. The sc_usage, fs_usage, and latency command-line tools use the kdebug facility. sc_usage displays ongoing system calls and various types of page faults. The output of fs_usage is restricted to system calls and page faults related to file system activity. latency monitors and displays scheduling and interrupt latency statistics. The kdebug facility also allows the CHUD toolkit to register a callback functionkdebug_chudhook()that will be invoked each time kdebug logging is attempted, regardless of whether kdebug tracing is enabled or not. Finally, the kdebug facility can be enabled to collect entropy and therefore can be used as a source of entropy for random number generation. The Mac OS X Security Server uses the kdebug facility to sample entropy.
Specifically, the kdebug facility can be enabled to operate in modes corresponding to one or more of the following mode bits at any given time: KDEBUG_ENABLE_TRACE, KDEBUG_ENABLE_ENTROPY, and KDEBUG_ENABLE_CHUD. The kdebug_enable global variable in the kernel holds these bits.
6.8.7.1. kdebug Tracing
The kdebug facility categorizes traced operations into classes, subclasses within classes, and codes within subclasses. Moreover, if a traced operation marks the beginning or end of a kernel function, its trace is tagged with DBG_FUNC_START and DBG_FUNC_END function qualifiers, respectively. Nonfunction traces are tagged with DBG_FUNC_NONE. Figure 637 shows an excerpt from kdebug's hierarchy of traced operation classification. Each trace record has a 32-bit debug code whose bits represent the operation's class, subclass, code, and function qualifier. The entire hierarchy is defined in bsd/sys/kdebug.h.
Figure 637. The composition of a debug code in the kdebug facility
[View full size image]
Code throughout the kernel submits operations for kdebug tracing using macros that resolve to either kernel_debug() or kernel_debug1(), both of which are implemented in bsd/kern/kdebug.c.
// bsd/sys/kdebug.h
#define KERNEL_DEBUG_CONSTANT(x,a,b,c,d,e) \
do { \
if (kdebug_enable) \
kernel_debug(x,a,b,c,d,e); \
} while(0)
#define KERNEL_DEBUG_CONSTANT1(x,a,b,c,d,e) \
do { \
if (kdebug_enable) \
kernel_debug1(x,a,b,c,d,e); \
} while(0)
...
kernel_debug1() is used during an execve() operation that follows a vfork() operationkernel_debug1() is a special version of kernel_debug() that receives the thread's identity as an argument instead of calling current_thread(), which kernel_debug() uses. current_thread() cannot be used in this particular case since it will return the parent thread.
Let us see how the BSD system call handler uses these macros. As shown in Figure 638, the handler submits trace records soon after starting and just before finishing. Note that it skips record generation if the system call number is 180 (SYS_kdebug_trace), which corresponds to the kdebug_trace() system call. Similar conceptually to the audit() system call, this call can be used by user programs to explicitly submit a trace record to the kdebug facility. As we will shortly see, the BSDDBG_CODE() macro computes a debug code given a subclass and a code within that subclass.
Figure 638. kdebug tracing in the BSD system call handler
// bsd/dev/ppc/systemcalls.c
void
unix_syscall(struct savearea *regs)
{
...
unsigned int cancel_enable;
flavor = (((unsigned int)regs->save_r0) == 0) ? 1 : 0;
if (flavor)
code = regs->save_r3;
else
code = regs->save_r0;
if (kdebug_enable && (code != 180)) {
if (flavor) // indirect system call
KERNEL_DEBUG_CONSTANT(
BSDDBG_CODE(DBG_BSD_EXCP_SC, code) | DBG_FUNC_START,
regs->save_r4, regs->save_r5,
regs->save_r6, regs->save_r7, 0);
else // direct system call
KERNEL_DEBUG_CONSTANT(
BSDDBG_CODE(DBG_BSD_EXCP_SC, code) | DBG_FUNC_START,
regs->save_r3, regs->save_r4, regs->save_r5,
regs->save_r6, 0);
}
...
// call the system call handler
...
if (kdebug_enable && (code != 180)) {
if (callp->sy_return_type == _SYSCALL_REG_SSIZE_T)
KERNEL_DEBUG_CONSTANT(
BSDDBG_CODE(DBG_BSD_EXCP_SC, code) | DBG_FUNC_END,
error, uthread->uu_rval[1], 0, 0, 0);
else
KERNEL_DEBUG_CONSTANT(
BSDDBG_CODE(DBG_BSD_EXCP_SC, code) | DBG_FUNC_END,
error, uthread->uu_rval[0], uthread->uu_rval[1],
0, 0);
}
thread_exception_return();
/* NOTREACHED */
}
The kdebug facility is accessed from user space through KERN_KDEBUG sysctl operations with CTL_KERN as the top-level sysctl identifier. Examples of supported operations include the following:
Enabling or disabling tracing (KERN_KDENABLE)
Cleaning up the relevant trace buffers (KERN_KDREMOVE)
Reinitializing the tracing facility (KERN_KDSETUP)
Specifying the trace buffer size to the kernel (KERN_KDSETBUF)
Specifying which process IDs to trace (KERN_KDPIDTR)
Specifying which process IDs to exclude (KERN_KDPIDEX)
Specifying trace points of interest to the kernel by class, by subclass, by debug code values, or by a range of debug code values (KERN_KDSETREG)
Retrieving trace buffer metainformation from the kernel (KERN_KDGETBUF)
Retrieving the trace buffer from the kernel (KERN_KDREADTR)
Note that process IDs can be either selected for or excluded from tracing. In the former case, the KDBG_PIDCHECK bit is set in the global kdebug-flags kernel variable (kdebug_flags), and the P_KDEBUG bit is set in the p_flag process structure field of each selected process. Thereafter, any process for which P_KDEBUG is not set is not traced. In the case of process ID exclusion, the KDBG_PIDEXCLUDE bit is set instead in kdebug_flags, and the P_KDEBUG bit is set for each excluded process.
As seen in the examples of supported kdebug operations, trace points of interest can be specified to the kernel in several ways: through a kdebug class (such as DBG_BSD), a subclass (such as DBG_BSD_EXCP_SC, which represents BSD system calls), up to four specific debug code values, or a range of such values. Figure 637 shows the structure of a debug code. The bsd/sys/kdebug.h header provides macros for constructing a debug code from its constituents.
Let us consider a specific example. Suppose we wish to use the kdebug facility to trace the use of the chdir() system call. The debug code for chdir() will have DBG_BSD as its class, DBG_BSD_EXP_SC as its subclass, and the system call's number (SYS_chdir) as its code. We can use the BSDDBG_CODE() macro to compute the code.
// bsd/sys/kdebug.h
#define KDBG_CODE(Class, SubClass, code) (((Class & 0xff) << 24) | \
((SubClass & 0xff << 16) | ((code & 0x3fff) << 2))
...
#define MACHDBG_CODE(SubClass, code) KDBG_CODE(DBG_MACH, SubClass, code)
#define NETDBG_CODE(SubClass, code) KDBG_CODE(DBG_NETWORK, SubClass, code)
#define FSDBG_CODE(SubClass, code) KDBG_CODE(DBG_FSYSTEM, SubClass, code)
#define BSDDBG_CODE(SubClass, code) KDBG_CODE(DBG_BSD, SubClass, code)
...
Before we look at a programming example, let us briefly discuss the operation of kernel_debug(), which is the center of kdebug activity in the kernel. It performs the following primary operations.
If the CHUD kdebug hook is registered, it calls the hook.
If entropy is being sampled, it adds an entry to the entropy buffer unless the buffer is full. We will look at entropy sampling in Section 6.8.7.2.
If KDBG_PIDCHECK is set, it returns without adding a trace record if the current process does not have P_KDEBUG set.
If KDBG_PIDEXCLUDE, which is mutually exclusive to KDBG_PIDCHECK, is set, kernel_debug() returns if the current process does not have P_KDEBUG set.
If KDBG_RANGECHECK is set, it checks whether the current debug code falls within the configured range of interesting trace points. If not, kernel_debug() returns.
If KDBG_VALCHECK is set, it compares the debug code (minus the function qualifier bits) with the four specific values, at least one of which must have been configured. If there is no match, kernel_debug() returns.
At this point, kernel_debug() records the trace entry, updates its bookkeeping data structures, and returns.
Let us now look at an example of using the kdebug facility in a user program. We will use kdebug to trace the chdir() system call. If a process ID is passed as an argument to the program, we will configure kdebug to trace only that process; otherwise, kdebug will trace on a system-wide basis. We will use kdebug's value-checking feature to configure it to trace only one specific debug codethat corresponding to the chdir() system call. Figure 639 shows the program and a sample of its usage. Note that only one program can use the kdebug tracing facility at a time.
Figure 639. Using the kdebug facility in a program
// kdebug.c
#define PROGNAME "kdebug"
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/sysctl.h>
#include <sys/ptrace.h>
#include <sys/syscall.h>
struct proc;
// Kernel Debug definitions
#define PRIVATE
#define KERNEL_PRIVATE
#include <sys/kdebug.h>
#undef KERNEL_PRIVATE
#undef PRIVATE
// Configurable parameters
enum {
KDBG_BSD_SYSTEM_CALL_OF_INTEREST = SYS_chdir,
KDBG_SAMPLE_SIZE = 16384,
KDBG_SAMPLE_INTERVAL = 100000, // in microseconds
};
// Useful constants
enum {
KDBG_FUNC_MASK = 0xfffffffc, // for extracting function type
KDBG_CLASS_MASK = 0xff000000, // for extracting class type
KDBG_CLASS_SHIFT = 24 // for extracting class type
};
// Global variables
int exiting = 0; // avoid recursion in exit handlers
size_t oldlen; // used while calling sysctl()
int mib[8]; // used while calling sysctl()
pid_t pid = -1; // process ID of the traced process
// Global flags
int trace_enabled = 0;
int set_remove_flag = 1;
// Mapping of kdebug class IDs to class names
const char *KDBG_CLASS_NAMES[256] = {
NULL, // 0
"DBG_MACH", // 1
"DBG_NETWORK", // 2
"DBG_FSYSTEM", // 3
"DBG_BSD", // 4
"DBG_IOKIT", // 5
"DBG_DRIVERS", // 6
"DBG_TRACE", // 7
"DBG_DLIL", // 8
"DBG_SECURITY", // 9
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
"DBG_MISC", // 20
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
"DBG_DYLD", // 31
"DBG_QT", // 32
"DBG_APPS", // 33
NULL,
};
// Functions that we implement (the 'u' in ukdbg represents user space)
void ukdbg_exit_handler(int);
void ukdbg_exit(const char *);
void ukdbg_setenable(int);
void ukdbg_clear();
void ukdbg_reinit();
void ukdbg_setbuf(int);
void ukdbg_getbuf(kbufinfo_t *);
void ukdbg_setpidcheck(pid_t, int);
void ukdbg_read(char *, size_t *);
void ukdbg_setreg_valcheck(int val1, int val2, int val3, int val4);
void
ukdbg_exit_handler(int s)
{
exiting = 1;
if (trace_enabled)
ukdbg_setenable(0);
if (pid > 0)
ukdbg_setpidcheck(pid, 0);
if (set_remove_flag)
ukdbg_clear();
fprintf(stderr, "cleaning up...\n");
exit(s);
}
void
ukdbg_exit(const char *msg)
{
if (msg)
perror(msg);
ukdbg_exit_handler(0);
}
// Enable or disable trace
// enable=1 enables (trace buffer must already be initialized)
// enable=0 disables
void
ukdbg_setenable(int enable)
{
mib[0] = CTL_KERN;
mib[1] = KERN_KDEBUG;
mib[2] = KERN_KDENABLE;
mib[3] = enable;
if ((sysctl(mib, 4, NULL, &oldlen, NULL, 0) < 0) && !exiting)
ukdbg_exit("ukdbg_setenable::sysctl");
trace_enabled = enable;
}
// Clean up relevant buffers
void
ukdbg_clear(void)
{
mib[0] = CTL_KERN;
mib[1] = KERN_KDEBUG;
mib[2] = KERN_KDREMOVE;
if ((sysctl(mib, 3, NULL, &oldlen, NULL, 0) < 0) && !exiting) {
set_remove_flag = 0;
ukdbg_exit("ukdbg_clear::sysctl");
}
}
// Disable any ongoing trace collection and reinitialize the facility
void
ukdbg_reinit(void)
{
mib[0] = CTL_KERN;
mib[1] = KERN_KDEBUG;
mib[2] = KERN_KDSETUP;
if (sysctl(mib, 3, NULL, &oldlen, NULL, 0) < 0)
ukdbg_exit("ukdbg_reinit::sysctl");
}
// Set buffer for the desired number of trace entries
// Buffer size is limited to either 25% of physical memory (sane_size),
// or to the maximum mapped address, whichever is smaller
void
ukdbg_setbuf(int nbufs)
{
mib[0] = CTL_KERN;
mib[1] = KERN_KDEBUG;
mib[2] = KERN_KDSETBUF;
mib[3] = nbufs;
if (sysctl(mib, 4, NULL, &oldlen, NULL, 0) < 0)
ukdbg_exit("ukdbg_setbuf::sysctl");
}
// Turn pid check on or off in the trace buffer
// check=1 turns on pid check for this and all pids
// check=0 turns off pid check for this pid (but not all pids)
void
ukdbg_setpidcheck(pid_t pid, int check)
{
kd_regtype kr;
kr.type = KDBG_TYPENONE;
kr.value1 = pid;
kr.value2 = check;
oldlen = sizeof(kd_regtype);
mib[0] = CTL_KERN;
mib[1] = KERN_KDEBUG;
mib[2] = KERN_KDPIDTR;
if ((sysctl(mib, 3, &kr, &oldlen, NULL, 0) < 0) && !exiting)
ukdbg_exit("ukdbg_setpidcheck::sysctl");
}
// Set specific value checking
void
ukdbg_setreg_valcheck(int val1, int val2, int val3, int val4)
{
kd_regtype kr;
kr.type = KDBG_VALCHECK;
kr.value1 = val1;
kr.value2 = val2;
kr.value3 = val3;
kr.value4 = val4;
oldlen = sizeof(kd_regtype);
mib[0] = CTL_KERN;
mib[1] = KERN_KDEBUG;
mib[2] = KERN_KDSETREG;
if (sysctl(mib, 3, &kr, &oldlen, NULL, 0) < 0)
ukdbg_exit("ukdbg_setreg_valcheck::sysctl");
}
// Retrieve trace buffer information from the kernel
void
ukdbg_getbuf(kbufinfo_t *bufinfop)
{
oldlen = sizeof(bufinfop);
mib[0] = CTL_KERN;
mib[1] = KERN_KDEBUG;
mib[2] = KERN_KDGETBUF;
if (sysctl(mib, 3, bufinfop, &oldlen, 0, 0) < 0)
ukdbg_exit("ukdbg_getbuf::sysctl");
}
// Retrieve some of the trace buffer from the kernel
void
ukdbg_read(char *buf, size_t *len)
{
mib[0] = CTL_KERN;
mib[1] = KERN_KDEBUG;
mib[2] = KERN_KDREADTR;
if (sysctl(mib, 3, buf, len, NULL, 0) < 0)
ukdbg_exit("ukdbg_read::sysctl");
}
int
main(int argc, char **argv)
{
int i, count;
kd_buf *kd;
char *kd_buf_memory;
kbufinfo_t bufinfo = { 0, 0, 0, 0 };
unsigned short code;
KDBG_CLASS_NAMES[255] = "DBG_MIG";
if (argc > 2) {
fprintf(stderr, "usage: %s [<pid>]\n", PROGNAME);
exit(1);
}
if (argc == 2)
pid = atoi(argv[1]);
code = KDBG_BSD_SYSTEM_CALL_OF_INTEREST;
// Arrange for cleanup
signal(SIGHUP, ukdbg_exit_handler);
signal(SIGINT, ukdbg_exit_handler);
signal(SIGQUIT, ukdbg_exit_handler);
signal(SIGTERM, ukdbg_exit_handler);
kd_buf_memory = malloc(KDBG_SAMPLE_SIZE * sizeof(kd_buf));
if (!kd_buf_memory) {
perror("malloc");
exit(1);
}
ukdbg_clear(); // Clean up related buffers
ukdbg_setbuf(KDBG_SAMPLE_SIZE); // Set buffer for the desired # of entries
ukdbg_reinit(); // Reinitialize the facility
if (pid > 0)
ukdbg_setpidcheck(pid, 1); // We want this pid
// We want this particular BSD system call
ukdbg_setreg_valcheck(BSDDBG_CODE(DBG_BSD_EXCP_SC, code), 0, 0, 0);
ukdbg_setenable(1); // Enable tracing
while (1) {
ukdbg_getbuf(&bufinfo); // Query information
oldlen = bufinfo.nkdbufs * sizeof(kd_buf); // How much to read?
ukdbg_read(kd_buf_memory, &oldlen); // Read that much
count = oldlen;
kd = (kd_buf *)kd_buf_memory;
for (i = 0; i < count; i++) {
char *qual = "";
uint64_t cpu, now;
int debugid, thread, type, class;
thread = kd[i].arg5;
debugid = kd[i].debugid;
type = debugid & KDBG_FUNC_MASK;
class = (debugid & KDBG_CLASS_MASK) >> KDBG_CLASS_SHIFT;
now = kd[i].timestamp & KDBG_TIMESTAMP_MASK;
cpu = (kd[i].timestamp & KDBG_CPU_MASK) >> KDBG_CPU_SHIFT;
if (debugid & DBG_FUNC_START)
qual = "DBG_FUNC_START";
else if (debugid & DBG_FUNC_END)
qual = "DBG_FUNC_END";
// Note that 'type' should be the system call we were looking for
// (type == BSDDBG_CODE(DBG_BSD_EXCP_SC, code) is true
printf("%lld: cpu %lld %s code %#x thread %p %s\n",
now,
cpu,
(KDBG_CLASS_NAMES[class]) ? KDBG_CLASS_NAMES[class] : "",
type,
(void *)thread,
qual);
}
usleep(KDBG_SAMPLE_INTERVAL);
}
}
$ gcc -Wall -I /path/to/xnu/bsd/ -o kdebug kdebug.c
$ ./kdebug # now use the 'cd' command from another shell
9009708884894: cpu 1 DBG_BSD code 0x40c0030 thread 0x47f9948 DBG_FUNC_START
9009708885712: cpu 1 DBG_BSD code 0x40c0030 thread 0x47f9948 DBG_FUNC_END
^Ccleaning up...
The file /usr/share/misc/trace.codes maps kdebug codes to operation names. It is used by programs such as sc_usage and latency to display debug codes in human-readable forms.
6.8.7.2. kdebug Entropy Collection
As we noted earlier, the kdebug facility can be enabled to sample system entropy. The samples are collected in a kernel buffer, which is then copied to user space. These steps occur within a single sysctl call. Input to this call includes a user buffer for receiving entropy entries, the buffer's size, and a timeout value. The Mac OS X Security Server (/usr/sbin/securityd) contains a system entropy management module that uses kdebug entropy collection. It performs the following operations.
It reads the saved entropy file (/var/db/SystemEntropyCache) on startup and seeds the random number generator (RNG) for initial use.
It periodically calls the kdebug facility to collect and retrieve system entropy, which it uses to seed the RNG.
It periodically saves the RNG's entropy to the entropy file for use across reboots.
// <darwin>/<securityd>/src/entropy.cpp
void
EntropyManager::collectEntropy()
{
int mib[4];
mib[0] = CTL_KERN;
mib[1] = KERN_KDEBUG;
mib[2] = KERN_KDGETENTROPY;
mib[3] = 1; // milliseconds of maximum delay
mach_timespec_t timings[timingsToCollect];
size_t size = sizeof(timings);
int ret = sysctl(mib, 4, timings, &size, NULL, 0);
...
}
The in-kernel handler for the KERN_KDGETENTROPY sysctl calls kdbg_getentropy() [bsd/kern/kdebug.c], which performs the following operations.
If entropy is already being sampled, it returns EBUSY. This is because there can be at most one instance of entropy collection in the kdebug facility.
It uses kmem_alloc() to allocate a kernel buffer large enough to hold the number of entropy entries requested. If this allocation fails, it returns EINVAL.
If the caller-specified timeout value is less than 10 ms, it alters the timeout to be 10 ms.
It enables entropy sampling by setting the KDEBUG_ENABLE_ENTROPY bit in the kdebug_enable variable.
It calls tsleep() [bsd/kern/kern_synch.c] to sleep for the timeout duration.
Upon being woken up, it disables entropy sampling by clearing the KDEBUG_ENABLE_ENTROPY bit in kdebug_enable.
It copies the kernel entropy buffer to the user buffer that was passed in the sysctl call.
It calls kmem_free() to free the in-kernel entropy buffer.
Enabling entropy collection causes kernel_debug() to collect timestamp entries in the entropy buffer. Note that this is independent of whether kdebug tracing is enabled or the CHUD hook is installed.
// bsd/kern/kdebug.c
void
kernel_debug(debugid, arg1, arg2, arg3, arg4, arg5)
{
...
if (kdebug_enable & KDEBUG_ENABLE_ENTROPY) {
// collect some more entropy
if (kd_entropy_index < kd_entropy_count) {
kd_entropy_buffer[kd_entropy_index] = mach_absolute_time();
kd_entropy_index++;
}
// do we have enough timestamp entries to fill the entropy buffer?
if (kd_entropy_index == kd_entropy_count) {
// disable entropy collection
kdebug_enable &= ~KDEBUG_ENABLE_ENTROPY;
kdebug_slowcheck &= ~SLOW_ENTROPY;
}
}
...
}
6.8.8. Low-Level Diagnostics and Debugging Interfaces
The Mac OS X kernel provides a low-level diagnostics and debugging interface that can be enabled at boot time by passing the diag argument to the kernel. Depending on the specific flags passed through this argument, the kernel enables specific features and behavior. The various flag bits that can be set in the diag value[19] are defined in osfmk/ppc/Diagnostics.h. Table 617 lists these flags and their purpose.
[19] The diag value is passed as a logical OR of these flag bits.
Table 617. Flags for Enabling Diagnostic Features at Boot Time Name
Value
Description
enaExpTrace
0x0000_0001
This bit enables ultra-low-level diagnostics tracing in the kernel. The built-in kernel debugger, KDB, can be used to view the trace records.
enaUsrFCall
0x0000_0002
This bit enables the firmware call interface to be used from user space. The interface provides hardware-dependent low-level functionality.
enaUsrPhyMp
0x0000_0004
This bit is unused.
enaDiagSCs
0x0000_0008
This bit enables the diagnostics system call interface.
enaDiagDM
0x0000_0010
If this bit is set, the driver for /dev/mem allows access to the entire physical memory (mem_actual), even if available physical memory has been limited through the maxmem boot-time argument.
enaDiagEM
0x0000_0020
This bit causes the special opcode 0, along with an extended opcode that is one of the X-form instructions capable of taking an alignment interrupt, to simulate an alignment exception. This is to facilitate debugging of the alignment handler.
enaDiagTrap
0x0000_0040
This bit enables the special diagnostics trap, which is of the form twi 31,r31,0xFFFX, with X being a hexadecimal digit. When enabled, the trap returns from the kernel with a return value of 1.
enaNotifyEM
0x0000_0080
Instructions that fail because of operand alignment can be emulated by low-level kernel code. This bit enables notification of such emulationsay, for logging of unaligned accesses. chudxnu_passup_alignment_exceptions() [osfmk/ppc/chud/chud_cpu.c]a CHUD toolkit internal functioncan set or clear this bit.
The flags listed in Table 617 are likely to be useful only to Mac OS X core kernel developers. Enabling any of the diagnostics or low-level debugging features on production systems is not recommended.
6.8.8.1. Firmware Call Interface
The firmware call interface is so called because it provides functionality that is considered an extension of the hardwareit is not an interface to Open Firmware. Its functionality includes the following:
Writing debugging information (such as register contents) to a printer or modem port (dbgDispCall)
Storing words to physical memory and clearing physical pages (StoreRealCall and ClearRealCall, respectively)
Loading BAT registers (LoadDBATsCall and LoadIBATsCall)
Creating fake I/O and decrementer interrupts (CreateFakeIOCall and CreateFakeDECCall, respectively)
Crashing the system immediately (Choke)
Shutting down the system immediately (CreateShutdownCTXCall)
Switching context (SwitchContextCall)
Preempting (DoPreemptCall)
Note that the kernel uses certain firmware calls during its regular operation. For example, _ml_set_interrupts_enabled() [osfmk/ppc/machine_routines_asm.s] can conditionally invoke the DoPreemptCall() system call from within the kernel. Similarly, context-switching assembly code in osfmk/ppc/cswtch.s uses the SwitchContextCall() firmware call.
Let us look at an example of using the firmware call interface from user space. A system call is treated as a firmware call if its number has its high-order bit set to 1. As we saw in Figure 612, the xcpSyscall() handler tests for firmware calls early. Whereas firmware calls are always allowed from supervisor state, they must be explicitly enabled for user space, as noted in Table 617. If xcpSyscall() finds the high-order bit set in the system call number, and the call is allowed, it forwards the call to FirmwareCall() [xnu/osfmk/ppc/Firmware.s]. The low-order bits in the system call number represent the index of the desired call in the firmware call table (FWtable), which is declared in osfmk/ppc/Firmware.s and populated in osfmk/ppc/FirmwareCalls.h. The lowest numbered firmware callCutTraceCall()has the number 0x80000000. Looking at osfmk/ppc/FirmwareCalls.h, we find that the Choke() call is at index 0xa, whereas the CreateShutdownCTXCall() call is at index 0xd.
Firmware call indices may not be the same across all kernel versions.
For example, if we make a system call with the value 0x8000000a in GPR0, the system will crash. Similarly, a call with the value 0x8000000d in GPR0 will cause the system to shut down instantly, as if all power to the system has been cut.
6.8.8.2. Diagnostics System Call Interface
The diagnostics system call interface is somewhat more generally interesting for experimentation than the firmware call interface. Its functionality includes routines for the following operations:
Adjusting the Timebase Registerused for testing drift recovery
Returning the physical address of a page
Accessing physical memory (including copying physical memory pages, and reading or writing individual bytes)
Soft-resetting the processor
Forcing all caches, including the TLB, to be reinitialized
Retrieving boot-screen information
A complete list of implemented diagnostics system calls can be seen in osfmk/ppc/Diagnostics.c. As shown in Table 612, the PowerPC-specific diagCall() system call has the number 0x6000. This system call invokes diagCall() [osfmk/ppc/Diagnostics.c]the dispatcher for these system calls. diagCall() uses the value in GPR3 to determine the specific diagnostics operation to perform. The available operations are defined in osfmk/ppc/Diagnostics.h.
// osfmk/ppc/Diagnostics.h
#define diagSCnum 0x00006000
#define dgAdjTB 0
#define dgLRA 1
#define dgpcpy 2
#define dgreset 3
#define dgtest 4
#define dgBMphys 5
#define dgUnMap 6
#define dgBootScreen 7
...
#define dgKfree 22
#define dgWar 23
...
Let us look at examples of using the diagnostics system calls. First, we create a common header filediagCommon.hcontaining code we will use in all examples in this section. Figure 640 shows diagCommon.h.
Figure 640. Common header file for using the diagnostics system call interface
// diagCommon.h
#ifndef _DIAG_COMMON_H_
#define _DIAG_COMMON_H_
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <ppc/types.h>
#define _POSIX_C_SOURCE
#include <stdlib.h>
#include <unistd.h>
struct savearea;
// These headers are not available outside of the kernel source tree
#define KERNEL_PRIVATE
#include <ppc/Diagnostics.h>
#include <console/video_console.h>
#undef KERNEL_PRIVATE
// The diagCall() prototype in Diagnostics.h is from the kernel's standpoint
// -- having only one argument: a pointer to the caller's save area. Our user-
// space call takes a variable number of arguments.
//
// Note that diagCall() does have a stub in libSystem.
//
// Here we declare a prototype with a variable number of arguments, define
// a function pointer per that prototype, and point it to the library stub.
typedef int (*diagCall_t)(int op, ...);
diagCall_t diagCall_ = (diagCall_t)diagCall;
// Defined in osfmk/vm/pmap.h, which may not be included from user space
#define cppvPsrc 2
#define cppvNoRefSrc 32
// Arbitrary upper limit on the number of bytes of memory we will handle
#define MAXBYTES (8 * 1024 * 1024)
#endif // _DIAG_COMMON_H_
Before the diagnostics system call interface can be used, it must be enabled at boot time by passing the diag=<number> boot argument, where <number> contains set bits corresponding to the enaDiagSCs constant (0x8), as noted in Table 617. Similarly, the firmware interface is enabled by passing set bits corresponding to the enaUsrFCall constant (0x2). For example, to enable both interfaces, pass diag=0xa, since 0xa is the logical OR of 0x8 and 0x2.
Retrieving Boot-Screen Information
In this example, we will write a program that uses the dgBootScreen() call to retrieve the boot-screen "video" information from the kernel. The information is maintained in a structure of type vc_info. The system console code in osfmk/console/video_console.c manages this structure. Figure 641 shows the output of our program on a system with a 1280x854 display attached to it. Note that the quantity labeled "physical address" shows the location of the raw framebuffer in physical memory.
Figure 641. Retrieving boot-screen information using a diagnostics system call
// diagBootScreen.c
#include "diagCommon.h"
int
main(int argc, char **argv)
{
struct vc_info vc_info;
if (diagCall_(dgBootScreen, &vc_info) < 0)
exit(1);
printf("%ldx%ld pixels, %ldx%ld characters, %ld-bit\n",
vc_info.v_width, vc_info.v_height,
vc_info.v_columns, vc_info.v_rows,
vc_info.v_depth);
printf("base address %#08lx, physical address %#08lx\n",
vc_info.v_baseaddr, vc_info.v_physaddr);
printf("%ld bytes used for display per row\n",
vc_info.v_rowscanbytes);
exit(0);
}
$ gcc -Wall -I /path/to/xnu/osfmk/ -o diagBootScreen diagBootScreen.c
$ ./diagBootScreen
1280x854 pixels, 160x53 characters, 32-bit
base address 0x2f72c000, physical address 0xb8010000
5120 bytes used for display per row
Retrieving the Physical Address for a Virtual Address
In this example, we will use the dgLRA() (where LRA stands for logical-to-real address) call to retrieve the physical page, and therefore the physical address, for a given virtual address in the address space of the calling process. If the virtual address is not mapped in the caller's address space, the dgLRA() system call returns a nonzero value. We can verify this program by retrieving the physical address of the page beginning at virtual address 0xFFFF8000as we saw earlier, this is the base virtual address of the comm area and should map to the same physical page in all user address spaces. Figure 642 shows the program.
Figure 642. Retrieving the physical address (if any) for a virtual address in the caller's address space
// diagLRA.c
#include "diagCommon.h"
#define PROGNAME "diagLRA"
int
main(int argc, char **argv)
{
u_int32_t phys, virt;
u_int64_t physaddr;
if (argc != 2) {
printf("usage: %s <virtual address in hex>\n", PROGNAME);
exit(1);
}
// Must be in hexadecimal
virt = strtoul(argv[1], NULL, 16);
phys = diagCall_(dgLRA, virt);
if (!phys) {
printf("virtual address %08x :: physical page none\n", virt);
exit(1);
}
physaddr = (u_int64_t)phys * 0x1000ULL + (u_int64_t)(virt & 0xFFF);
printf("virtual address %#08x :: physical page %#x (address %#llx)\n",
virt, phys, physaddr);
exit(0);
}
$ gcc -Wall -I /path/to/xnu/osfmk/ -o diagLRA diagLRA.c
$ ./diagLRA 0x0
virtual address 00000000 :: physical page none
$ ./diagLRA 0xFFFF8000
virtual address 0xFFFF8000 :: physical page 0x1669 (address 0x1669000)
...
Examining Physical Memory
The dgpcpy() diagnostics system call copies physical memory into a supplied buffer. In this example, we will write a program that uses this call to fetch physical memory and dumps it on the standard output. Thereafter, we can either redirect the program's output to a file or pipe it through a utility such as hexdump to view the memory's contents in different formats. Figure 643 shows the program.
Figure 643. Retrieving physical memory using a diagnostics system call
// diagpcpy.c
#include "diagCommon.h"
#define PROGNAME "diagpcpy"
void usage(void);
int
main(int argc, char **argv)
{
int ret;
u_int32_t phys;
u_int32_t nbytes;
char *buffer;
if (argc != 3)
usage();
phys = strtoul(argv[1], NULL, 16);
nbytes = strtoul(argv[2], NULL, 10);
if ((nbytes < 0) || (phys < 0))
usage();
nbytes = (nbytes > MAXBYTES) ? MAXBYTES : nbytes;
buffer = (char *)malloc(nbytes);
if (buffer == NULL) {
perror("malloc");
exit(1);
}
// copy physical to virtual
ret = diagCall_(dgpcpy, 0, phys, 0, buffer, nbytes, cppvPsrc|cppvNoRefSrc);
(void)write(1, buffer, nbytes);
free(buffer);
exit(0);
}
void
usage(void)
{
printf("usage: %s <physical addr> <bytes>\n", PROGNAME);
printf("\tphysical address must be specified in hexadecimal\n");
printf("\tnumber of bytes to copy must be specified in decimal\n");
exit(1);
}
$ gcc -Wall -I /path/to/xnu/osfmk/ -o diagpcpy diagpcpy.c
...
We can test the operation of our diagpcpy program by examining physical memory that is known to contain specific information. Recall from Figure 628 that the beginning of the comm area contains a string signature. Moreover, we know that the comm area should begin at virtual address 0xFFFF8000 in every user virtual address space, and we determined the corresponding physical address using our diagLRA program in a previous example.
$ ./diagLRA 0xFFFF8000
virtual address 0xFFFF8000 :: physical page 0x1669 (address 0x1669000)
$ ./diagpcpy 0x1669000 16 | strings
commpage 32-bit
$
Let us look at another example. We know that the exception vectors reside in physical memory starting at address 0x0. We can retrieve the contents of that page and compare them with the contents of the __interrupts section in the __VECTORS segment within the kernel executable.
$ ./diagpcpy 0x0 4096 > /tmp/phys0.out
$ hexdump -v /tmp/phys0.out | less
...
0000100 7db2 43a6 7d73 43a6 81a0 00f0 7d60 0026
0000110 2c0d 0001 4082 001c 3960 0000 9160 00f0
0000120 8080 00f4 8060 00f8 7c88 03a6 4e80 0020
...
$ otool -s __VECTORS __interrupts /mach_kernel | less
/mach_kernel:
Contents of (__VECTORS,__interrupts) section
...
00000100 7db243a6 7d7343a6 81a000f0 7d600026
00000110 2c0d0001 4082001c 39600000 916000f0
00000120 808000f4 806000f8 7c8803a6 4e800020
...
Note that the hexdump output is formatted somewhat differently from otool's output. The modern-day hexdump program's output format can be configured through format strings. In this example, you can make the hexdump output identical to otool's as follows:
$ echo '"%07.7_Ax\\n"\n"%07.7_ax " 4/4 "%08x " "\\n"' | \
hexdump -v -f /dev/stdin /tmp/phys0.out
...
00000100 7db243a6 7d7343a6 81a000f0 7d600026
00000110 2c0d0001 4082001c 39600000 916000f0
00000120 808000f4 806000f8 7c8803a6 4e800020
...
Finally, let us retrieve a few bytes from the physical address 0x5000 and attempt to interpret them as a character string. We will see the reason for this string in Section 6.8.9.1.
$ ./diagpcpy 0x5000 8 | strings
Hagfish
Capturing a Screenshot of the Text Console
We earlier determined the physical base address of the boot display's framebuffer using the diagBootScreen program. Since diagpcpy allows us to dump physical memory, we could capture a raw screenshot of the display using these two programs. In particular, this gives us a way of capturing a screenshot of the textual console. Let us consider the example of the display shown in Figure 641; the relevant information is repeated here.
$ ./diagBootScreen
1280x854 pixels, 160x53 characters, 32-bit
base address 0x2f72c000, physical address 0xb8010000
5120 bytes used for display per row
Given the information dumped by diagBootScreen, we can see that our screenshot capturing on this particular system would involve copying a certain amount of physical memory starting at physical address 0xb8010000. Since this is a 32-bit framebuffer and there are 1280 x 854 pixels, the number of bytes we need to retrieve is 4 x 1280 x 854, which is 4,372,480.
$ ./diagpcpy 0xb8010000 4372480 > display.dump
$ file display.dump
display.dump: data
Note that diagpcpy is not the only way to read physical memory from user space. Given the appropriate privileges, a user program can read physical memory and kernel virtual memory through the /dev/mem and /dev/kmem devices, respectively. The kernel functions that serve these devices are implemented in bsd/dev/ppc/mem.c.
At this point, the display.dump file contains raw pixel dataa linear sequence of 32-bit pixel values in row-major order. When treated as big-endian, each pixel value contains a leading pad byte followed by 8-bit red, green, and blue components, in that order. We can convert this raw data to an image formatsay, TIFF or JPEGby using a variety of image-processing tools. For example, we can write a trivial Perl script to remove the padding from each pixel value to create a new raw pixel data file, which can then be converted to easily viewable image formats. The following example uses the freely available rawtoppm and ppmtojpeg command-line programs.
$ cat unpad.pl
#! /usr/bin/perl -w
my $PROGNAME = "unpad";
if ($#ARGV != 1) {
die "usage: $PROGNAME <infile> <outfile>\n";
}
open(I, "<$ARGV[0]") or die "$!\n";
open(O, ">$ARGV[1]") or die "$!\n";
my $ibuf;
while (sysread(I, $buf, 4) == 4) {
my ($pad, $r, $g, $b) = unpack('C4', $buf);
$buf = pack('C3', $r, $g, $b);
syswrite(O, $buf, 3);
}
close(I);
close(O);
exit(0);
$ ./unpad.pl display.dump display-rgb.raw
$ rawtoppm -rgb -interpixel 1280 854 display-rgb.raw > display.ppm
$ ppmtojpeg display.ppm > display.jpg
A user program can retrieve the base address of a framebuffer using the CGDisplayBaseAddress() Quartz Services API call. Thereafter, the program can access and modify the framebuffer memorysay, using the read() and write() system calls. We will see an API-compliant version of the screenshot-capturing example in Chapter 10.
6.8.9. Low-Level Kernel Tracing
In addition to the various tracing facilities we have seen so far, the Mac OS X kernel contains yet another tracing facility used for low-level tracing. We have alluded to this facility earlier in this chapter in the context of the CutTrace() system call, which is the means by which kernel code logs low-level traces into a kernel buffer. The buffer can be examined from within KDB. Before we discuss this tracing mechanism, let us look at the low-memory global (lowglo) data structures, which also include the work area for this mechanism.
6.8.9.1. Low-Memory Global Data Structures
We have seen earlier that the first 32KB of physical memorythe low memorycontains critical kernel data and code. For example, the PowerPC exception vectors begin at physical address 0x0. The low-level exception filter table (xcpTable) and the first-level system call dispatch table (scTable), both of which are implemented in osfmk/ppc/lowmem_vectors.s, also reside in low memory. Another low-memory area instantiated in lowmem_vectors.s is lowGloa structure of type lowglo that contains global (as opposed to per-processor) constants, data areas, and pointers. These entities are accessed by kernel code directly using absolute addresses. Hence, they must reside in low physical memory. The logGlo area starts at physical address 0x5000 and is a page long. The next physical pagestarting at address 0x6000is a shared page mapped into the kernel's address space; it can be used for low-level kernel debugging. Figure 644 shows the structure of the lowGlo area.
Figure 644. The low-memory global data area
[View full size image]
In Section 6.8.8.2, we found that memory at physical address 0x5000 contains the word Hagfish. To be precise, it is the string "Hagfish" (with a trailing space). It is an "eyecatcher" string used as a system verification code at the beginning of the lowGlo area. Let us use our diagpcpy program from Section 6.8.8.2 to glean some more information from this area.
Kernel Version String
As shown in Figure 644, physical address 0x501C contains a pointer to the kernel version string.
$ ./diagpcpy 0x501C 4 | hexdump
0000000 0033 1da0
0000004
$ ./diagpcpy 0x00331da0 128 | strings
Darwin Kernel Version 8.6.0: ... root:xnu-792.6.70.obj~1/RELEASE_PPC
8.6.0
Darwin
Per-Processor Information Areas
As shown in Figure 644, physical address 0x5010 points to the start of the array containing per-processor information entries. Each entry is a structure of type per_proc_entry, which contains a pointer to a structure of type per_proc_info. The latter contains a variety of static and dynamic information about a processorfor example, the hardware exception counters (struct hwCtrs). Figure 645 shows excerpts from these structures, some of which we came across in Chapter 5.
Figure 645. Data structures for holding per-processor information
// osfmk/ppc/exception.h
#pragma pack(4)
struct hwCtrs {
unsigned int hwInVains;
unsigned int hwResets;
unsigned int hwMachineChecks;
unsigned int hwDSIs;
unsigned int hwISIs;
unsigned int hwExternals;
unsigned int hwAlignments;
unsigned int hwPrograms;
...
};
#pragma pack()
typedef struct hwCtrs hwCtrs;
...
#pragma pack(4)
struct per_proc_info {
unsigned int cpu_number;
...
hwCtrs hwCtr; // begins at offset 0x800 within the structure
...
}
#pragma pack()
...
#define MAX_CPUS 256
struct per_proc_entry {
addr64_t ppe_paddr;
unsigned int ppe_pad4[1];
struct per_proc_info *ppe_vaddr;
};
extern struct per_proc_entry PerProcTable[MAX_CPUS-1];
Let us retrieve the value of a particular hardware countersay, hwProgramsusing diagpcpy. We will do so for the first processor, in case there are multiple processors. Given the information in Figures 644 and 645, we can compute the physical address of interest using the following information.
The address 0x5010 contains a pointer to PerProcTablewe are interested in the first entry of PerProcTable.
The ppe_addr field is at an offset of 12 bytesthe sum of the sizes of an addr64_t and an unsigned int.
Resolving the ppe_addr pointer gives us the per_proc_info structure of the processor.
The hwCtr structure is at an offset 0x800 within the per_proc_info structure.
The hwPrograms counter is at an offset of 28 bytesseven times the size of an unsigned intwithin the hwCtr structure, and therefore, at an offset of 0x81c (0x800 + 28) bytes within per_proc_info.
Let us now retrieve the value of hwPrograms.
$ ./diagpcpy 0x5010 4 | hexdump # this will give us the address of PerProcTable
00000000 0035 d000
00000004
$ ./diagpcpy 0x35d000 16 | hexdump # fourth 32-bit word is the first
# processor's ppe_vaddr
00000000 0000 0000 0035 e000 0000 0000 0035 e000
00000010
$ ./diagpcpy 0x35e81c 4 | hexdump # add 0x81c to get the address of hwPrograms
00000000 0000 0000
00000004
In this example, the counter's value is 0. Let us execute a trivial program that we know will increment the value of this counter. For example, executing an illegal hardware instructionsay, a supervisor-only instruction in user modewill result in a T_PROGRAM exception, which is counted by hwPrograms. Figure 646 shows a program that causes a T_PROGRAM to be generated.
Figure 646. Causing an exception and retrieving the corresponding counter from the kernel
// gentprogram.c
#if defined(__GNUC__)
#include <ppc_intrinsics.h>
#endif
int
main(void)
{
return __mfspr(1023);
}
$ gcc -Wall -o gentprogram gentprogram.c
$ ./gentprogram
zsh: illegal hardware instruction ./gentprogram
$ ./diagpcpy 0x35e81c 4 | hexdump
00000000 0000 0001
00000004
$ ./gentprogram; ./gentprogram; ./gentprogram
zsh: illegal hardware instruction ./gentprogram
zsh: illegal hardware instruction ./gentprogram
zsh: illegal hardware instruction ./gentprogram
$ ./diagpcpy 0x35e81c 4 | hexdump
00000000 0000 0004
00000004
Figure 647 shows a portion of exception-handling code that increments various hardware exception counters. Note that the order of counter fields within the hwCtr structure is matched with the definition of exception numbersan exception number is the offset of the corresponding counter within hwCtr. For example, T_PROGRAM is defined to be (0x07 * T_VECTOR_SIZE) in osfmk/ppc/exception.h, with T_VECTOR_SIZE being 4.
Figure 647. Maintenance of hardware exception counters in the kernel
// osfmk/ppc/genassym.c
...
DECLARE("hwCounts", offsetof(struct per_proc_info *, hwCtr);
...
; osfmk/ppc/lowmem_vectors.s
.L_exception_entry:
...
xcpCommon:
...
Redrive:
...
mfsprg r2,0 ; restore per_proc (SPRG0 contains per_proc ptr)
...
la r12,hwCounts(r2) ; point to the exception count area
...
add r12,r12,r11 ; point to the count (r11 contains T_XXX)
lwz r25,0(r12) ; get the old value
...
add r25,r25,r24 ; count this one (r24 will be 1 or 0)
...
stw r25,0(r12) ; store it back
...
6.8.9.2. Low Tracing
Let us now continue our discussion of "CutTrace" low-level tracing, or simply low tracing. Figure 644 shows an area called lgTrcWork: This is the control block for low tracing. It is a structure of type TRaceWork.
// osfmk/ppc/low_trace.h
typedef struct traceWork {
unsigned int traceCurr; // Address of next slot
unsigned int traceMask; // Types to be traced
unsigned int traceStart; // Start of trace table
unsigned int traceEnd; // End of trace table
unsigned int traceMsnd; // Saved trace mask
unsigned int traceSize; // Size of trace table
unsigned int traceGas[2];
} traceWork;
Similarly, lgdgWork is the diagnostic work areait is a structure of type diagWork, which is declared in osfmk/ppc/Diagnostics.h. One of this structure's fields, dgFlags, stores the diagnostic flags. The flags passed through the diag boot-time argument are stored here.
Low tracing is enabled by setting the enaExpTrace bit in the value of the diag boot-time argument. It can be limited to a specific processor by providing the processor number through the ctrc boot-time argument. Moreover, the size of the kernel buffer used for low tracing can be adjusted through the tb boot-time argument. ppc_init() [osfmk/ppc/ppc_init.c] processes these arguments during early system startup. Figure 648 shows this processing.
Figure 648. Processing of low-tracing-related boot-time arguments during system startup
// osfmk/ppc/genassym.c
...
DECLARE("trcWork", offsetof(struct lowglo *, lgTrcWork));
...
// osfmk/ppc/ppc_init.c
void
ppc_init(boot_args *args)
{
...
// Set diagnostic flags
if (!PE_parse_boot_arg("diag", &dgWork.dgFlags))
dgWork.dgFlags = 0;
...
// Enable low tracing if it is requested
if (dgWork.dgFlags & enaExpTrace)
trcWork.traceMask = 0xFFFFFFFF;
// See if tracing is limited to a specific processor
if (PE_parse_boot_arg("ctrc", &cputrace)) {
trcWork.traceMask = (trcWork.traceMask & 0xFFFFFFF0) | (cputrace & 0xF);
}
// See if we have a nondefault trace-buffer size
if (!PE_parse_boot_arg("tb", &trcWork.traceSize)) {
#if DEBUG
trcWork.traceSize = 32; // Default 32-page trace table for DEBUG
#else
trcWork.traceSize = 8; // Default 8-page trace table for RELEASE
}
// Adjust trace table size, if not within minimum/maximum limits
if (trcWork.traceSize < 1)
trcWork.traceSize = 1; // Must be at least 1 page
if (trcWork.traceSize > 256)
trcWork.traceSize = 256; // Can be at most 256 pages
// Convert from number of pages to number of bytes
trcWork.traceSize = trcWork.traceSize * 4096;
...
}
Later during the system startup, pmap_bootstrap() [osfmk/ppc/pmap.c] reserves physical memory for the trace table.
// osfmk/ppc/pmap.c
void
pmap_bootstrap(uint64_t memsize, vm_offset_t *first_avail, unsigned int kmapsize)
{
...
trcWork.traceCurr = (unsigned int)addr; // set first trace slot to use
trcWork.traceStart = (unsigned int)addr; // set start of trace table
trcWork.traceEnd = (unsigned int)addr + trcWork.traceSize; // set end
...
}
As we noted earlier, low tracing is performed through the CutTrace() system call, a firmware call that is also an ultra-fast trapit is handled in osfmk/ppc/lowmem_vectors.s (in fact, before any other ultra-fast trap). The firmware call interface provides a stub, dbgTrace() [osfmk/ppc/Firmware.s], to call CutTrace().
; osfmk/ppc/Firmware.s
;
; dbgTrace(traceID, item1, item2, item3, item4)
;
.align 5
.globl EXT(dbgTrace)
LEXT(dbgTrace)
mr r2,r3 ; trace ID
mr r3,r4 ; item1
lis r0,HIGH_ADDR(CutTrace) ; top half of firmware call number
mr r4,r5 ; item2
mr r5,r6 ; item3
ori r0,r0,LOW_ADDR(CutTrace) ; bottom half
mr r6,r7 ; item4
sc ; invoke the system call
blr ; done
Various portions of the kernel add low-trace records either by calling dbgTrace() or by directly invoking the CutTrace() system call. In the latter case, the trace identifier is passed in GPR2. Figure 649 shows an example of kernel code creating a low-trace record.
Figure 649. An example of low-trace record generation by kernel code
// osfmk/ipc/ipc_kmsg.c
mach_msg_return_t
ipc_kmsg_get(mach_vm_address_t msg_addr,
mach_msg_size_t size,
ipc_kmsg_t *kmsgp)
{
...
#ifdef ppc
if (trcWork.traceMask)
dbgTrace(0x1100,
(unsigned int)kmsg->ikm_header->msgh_id,
(unsigned int)kmsg->ikm_header->msgh_remote_port,
(unsigned int)kmsg->ikm_header->msgh_local_port,
0);
#endif
...
}
A low-trace record is a structure of type LowTraceRecord, which is declared in osfmk/ppc/low_trace.h. Information contained in a LowTraceRecord structure includes the following:
Processor number
Exception code
Upper and lower parts of the Timebase Register
Contents of the following registers: CR, DSISR, SRR0, SRR1, DAR, LR, CTR, and GPRs 0 through 6
Save area
A convenient way to view low-trace records is through the built-in kernel debugger KDB, whose lt command formats and displays these records.
db{0}> lt 0
...
00ADEA80 0 00000002 FD6D0959 - 0C00
DAR/DSR/CR: 00000000A000201C 40000000 84024A92
SRR0/SRR1 00000000000D6D00 1000000000001030
LR/CTR 00000000000D61F4 00000000000344A8
R0/R1/R2 FFFFFFFF80000000 000000001759BD00 0000000000004400
R3/R4/R5 0000000002626E60 000000000002CD38 0000000001E5791C
R6/sv/rsv 000000002FD78780 0000000000000000 00000000
6.9. Virtual Machine Monitor
The Mac OS X kernel (PowerPC) implements a virtual machine monitor (VMM) facility, which a user-space program can use to dynamically create and manipulate virtual machine (VM) contexts. Each VM instance has its own processor state and address space, both of which are controlled by the VMM. A program executing in a VM is referred to as a guest. The facility is primarily implemented in the vmachmon.h, vmachmon.c, and vmachmon_asm.s files within the osfmk/ppc/ directory. The files osfmk/ppc/hw_vm.s and osfmk/ppc/hw_exception.s also contain support code for the VMM facility.
Let us revisit Figure 68, in which we saw that a thread's machine-dependent statethe machine_thread structure [osfmk/ppc/thread.h]contains pointers to VMM control table (vmmCntrlTable) and VMM control table entry (vmmCntrlEntry) structures. vmmCntrlTable is non-NULL for a thread that is using the VMM facility. When the thread is running a VM, its vmmCntrlEntry points to the current emulation context but is NULL otherwise. Figure 650 shows these data structures.
Figure 650. Control data structures for the VMM facility
// osfmk/ppc/vmachmon.h
#define kVmmMaxContexts 32
...
typedef struct vmmCntrlEntry {
unsigned int vmmFlags; // Assorted control flags
unsigned int vmmXAFlgs; // Extended Architecture (XA) flags
// Address of context communication area
vmm_state_page_t *vmmContextKern; // Kernel virtual address
ppnum_t vmmContextPhys; // Physical address
vmm_state_page_t *vmmContextUser; // User virtual address
facility_context vmmFacCtx; // Header for VMX and FP contexts
pmap_t vmmPmap; // Last dispatched pmap
uint64_t vmmTimer; // Last set timer value (0 if unset)
unsigned int vmmFAMintercept; // FAM intercepted exceptions
} vmmCntrlEntry;
typedef struct vmmCntrlTable {
unsigned int vmmGFlags; // Global flags
addr64_t vmmLastMap; // Last vaddr mapping mode
// An entry for each possible VMM context
vmmCntrlEntry vmmc[kVmmMaxContexts];
pmap_t vmmAdsp[kVmmMaxContexts]; // Guest address space maps
} vmmCntrlTable;
...
For each VM, the VMM allocates a page of memory for holding the VM's context communications area, which is accessed both as a vmm_comm_page_t structure and a vmm_state_page_t structurethe former embeds the latter.
// osfmk/ppc/vmachmon.h
typedef struct vmm_comm_page_t {
union {
vmm_state_page_t vmcpState; // Reserve area for state
unsigned int vmcpPad[768]; // Reserve state for 3/4 page state area
} vmcpfirst;
unsigned int vmcpComm[256]; // Last 1024 bytes used as a communications
// area in a function-specific manner
} vmm_comm_page_t;
...
A VM's processor state is stored in a vmm_processor_state_t structure within a vmm_state_page_t structure. A vmm_processor_state_t includes the processor's general-purpose, floating-point, vector, and special-purpose registers. Note that the vmcpComm field of the vmm_comm_page_t structure is used as a general-purpose communications buffer by several VMM functions. For example, the vmm_map_list function, which maps a list of pages into a guest address space, reads the list of pages as a sequence of { host virtual address, guest virtual address } pairs from vmcpComm.
6.9.1. Features
The specific features provided by the VMM facility on a system depend on the host processor and the facility's version. The following are examples of VMM features.
kVmmFeature_LittleEndian The VMM supports a VM to be run in little-endian mode. This feature is available only on PowerPC processors that implement the optional little-endian facility. Therefore, it is unavailable on G5-based systems.
kVmmFeature_Stop The VMM supports stopping and resuming VMs.
kVmmFeature_ExtendedMapping The VMM supports extended protection modes for address space mappings.
kVmmFeature_ListMapping The VMM supports mapping a list of pages into and unmapping a list of pages from guest address spaces.
kVmmFeature_FastAssist The VMM supports an optimization called fast assist mode (FAM). In this mode, the ultra-fast-path VMM system calls are valid. As noted in Table 615, the calls are handled by vmm_ufp() [osfmk/ppc/vmachmon_asm.s], which ensures that the incoming system call number is within the range of calls designated as FAM calls (kVmmResumeGuest tHRough kVmmSetGuestRegister).
kVmmFeature_XA The VMM supports getting and setting extended architecture (XA) flags in the control table entry of each VM context controlled by it.
kVmmFeature_SixtyFourBit The VMM provides 64-bit support on 64-bit host processors.
kVmmFeature_MultAddrSpace The VMM allows multiple address spaces, with each address space capable of handling the maximum virtual address supported by the host processor. A guest VM may be launched using any one address space.
6.9.2. Using the VMM Facility
The VMM facility exports its functionality through routines such as those for initializing a VM context (vmm_init_context), releasing a VM context (vmm_tear_down_context), and mapping a page from the host address space to a guest address space (vmm_map_page). Most of these routines do not have corresponding stubs in a user-space library; they are accessed using a dispatcher routine (vmm_dispatch), which does have a user-space stub. When the C library is compiled, it picks up assembly stubs for Mach trapsincluding VMM-related trapsfrom header files such as <mach/syscall_sw.h> and <mach/ppc/syscall_sw.h>.
$ nm -oj /usr/lib/libSystem.dylib | grep -i vmm
/usr/lib/libSystem.dylib:mach_traps.So:_vmm_dispatch
/usr/lib/libSystem.dylib:mach_traps.So:_vmm_get_features
/usr/lib/libSystem.dylib:mach_traps.So:_vmm_get_version
/usr/lib/libSystem.dylib:mach_traps.So:_vmm_init_context
/usr/lib/libSystem.dylib:mach_traps.So:_vmm_stop_vm
The vmm_dispatch() system call allows all exported VMM routines to be invoked from user space. The index of the desired routine is passed as the first argument to vmm_dispatch(), followed by arguments specific to that function. Here is an example of what using the VMM facility looks like.
// Get VMM version
version = vmm_dispatch(kVmmGetVersion);
...
// Get VMM features to know what we may or may not use
features = vmm_dispatch(kVmmGetFeatures);
// Allocate page-aligned memory for use with the VMM/VMs
kr = vm_allocate(myTask, &vmmCommPage, pageSize, TRUE);
...
kr = vm_allocate(...);
...
// Initialize a new VM context
kr = vmm_dispatch(kVmmInitContext, version, vmmCommPage);
// Set up the VM's initial processor state
...
// Map pages, or page lists, into the VM's address space
// Actual pages to map are in a separate "communication" page
kr = vmm_dispatch(kVmmMapList, vmmIndex, nPages, is64bit);
// Launch the VM
// Mapping a page and setting the VM running can be combined
kr = vmm_dispatch(kVmmMapExecute, vmmIndex, aPage, vAddress, protectionBits);
// Handle things when control comes back to the VMM
...
// Stop and resume the VM
...
// Tear down the VM context
kr = vmm_dispatch(kVmmTearDownContext, vmmIndex);
6.9.3. Example: Running Code in a Virtual Machine
Let us now look at a real programming example of using the VMM facility. In our program, vmachmon32, we will perform the following sequence of steps.
Retrieve the VMM version and feature set supported by the kernel using vmm_get_version() and vmm_get_features(), respectively.
Allocate page-aligned memory using Mach's vm_allocate() memory allocation function. Specifically, we will allocate a page for the VM's state, a page for the VM's stack, and a page for the VM's text (code).
Set the VM's program counter to the beginning of the text page.
Set the VM's stack pointer to the end of the stack page, while taking the Red Zone into account.
Populate the text page either with a handcrafted sequence of instructions or with machine code for a function that calculates the factorial of its argument. We will obtain the function's machine code by statically compiling its C source.
Map the stack page and the text page into the VM's address space.
Set the VM running. When the VM runs out of code to execute, we will ensure that it returns to the VMM by making the final instruction that it executes illegal.
Moreover, we will print the contents of several VM registers at the end of the program, which will allow us to see the result of the code that the VM ran. You can run full-fledged programs (including operating systems)rather than primitive codewithin a VM provided the resources needed for the program to run are made available in the VM's address space.
Figure 651 shows the source for vmachmon32. It is liberally annotated with comments, including further description of using the VMM facility.
Figure 651. A program to run machine code within a VM using the VMM facility
// vmachmon32.c
// Mac OS X Virtual Machine Monitor (Vmm) facility demonstration
#define PROGNAME "vmachmon32"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <mach/mach.h>
#include <architecture/ppc/cframe.h>
#ifndef _VMACHMON32_KLUDGE_
// We need to include xnu/osfmk/ppc/vmachmon.h, which includes several other
// kernel headers and is not really meant for inclusion in user programs.
// We perform the following kludges to include vmachmon.h to be able to
// compile this program:
//
// 1. Provide dummy forward declarations for data types that vmachmon.h
// needs, but we will not actually use.
// 2. Copy vmachmon.h to the current directory from the kernel source tree.
// 3. Remove or comment out "#include <ppc/exception.h>" from vmachmon.h.
//
struct savearea; // kludge #1
typedef int ReturnHandler; // kludge #1
typedef int pmap_t; // kludge #1
typedef int facility_context; // kludge #1
#include "vmachmon.h" // kludge #2
#endif
#define OUT_ON_MACH_ERROR(msg, retval) \
if (kr != KERN_SUCCESS) { mach_error("*** " msg ":" , kr); goto out; }
// vmm_dispatch() is a PowerPC-only system call that allows us to invoke
// functions residing in the Vmm dispatch table. In general, Vmm routines
// are available to user space, but the C library (or another library) does
// not contain stubs to call them. Thus, we must go through vmm_dispatch(),
// using the index of the function to call as the first parameter in GPR3.
//
// Since vmachmon.h contains the kernel prototype of vmm_dispatch(), which
// is not what we want, we will declare our own function pointer and set
// it to the stub available in the C library.
//
typedef kern_return_t (* vmm_dispatch_func_t)(int, ...);
vmm_dispatch_func_t my_vmm_dispatch;
// Convenience data structure for pretty-printing Vmm features
struct VmmFeature {
int32_t mask;
char *name;
} VmmFeatures[] = {
{ kVmmFeature_LittleEndian, "LittleEndian" },
{ kVmmFeature_Stop, "Stop" },
{ kVmmFeature_ExtendedMapping, "ExtendedMapping" },
{ kVmmFeature_ListMapping, "ListMapping" },
{ kVmmFeature_FastAssist, "FastAssist" },
{ kVmmFeature_XA, "XA" },
{ kVmmFeature_SixtyFourBit, "SixtyFourBit" },
{ kVmmFeature_MultAddrSpace, "MultAddrSpace" },
{ kVmmFeature_GuestShadowAssist, "GuestShadowAssist" },
{ kVmmFeature_GlobalMappingAssist, "GlobalMappingAssist" },
{ kVmmFeature_HostShadowAssist, "HostShadowAssist" },
{ kVmmFeature_MultAddrSpaceAssist, "MultAddrSpaceAssist" },
{ -1, NULL },
};
// For Vmm messages that we print
#define Printf(fmt, ...) printf("Vmm> " fmt, ## __VA_ARGS__)
// PowerPC instruction template: add immediate, D-form
typedef struct I_addi_d_form {
u_int32_t OP: 6; // major opcode
u_int32_t RT: 5; // target register
u_int32_t RA: 5; // register operand
u_int32_t SI: 16; // immediate operand
} I_addi_d_form;
// PowerPC instruction template: unconditional branch, I-form
typedef struct branch_i_form {
u_int32_t OP: 6; // major opcode
u_int32_t LI: 24; // branch target (immediate)
u_int32_t AA: 1; // absolute or relative
u_int32_t LK: 1; // link or not
} I_branch_i_form;
// PowerPC instruction template: add, XO-form
typedef struct I_add_xo_form {
u_int32_t OP: 6; // major opcode
u_int32_t RT: 5; // target register
u_int32_t RA: 5; // register operand A
u_int32_t RB: 5; // register operand B
u_int32_t OE: 1; // alter SO, OV?
u_int32_t XO: 9; // extended opcode
u_int32_t Rc: 1; // alter CR0?
} I_add_xo_form;
// Print the bits of a 32-bit number
void
prbits32(u_int32_t u)
{
u_int32_t i = 32;
for (; i > 16 && i--; putchar(u & 1 << i ? '1' : '0'))
;
printf(" ");
for (; i--; putchar(u & 1 << i ? '1' : '0'))
;
printf("\n");
}
// Function to initialize a memory buffer with some machine code
void
initGuestText_Dummy(u_int32_t *text,
vm_address_t guestTextAddress,
vmm_regs32_t *ppcRegs32)
{
// We will execute a stream of a few instructions in the virtual machine
// through the Vmm (that is, us). I0 and I1 will load integer values into
// registers GPR10 and GPR11. I3 will be an illegal instruction. I2 will
// jump over I3 by unconditionally branching to I4, which will sum GPR10
// and GPR11, placing their sum in GPR12.
//
// We will allow I5 to either be illegal, in which case control will
// return to the Vmm, or, be a branch to itself: an infinite
// loop. One Infinite Loop.
//
I_addi_d_form *I0;
I_addi_d_form *I1;
I_branch_i_form *I2;
// I3 is illegal
I_add_xo_form *I4;
I_branch_i_form *I5;
// Guest will run the following instructions
I0 = (I_addi_d_form *)(text + 0);
I1 = (I_addi_d_form *)(text + 1);
I2 = (I_branch_i_form *)(text + 2);
text[3] = 0xdeadbeef; // illegal
I4 = (I_add_xo_form *)(text + 4);
// Possibly overridden by an illegal instruction below
I5 = (I_branch_i_form *)(text + 5);
// Use an illegal instruction to be the last inserted instruction (I5)
// in the guest's instruction stream
text[5] = 0xfeedface;
// Fill the instruction templates
// addi r10,0,4 ; I0
I0->OP = 14;
I0->RT = 10;
I0->RA = 0;
I0->SI = 4; // load the value '4' in r10
// addi r11,0,5 ; I1
I1->OP = 14;
I1->RT = 11;
I1->RA = 0;
I1->SI = 5; // load the value '5' in r11
// ba ; I2
// We want to branch to the absolute address of the 5th instruction,
// where the first instruction is at guestTextAddress. Note the shifting.
//
I2->OP = 18;
I2->LI = (guestTextAddress + (4 * 4)) >> 2;
I2->AA = 1;
I2->LK = 0;
// I3 is illegal; already populated in the stream
// add r12,r10,r11 ; I4
I4->OP = 31;
I4->RT = 12;
I4->RA = 10;
I4->RB = 11;
I4->OE = 0;
I4->XO = 266;
I4->Rc = 0;
// I5 is illegal or an infinite loop; already populated in the stream
Printf("Fabricated instructions for executing "
"in the guest virtual machine\n");
}
// Function to initialize a memory buffer with some machine code
void
initGuestText_Factorial(u_int32_t *text,
vm_address_t guestTextAddress,
vmm_regs32_t *ppcRegs32)
{
// Machine code for the following function:
//
// int
// factorial(int n)
// {
// if (n <= 0)
// return 1;
// else
// return n * factorial(n - 1);
// }
//
// You can obtain this from the function's C source using a command-line
// sequence like the following:
//
// $ gcc -static -c factorial.c
// $ otool -tX factorial.o
// ...
//
u_int32_t factorial_ppc32[] = {
0x7c0802a6, 0xbfc1fff8, 0x90010008, 0x9421ffa0,
0x7c3e0b78, 0x907e0078, 0x801e0078, 0x2f800000,
0x419d0010, 0x38000001, 0x901e0040, 0x48000024,
0x805e0078, 0x3802ffff, 0x7c030378, 0x4bffffc5,
0x7c621b78, 0x801e0078, 0x7c0201d6, 0x901e0040,
0x807e0040, 0x80210000, 0x80010008, 0x7c0803a6,
0xbbc1fff8, 0x4e800020,
};
memcpy(text, factorial_ppc32, sizeof(factorial_ppc32)/sizeof(u_int8_t));
// This demo takes an argument in GPR3: the number whose factorial is to
// be computed. The result is returned in GPR3.
//
ppcRegs32->ppcGPRs[3] = 10; // factorial(10)
// Set the LR to the end of the text in the guest's virtual address space.
// Our demo will only use the LR for returning to the Vmm by placing an
// illegal instruction's address in it.
//
ppcRegs32->ppcLR = guestTextAddress + vm_page_size - 4;
Printf("Injected factorial instructions for executing "
"in the guest virtual machine\n");
}
// Some modularity... these are the demos our program supports
typedef void (* initGuestText_Func)(u_int32_t *, vm_address_t, vmm_regs32_t *);
typedef struct {
const char *name;
initGuestText_Func textfiller;
} Demo;
Demo SupportedDemos[] = {
{
"executes a few hand-crafted instructions in a VM",
initGuestText_Dummy,
},
{
"executes a recursive factorial function in a VM",
initGuestText_Factorial,
},
};
#define MAX_DEMO_ID (sizeof(SupportedDemos)/sizeof(Demo))
static int demo_id = -1;
void
usage(int argc, char **argv)
{
int i;
if (argc != 2)
goto OUT;
demo_id = atoi(argv[1]);
if ((demo_id >= 0) && (demo_id < MAX_DEMO_ID))
return;
OUT:
fprintf(stderr, "usage: %s <demo ID>\nSupported demos:\n"
" ID\tDescription\n", PROGNAME);
for (i = 0; i < MAX_DEMO_ID; i++)
fprintf(stderr, " %d\t%s\n", i, SupportedDemos[i].name);
exit(1);
}
int
main(int argc, char **argv)
{
int i, j;
kern_return_t kr;
mach_port_t myTask;
unsigned long *return_params32;
vmm_features_t features;
vmm_regs32_t *ppcRegs32;
vmm_version_t version;
vmm_thread_index_t vmmIndex; // The VM's index
vm_address_t vmmUStatePage = 0; // Page for VM's user state
vmm_state_page_t *vmmUState; // It's a vmm_comm_page_t too
vm_address_t guestTextPage = 0; // Page for guest's text
vm_address_t guestStackPage = 0; // Page for guest's stack
vm_address_t guestTextAddress = 0;
vm_address_t guestStackAddress = 0;
my_vmm_dispatch = (vmm_dispatch_func_t)vmm_dispatch;
// Ensure that the user chose a demo
usage(argc, argv);
// Get Vmm version implemented by this kernel
version = my_vmm_dispatch(kVmmGetVersion);
Printf("Mac OS X virtual machine monitor (version %lu.%lu)\n",
(version >> 16), (version & 0xFFFF));
// Get features supported by this Vmm implementation
features = my_vmm_dispatch(kVmmvGetFeatures);
Printf("Vmm features:\n");
for (i = 0; VmmFeatures[i].mask != -1; i++)
printf(" %-20s = %s\n", VmmFeatures[i].name,
(features & VmmFeatures[i].mask) ? "Yes" : "No");
Printf("Page size is %u bytes\n", vm_page_size);
myTask = mach_task_self(); // to save some characters (sure)
// Allocate chunks of page-sized page-aligned memory
// VM user state
kr = vm_allocate(myTask, &vmmUStatePage, vm_page_size, VM_FLAGS_ANYWHERE);
OUT_ON_MACH_ERROR("vm_allocate", kr);
Printf("Allocated page-aligned memory for virtual machine user state\n");
vmmUState = (vmm_state_page_t *)vmmUStatePage;
// Guest's text
kr = vm_allocate(myTask, &guestTextPage, vm_page_size, VM_FLAGS_ANYWHERE);
OUT_ON_MACH_ERROR("vm_allocate", kr);
Printf("Allocated page-aligned memory for guest's " "text\n");
// Guest's stack
kr = vm_allocate(myTask, &guestStackPage, vm_page_size, VM_FLAGS_ANYWHERE);
OUT_ON_MACH_ERROR("vm_allocate", kr);
Printf("Allocated page-aligned memory for guest's stack\n");
// We will lay out the text and stack pages adjacent to one another in
// the guest's virtual address space.
//
// Virtual addresses increase -->
// 0 4K 8K 12K
// +--------------------------------------------+
// | __PAGEZERO | GUEST_TEXT | GUEST_STACK |
// +--------------------------------------------+
//
// We put the text page at virtual offset vm_page_size and the stack
// page at virtual offset (2 * vm_page_size).
//
guestTextAddress = vm_page_size;
guestStackAddress = 2 * vm_page_size;
// Initialize a new virtual machine context
kr = my_vmm_dispatch(kVmmInitContext, version, vmmUState);
OUT_ON_MACH_ERROR("vmm_init_context", kr);
// Fetch the index returned by vmm_init_context()
vmmIndex = vmmUState->thread_index;
Printf("New virtual machine context initialized, index = %lu\n", vmmIndex);
// Set a convenience pointer to the VM's registers
ppcRegs32 = &(vmmUState->vmm_proc_state.ppcRegs.ppcRegs32);
// Set the program counter to the beginning of the text in the guest's
// virtual address space
ppcRegs32->ppcPC = guestTextAddress;
Printf("Guest virtual machine PC set to %p\n", (void *)guestTextAddress);
// Set the stack pointer (GPR1), taking the Red Zone into account
#define PAGE2SP(x) ((void *)((x) + vm_page_size - C_RED_ZONE))
ppcRegs32->ppcGPRs[1] = (u_int32_t)PAGE2SP(guestStackAddress); // 32-bit
Printf("Guest virtual machine SP set to %p\n", PAGE2SP(guestStackAddress));
// Map the stack page into the guest's address space
kr = my_vmm_dispatch(kVmmMapPage, vmmIndex, guestStackPage,
guestStackAddress, VM_PROT_ALL);
Printf("Mapping guest stack page\n");
// Call the chosen demo's instruction populator
(SupportedDemos[demo_id].textfiller)((u_int32_t *)guestTextPage,
guestTextAddress, ppcRegs32);
// Finally, map the text page into the guest's address space, and set the
// VM running
//
Printf("Mapping guest text page and switching to guest virtual machine\n");
kr = my_vmm_dispatch(kVmmMapExecute, vmmIndex, guestTextPage,
guestTextAddress, VM_PROT_ALL);
// Our demo ensures that the last instruction in the guest's text is
// either an infinite loop or illegal. The monitor will "hang" in the case
// of an infinite loop. It will have to be interupted (^C) to gain control.
// In the case of an illegal instruction, the monitor will gain control at
// this point, and the following code will be executed. Depending on the
// exact illegal instruction, Mach's error messages may be different.
//
if (kr != KERN_SUCCESS)
mach_error("*** vmm_map_execute32:", kr);
Printf("Returned to vmm\n");
Printf("Processor state:\n");
printf(" Distance from origin = %lu instructions\n",
(ppcRegs32->ppcPC - vm_page_size) >> 2);
printf(" PC = %p (%lu)\n",
(void *)ppcRegs32->ppcPC, ppcRegs32->ppcPC);
printf(" Instruction at PC = %#08x\n",
((u_int32_t *)(guestTextPage))[(ppcRegs32->ppcPC - vm_page_size) >> 2]);
printf(" CR = %#08lx\n"
" ", ppcRegs32->ppcCR);
prbits32(ppcRegs32->ppcCR);
printf(" LR = %#08lx (%lu)\n",
ppcRegs32->ppcLR, ppcRegs32->ppcLR);
printf(" MSR = %#08lx\n"
" ", ppcRegs32->ppcMSR);
prbits32(ppcRegs32->ppcMSR);
printf(" return_code = %#08lx (%lu)\n",
vmmUState->return_code, vmmUState->return_code);
return_params32 = vmmUState->vmmRet.vmmrp32.return_params;
for (i = 0; i < 4; i++)
printf(" return_params32[%d] = 0x%08lx (%lu)\n", i,
return_params32[i], return_params32[i]);
printf(" GPRs:\n");
for (j = 0; j < 16; j++) {
printf(" ");
for (i = 0; i < 2; i++) {
printf("r%-2d = %#08lx ", j * 2 + i,
ppcRegs32->ppcGPRs[j * 2 + i]);
}
printf("\n");
}
// Tear down the virtual machine ... that's all for now
kr = my_vmm_dispatch(kVmmTearDownContext, vmmIndex);
OUT_ON_MACH_ERROR("vmm_init_context", kr);
Printf("Virtual machine context torn down\n");
out:
if (vmmUStatePage)
(void)vm_deallocate(myTask, vmmUStatePage, vm_page_size);
if (guestTextPage)
(void)vm_deallocate(myTask, guestTextPage, vm_page_size);
if (guestStackPage)
(void)vm_deallocate(myTask, guestStackPage, vm_page_size);
exit(kr);
}
Virtual PC
The Virtual PC software for Mac OS X is implemented using the VMM facility. In the xnu kernel source corresponding to Mac OS X 10.0, the VMM source is copyrighted to Connectix Corporation, the developer of Virtual PC. Connectix was later acquired by Microsoft.
The code in Figure 651 provides two demonstrations, demo 0 and demo 1. Demo 0 calls initGuestText_Dummy() to populate the VM's text page with a contrived sequence of instructions terminated by an illegal instruction. The first few words of the page are as follows, assuming the page starts at virtual address addr:
addr+00 addi r10,0,4 ; load the value '4' in r10
addr+04 addi r11,0,5 ; load the value '5' in r11
addr+08 ; branch to addr+16
addr+12 0xdeadbeef ; no such instruction
addr+16 add r12,r11,r11 ; place (r10 + r11) in r12
addr+20 0xfeedface ; no such instruction
The VM bypasses the first illegal instruction in the sequence by branching over it. When execution reaches the second illegal instruction, control returns back to our host program. Alternatively, you can make the last instruction be an infinite loop, in which case the VM will run until interrupted.
When vmachmon32 finishes running demo 0, the VM's GPR10, GPR11, and GPR12 should contain the values 4, 5, and 9, respectively. Moreover, the program counter should contain addr+20, where addr is the starting address of the guest's text page. Figure 652 shows the result of running demo 0.
Figure 652. Result of running a sequence of machine instructions in a VM using the vmachmon32 program
$ gcc -Wall -o vmachmon32 vmachmon32.c
$ ./vmachmon32
usage: vmachmon32 <demo ID>
Supported demos:
ID Description
0 executes a few hand-crafted instructions in a VM
1 executes a recursive factorial function in a VM
$ ./vmachmon32 0
Vmm> Mac OS X virtual machine monitor (version 1.7)
Vmm> Vmm features:
LittleEndian = Yes
Stop = Yes
ExtendedMapping = Yes
ListMapping = Yes
FastAssist = Yes
XA = Yes
SixtyFourBit = No
MultAddrSpace = No
GuestShadowAssist = Yes
GlobalMappingAssist = No
HostShadowAssist = No
MultAddrSpaceAssist = No
Vmm> Page size is 4096 bytes
Vmm> Allocated page-aligned memory for virtual machine user state
Vmm> Allocated page-aligned memory for guest's text
Vmm> Allocated page-aligned memory for guest's stack
Vmm> New virtual machine context initialized, index = 1
Vmm> Guest virtual machine PC set to 0x00001000
Vmm> Guest virtual machine SP set to 0x00002f20
Vmm> Mapping guest stack page
Vmm> Fabricated instructions for executing in the guest virtual machine
Vmm> Mapping guest text page and switching to guest virtual machine
*** vmm_map_execute32 (os/kern) not receiver
Vmm> Returned to vmm
Vmm> Processor state:
Distance from origin = 5 instructions
PC = 0x00001014 (4116)
Instruction at PC = 0x00000060
CR = 0x00000000
0000000000000000 0000000000000000
LR = 0x00000000 (0)
MSR = 0x0008d030
0000000000001000 1101000000110000
return_code = 0x00000007 (7)
return_params32[0] = 0x00001000 (4096)
return_params32[1] = 0x40000000 (1073741824)
return_params32[2] = 0x00000000 (0)
return_params32[3] = 0x00000000 (0)
GPRs:
r0 = 0x00000000 r1 = 0x00002f20
r2 = 0x00000000 r3 = 0x00000000
r4 = 0x00000000 r5 = 0x00000000
r6 = 0x00000000 r7 = 0x00000000
r8 = 0x00000000 r9 = 0x00000000
r10 = 0x00000004 r11 = 0x00000005
r12 = 0x00000009 r13 = 0x00000000
r14 = 0x00000000 r15 = 0x00000000
r16 = 0x00000000 r17 = 0x00000000
r18 = 0x00000000 r19 = 0x00000000
r20 = 0x00000000 r21 = 0x00000000
r22 = 0x00000000 r23 = 0x00000000
r24 = 0x00000000 r25 = 0x00000000
r26 = 0x00000000 r27 = 0x00000000
r28 = 0x00000000 r29 = 0x00000000
r30 = 0x00000000 r31 = 0x00000000
Vmm> Virtual machine context torn down
As noted in the program's comments in Figure 651, compiling vmachmon32.c requires that you copy osfmk/ppc/vmachmon.h from the kernel source tree to the current directory (with respect to vmachmon32.c). Additionally, the source line that includes <ppc/exception.h> must be commented out in vmachmon.h.
Demo 1 populates the guest's text page by calling initGuestText_Factorial(), which copies machine instructions for a recursive factorial function into the page and populates the LR so that the function returns to an address containing an illegal instruction. The function takes a single argument in GPR3: the number whose factorial to compute. It returns the computed factorial in GPR3. Again, we will verify the program's working by examining the register dump at the end of the program's execution. Figure 653 shows the result of running demo 1. The vmachmon32 code shown in Figure 651 passes 10 as an argument to the factorial function. Correspondingly, GPR3 should contain 0x00375f00 as the result.
Figure 653. Result of running a recursive factorial function within a VM using the vmachmon32 program
$ ./vmachmon32 1
Vmm> Mac OS X virtual machine monitor (version 1.7)
...
Vmm> Returned to vmm
Vmm> Processor state:
Distance from origin = 1023 instructions
PC = 0x00001ffc (8188)
Instruction at PC = 0x00000000
CR = 0x00000002
0000000000000000 0000000000000010
LR = 0x00001ffc (8188)
MSR = 0x0008d030
0000000000001000 1101000000110000
return_code = 0x00000007 (7)
return_params32[0] = 0x00001000 (4096)
return_params32[1] = 0x40000000 (1073741824)
return_params32[2] = 0x00000000 (0)
return_params32[3] = 0x00000000 (0)
GPRs:
r0 = 0x00001ffc r1 = 0x00002f20
r2 = 0x00058980 r3 = 0x00375f00
r4 = 0x00000000 r5 = 0x00000000
...
Vmm> Virtual machine context torn down
6.10. Compiling the Kernel
In this section, we will briefly discuss the kernel compilation procedure in Mac OS X. Depending on the Darwin release, you might need the following Darwin packages to compile the xnu kernel:
bootstrap_cmds
cctools
IOKitUser
kext_tools
Libstreams
xnu
Mac OS X does not have a publicly well-defined kernel compilation procedure. Compiling the kernel requires certain tools and libraries that are part of Darwin but are not installed by default. These prerequisites must be compiled from source. However, they are few in number, and it is straightforward to compile them. Moreover, depending on the specific Darwin release you use, you may require more or fewer steps than the ones described in this section.
6.10.1. Retrieving Prerequisite Packages
We begin by retrieving and unpacking the required package archives.[20] Let us assume all package archives have been expanded within the /work/darwin/ directory. Each package will expand into a directory whose name includes the package's name and version. We will omit the version number in the examples in this section.
[20] On some Mac OS X versions, the header file ar.h may be missing from the Kernel framework's Headers/ subdirectory. If this is the case, copy /usr/include/ar.h to /System/Library/Frameworks/Kernel.framework/Headers/.
Compiler Version Dependency
Mac OS X releases have typically included two versions of the GNU C compiler: a default version (the gcc package) and another version for compiling the operating system (gcc_os). For example, on Mac OS X 10.4, the kernel is compiled using GCC 3.3, whereas the default compiler is GCC 4.0.
You can switch between compiler versions through the gcc_select command-line tool. Before compiling the kernel or the prerequisite packages, you should switch to the kernel-specific GNU C compiler for your system.
6.10.2. Compiling Prerequisite Packages
Change directory to bootstrap_cmds/relpath.tproj/, and run make to compile and install the relpath tool, which calculates the relative path from a given directory to a given path.
$ cd /work/darwin/bootstrap_cmds/relpath.tproj
$ sudo make install
...
$ relpath /usr/local/ /usr/local/bin/relpath
bin/relpath
relpath is used in various "doconf" shell scripts in the kernel source tree to set up build paths during the precompilation ("config") stage of the kernel build process.
The tools and libraries that you compile and install from these support packages are installed by default in the /usr/local/ directory hierarchy. You must ensure that /usr/local/ is in your shell's path. A benefit of confining this software to /usr/local/ is that you will not accidentally overwrite any standard files belonging to the operating system.
The rest of the packages are needed to satisfy the following requirements:
Compilation of the libsa in-kernel library
Generation of symbol sets for various kernel components after compilation finishesthis requires the kextsymboltool program
Figure 654 shows dependencies between the packages. libsa links against a static kernel link editor library (libkld.a), which comes from the cctools package. libkld.a in turn depends on the Libstreams package, the libmacho library, and the libstuff library. libmacho and libstuff come from the cctools package. libsa also requires a tool called seg_hack, which can change all segment names in a Mach-O file to the one specified on the command line. The kernel compilation process uses seg_hack to change segment names to __KLD while compiling libsa. We can satisfy these dependencies by appropriately traversing the graph shown in Figure 654.
Figure 654. Software dependencies in the kernel build process
[View full size image]
Change directory to the Libstreams package, and run make to compile and copy the required headers and libraries to the /usr/local/ hierarchy.
$ cd /work/darwin/Libstreams/
$ sudo make install
...
The components within the cctools package must be compiled in a particular sequence since they have internal dependencies.
Change directory to cctools/libstuff/, and run make. No installation is required since we need the result of this build only to compile other components in this package.
Change directory to cctools/misc/, and run the following commands, which compile and install seg_hack in /usr/local/bin/ with the appropriate ownership and permissions.
$ make seg_hack.NEW
$ sudo install -o root -g wheel -m 0755 seg_hack.NEW /usr/local/bin/seg_hack
Change directory to cctools/libmacho/, and run make. Again, no installation is required.
Change directory to cctools/ld/, and run the following commands.
$ make kld_build
$ sudo install -o root -g wheel -m 0755 static_kld/libkld.a /usr/local/lib/
$ sudo ranlib /usr/local/lib/libkld.a
Finally, we need to compile kextsymboltool, which the build process uses for generating symbol sets from compiled kernel components. BSDKernel.symbolset, IOKit.symbolset, Libkern.symbolset, and Mach.symbolset are examples of the resultant files that exist in the BUILD directory hierarchy of a compiled kernel source tree. These Mach-O relocatable object files contain nothing besides a symbol table, for example:
$ cd /work/darwin/xnu/BUILD/obj/DEBUG_PPC/
$ file Mach.symbolset
Mach.symbolset: Mach-O object ppc
$ otool -l Mach.symbolset
Mach.symbolset:
Load command 0
cmd LC_SYMTAB
cmdsize 24
symoff 52
nsyms 2652
stroff 31876
strsize 49748
$ cd /work/darwin/xnu/BUILD/obj/RELEASE_PPC/
$ otool -l Mach.symbolset
Mach.symbolset:
Load command 0
cmd LC_SYMTAB
cmdsize 24
symoff 52
nsyms 52
stroff 676
strsize 1128
The source for kextsymboltool is in the kext_tools package. However, its compilation depends on header files from the IOKitUser and cctools packages. Change directory to kext_tools/, and run the following commands.
$ mkdir IOKit
$ ln -s /work/darwin/IOKitUser/kext.subproj IOKit/kext
$ gcc -I /work/darwin/cctools/include -I . -o kextsymboltool kextsymbool.c
$ sudo install -o root -g wheel -m 0755 kextsymboltool /usr/local/bin/
6.10.3. Compiling the xnu Package
At this point, we are ready to compile the kernel. Change directory to xnu/, and run the following command to initiate kernel compilation.
$ make exporthdrs && make all
This will create a standard (RELEASE configuration) kernel, which will be available as xnu/BUILD/obj/RELEASE_PPC/mach_kernel at the end of the compilation. Alternative build configurations include DEBUG and PROFILE, which yield debugging and profiling versions, respectively, of the kernel. The following command will compile a debug version, which will be available as xnu/BUILD/obj/DEBUG_PPC/mach_kernel at the end of the compilation:
$ make exporthdrs && make KERNEL_CONFIGS=DEBUG all
Regardless of which kernel configuration you build, a kernel object file with full symbolic information is generated as mach_kernel.sys in the same directory as mach_kernel. A typical mach_kernel.sys is several times larger than the corresponding mach_kernel and contains more symbols. Table 618 compares some aspects of release, profile, and debug builds for a particular kernel version.
Table 618. A Comparison of the End Products of Different Kernel Build Configurations RELEASE
PROFILE
DEBUG
Size of mach_kernel
4.06MB
4.31MB
4.57MB
Symbols in mach_kernel
11,677
11,679
19,611
Strings in mach_kernel
265KB
265KB
398KB
Size of mach_kernel.sys
25.37MB
25.48MB
27.04MB
Symbols in mach_kernel.sys
18,824
18,824
19,611
Strings in mach_kernel.sys
16.13MB
16.12MB
17.24MB
6.10.4. DarwinBuild
A convenient way to compile the xnu kernel (in fact, any Darwin package in general) is through DarwinBuilda specialized open source tool that provides a build environment similar to Apple's internal build environment. DarwinBuild's usefulness is evident when building complex packages with a large number of dependencies, for example, the system library. Specifically, DarwinBuild consists of two primary harness programs: darwinbuild, which builds software, and darwinxref, which parses property list files containing project information and performs a variety of operations such as resolving dependencies, finding files, and loading indexes.