On many architectures, calling a function does not just mean jumping straight to an address. For example, on Itanium calling a function involves setting a global pointer register before jumping to the function code. The global pointer is used as a known fixed point which can be offset against very quickly. Thus a function is described by two things; the code address and a global pointer value. Unsurprisingly, this information is kept in a function descriptor.
Now, the general process when calling a function from a dynamically linked library is as follows:
- Your code calls a stub entry in the Procedure Lookup Table (PLT).
- The PLT stub reads the function descriptor (address and GP) to call from the Global Offset Table (GOT).
- At first, this will refer to a function in the dynamic linker which will find the library and function you actually want to call, and then patch the GOT function descriptor entry so next time the PLT stub looks at it, you go there directly.
This all works fine. The problem occurs when you try to take the address of a function. On x86 you can just read the function address from the GOT and return it. However, with function descriptors you can not just return the code address -- it is invalid to call the function without the GP set correctly. You can't return the address of your copy of function descriptor, because then what should be the same function doesn't have the same address, which is obviously incorrect.
The Itanium solution is an "official" function descriptor. This is kept by the dynamic linker in private memory, and whenever a function address is requested it will be returned. Consider pretty much the smallest possible example:
$ cat libtest.c void* function(void) { return function; } $ cat test.c extern void* function(void); int main(void) { void*(*ptr)(void) = function; } $ gcc -shared -o libtest.so ./libtest.c $ gcc -o test test.c -Wl,-L. -ltest
If we have a look at the relocations in the library we see the following
$ readelf --relocs ./libtest.so Relocation section '.rela.dyn' at offset 0x440 contains 10 entries: Offset Info Type Sym. Value Sym. Name + Addend [...] 000000010cc0 000b00000047 R_IA64_FPTR64LSB 00000000000007c0 function + 0
hen the dynamic linker is going through the relocations, it responds to any R_IA64_FPTR64LSB relocations by creating a new official function descriptor.
Now we can have a look at the binary
$ readelf --relocs ./test Relocation section '.rela.dyn' at offset 0x458 contains 3 entries: Offset Info Type Sym. Value Sym. Name + Addend [...] 6000000000000e80 000400000047 R_IA64_FPTR64LSB 0000000000000000 function + 0
This is a bit different, and requires some more investigation. Firstly lets look at the disassembly:
40000000000007c0 <main>: 40000000000007c0: 02 10 00 18 00 21 [MII] mov r2=r12 40000000000007c6: e0 40 05 00 48 00 addl r14=40,r1;; 40000000000007cc: 00 00 04 00 nop.i 0x0 40000000000007d0: 0a 70 00 1c 18 10 [MMI] ld8 r14=[r14];; 40000000000007d6: 00 70 08 30 23 80 st8 [r2]=r14 40000000000007dc: 01 10 00 84 mov r12=r2 40000000000007e0: 11 00 00 00 01 00 [MIB] nop.m 0x0 40000000000007e6: 00 00 00 02 00 80 nop.i 0x0 40000000000007ec: 08 00 84 00 br.ret.sptk.many b0;;
We know that r1 points to the GOT (in fact, that is the GP value). We see that r14 is being loaded with a value from the GOT which is then de-referenced. Notice the offset of the address, correlating with the section output
There are 41 section headers, starting at offset 0x1690: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [...] [25] .got PROGBITS 6000000000000e58 00000e58 0000000000000050 0000000000000000 WAp 0 0 8
Now, 0x6000000000000e80 - 0x6000000000000e58 = 0x28 = 40 which is the offset we are loading from the GOT! So we can deduce that a R_IA64_FPTR64LSB relocation with a symbol value of 0 gets the dynamic linker to setup a pointer to the official function descriptor in the GOT. This is the value returned when you take the address of a function, so now function comparisions work!
Power and PA-RISC are two other examples of architectures that use function descriptors. I think they are generally a feature of architectures who are not register-starved, because you generally pass along a pointer value to global data, which makes offsetting into that global data much faster than looking it up from memory.