I’m writing an x86 binary interpreter.
At the moment I’m dealing with loading the executable file and shared objects. However I’m stuck with some doubts:
1) Does the dynamic linker/loader concatenate .ini sections of the main executable and shared libraries to generate just one .ini section for the process image? And for the .fini section?
2) Does it concatenate the many symbol and string table?
3) I’m lost about relocations. They happen should be made at the moment the binary is loaded or just when a procedure is called? I guess I didn’t understand how the dynamic linker/loader manages the relocations.
4) Why there exist .hash and .gnu.hash sections? Why do I need ‘hash’ a symbol?
Links, comments and oviously answers are welcome.
As far as the loader is concerned, sections don’t matter — they’re ignored. The loader only looks at segments, and each loadable segment of the executable is loaded at the specified address. The loader will then trigger the dynamic linker (if called for in the executable) to deal with shared objects. Generally the symbol and string tables are not in loadable segments, so the loader ignores them.
So answering your questions in turn:
1) The loader ignores the .init and .fini sections. They will generally be part of some loadable segment and the initial code in the exectuable will run the code in the .init section. The dynamic linker will load the segments of shared objects, and call each entry point which will similarly call the .init code which is in some loaded segment
2) The string/symbol tables are only meaningful for linking, not loading. So the dynamic linker will look at them to resolve any relocations and build jump tables
3) Relocations are mostly use for (static) linking — executables should never have them and they should be rare in shared objects (which are generally built position-independent so none are needed). Some dynamic linkers can’t deal with relocation at all (not sure about the normal linux dynamic linker), so they can’t load shared objects that still have relocations
4) .hash sections are just an optimization to speed up the lookup of symbols — rather than doing a linear search through the symbol table for a specific symbol, a .hash section will take you directly to it. You can safely ignore them and do symbol lookups slowly if you prefer.
edit
A short somewhat vague description of what an ELF loader does:
Reads the program header of the ELF file
Load all the LOAD segments of the file into memory.
If there’s an INTERP entry in the program header, recursively load that binary
call the entry point of the program.
That’s pretty much it (there’s some extra cruft about setting up the stack, but that’s not clearly part of the loader rather than part of process setup before the loader even runs).
For a statically linked executable, there’s no INTERP entry, so that’s pretty much it. For a dynamically linked executable, the INTERP section will be something like “/lib/ld-linux.so.2” (a string), so the recursive call to the loader will read that binary file, load all the LOAD sections, notice there’s no INTERP section (so no further recursive call), call the entry point, and then return (at which point the loader for the base executable will call the entry point of the base execuatable).
Now the dynamic linker is that second executable (/lib/ld-linux.so.2) that got loaded. What it does is go and read the .dynamic section of the original binary. This will tell it a list of shared objects to load, and table (the .plt section — program load table) to fill in with addresses of specific symbols in those shared objects. So it will load those shared objects, look up the symbols in them, and stick their addresses into that table. Each shared object will have its own .dynamic section which will be recursively dealt with by the dynamic linker. The symbol lookups look at all symbols in all objects loaded so far, so symbols in the main program may ‘override’ symbols in other shared objects and have their addresses stuck in the .plt for the shared object. After each shared object and all its dependencies have been loaded, the entry point for the shared object is called. If two shared objects depend on each other (which is legal) both will be loaded and have their .plts resolved and then both entry points will be called, but not in any particularly defined order.
Note that in all the above, relocations never come into it. Where relocations might come into things is when a shared object can’t be loaded at the (virtual) address specified in the shared object (because something else was already loaded at that address). When that happens, the shared object needs to be relocated to load at a different address, which involves looking at all the relocation entries in the object for things that need to be patched to deal with the relocated addresses.
Finally, a symbol reference only has an offset in the symbol table of the object containing the reference — the linker needs to look up the symbol name (string) in the symbol tables of all the other objects that have been loaded to figure out what it refers to. Every object has its own symbol table, and those tables aren’t combined other than logically. A symbol lookup goes through all the objects that have been loaded so far, lookup up that symbol in each symbol table in turn, looking for an entry that defines the symbol.