Shared Libraries, Many Methods.
Nearly every OS implements some library functionality for use by programs. For some this is limited to just what is provided by the OS, though many implement a means of having additional Shared Libraries, with various means of calling the functions provided. It is the additional shared libraries that we will speak of here.
There are many different ways in which support for shared libraries are implemented. Some systems use software inturrupts for extended shared library calls (and usually do not use the word library, even though it is). Some use a dispach function that all calls are directed through for all libraries. Some use a table per library that is called by using the offset within the function pointer table. And some use run-time linking to implement shared libraries. These four most common means of implementing shared libraries each have there pros and cons, here in we will look at each of them.
As it is not needed to be too detailed for the points at hand, much of the following is noticably simplified, just giving the information needed to get the point accross.
|Soft Inturrupt Libs:|
In some systems people give this little thought, and even in systems where it is obvious, they do not see the library aspect. Code of a number of Operating Systems that provides services to many programs is a form of library.
A number of systems provide a means of adding Soft Inturrupt Libraries beyond what the system provides. The most in deapth example I know of is RISC OS, which uses this mechinism for many OS extensions, as well as for extended functionality.
In RISC OS, being our example, Modules can provide SWI (SoftWare Inturrupt) services that can be called by applications, or other modules. This provides for a fairly extensive library system, limitted only by the available bits in the way RISC OS allocates SWIs using the ARM SWI opcode that has 24 bits for a comment feild (used to specify the service [library function / procedure] to call). As 4 bits are used to specify the OS, one bit to specify error returning, and 6 bits for the service in a given module, this leaves 13 bits to specify the library (the chunk number), which gives us up to 8192 chunks, each providing up to 64 routines / functions.
RISC OS is a bit unique in the way it does this, having such well defined chunks to devide up handlers. RISC OS also provides calls to translate between the textual name of a SWI service and the actual SWI number used to call it.
The only real dissadvantages of this kind of system are the need to maintain a list of what is used, so to avoid conflict. For this reason I have often recommended introducing a dynamic runtime SWI allocation system. Since the OS provides a means of name to SWI number translation, this could be a very good solution to keep new modules being written.
There is also the issue on newer ARM CPUs of contaminating the DCache with code.
Of course the same concept can be implemented on other Operating Systems and other CPUs. The way things are mapped out can vary in other systems. The OABI Linux ARM API is an example of a system that uses this for some parts, and another means for other parts.
|Single Vector Dispatching:|
Some systems provide a library call dispacher that is called at a static location, with the information to dispatch the call to the correct library. A good example here is GS/OS which has 4 call dispatch routines located at E1:0000, E1:0004, E1:0008, and E1:000C, the first two for System "ToolSets" (GS/OS name for libraries), and the second two for User "ToolSets".
GS/OS as our example: When calling the dispatch vector, the ToolSet number, as well as the function / procedure number within the ToolSet is combined into a 16 bit value in the X register (WDC65816 CPU), with the ToolSet number in the low byte and the call number in the high byte. All parameters are passed on the stack. Thus in the case of GS/OS there is a limit of 256 system tools, and 256 user tools, with no more than 256 functions / procedures per library (total maximum of 131072 calls possible).
The potential call number conflicts are the only real potential dissadvantage of such a system. Again we are back to the question of implementing a system for dynamic load time allocation of user ToolSets so as to not be limited by potential conflict, though it needs to be agreed on by enough to propogate it.
This is a method that is often very heavily implemented in tandom with other methods of implementing libraries.
|Multiple Vector Dispatching:|
On some sytems, instead of having a single call vector for all calls, they implement seperate call vectors, often with some still dispatching multiple functions. Good examples of this include Acorn MOS, Commodore KERNAL, Atari 8-bit systems, and many others. Note that Acorn MOS is a bit of a hybrid, as most of its call vectors lead to multi-function dispatching.
This method, while effecient, is a little more to manage than the other methods mentioned herein. Though for small systems with a limited number of calls it can be very effecient.
Even systems like RISC OS, Atari TOS, etc, have this method for some things, working togather with there primary methods of library calling.
|PER LIB CALL TABLE:|
In some systems you get a base address of the libraries function table, and use offsets of the function numbers within the library. This is the case on 32-bit OS/2, Amiga OS and a few others. In some cases the table is a series of pointers to function / procedure entry points, in others the table is a set of branch instructions to branch to the procedure / function.
There are very few possible negitives, and this method is well suited to any memory model (flat, segmented, page protected, etc). When it is needed to enter system / supervisor mode of the CPU for a call, this is done by having the code marked as non-executable and taking advantage of the page fault (as this would only be of need on a system that has protected memory in most cases).
In a system using this method, most library calls will run in user mode, with seperate data spaces for each task (and if needed a shared data space). The per task data spaces would belong to the task, and thus be marked as with the task during task switches.
There are a number of varients on this, many of which are a hybrid between this means and one of the others.
|Run Time Linked Libraries.:|
With all the more effecient ways of doing shared libraries, I have really never understood this one. This is the method used by almost any OS that uses shared libraries and uses ELF or PE type executables and libraries. This method requires patching of the Libraries and Executable images at load time, not quite as bad as static linking, though still quite significant.
The single biggest downfall of this method is that it adds to the CPU cycles needed to load anyting that uses any externel library (including for system calls). It also requires use of some extra RAM temporarily while patching up the loaded images. Both of these issues can be quite noticable, especially with some redicoulous projects that use more than 40 shared libraries.
Most systems that use this method do not use others (excepting OS/2, where this is the 16-bit method, and can be in some 32-bit stuff). Thus it is usually better to use static linking, especially for larger projects, if your OS uses this method. Some well known Operating Systems that use this method include Linux, BSD, BeOS, MS-Windows (all versions), ReactOS, AROS, and many more. Oh well, such as life.
It should be noted that these are the general concepts. Many systems use more than one of the listed methods of handling Shared Libraries, and some do even better with evolutions of the base concepts.
It is unfortunate that in many current systems the less effecient method of run time linking (AKA Dynamic Linking) has caught on and stuck. Dynamic linking works, though it is one of if not the least effecient way of handling Shared Libraries, at least at load time.
The methods of Shared library handling is the core of any OS design. As even an OS likel CP/M is itself a set of shared function libraries. Today even more so is this an important issue and thought to give.
Any of the methods can be implemented by the compilers for High Level Languages in a way that is transparent to the source code they compile. Thus none of them have any advantage or dissadvantage in that area.