Thursday, December 08, 2005

Look Ma, I'm Lazy!!!!

Lazy binding on Linux implies that dynamic symbols are not resolved by either the link-editor during link-time or the dynamic linker at run-time, until the first time the symbols are actually referenced. Referencing in the case of variables means accessing them & in the case of functions, it means calling them. Let's prove Linux actually does what it claims by means of a small program.
I'm considering only the ELF format for analysis here. First off, all symbols (static or dynamic) have a symbol-table entry specifying its name ( actually an index into the string table) & an offset (or virtual address). Functions are also symbols, hence they too reside in this symbol table. Furthermore, all programs that are dynamically linked with a shared library also have stub-code within the executable for all functions that are called in the shared-library. This is used at runtime to find its actual address. The stub-code resides in a segment of the memory called PLT ( Procedure Linkage Table ). This is usually ro (read-only) memory. The executable in memory also has a GOT ( Global Offset Table ) that contains the actual address of the symbol at run-time. Whenever a symbol's address has to be resolved at runtime, the dynamic linker queries for the symbol in each of the dependent shared-libraries, finds out the base-address of the library where this symbol is present and populates the symbol's actual run-time address into the GOT. Now, the first instruction of the stub-code in the PLT is always of the form:
jmp *(got-address).
Here, got-address specifies the location in the GOT, where the address of the function resides. Initially, however, got-address points to the next instruction in the PLT itself, which pushes certain identifiers for the dynamic linker and jumps to the linker code itself. Now, the dynamic linker locates the load-address of the function and modifies the GOT entry to contain the actual address. So, the next time a function call is made, the overhead of skipping to the dynamic linker is omitted and the function is called directly.
The program that demonstrates all this is shown below (assuming there's a function called SO_FUNC in a shared-library and this file is linked with that library) :
extern void SO_FUNC(void);
int main(void)
{
Elf32_Addr *gotAddr, *lazyAddr, *pltAddr;

pltAddr = (Elf32_Addr*)SO_FUNC;
//Skip the opcode and MOD-REG-R/M....
gotAddr = (Elf32_Addr*)( (char*)(pltAddr)+0x2 );
lazyAddr = (Elf32_Addr*)(*gotAddr);
printf("Address of SO_FUNC before the Call: %x\n", *lazyAddr);
SO_FUNC();
printf("Address of SO_FUNC after the Call: %x\n", *lazyAddr);
}
Output:
Address of SO_FUNC before the Call: 804834e
Address of SO_FUNC after the Call: 40024704
As we see, the address of SO_FUNC before & after the call are different. To further confirm this, we can look at /proc//maps, where pid is the process-id of the above process and see that the address 0x40024704 falls in the memory mapped for that shared library where the function is present.
Of course, this the lazy way of doing this. The correct method would be to locate the address of
the function from the symbol table, its GOT entry from the relocation section ( the rel & rela sections are somehow not present in memory, however ), and then print its contents. But then, to do that, we need to read the ELF header, the Program Header & the Dynamic Segment or use
hashing. Then again, all programmers are born lazy.

No comments: