打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
QNX Developer Support

Dynamic Linking

Shared objects

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.

Statically linked

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.

Dynamically linked

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).

Augmenting code at runtime

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).

How shared objects are used

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.

ELF format

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.

ELF without COFF

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 process

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.

Runtime linker

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):

  1. If the requested shared library isn't already loaded in memory, the runtime linker loads it. If the shared library name is fully qualified (i.e. begins with a slash), it's loaded directly from the specified location. If it can't be found there, no further searches are performed. If it's not a fully qualified pathname, the runtime linker searches for it in the directories specified by LD_LIBRARY_PATH only if the program isn't marked as setuid.
  2. If the shared library still isn't found, and if the executable's dynamic section contains a DT_RPATH tag, then the path specified by DT_RPATH is searched next.
  3. If the shared library still isn't found, then the runtime linker searches for the default library search path as specified by the LD_LIBRARY_PATH environment variable to procnto. If none has been specified, then the default library path is set to the image filesystem's path.
  4. Once the requested shared library is found, it's loaded into memory. For ELF shared libraries, this is a very efficient operation: the runtime linker simply needs to use the mmap() call twice to map the two load segments into memory.
  5. The shared library is then added to the internal list of all libraries that the process has loaded. The runtime linker maintains this list.
  6. The runtime linker then decodes the dynamic section of the shared object.

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.

Loading a shared library at runtime

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.

Symbol name resolution

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:

  1. The shared library.
  2. The global list.
  3. Any dependent objects that the shared library references(i.e. any other libraries that the shared library was linkedagainst).

The runtime linker's scoping behavior can be changed in twoways when dlopen()'ing a shared library:

  • When the program loads a new library, it may instruct the runtime linker to place the library's symbols on the global list by passing the RTLD_GLOBAL flag to the dlopen() call. This will make the library's symbols available to any libraries that are subsequently loaded.
  • The list of objects that are searched when resolving the symbols within the shared library can be modified. If the RTLD_GROUP flag is passed to dlopen(), then only objects that the library directly references will be searched for symbols. If the RTLD_WORLD flag is passed, only the objects on the global list will be searched.

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
AndroidLinker与SO加壳技术之上篇
LD的
Packing DLLs in your EXE | Dr Dobb's
程序编译链接运行时对库关系的探讨
Statically compiled Go programs, always, even with cgo, using musl
Shared Libraries without an MMU
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服