天天看点

Position Independent Code (PIC) in shared libraries on x64

On x86, while function references (with the call instruction) use relative offsets from the instruction pointer, data references (with the mov instruction) only support absolute addresses. As we’ve seen in the previous article, this makes PIC code somewhat less efficient, since PIC by its nature requires making all offsets IP-relative; absolute addresses and position independence don’t go well together.

x64 fixes that, with a new "RIP-relative addressing mode", which is the default for all 64-bit movinstructions that reference memory (it’s used for other instructions as well, such as lea). A quote from the "Intel Architecture Manual vol 2a":

A new addressing form, RIP-relative (relative instruction-pointer) addressing, is implemented in 64-bit mode. An effective address is formed by adding displacement to the 64-bit RIP of the next instruction.

The displacement used in RIP-relative mode is 32 bits in size. Since it should be useful for both positive and negative offsets, roughly +/- 2GB is the maximal offset from RIP supported by this addressing mode.

For easier comparison, I will use the same C source as in the data reference example of the previous article:

Let’s look at the disassembly of ml_func:

The most interesting instruction here is at 0x5f6: it places the address of myglobal into rax, by referencing an entry in the GOT. As we can see, it uses RIP relative addressing. Since it’s relative to the address of the next instruction, what we actually get is 0x5fd + 0x2009db = 0x200fd8. So the GOT entry holding the address of myglob is at 0x200fd8. Let’s check if it makes sense:

GOT starts at 0x200fc8, so myglob is in its third entry. We can also see the relocation inserted for the GOT reference to myglob:

Indeed, a relocation entry for 0x200fd8 telling the dynamic linker to place the address of myglobinto it once the final address of this symbol is known.

Now let’s see how function calls work with PIC code on x64. Once again, we’ll use the same example from the previous article:

Disassembling ml_func, we get:

The call is, as before, to ml_util_func@plt. Let’s see what’s there:

So, the GOT entry holding the actual address of ml_util_func is at 0x200aa2 + 0x566 = 0x201008.

And there’s a relocation for it, as expected:

In both examples, it can be seen that PIC on x64 requires less instructions than on x86. On x86, the GOT address is loaded into some base register (ebx by convention) in two steps – first the address of the instruction is obtained with a special function call, and then the offset to GOT is added. Both steps aren’t required on x64, since the relative offset to GOT is known to the linker and can simply be encoded in the instruction itself with RIP relative addressing.

When calling a function, there’s also no need to prepare the GOT address in ebx for the trampoline, as the x86 code does, since the trampoline just accesses its GOT entry directly through RIP-relative addressing.

Note how myglob is accessed here, in instruction at address 0xa. It expects the linker to patch in a relocation to the actual location of myglob into the operand of the instruction (so no GOT redirection is required):

Here is the R_X86_64_PC32 relocation the linker was complaining about. It just can’t link an object with such relocation into a shared library. Why? Because the displacement of the mov (the part that’s added to rip) must fit in 32 bits, and when a code gets into a shared library, we just can’t know in advance that 32 bits will be enough. After all, this is a full 64-bit architecture, with a vast address space. The symbol may eventually be found in some shared library that’s farther away from the reference than 32 bits will allow to reference. This makes R_X86_64_PC32 an invalid relocation for shared libraries on x64.

It turns out that to make the compiler generate non-PIC code on x64 that actually pleases the linker, only the large code model is suitable, because it’s the least restrictive. Remember how I explained why the simple relocation isn’t good enough on x64, for fear of an offset which will get farther than 32 bits away during linking? Well, the large code model basically gives up on all offset assumptions and uses the largest 64-bit offsets for all its data references. This makes load-time relocations always safe, and enables non-PIC code generation on x64. Let’s see the disassembly of the first example compiled without -fpic and with -mcmodel=large:

Now let’s see the relocations:

Note the relocation type has changed to R_X86_64_64, which is an absolute relocation that can have a 64-bit value. It’s acceptable by the linker, which will now gladly agree to link this object file into a shared library.

Some judgmental thinking may bring you to ponder why the compiler generated code that isn’t suitable for load-time relocation by default. The answer to this is simple. Don’t forget that code also tends to get directly linked into executables, which don’t require load-time relocations at all. Therefore, by default the compiler assumes the small code model to generate the most efficient code. If you know your code is going to get into a shared library, and you don’t want PIC, then just tell it to use the large code model explicitly. I think gcc‘s behavior makes sense here.

Another thing to think about is why there are no problems with PIC code using the small code model. The reason is that the GOT is always located in the same shared library as the code that references it, and unless a single shared library is big enough for a 32-bit address space, there should be no problems addressing the PIC with 32-bit RIP-relative offsets. Such huge shared libraries are unlikely, but in case you’re working on one, the AMD64 ABI has a "large PIC code model" for this purpose.

This article complements its predecessor by showing how PIC works on the x64 architecture. This architecture has a new addressing mode that helps PIC code be faster, and thus makes it more desirable for shared libraries than on x86, where the cost is higher. Since x64 is currently the most popular architecture used in servers, desktops and laptops, this is important to know. Therefore, I tried to focus on additional aspects of compiling code into shared libraries, such as non-PIC code. If you have any questions and/or suggestions on future directions to explore, please let me know in the comments or by email.

<a href="http://eli.thegreenplace.net/2011/11/11/position-independent-code-pic-in-shared-libraries-on-x64/#id1">[1]</a>

As always, I’m using x64 as a convenient short name for the architecture known as x86-64, AMD64 or Intel 64.

<a href="http://eli.thegreenplace.net/2011/11/11/position-independent-code-pic-in-shared-libraries-on-x64/#id2">[2]</a>

Into eax and not rax because the type of myglob is int, which is still 32-bit on x64.

<a href="http://eli.thegreenplace.net/2011/11/11/position-independent-code-pic-in-shared-libraries-on-x64/#id3">[3]</a>

By the way, it would be much less "painful" to tie down a register on x64, since it has twice as many GPRs as x86.

<a href="http://eli.thegreenplace.net/2011/11/11/position-independent-code-pic-in-shared-libraries-on-x64/#id4">[4]</a>

It also happens if we explicitly specify we don’t want PIC by passing -fno-pic to gcc.

<a href="http://eli.thegreenplace.net/2011/11/11/position-independent-code-pic-in-shared-libraries-on-x64/#id5">[5]</a>

Note that unlike other disassembly listings we’ve been looking at in this and the previous article, this is an object file, not a shared library or executable. Therefore it will contain some relocations for the linker.

<a href="http://eli.thegreenplace.net/2011/11/11/position-independent-code-pic-in-shared-libraries-on-x64/#id6">[6]</a>

For some good information on this subject, take a look at the AMD64 ABI, and man gcc.

<a href="http://eli.thegreenplace.net/2011/11/11/position-independent-code-pic-in-shared-libraries-on-x64/#id7">[7]</a>

Some assemblers call this instruction movabs to distinguish it from the other mov instructions that accept a relative argument. The Intel architecture manual, however, keeps naming it just mov. Its opcode format is REX.W + B8 + rd.

Related posts:

<a href="http://eli.thegreenplace.net/2011/11/03/position-independent-code-pic-in-shared-libraries/">Position Independent Code (PIC) in shared libraries</a>

<a href="http://eli.thegreenplace.net/2011/08/25/load-time-relocation-of-shared-libraries/">Load-time relocation of shared libraries</a>

<a href="http://eli.thegreenplace.net/2012/01/03/understanding-the-x64-code-models/">Understanding the x64 code models</a>

<a href="http://eli.thegreenplace.net/2011/02/04/where-the-top-of-the-stack-is-on-x86/">Where the top of the stack is on x86</a>

<a href="http://eli.thegreenplace.net/2010/02/13/finding-out-the-mouse-click-position-on-a-canvas-with-./">Finding out the mouse click position on a canvas with Javascript</a>

继续阅读