airs-notes/linkers-5.md

185 lines
10 KiB
Markdown
Raw Normal View History

2021-01-12 20:17:52 +00:00
# Linkers part 5
## Shared Libraries Redux
Yesterday I talked about how shared libraries work. I realized that I should
say something about how linkers implement shared libraries. This discussion
will again be ELF specific.
When the program linker puts position dependent code into a shared library, it
has to copy more of the relocations from the object file into the shared
library. They will become dynamic relocations computed by the dynamic linker at
runtime. Some relocations do not have to be copied; for example, a PC relative
relocation to a symbol which is local to shared library can be fully resolved
by the program linker, and does not require a dynamic reloc. However, note that
a PC relative relocation to a global symbol does require a dynamic relocation;
otherwise, the main executable would not be able to override the symbol. Some
relocations have to exist in the shared library, but do not need to be actual
copies of the relocations in the object file; for example, a relocation which
computes the absolute address of symbol which is local to the shared library
can often be replaced with a `RELATIVE` reloc, which simply directs the dynamic
linker to add the difference between the shared librarys load address and its
base address. The advantage of using a `RELATIVE` reloc is that the dynamic
linker can compute it quickly at runtime, because it does not require
determining the value of a symbol.
For position independent code, the program linker has a harder job. The
compiler and assembler will cooperate to generate special relocs for position
independent code. Although details differ among processors, there will
typically be a `PLT` reloc and a `GOT` reloc. These relocs will direct the program
linker to add an entry to the PLT or the GOT, as well as performing some
computation. For example, on the i386 a function call in position independent
code will generate a `R_386_PLT32` reloc. This reloc will refer to a symbol as
usual. It will direct the program linker to add a PLT entry for that symbol,
if one does not already exist. The computation of the reloc is then a
PC-relative reference to the PLT entry. (The `32` in the name of the reloc
refers to the size of the reference, which is 32 bits). Yesterday I described
how on the i386 every PLT entry also has a corresponding GOT entry, so the
`R_386_PLT32` reloc actually directs the program linker to create both a PLT
entry and a GOT entry.
When the program linker creates an entry in the PLT or the GOT, it must also
generate a dynamic reloc to tell the dynamic linker about the entry. This will
typically be a `JMP_SLOT` or `GLOB_DAT` relocation.
This all means that the program linker must keep track of the PLT entry and the
GOT entry for each symbol. Initially, of course, there will be no such entries.
When the linker sees a PLT or GOT reloc, it must check whether the symbol
referenced by the reloc already has a PLT or GOT entry, and create one if it
does not. Note that it is possible for a single symbol to have both a PLT entry
and a GOT entry; this will happen for position independent code which both
calls a function and also takes its address.
The dynamic linkers job for the PLT and GOT tables is to simply compute the
`JMP_SLOT` and `GLOB_DAT` relocs at runtime. The main complexity here is the
lazy evaluation of PLT entries which I described yesterday.
The fact that C permits taking the address of a function introduces an
interesting wrinkle. In C you are permitted to take the address of a function,
and you are permitted to compare that address to another function address. The
problem is that if you take the address of a function in a shared library, the
natural result would be to get the address of the PLT entry. After all, that is
address to which a call to the function will jump. However, each shared library
has its own PLT, and thus the address of a particular function would differ in
each shared library. That means that comparisons of function pointers generated
in different shared libraries may be different when they should be the same.
This is not a purely hypothetical problem; when I did a port which got it
wrong, before I fixed the bug I saw failures in the Tcl shared library when it
compared function pointers.
The fix for this bug on most processors is a special marking for a symbol which
has a PLT entry but is not defined. Typically the symbol will be marked as
undefined, but with a non-zero valuethe value will be set to the address of
the PLT entry. When the dynamic linker is searching for the value of a symbol
to use for a reloc other than a `JMP_SLOT` reloc, if it finds such a specially
marked symbol, it will use the non-zero value. This will ensure that all
references to the symbol which are not function calls will use the same value.
To make this work, the compiler and assembler must make sure that any reference
to a function which does not involve calling it will not carry a standard PLT
reloc. This special handling of function addresses needs to be implemented in
both the program linker and the dynamic linker.
## ELF Symbols
OK, enough about shared libraries. Lets go over ELF symbols in more detail.
Im not going to lay out the exact data structuresgo to the ELF ABI for that.
Im going to take about the different fields and what they mean. Many of the
different types of ELF symbols are also used by other object file formats, but
I wont cover that.
An entry in an ELF symbol table has eight pieces of information: a name, a
value, a size, a section, a binding, a type, a visibility, and undefined
additional information (currently there are six undefined bits, though more may
be added). An ELF symbol defined in a shared object may also have an associated
version name.
The name is obvious.
For an ordinary defined symbol, the section is some section in the file
(specifically, the symbol table entry holds an index into the section table).
For an object file the value is relative to the start of the section. For an
executable the value is an absolute address. For a shared library the value is
relative to the base address.
For an undefined reference symbol, the section index is the special value
`SHN_UNDEF` which has the value `0`. A section index of `SHN_ABS` (`0xfff1`)
indicates that the value of the symbol is an absolute value, not relative to
any section.
A section index of `SHN_COMMON` (`0xfff2`) indicates a common symbol. Common
symbols were invented to handle Fortran common blocks, and they are also often
used for uninitialized global variables in C. A common symbol has unusual
semantics. Common symbols have a value of zero, but set the size field to the
desired size. If one object file has a common symbol and another has a
definition, the common symbol is treated as an undefined reference. If there is
no definition for a common symbol, the program linker acts as though it saw a
definition initialized to zero of the appropriate size. Two object files may
have common symbols of different sizes, in which case the program linker will
use the largest size. Implementing common symbol semantics across shared
libraries is a touchy subject, somewhat helped by the recent introduction of a
type for common symbols as well as a special section index (see the discussion
of symbol types below).
The size of an ELF symbol, other than a common symbol, is the size of the
variable or function. This is mainly used for debugging purposes.
The binding of an elf symbol is global, local, or weak. A global symbol is
globally visible. A local symbol is only locally visible (e.g., a static
function). Weak symbols come in two flavors. A weak undefined reference is like
an ordinary undefined reference, except that it is not an error if a relocation
refers to a weak undefined reference symbol which has no defining symbol.
Instead, the relocation is computed as though the symbol had the value zero.
A weak defined symbol is permitted to be linked with a non-weak defined symbol
of the same name without causing a multiple definition error. Historically
there are two ways for the program linker to handle a weak defined symbol. On
SVR4 if the program linker sees a weak defined symbol followed by a non-weak
defined symbol with the same name, it will issue a multiple definition error.
However, a non-weak defined symbol followed by a weak defined symbol will not
cause an error. On Solaris, a weak defined symbol followed by a non-weak
defined symbol is handled by causing all references to attach to the non-weak
defined symbol, with no error. This difference in behaviour is due to an
ambiguity in the ELF ABI which was read differently by different people. The
GNU linker follows the Solaris behaviour.
The type of an ELF symbol is one of the following:
* `STT_NOTYPE`: no particular type.
* `STT_OBJECT`: a data object, such as a variable.
* `STT_FUNC`: a function
* `STT_SECTION`: a local symbol associated with a section. This type of symbol
is used to reduce the number of local symbols required, by changing all
relocations against local symbols in a specific section to use the
STT_SECTION symbol instead.
* `STT_FILE`: a special symbol whose name is the name of the source file which
produced the object file.
* `STT_COMMON`: a common symbol. This is the same as setting the section index
to `SHN_COMMON`, except in a shared object. The program linker will normally
have allocated space for the common symbol in the shared object, so it will
have a real section index. The `STT_COMMON` type tells the dynamic linker
that although the symbol has a regular definition, it is a common symbol.
* `STT_TLS`: a symbol in the Thread Local Storage area. I will describe this in
more detail some other day.
ELF symbol visibility was invented to provide more control over which symbols
were accessible outside a shared library. The basic idea is that a symbol may
be global within a shared library, but local outside the shared library.
* `STV_DEFAULT`: the usual visibility rules apply: global symbols are visible
everywhere.
* `STV_INTERNAL`: the symbol is not accessible outside the current executable
or shared library.
* `STV_HIDDEN`: the symbol is not visible outside the current executable or
shared library, but it may be accessed indirectly, probably because some code
took its address.
* `STV_PROTECTED`: the symbol is visible outside the current executable or
shared object, but it may not be overridden. That is, if a protected symbol
in a shared library is referenced by other code in the shared library, that
other code will always reference the symbol in the shared library, even if
the executable defines a symbol with the same name.
Ill described symbol versions later.
More tomorrow.