![]() | ![]() | ![]() | ![]() |
In a typical system, a number of programs will be running.Each program relies on a number of functions, some of whichwill be "standard" C library functions, likeprintf(),malloc(),write(), etc.
If every program uses the standard C library, it followsthat each program would normally have a unique copy of thisparticular library present within it. Unfortunately, thisresults in wasted resources. Since the C library is common,it makes more sense to have each program referencethe common instance of that library, instead of havingeach program contain a copy of thelibrary. This approach yields several advantages, not theleast of which is the savings in terms of total systemmemory required.
The term statically linked means that the programand the particular library that it's linked against arecombined together by the linker at linktime. This means thatthe binding between the program and the particular libraryis fixed and known at linktime -- well in advance ofthe program ever running. It also means that we can't changethis binding, unless we relink the program with a newversion of the library.
You might consider linking a program statically in caseswhere you weren't sure whether the correct version of alibrary will be available at runtime, or if you were testinga new version of a library that you don't yet want to installas shared.
Programs that are linked statically are linked againstarchives of objects (libraries) that typicallyhave the extension of .a. An example of sucha collection of objects is the standard C library,libc.a.
The term dynamically linked means that theprogram and the particular library it references arenot combined together by the linker at linktime.Instead, the linker places information into the executablethat tells the loader which shared object module the code isin and which runtime linker should be used to find and bindthe references. This means that the binding between theprogram and the shared object is done at runtime -- before the program starts, the appropriate sharedobjects are found and bound.
This type of program is called a partially boundexecutable, because it isn't fully resolved -- the linker, at linktime, didn't cause all thereferenced symbols in the program to be associated withspecific code from the library. Instead, the linker simplysaid: "This program calls some functions within aparticular shared object, so I'll just make a note ofwhich shared object these functions are in, andcontinue on." Effectively, this defers the bindinguntil runtime.
Programs that are linked dynamically are linked againstshared objects that have the extension .so.An example of such an object is the shared object version ofthe standard C library, libc.so.
You use a command-line option to the compiler driverqccto tell the tool chain whether you're linkingstatically or dynamically. This command-line option thendetermines the extension used (either .a or.so).
Taking this one step further, a program may not know whichfunctions it needs to call until it's running. While thismay seem a little strange initially (after all, howcould a program not know what functions it's goingto call?), it really can be a very powerful feature. Here'swhy.
Consider a "generic" disk driver. It starts,probes the hardware, and detects a hard disk. The driverwould then dynamically load theio-blkcode tohandle the disk blocks, because it found a block-orienteddevice. Now that the driver has access to the disk at theblock level, it finds two partitions present on the disk: aDOS partition and a QNX 4 partition. Rather than force thedisk driver to contain filesystem drivers for all possiblepartition types it may encounter, we kept it simple: itdoesn't have any filesystem drivers! At runtime, itdetects the two partitions and then knows that itshould load thefs-dos.soandfs-qnx4.sofilesystem code to handle those partitions.
By deferring the decision of which functions to call, we'veenhanced the flexibility of the disk driver (and alsoreduced its size).
To understand how a program makes use of shared objects,let's first see the format of an executable and then examinethe steps that occur when the program starts.
QNX Neutrino uses the ELF (Executable and Linking Format) binaryformat, which is currently used in SVR4 Unix systems. ELFnot only simplifies the task of making shared libraries, butalso enhances dynamic loading of modules at runtime.
In the following diagram, we show two views of an ELF file: the linking view and the execution view. The linking view, which is used when the program or library is linked, dealswith sections within an object file. Sectionscontain the bulk of the object file information: data,instructions, relocation information, symbols, debugginginformation, etc. The execution view, which is used when theprogram runs, deals with segments.
At linktime, the program or library is built by merging together sections with similar attributes into segments.Typically, all the executable and read-only data sectionsare combined into a single "text" segment,while the data and "BSS"s are combined into the"data" segment. These segments are calledload segments, because they need to be loaded inmemory at process creation. Other sections such as symbolinformation and debugging sections are merged into other,nonload segments.
Object file format: linking view and execution view.
Most implementations of ELF loaders are derived fromCOFF (Common Object File Format) loaders; theyuse the linking view of the ELF objects at load time. Thisis inefficient because the program loader must load theexecutable using sections. A typical program could contain alarge number of sections, each of which would have to belocated in the program and loaded into memory separately.
QNX Neutrino, however, doesn't rely at all on the COFF techniqueof loading sections. When developing our ELF implementation,we worked directly from the ELF spec and kept efficiencyparamount. The ELF loader uses the "executionview" of the program. By using the execution view,the task of the loader is greatly simplified: all it has todo is copy to memory the load segments (usually two) of theprogram or library. As a result, process creation andlibrary loading operations are much faster.
The diagram below shows the memory layout of a typicalprocess. The process load segments (corresponding to"text" and"data" in the diagram) are loaded atthe process's base address. The main stack is located justbelow and grows downwards. Any additional threads that arecreated will have their own stacks, located below the mainstack. Each of the stacks is separated by a guard page todetect stack overflows. The heap is located above theprocess and grows upwards.
Process memory layout on an x86.
In the middle of the process's address space, a large regionis reserved for shared objects. Shared libraries are locatedat the top of the address space and grow downwards.
When a new process is created, the process manager firstmaps the two segments from the executable into memory. Itthen decodes the program's ELF header. If the program headerindicates that the executable was linked against a sharedlibrary, the process manager will extract the name of the dynamic interpreter from the program header. Thedynamic interpreter points to a shared library that containsthe runtime linker code. The process manager willload this shared library in memory and will then passcontrol to the runtime linker code in this library.
The runtime linker is invoked when a program that was linkedagainst a shared object is started or when a program requests that ashared object be dynamically loaded. The runtime linker is containedwithin the C runtime library.
The runtime linker performs several tasks when loading ashared library (.so file):
This dynamic section provides information to the linkerabout other libraries that this library was linked against. It alsogives information about the relocations that need to beapplied and the external symbols that need to be resolved.The runtime linker will first load any other required shared libraries(which may themselves reference other shared libraries). It will thenprocess the relocations for each library. Some of theserelocations are local to the library, while others requirethe runtime linker to resolve a global symbol. In the lattercase, the runtime linker will search through the list oflibraries for this symbol. In ELF files, hash tables areused for the symbol lookup, so they're very fast. The orderin which libraries are searched for symbols is veryimportant, as we'll see in the section on "Symbol name resolution" below.
Once all relocations have been applied, any initializationfunctions that have been registered in the shared library's initsection are called. This is used in some implementations ofC++ to call global constructors.
A process can load a shared library at runtime by using thedlopen()call, which instructs the runtime linkerto load this library. Once the library is loaded, the program cancall any function within that library by using thedlsym()call to determine its address.
![]() | Remember: shared libraries are available only to processes that aredynamically linked. |
The program can also determine the symbol associated with agiven address by using thedladdr()call.Finally, when the process no longer needs the shared library, it cancalldlclose() to unload the library from memory.
When the runtime linker loads a shared library, thesymbols within that library have to be resolved. The orderand the scope of the symbol resolution are important. If ashared library calls a function that happens to exist by the same namein several libraries that the program has loaded, the orderin which these libraries are searched for this symbol iscritical. This is why the OS defines several options thatcan be used when loading libraries.
All the objects (executables and libraries) that have globalscope are stored on an internal list (the globallist). Any global-scope object, by default, makesavailable all of its symbols to any shared library that getsloaded. The global list initially contains the executableand any libraries that are loaded at the program's startup.
By default, when a new shared library is loaded by using thedlopen()call, symbols within that library areresolved by searching in this order through:
The runtime linker's scoping behavior can be changed in twoways when dlopen()'ing a shared library:
![]() | ![]() | ![]() | ![]() |
联系客服