ELF FAQ for BSD/OS 4.x, version 1.3

Basic Questions

Advanced Questions

Document History

Basic Questions

What is ELF?

ELF is a file format for executable files and core files. ELF was introduced in Unix systems as a replacement for the traditional `a.out' file format, and it has been adopted by other Unix-like systems such as Linux. BSD/OS 4.x uses ELF for virtually all of its executable files, and all of its core files. Library files and the kernel are also in ELF format. ELF is an acronym for `Executable and Linking Format'.

Why did BSD/OS switch to ELF?

The main reason for switching to ELF was to implement dynamic linking. A number of third-party suppliers and customers had requested this feature. Dynamic linking allows programs to delay the mapping of names in program sources (`symbols') to code or variables inside libraries until the program runs. This delayed mapping makes it easy to change shared libraries without affecting the programs that use them.

Since so many other unix-like systems use ELF, we also expect to benefit from the development that is going into ELF tools. And ELF representations are richer than the traditional a.out file representations, so we have more flexibility in the way that we can build executable files.

How do I use ELF?

In BSD/OS 4.x, the standard tools for building and manipulating programs are now configured to generate and accept ELF as the standard format. Unless you are an advanced developer, you probably won't notice any significant difference in the way that you build and run programs. The C compiler cc(1) produces dynamically linked ELF files by default.

What happens to a.out programs?

Your old a.out programs should continue to work as they did before. The operating system continues to recognize and execute a.out programs. Tools that operate on executable files and object files (incomplete executable files) handle both ELF and a.out file formats. The debugger gdb(1) can debug both ELF and a.out files.

Should I rebuild my programs for 4.x anyway?

BSD/OS 4.x contains a number of bug fixes and changes in header files and libraries. If you want your programs to take advantage of these changes, you should rebuild them. Because of potential header file consistency issues, you should always remove old object files (usually done with `make clean') before rebuilding. There is no substantial advantage in efficiency for ELF programs over a.out programs, so don't rebuild on that account. If your program did not already use shared libraries, then it may become smaller when it is dynamically linked. If you were using GCC 1 (cc), then GCC 2 may compile many of your programs into more efficient code, although code size can be larger.

How do I build binaries that don't use shared libraries at all?

The cc(1) and ld(1) programs use a new flag -static to indicate that a program should not be linked with dynamically linked shared libraries. The shlicc(1) program explicitly builds ELF programs with statically linked shared libraries, but it also respects the -static flag.

Are ELF programs smaller than a.out programs?

The standard compiler tools in earlier releases of BSD/OS did not create programs that used shared libraries. The standard compiler tools in BSD/OS 4.x do create programs that use dynamically linked shared libraries by default, so programs built under BSD/OS 4.x will usually shrink. In some cases, they may shrink drastically.

If you build your programs to use statically linked shared libraries with shlicc, then the ELF versions of your programs will probably be about the same size as the a.out versions. ELF data structure overhead is greater than a.out overhead, but ELF files don't require alignment padding, so that often saves space. The very smallest programs will usually be bigger as ELF files, but larger programs may be smaller as ELF files.

ELF programs do use a little less memory than a.out programs. Because the stack, text and data segments are normally mapped by a single page table page, there is usually one less page of kernel memory devoted to the ELF version of a program than to the a.out version.

Who developed the ELF software in BSD/OS 4.x?

The ELF compiler tools are newer or differently configured versions of programs from the GNU Project of the Free Software Foundation. BSD/OS provides the source code for the GNU programs and libraries that we ship on our contributed software CD-ROM, which we include in all of our binary and source distributions. The dynamic linker in BSD/OS 4.x comes from the Linux operating system, with a few modifications by BSD/OS developers. Some of the other programs that deal with executable files also come from the GNU project, while others are modifications of BSD programs from the University of California or were implemented from scratch by BSD/OS developers. The operating system code for loading ELF programs and generating ELF core files was developed by BSD/OS developers. All of the compiler tools, and the ELF dynamic loader, are available as source code at no cost.

Can I run ELF programs from systems other than BSD/OS?

That would be nice, wouldn't it? The answer is, `Maybe'. Starting with a beta program during the BSD/OS 4.0.1 lifecycle, a form of Linux emulation has been available as the LAP (Linux Application Platform) system. With the release of BSD/OS 4.1 (and higher) this has been part of standard system software. Significant improvements to the LAP codebase were made to the LAP software for the BSD/OS 4.3 release. As of the BSD/OS 4.3 release, programs that used the clone() system call in Linux are supported. (Note: the Linux pthreads implementation uses clone().)

Why doesn't BSD/OS 4.x ship with the latest version of gcc, gdb and binutils?

BSD/OS generally doesn't ship with the absolute latest versions of the compiler (gcc), debugger (gdb) and other binary object file tools (binutils). This is because the preparation of a commercial operating system takes a significant amount of time. The compiler, debugger and binary tools are among the first of the programs to be frozen for a given release. Often there are releases of those programs after the toolchain freeze for a given BSD/OS release, but before the BSD/OS release ships.

GCC, GDB 4.16 and Binutils 2.8.1 were the current versions of the GNU tools at the time that the development tool sources were frozen for BSD/OS 4.0. By the time that BSD/OS 4.0 was released, GCC 2.8.1, GDB 4.17 and Binutils 2.9 had been released.

GCC 2.95.2, GDB 4.17 and Binutils 2.9 were the current versions of the GNU tools at the time that the development tool sources were frozen for BSD/OS 4.2. By the time that BSD/OS 4.2 was released, GCC 2.95.2, GDB 4.18 and Binutils 2.10.1 had been released.

There are plans to make more current versions of the compiler tools available for ftp by all customers. (As it turns out, these plans were never realized -- at least for the BSD/OS 4.0 release.)

Can you give me a quick summary of the advantages and disadvantages of all of the new options for ELF programs?

Dynamically linked ELF executable files:

Statically linked ELF executable files that use shared libraries:

Statically linked ELF executable files that don't use shared libraries:

Advanced Questions

What is different about the compiler cc(1)?

In BSD/OS 4.0, the C compiler cc is officially GCC 2. The gcc and gcc2 interfaces are identical to cc. The GCC 1 compiler has been retired, since we didn't want to make it support ELF. On some very small installations, it may no longer be possible to build programs with cc in a reasonable period of time, because the GCC 2 compiler demands more resources than the old GCC 1 compiler. The GCC 2 manual may be accessed on-line using `info gcc'.

The C compiler now produces ELF assembly code. See below for more information about ELF assembly syntax. There are several stylistic changes in the assembly output. For example, const data and strings are placed in the .rodata section, a new section that contains read-only data. Temporary labels begin with `.L' instead of just `L'. The size and `type' of functions and data are recorded in .size and .type directives, respectively. Most of the other changes are purely cosmetic.

Under ELF, the compiler passes the names of C functions and variables (`symbols') to the assembler unchanged. This is a major change in practice from a.out, in which symbols from C were always prepended with an underscore (`_'). For example, the function main() was given the assembly name _main in a.out, but under ELF it is simply main. (See below for details on how BSD/OS copes with this change.)

When you compile and link a program using cc, the compiler arranges to generate a dynamically linked ELF file, which will load dynamically linked shared libraries when it runs. In previous versions of BSD/OS, the only way to create programs that loaded shared libraries was to link with the shlicc program. The shlicc program still exists and still links with statically linked shared libraries; see below for more information about dynamically linked shared libraries and statically linked shared libraries.

Cc now supports a -fPIC flag that instructs it to generate position-independent code. Dynamically linked ELF libraries must be compiled using this flag. Executable programs should never be compiled using -fPIC, however.

The compiler generates the dbx `stabs' debugging format by default with -g, rather than the ELF-specific DWARF format. We settled on stabs because they seem to be well supported. If you prefer to use DWARF, you may specify -gdwarf.

What is different about the assembler as(1)?

BSD/OS 4.0 uses GNU as 2.8.1. Previous releases of BSD/OS used GNU as 1.38. GNU as version 2 contains many bug fixes and new features. In particular, GNU as version 2 handles Pentium and later opcodes, and has a .code16 pseudo-op for 8086-mode coding. For a full description of the features of GNU as version 2, see the manual, which can be accessed on-line using `info as'.

The assembler now accepts a superset of ELF and a.out pseudo-ops. Where there is ambiguity, the assembler uses the ELF semantics. Since ELF supports more features, particularly in the area of alignment, some pseudo-ops have features that were not previously available or useful.

One area of ambiguity is the .align pseudo-op. Formerly, the argument to .align was an exponent; for example, .align 2 caused 4-byte alignment, because 2 to the 2nd power is 4. With ELF, the argument to .align is simply the number of bytes; hence, .align 2 produces 2-byte alignment. If you still want to use a power-of-2 alignment argument, you can give the new explicit .p2align pseudo-op. Note that the old assembler could only guarantee a maximum alignment of 4 bytes; since ELF can represent file alignment more finely, it guarantees that larger alignments will be preserved when object files are linked together.

Here are a few of the more interesting new features:

The new assembler will only generate ELF output files. If you need to create an a.out output file, or if you have assembly source that can't be built using the ELF assembler, you can use /usr/old/as, which is GNU as version 1.38. (Beware of putting /usr/old in your search path ahead of /usr/bin!)

How do I write position-independent code?

Position-independent code or PIC is code that does not need to be modified in order to run at different addresses in memory. Position-independent code doesn't need to be relocated; that is, the pointers in the code don't need to be patched to refer to the actual addresses that the code and data may eventually be loaded at. PIC greatly reduces the overhead of the dynamic linker, since it allows the linker to share memory containing code (`pure text') among programs even when the code is loaded at different addresses in different processes.

The compiler tools and the dynamic linker contain special support for PIC. When you compile with -fPIC, the compiler generates code for accessing external and static data that uses a table of pointers called the global offset table or GOT. When the code accesses the data, it first looks up the address of the GOT using PC-relative addressing, and then it indexes into the GOT to retrieve the specific address of the data. Because of the PC-relative addressing, the code that accesses the GOT works fine regardless of where the code was loaded in memory, as long as the GOT is at a known offset from the code. Although this technique requires a double indirection, the first indirection (the address of the GOT) can be cached in a register, so it's not quite as expensive as it appears.

Note that compiling for PIC will also cause all calls to external functions to use double indirections too. This applies even to external functions defined in the same file as the call. Since function calls are typically PC-relative already, the reason for this is not to avoid relocations but to allow the dynamic linker to override the definition of a function.

So what if you need to write PIC in assembly language? The compiler can't help you here; the assembler has no -fPIC flag. The most common two situations are references to data and calls to functions:

There are other situations that won't be described here (for example, calling function pointers); the best way to elucidate these nasty cases is to write C code to do the significant part of the job, then compile the C code to assembly with -fPIC to see how the compiler solves the problem.

What is different about the linker ld(1)?

BSD/OS 4.0 uses the GNU binutils 2.8.1 linker. This linker is substantially different from the GNU linker that we provided in previous releases. For a full description, you may view the on-line documentation with `info ld'. Here are some of the highlights:

How do I build a.out programs?

Ld creates ELF programs by default. To make it create an a.out program, you need to use the -m flag to switch to a.out emulation. The a.out emulation is called i386bsdi. You must also add the -static flag to suppress dynamic linking, and you will almost always need the -e flag to provide an explicit entry point. A typical link line might look like this:

$ ld -m i386bsdi -static -e _start -o program \
    /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtbegin.o \
    program.o -lc -lgcc /usr/lib/crtend.o /usr/lib/crtn.o

This example links an a.out or ELF object named program.o into an a.out program using ELF start-up files and libraries. You may use a.out start-up files or libraries instead, although the start symbol may need to be different.

Note that merely building an a.out program does not guarantee that the program will run on earlier versions of BSD/OS. If a 4.x system call is unsupported or uses different data structures from earlier versions of BSD/OS, then you can't use it in a program that you build under 4.x for use on a 3.x system. Most system calls are in fact compatible from 3.x into 4.x, but we can't guarantee that an a.out program built under 4.x will work on earlier versions of BSD/OS.

Can I use my old a.out libraries to build ELF programs?

In general, yes, you may. You don't need to use any special compiler or linker flags; just include the a.out objects or libraries as you would include ELF objects or libraries. However:

We can't guarantee that this practice will always work, but our experience is that it usually does work. For example, X11 libraries will usually work, because the X11 interfaces have not changed significantly since earlier releases. On the other hand, routing and interface configuration information has changed since earlier releases in incompatible ways, so old libraries that handle routing or interface configuration may not work.

NOTE: We now know of a problem with linking against large numbers of a.out objects with the GNU linker. The ld(1) program may fail, complaining that it can't allocate memory. The ld(1) program appears to have a memory leak that affects only a.out objects, and it causes it to fail to reclaim mmap(2) memory. This in turn causes ld(1) to exceed a kernel limit on the maximum number of distinct memory regions per process. You can work around this problem by linking as root, or by raising the limit for non-root users using sysctl(1).

What is different about the debugger gdb(1)?

BSD/OS 4.0 includes gdb 4.16, the same version as in BSD/OS 3.1. We changed the configuration so that it could support debugging of ELF, a.out and COFF programs. Gdb can handle ELF format core files with both ELF and a.out programs; this is useful since the ELF core file format is considerably richer than the old a.out format, allowing us to include all writable data in the address space in the core file, not just the program's data segment, heap and stack. Gdb still doesn't understand the statically linked shared libraries, but it will correctly debug dynamically linked shared libraries, including shared libraries that were built with -g. The complete documentation for gdb is on-line and may be accessed using `info gdb'.

BSD/OS 4.0 comes with the ddd graphical interface for gdb. Ddd 2.2.3 is an X11R6 program that supplements the text-oriented gdb interface with buttons and menus. Ddd can display data structures graphically, using arrows and charts to show where pointers point. Through an oversight, the ddd(1) manual page is only installed if you use installsw(1) to load the MANSRC manual page source package, but the program has extensive online help if you prefer not to install the manual page.

What other programs have changed in order to support ELF?

Here is a quick summary:

See the manual pages for details about new or modified programs.

What library routines and header files have changed to support ELF?

We changed the nlist() routine to handle 32-bit ELF, 64-bit ELF and a.out format files. It now uses file mapping to read symbol tables and string tables. Since there can be confusion about whether a symbol needs to be prepended with an underscore, nlist() checks for the requested symbol with or without its prepended underscore. The nlist structure itself did not change for ELF, although ELF's symbols look nothing like nlist records.

Since ELF programs put their user stack in a different location from a.out programs, we had to change the -lkvm library to look for each process's stack at a location specific to that process, and to avoid making assumptions about the layout of the process.

The headers <a.out.h> and <sys/exec.h> still describe a.out format files. The new header <sys/elf.h> describes ELF format files. You may include both <a.out.h> and <sys/elf.h> in the same source file.

What is different about ELF kernels?

The biggest change is the new support for ELF user programs on the Intel architecture. The kernel exec code has been reorganized somewhat; we broke out support for different executable file formats into separate files, and added new common code for loading segments. The exec code was designed from the beginning to handle complex formats like ELF, so the transition was not particularly arduous; and in any event, our PowerPC port of BSD/OS 2.1 included ELF support in the kernel, which was easy to port to 3.x and the Intel architecture. We now configure both ELF and a.out support into every system. The system also generates exclusively ELF format core files. The executable file format now controls the location of the user stack; ELF, a.out and COFF programs all use different initial stack locations.

Apart from the ELF support for user programs, there isn't that much of a difference. ELF format kernels required an upgrade to the /boot standalone utility to boot them. The /boot utility supports both a.out and ELF kernels, although this isn't too useful since a.out kernels can't run ELF programs and some core 3.x programs won't run with ELF kernels (in particular, ifconfig and routing programs, plus kvm_mkdb). The kvm_mkdb utility had to change in order to be able to read the kernel's ELF symbol table. The kernel can now be configured to load at specific addresses without special hacks involving a_entry. Because of the upgraded assembler in BSD/OS 4.x, we were able to retire many old botches in assembly source files and inline assembly code. We fixed sources in both the kernel and the standalone code to take advantage of the new tools.

What does make(1) do differently?

The BSD make program is based on the Sprite make program, and it supports a set of standard header files that allow most makefiles to be short stubs that set a make variable or two and include a standard header. In previous versions of BSD/OS, the standard headers did not build or install shared libraries. In BSD/OS 4.x, the <bsd.lib.mk> header now builds and installs both statically linked and dynamically linked shared libraries, when appropriate. See below for details.

Since ELF systems use the System V archive format (see ar(5)), and make can compute dependencies on individual archive members, we had to change make to handle System V archives. Very few people use this feature...

How do I build dynamically linked programs?

If you are just compiling programs using cc, then you will now generate dynamically linked programs that use the system's dynamically linked shared libraries. If you use make to build programs, but you don't use the system make headers like <bsd.prog.mk>, then you will compile and link with cc and your programs will be dynamically linked.

However, if you do use the system make headers, your programs will be linked against the system's statically linked shared libraries using shlicc or shlicc++. To force the system to use cc instead of shlicc, you must set the make variable LDCC to cc:


The LDCC variable specifies the C compiler and flags to use for linking programs.

How do I build programs with statically linked shared libraries?

Use the shlicc program (or shlicc++ for C++ programs). The answer to this question hasn't changed since BSD/OS 3.x, but since we now support two kinds of shared libraries, dynamically linked and statically linked, it's useful to point out that the old shared library tools are still available. The shlicc program creates ELF files instead of a.out files in BSD/OS 4.x, but it uses the same techniques for linking and loading shared libraries as before. (With one interesting exception: the statically linked shared C library is the ELF `interpreter' for programs that shlicc links. Since the system must load the ELF interpreter, this saves a few mmap() calls in the application.)

Note that the system's make headers still arrange to link programs with shlicc by default. Most of the programs in the BSD/OS 4.x distribution use statically linked shared libraries. See below for a discussion.

How do I build a program that dynamically loads a shared object (with dlopen(3))?

The dlopen() function lets you load code and data into a running program and execute or read it. In earlier versions of BSD/OS, we supported a version of dlopen() that used regular a.out object files (`.o' files). This implementation did the job well enough to support programs like perl, but it was not compatible with the rest of the Unix community, which mostly used the ELF dynamic linker to load shared objects (`.so' files, not `.o' files) into running programs. For BSD/OS 4.x, we have ported the dlopen() suite of functions from the Linux operating system to go with our port of the Linux dynamic linker, so you can now do dynamic linking the same way that most Unix and Unix-like systems do it. Makefiles for programs that use dlopen() often work unchanged in BSD/OS 4.x; this was definitely false for earlier versions of BSD/OS.

Here are some tips for building programs that use dlopen() under BSD/OS 4.x:

For more details, see the manual page dlopen(3).

How do I tell the dynamic linker where to find shared objects?

When you run the dynamic linker, it gets the name of a shared library or shared object, and tries to find a corresponding full pathname. The process is similar to looking up the pathname to a program in the shell. As with the shell, you can use environment variables to control paths that the dynamic linker checks:

For more details, see ld.so(8) and ldconfig(8).

What sorts of problems can cause programs not to build with ELF tools?

There don't seem to be many problems that cause programs to fail to build under BSD/OS 4.x, if they built properly under BSD/OS 3.0. Here are a few problems that we do know about:

How do I build shared libraries with make(1)?

To build and install shared libraries using makefiles, you have two choices: you can use BSD/OS's built-in makefile headers, which have rules that are specifically designed for building shared libraries, or you can cook up something by hand. Under this heading, we will discuss the approach with makefile headers; see below for details about building dynamically linked shared libraries without the makefile headers, and further below for details about statically linked shared libraries. See the make(1) manual page for details about the syntax and features of BSD/OS's Sprite-based make program. BSD/OS also supplies GNU make as gmake; we don't supply a set of gmake headers, however (mostly because we use the Sprite-based make internally). (Our X11R6 imake headers support automatic construction of shared libraries, but since just about every X library source package seems to contain its own tools for building shared libraries, we haven't attempted to make our imake headers work outside the X11R6 tree. Check out the file /usr/X11R6/lib/X11/config/bsdiLib.rules to see what we did.)

The BSD/OS makefile header that builds libraries is named <bsd.lib.mk>. Although I'm calling it a header, it is traditionally included at the bottom of the makefile using the .include directive. An extremely simple library makefile might look like this:

LIB=    foo
SRCS=   foo.c
MAN3=   foo.0

.include <bsd.lib.mk>

This simple makefile will allow you to build and install the following:

The usual make idiom for building and installing looks like this:

make obj           # create a subdirectory to hold object files
make depend        # analyze dependencies among source files and header files
make               # compile the library and format the manual page (if any)
make install       # install the library and the manual page (if any)

Note that you need write permission in /usr/obj if you want to run `make obj'. The obj subdirectory is optional but convenient; if you don't want the automatically created subdirectory under /usr/obj, then you can create one using `mkdir obj'.

You don't need to re-do any of these steps if they succeeded on a previous attempt, unless you have changed something that make isn't smart enough to detect. For example, you may need to run `make depend' again if you changed the makefile to add or remove a source file from the list of source files. You can run `make clean' to remove the generated files that are cheap and easy to re-create, or `make cleandir' to clean thoroughly (a new `make depend' is needed after a `make cleandir').

Certain make variables that affect the behavior of <bsd.lib.mk>:

How do I build dynamically linked shared libraries by hand?

If you don't want to use BSD/OS's make header feature (see above), or if you have other issues such as a complex pre-existing makefile, then you will have to create your shared libraries `by hand'. Here are the essential steps:

How do I install new dynamically linked shared libraries?

These instructions apply regardless of how you build a dynamically linked shared library:

Can I debug dynamically linked shared libraries?

Gdb can almost always handle code and data in shared libraries and shared objects. We have experienced a few odd situations in which gdb gets confused about the number of breakpoints that it needs to look for when loading a shared object with dlopen(3). When this bug crops up, you can normally work around it by interrupting the program and running it again under gdb. For example:

$ gdb program
(gdb) run
[... program calls dlopen(3) ...]
[... program hangs ...]
^C(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: program
[... the program works this time ...]

This bug has been fixed in our port of gdb 4.17, which we hope to release soon.

Note that you can compile your shared library (or shared object) source files with -g so that you can subsequently perform source code debugging on shared library functions.

How do I build statically linked shared libraries?

Although statically linked shared libraries are now ELF files, the procedure for building them by hand has remained the same since BSD/OS 3.x. Alas, it's still an arcane process...

The most important part of creating a statically linked shared library is to create a record for the shared library in the /etc/shlib.map file (or a private shlib.map file). The shlib.map file uses Bourne shell syntax, although it is not a complete Bourne shell script; different users of the file interpret it in different ways. However, it is useful to know that comments, variables and line continuations are exactly the same in a shlib.map file as they are in the shell. The file contains a MAP record for each known statically linked shared library. A MAP record begins with the MAP keyword and has at least 5 fields (arguments):

  1. The library name as it appears on a link line; for example, -lfoo.
  2. The text address of the library. This address must be unique across all of the libraries that might conceivably be loaded with this one. The address must be page-aligned. A comment at the top of /etc/shlib.map describes the current address space layout for the libraries that BSDI distributes. The address is given in hexadecimal with no 0x prefix.
  3. The data address of the library. The syntax for this address is the same as for the text address. The data address must be unique, it must be page-aligned, it must be greater than the text address and it must provide enough room for the entire text segment of the library to fit below it without overlap. No overlap detection is attempted - your library will just fail mysteriously if there are overlaps. Also, there must be enough room beneath the text address of the library with the next higher address for both the data segment and bss segment of the library to fit, or again you may see mysterious failures.
  4. The path to the archive form of the library, for example /usr/lib/libfoo.a. The standard /etc/shlib.map file defines some handy shell variables that abbreviate the usual directory names. Note that the directory prefix for the archive library is also used as the directory prefix for the `stub library' (libfoo_s.4.0.0.a), for the default const and except files (see shlib(8) for information about these files) and for the default loader source file (ditto).
  5. The path to the shared library, for example /shlib/libfoo_s.4.0.0. Again, useful prefixes are defined as shell variables. If your library will be used by programs that run in single user mode, put it in /shlib; otherwise, you can choose another appropriate directory. If you use the standard make headers, the directory prefix for this path will be used to set the default location for the dynamically linked shared library.

Any remaining fields are passed to the shlib program; these fields are often use to pass options. The most common usages are as follows:

Statically linked shared libraries (libfoo_s.4.0.0) are created from archive libraries (libfoo.a), so building a statically linked shared library follows the same steps as as building an archive library:

In the next step, we show how a statically linked shared library is linked and installed with one command.

How do I install statically linked shared libraries?

As in earlier versions of BSD/OS, the mkshlib command links and installs statically linked shared libraries. It works by parsing the shlib.map file and creating an appropriate shlib command, which it prints and executes. The shlib program does the hard work of laying out the shared library, building the jump tables, linking it and installing it along with the `stub' library. (The stub library contains addresses of functions and data in the shared library, and the shlicc program links your program against stub libraries rather than the shared library image itself.) In BSD/OS 4.x, the statically linked shared libraries are ELF executable files but they are otherwise very similar to the old a.out shared libraries. (One arcane issue: The ELF libraries put their .rodata read-only data segment at the start of the text segment, so that changes in the sizes of functions won't affect the offsets of read-only data. This ameliorates one of the many disadvantages of statically linked shared libraries.)

If you use the standard BSD/OS make header, <bsd.lib.mk>, you do not need to run mkshlib after executing `make install' any more. The statically linked shared library now gets built when all the other libraries get built, so that you get feedback about linking errors at build time rather than at installation time. This does mean that make doesn't use mkshlib to build or install statically linked shared libraries - instead, it uses ugly code in <bsd.lib.mk>.

What exactly do ELF files look like inside?

They are much fancier than a.out binaries. They always have at least two headers, sometimes three. There is a fixed-size initial header that contains general information about the architecture for which the program was compiled, its byte order and word size, and the sizes and locations of other headers. If you want to run or debug an ELF file, it must contain a program header table, which describes how the various parts of the file are (or were) loaded into memory. If you want to link an ELF file, it must have a section header table, which describes how chunks of the file can be combined with chunks from other files. Unlike the a.out format, an ELF file may have an arbitrary number of loadable segments. Our compiler and debugger support two symbolic debugging formats, the DBX/GDB format and the SVr4 DWARF format; we default to DBX/GDB format.

ELF binaries may have interpreters that are really shared library loader programs. `Interpreted' ELF binaries aren't run directly; instead, the kernel loads both the binary and the shared library loader into virtual memory and starts up the loader. The loader does its job and then runs the binary, which may call back into the loader. Both the dynamically linked shared libraries and the statically linked shared libraries use this feature. Since an ELF binary doesn't need to bootstrap itself, we don't need to waste space in the binary on a bootstrap.

ELF core files are special ELF files that contain copies of all the writable memory in the program that died, along with a special segment for register and signal information. This is an improvement over traditional core files, which left no information about shared libraries in the core image. Note that all binaries now generate ELF core files, no matter what format the binary itself has. GDB should handle ELF core files for a.out binaries just fine.

For detailed information on ELF headers, see the manual page elf(5).

How are ELF programs laid out in memory?

ELF programs have a somewhat different layout from a.out programs. The stack is the segment with the lowest address; it normally grows down from 0x08048000. The text or code segment starts contiguously above the stack. The data segment follows the text segment. The data segment isn't normally page-aligned in the ELF file; instead, the beginning of the data segment overlaps with the end of the text segment in the file, and when the system loads the program, it maps this shared page twice, once with read-only protection for the end of the text segment and once with read-write protection for the start of the data segment. The second mapping immediately follows the first one. The bss or zero-filled data segment takes up no space in the ELF file, even for padding; the system zeroes out any part of the last page of data that is beyond the end of the data segment, and adds enough zero-filled pages to complete the segment. The heap or dynamically-allocated memory segment follows the bss segment.

The odd location of the segments has a number of benefits. Because the stack segment doesn't normally run all the way to address 0, the first chunk of the address space is reserved, which catches many null pointer errors. Because the stack normally fits in the same 4 megabyte chunk of address space as the text and data, statically linked unshared programs need only one page table page, instead of the two that they required in the a.out format.

Shared libraries live at different addresses depending on whether they were statically linked or dynamically linked. Statically linked shared libraries have fixed addresses described in the /etc/shlib.map file. These addresses haven't changed much since BSD/OS 3.x; the most notable change was to bump the -ltermcap and -lcurses libraries to higher addresses to accommodate the fatter new math library -lm. Dynamically linked shared libraries can in principle be loaded anywhere in the address space as long as they don't overlap with something else; typically they are loaded starting at MAXDSIZ plus the address of the data segment, which usually puts them at 0x0c048000 or higher. The dynamic linker is not loaded at a fixed address -- it must relocate its internal pointers to match the address that the kernel assigns it.

How does the dynamic linker work?

The dynamic linker /shlib/ld-bsdi.so is responsible for assembling the full, executable version of a program in memory, using pieces called dynamically linked shared libraries. Here is a quick overview of dynamic linking.

When you build a program for dynamic linking, the compiler cc and static linker ld put special data structures into the ELF file. These include:

These data structures are placed in special locations in the ELF file so that the strip program won't remove them.

When you run the program, the system loads the standard segments of the ELF file into memory. It notices the interpreter segment that contains the name of the dynamic linker, and it loads that ELF file too. The dynamic linker has the usual text and data segments but lacks its own stack; instead, several parameters relevant to the dynamic linker are passed on the program's stack (see auxv(5)). When the system passes control to the new process image that contains your program, it starts at the dynamic linker's entry point, not your program's entry point.

The dynamic linker first gets the information it needs from the stack and relocates itself; that is, it patches pointers so that they refer to the linker's actual location in memory, which was not fixed when the linker was built. This bootstrapping procedure is somewhat ugly, but it's necessary. The linker performs some initializations, then it assembles the list of shared libraries that need to be loaded, and loads them and links them. The linker locates the shared libraries using the list of library names in the program and a database that maps library names to pathnames in /etc/ld.so.cache.

To load a shared library, the dynamic linker must choose an address range in virtual memory that won't overlap with something else, then use mmap() to put the contents of the various segments in the ELF file into that range. Once the shared library has been loaded, the dynamic linker reads the special data structures in the library. It builds a list of the correct locations of functions and data using symbols, and it uses the relocation tables to patch pointers that refer to those functions and data. The pointers in shared libraries are carefully segregated so that relatively few pages of memory need to be patched (using techniques of position independent code). The linker postpones patching the procedure linkage table - it trickily arranges to call itself when you call an unlinked function in your program or in a shared library, and so it only needs to patch pointers to functions that your program actually calls. (Once patched, the function pointer stays patched, so that you don't have to pay the linking cost again.)

When the dynamic linker has loaded the shared libraries and finished linking, it jumps to the start address of your program and your code (finally) executes...

For some specific details about the dynamic linker, see the ld.so(8) manual page.

How can I tell whether an ELF program was built under BSD/OS?

If you build an ELF program under BSD/OS 4.x using cc or shlicc, it will include a small ELF `note' segment called a brand. A brand note identifies the operating system that generated an ELF binary. It isn't a required part of an ELF binary, but providing a brand is good citizenship... Using GCC 2 extensions, the brand is implemented as follows:

#define OSNAME  "BSD/OS"

static const struct {
    int32_t namelen;
    int32_t desclen;
    int32_t type;
    char name[roundup(sizeof (OSNAME), sizeof (int32_t))];
    char desc[roundup(sizeof (RELEASE), sizeof (int32_t))];
} ident __attribute__ ((__section__ (".note.ident"))) = {
    sizeof (OSNAME),
    sizeof (RELEASE),

To translate this into English, the note segment contains a data structure consisting of three 32-bit words followed by two strings. The strings are padded to 32-bit alignment and are nul-terminated. The first two words are the lengths of the two strings, including the nul bytes but excluding padding. The third word is the constant 1. The first string is always BSD/OS (with a trailing nul byte). The second string is the output from `uname -r' plus a terminating nul byte. Other operating systems also provide brand notes; they can be distinguished by the operating system name in the first string.

What are the advantages of dynamically linked shared libraries over statically linked shared libraries?

For developers, dynamically linked shared libraries have some big advantages over statically linked shared libraries:

What are the advantages of statically linked shared libraries?

There are still a couple of advantages to statically linked shared libraries:

Despite these advantages, we may phase out statically linked shared libraries in future releases, primarily because maintaining multiple types of libraries is annoying extra work. Also, there's a reasonable chance that in the future, dynamically linked programs from previous releases might be compatible with libraries from the current release, so we wouldn't have to ship as many old versions of libraries for the purpose of compatibility.

How are the programs in the distribution created?

We still build almost all of the programs in the distribution using shlicc. A few that require dlopen(), use interposition or had seriously hairy makefiles are provided as dynamically linked programs.

What are the differences between version 1.0 of the FAQ and version 1.1?

What are the differences between version 1.1 of the FAQ and version 1.2?

What are the differences between version 1.2 of the FAQ and version 1.3?

Many thanks to everyone who contributed review comments about the ELF FAQ version 1.0! -- Donn Seeley

Copyright © 1994-2001 BSDi - All rights reserved.
Portions Copyright © 2000-2001 Kurt J. Lidl - All rights reserved.