Mac OS X Internals: A Systems Approach
2.6. The Runtime Architecture
Given a loose enough definition of a runtime environment, one can say that modern operating systems often provide multiple runtime environments. For example, whereas the Java virtual machine on Mac OS X is a runtime environment for Java programs, the virtual machine implementation itself executes in another, "more native" runtime environment. Mac OS X has several runtime environments for applications, as we will see later in this chapter. However, an operating system typically has only a single lowest-level (or "native") runtime environment that we will refer to as the runtime environment. The foundation of the runtime environment is the runtime architecture, which has the following key aspects.
Mac OS X has only one runtime architecture: Mach-O. The name refers to the Mach Object File Format, although the term "Mach" is somewhat of a misnomer in this case since Mach is not meant to understand any object file format. Neither is Mach aware of the runtime conventions of user-space programs. The Mac OS X kernel, however, does understand the Mach-O format. In fact, Mach-O is the only binary format that the kernel can load[20]using the execve()[21] system call, which is implemented in the BSD portion of the Mac OS X kernel. [20] Note that we explicitly say binary format: The kernel can arrange for scripts to run. [21] The execve() system call executes the specified program, which may be a binary executable or a script, in the address space of the calling process. 2.6.1. Mach-O Files
Mac OS X uses Mach-O files for implementing several types of system components, for example, the following:
We will discuss frameworks, umbrella frameworks, and bundles later in this chapter. Before we continue, let us enumerate some programs that are useful in creating, analyzing, or manipulating Mach-O files. Such programs include the following:
A Mach-O file contains a fixed-size header (see Figure 23) at the very beginning, followed by typically several variable-sized load commands, followed by one or more segments. Each segment can contain one or more sections. Figure 23. The structure of the Mach-O header (32-bit version)
The Mach-O header describes the features, layout, and linking characteristics of the file. The filetype field in the Mach-O header indicates the type and, therefore, the purpose of the file. Mach-O file types include the following:
For executable files, one of the load commands (LC_LOAD_DYLINKER) in the Mach-O header specifies the path to the linker to be used for loading the program. By default, this load command specifies the standard dynamic linker, dyld (/usr/lib/dyld), which itself is a Mach-O file of type MH_DYLINKER. The kernel and dyld (or in theory, another dynamic linker, if one is specified) together prepare a Mach-O binary for execution using the following sequence of operations, which has been simplified for brevity.[23] [23] Further details of program execution by the kernel are discussed in Section 7.5.
Let us look at the example of a trivial executable. Figure 24 shows a C program that is compiled to an executable called empty. Figure 24. A trivial C program to be compiled to an "empty" executable
Figure 25 shows the use of the otool program to list the load commands contained in empty. Figure 25. Displaying the load commands in an executable's Mach-O header
The LC_UNIXTHREAD load command shown in Figure 25 contains the initial values of the program's registers. In particular, the srr0 PowerPC register[24] contains the address of the entry point function0x23cc in this case. As we can verify by using the nm program, this address belongs to a function called start(). Consequently, empty begins execution in this function, which comes from the language runtime stub /usr/lib/crt1.o. The stub initializes the program's runtime environment state before calling the main() function. The compiler links in crt1.o during compilation. [24] Chapter 3 discusses the PowerPC architecture in detail. Note that if the Mach-O file in Figure 25 were an x86 executable, its LC_UNIXTHREAD command would contain x86 register state. In particular, the eip register would contain the address of the start() function.
Depending on aspects such as the program being compiled, the programming language, the compiler, and the operating system, more than one such stub may be linked in during compilation. For example, bundles and dynamic shared libraries on Mac OS X are linked along with /usr/lib/bundle1.o and /usr/lib/dylib1.o, respectively.
2.6.2. Fat Binaries
We came across "fat" binaries in Chapter 1, when we looked at NEXTSTEP. Since NEXTSTEP ran on multiple platforms such as Motorola 68K, x86, HP PA-RISC, and SPARC, it was rather easy to come across multifat binaries. Fat binaries first became useful on Mac OS X with the advent of 64-bit user address space support, since a fat binary could contain both 32-bit and 64-bit Mach-O executables of a program. Moreover, with Apple's transition to the x86 platform, fat binaries become still more important: Apple's Universal Binary format is simply another name for fat binaries. Figure 26 shows an example of creating a three-architecture fat binary on Mac OS X.[25] The lipo command can be used to list the architecture types in a fat file. It is also possible to build a fat Darwin kernelone that contains the kernel executables for both the PowerPC and x86 architectures in a single file. [25] Apple's build of GCC 4.0.0 or higher is required to create fat binaries on Mac OS X. Figure 26. Creating fat binaries
Figure 27 shows the structure of a fat binary containing PowerPC and x86 executables. Note that a fat binary is essentially a wrappera simple archive that concatenates Mach-O files for multiple architectures. A fat binary begins with a fat header (struct fat_header) that contains a magic number followed by an integral value representing the number of architectures whose binaries reside in the fat binary. The fat header is followed by a sequence of fat architecture specifiers (struct fat_arch)one for each architecture contained in the fat binary. The fat_arch structure contains the offset into the fat binary at which the corresponding Mach-O file begins. It also includes the size of the Mach-O file, along with a power of 2 value that specifies the alignment of the offset. Given this information, it is straightforward for other programsincluding the kernelto locate the code for the desired architecture within a fat binary. Figure 27. A Universal Binary containing PowerPC and x86 Mach-O executables
Note that although a platform's Mach-O file in a fat binary follows that architecture's byte ordering, the fat_header and fat_arch structures are always stored in the big-endian byte order. 2.6.3. Linking
Dynamic linking is the default on Mac OS Xall normal user-level executables are dynamically linked. In fact, Apple does not support static linking of user-space programs (Mac OS X does not come with a static C library). One reason for not supporting static linking is that the binary interface between the C library and the kernel is considered private. Consequently, system call trap instructions should not appear in normally compiled executables. Although you can statically link object files into a static archive library,[26] the language runtime stub that would yield statically linked executables doesn't exist. Therefore, a statically linked user executable cannot be generated using the default tools. [26] Static archive libraries can be used for distributing code that is not desirable in a shared library but is otherwise usable while compiling multiple programs.
Mac OS X kernel extensions must be statically linked. However, kernel extensions are not Mach-O executables (MH_EXECUTE) but Mach-O object files (MH_OBJECT). The otool command can be used to display the names and version numbers of the shared libraries used by an object file. For example, the following command determines the libraries that launchd depends on (launchd's library dependencies are interesting because it is the first program to execute in user spacetypically, such programs are statically linked on Unix systems): $ otool -L /sbin/launchd # PowerPC version of Mac OS X /sbin/launchd: /usr/lib/libbsm.dylib (...) /usr/lib/libsm.dylib (...) $ otool -L /sbin/launchd # x86 version of Mac OS X /sbin/launchd: /usr/lib/libsm.dylib (...) /usr/lib/libgcc_s.1.dylib (...) /usr/lib/libSystem.B.dylib (...)
Figure 28 shows examples of compiling a dynamic shared library, compiling a static archive library, and linking with the two libraries. Figure 28. Compiling dynamic and static libraries
When compiling a dynamic shared library, you can specify a custom initialization routine that will be called before any symbol is used from the library. Figure 29 shows an example. Figure 29. Using a custom initialization routine in a dynamic shared library
Other notable aspects of the Mach-O runtime architecture include multiple binding styles, two-level namespaces, and weakly linked symbols. 2.6.3.1. Multiple Binding Styles
When binding imported references in a program, Mac OS X supports just-in-time (lazy) binding, load-time binding, and prebinding. With lazy binding, a shared library is loaded only when a symbol from that library is used for the first time. Once the library is loaded, not all of the program's unresolved references from that library are bound immediatelyreferences are bound upon first use. With load-time binding, the dynamic linker binds all undefined references in a program when it is launchedor, as in the case of a bundle, when it is loaded. We will look at details of prebinding in Section 2.8.4. 2.6.3.2. Two-Level Namespaces
A two-level namespace implies that imported symbols are referenced both by the symbol's name and by the name of the library containing it. Mac OS X uses two-level namespaces by default. When two-level namespaces are being used, it is not possible to use dyld's DYLD_INSERT_LIBRARIES environment variable[27] to preload libraries before the ones specified in a program. It is possible to ignore two-level namespace bindings at runtime by using dyld's DYLD_FORCE_FLAT_NAMESPACE environment variable, which forces all images in the program to be linked as flat-namespace images. However, doing so may be problematic when the images have multiply defined symbols. [27] This variable is analogous to the LD_PRELOAD environment variable supported by the runtime linker on several other platforms. 2.6.3.3. Weakly Linked Symbols
A weakly linked symbol[28] is one whose absence does not cause the dynamic linker to throw a runtime binding errora program referencing such a symbol will execute. However, the dynamic linker will explicitly set the address of a nonexistent weak symbol to NULL. It is the program's responsibility to ensure that a weak symbol exists (i.e., its address is not NULL) before using it. It is also possible to link to an entire framework weakly, which results in all of the framework's symbols being weakly linked. Figure 210 shows an example of using a weak symbol. [28] We use the term "symbol" interchangeably with the term "reference" in this discussion. A reference is to a symbol, which may represent code or data. Figure 210. Using weak symbols on Mac OS X 10.2 and newer
2.6.3.4. dyld Interposing
Beginning with Mac OS X 10.4, dyld does support programmatic interposing of library functions, although the corresponding source code is not included in the open source version of dyld. Suppose you wish to use this feature to intercept a C library function, say, open(), with your own function, say, my_open(). You can achieve the interposing by creating a dynamic shared library that implements my_open() and also contains a section called __interpose in its __DATA segment. The contents of the __interpose section are the "original" and "new" function-pointer tuplesin this case, { my_open, open }. Using the DYLD_INSERT_ LIBRARIES variable with such a library will enable the interposing. Note that my_open() can call open() normally, without having to first look up the address of the "open" symbol. Figure 211 shows an example of dyld interposingthe programmer-provided library "takes over" the open() and close() functions. Figure 211. Interposing a library function through dyld
As a word of caution, one must note that calls to certain library functions could lead to recursive invocations of the interposer functions. For example, the implementation of printf() may call malloc(). If you are interposing malloc() and calling printf() from within your version of malloc(), a recursive situation can arise.
|
Категории