pete > courses > CS 315 Fall 26 > lecture 09: linking


Lecture 09: linking

Goals


let’s start with this sample program today:

hello-world.c

with linking at the forefront of our consciousness… where does printf come from?

when we compile this:

$ gcc -Wall -pedantic -o hello-world hello-world.c

we don’t tell gcc to link any files that would contain the machine code for printf

conclusion: it must be brought in implicitly

what, however, is "it" ?

answer: the Standard C Library contains all the library functions we’ve looked at to this point (printf, strcpy, getopt, etc)


clearly the aforementioned library gets linked without us telling gcc to do so

you might wonder whether we can ask gcc not to link it

and so we can, with the -nostdlib option—which is a linking option, not a compilation option, as shown here:

$ gcc -Wall -pedantic -c -o hello-world.o hello-world.c
$ gcc -nostdlib -o hello-world-nolibc hello-world.o
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000001030
/usr/bin/ld: hello-world.o: in function `main':
/home/pete/tmp/315-linking/hello-world.c:10: undefined reference to `printf'
/usr/bin/ld: /home/pete/tmp/315-linking/hello-world.c:11: undefined reference to `puts'
collect2: error: ld returned 1 exit status

predictably, it doesn’t work: the linking step fails because our program needs printf and the linker can’t find it

(note that it’s looking for puts: this is a simpler function to print a string; the compiler automatically downgrades printf where it can—ie, where printf is not being asked to do any formatting and instead just outputting a string)


to this point, we’ve been ignoring some nuance to linking

the method considered thus far is referred to as static linking

and it is not the method used by default for libraries like libc

to demonstrate why it is not used by default, let us link libc statically and see the consequences:

(note the -static linking option)

$ gcc -g -Wall -pedantic -c -o hello-world.o hello-world.c
$ gcc  -static -o hello-world-static hello-world.o
$ ls -l hello-world-static
-rwxr-xr-x 1 pete pete 762480 Oct 11 00:51 hello-world-static

note how big the executable is: it’s over 700 kilobytes

this is the consequence of bringing the machine code for printf into the executable


what are the drawbacks to this method?

consider that pretty much every program is going to use functions from libc

thus, popular libraries get duplicated in every program: this duplication is wasteful

additionally, if a new version of library comes out, every program that uses it needs to be re-linked


a solution is to perform linking when the program is run, as part of the loading phase

loading is the task of bringing the program file on disk into memory to make it a process

(the terms "linking and loading" often go together)

what would this look like?

presumably, when the program is run…


so what are the drawbacks to this approach?

consider this program: pathological.c

first of all, lots of duplicated effort: do_stuff is called 8 times and the same patching will therefore happen 8 times

secondly, possibly a lot of wasted effort—if argc is less than 2, those 10 functions calls at the bottom are never used

in a silly little program like this, these are both minor hindrances

but in big programs, consisting of millions of lines of code (which is not at all unreasonable) patching up every call at runtime incurs an unreasonable performance hit


therefore, can we defer linking even further?

when is the absolute latest time we could potentially get away with?

when the function in question is called

how on earth could this work?!


here’s how:

the program keeps a table of all external functions called (ie, all functions that are to be linked at runtime)

at runtime, all the addresses in this table are initially "empty", signifying that none of the functions have been linked

when compiling the program, the compiler does not put in a "call do_stuff" instruction

instead, it puts in a call to a stub function that performs a two simple steps:


vocab time

the table that contains the address of each external function is called the global offset table (GOT)

the collection of functions (one for each external function) that look up the entry in the GOT and possibly call the linker is the procedure linkage table (PLT)


these two are other things that need to be stored in the program

our list of such things is now quite hefty:

all of this stuff has to be organized in the program so that both the compiler can produce it and the kernel can read it

which implies that there must be a standard for doing so, and indeed there is

on most UNIX systems (OS X excluded) the modern standard is the "executable and linkable format" (ELF)

Windows uses the "portable executable" (PE) format

OS X uses the "Mach-O" format


much of what we’re discussing here is metadata—that is, data about data

for example, the program instructions themselves are the primary thing here

but a lot of information about those instructions is necessary for the program to run (eg, necessary libraries, GOT, PLT)

which makes the latter metadata

(though, to be completely fair, the distinction between data and metadata can be extremely thin—all of these pieces of information are necessary for the program to run, after all)

since metadata often precedes data in a single file, the term headers is often used for the former

therefore, one might speak of "examining the ELF headers" when they mean "examining the metadata of an ELF file"


so let’s do that

there exists a cunningly-named program called readelf that.. reads the ELF headers (and shows them to you)

it’s easiest to just ask for all of them, which is accomplished using the -a parameter:

$ readelf -a hello-world

that produces a ton of data, so it’s often helpful to pass it through less, as we did last time:

$ readelf -a hello-world | less

poking around through that shows a lot of recognizable names and acronyms


a brief recap

normally, a function call appears in assembly (and therefore machine code) as a 'call' instruction that jumps to the first address of the target function

this address is called a reference

this cannot be done, however, when the function does not exist in the same file (which is useful for a number of reasons, previously discussed)

in this case, such a reference is undefined

linking is the process by which object files are combined and undefined references are resolved (ie, the unresolved reference is replaced with the address of the function which now exists in the combined result)

when linking happens at compile-time, it is called static linking

there are some benefits (also previously discussed) to deferring linking until runtime, in which case it is called dynamic linking

dynamic linking depends upon two data structures:

we will see this all shortly, using this example program: hello-world.c

note that it contains two calls to puts

if all goes as claimed, the first call will induce dynamic linking, and the second call will take advantage of it


let us start with objdump, which we can use to perform disassembly

there’s a lot of machine code in the hello-world program, so we’ll pipe it through less

$ objdump -j .text -d hello-world | less

(recall that ELF files are organized into sections, and the machine code for the functions you write lives in the text section)

we can find the machine code for the main function by scrolling through until we see the label <main> (it’s near the bottom)

what we do not see, however, is anything related to the GOT or PLT

this is strange, especially in the case of the PLT, because I’ve claimed it’s code, and therefore we might expect it to show up in the text section


therefore, let us turn to readelf, which decodes all the ELF metadata

(recall that ELF is the method for organizing data within an executable program or library)

readelf also produces a lot of output, so we will pipe it through less, too

$ readelf -a hello-world | less

when we scroll through, we start to see some promising acronyms

for instance, in the second chunk (labeled "Section Headers"), we see entries for both PLT and GOT:

  [13] .plt              PROGBITS         0000000000001020  00001020
       0000000000000020  0000000000000010  AX       0     0     16
  [22] .got              PROGBITS         0000000000003fd8  00002fd8
       0000000000000028  0000000000000008  WA       0     0     8

this implies that they are there, just not in the text section


since the PLT entries are code, and we now know the name of the section in which they reside, we can use objdump to check them out

$ objdump -j .plt -d hello-world

and the PLT entry for puts is right there:

0000000000001030 <puts@plt>:
    1030:   ff 25 e2 2f 00 00       jmpq   *0x2fe2(%rip)        # 4018 <puts@GLIBC_2.2.5>
    1036:   68 00 00 00 00          pushq  $0x0
    103b:   e9 e0 ff ff ff          jmpq   1020 <.plt>

don’t worry about understanding what it does yet; for now, it’s enough to have found it

the GOT doesn’t contain executable code, so it doesn’t make sense to try to disassemble it


instead, let us turn to gdb to examine the runtime state of the program as it goes through dynamic linking

(note that the program must be compiled with the -g flag for gdb to give us complete information)

$ gdb hello-world

I’ll start by inserting breakpoints at both calls to puts

this means that, when I run it within gdb, execution will pause when it hits a breakpoint, and then I can examine memory, registers, and variables, and I can step forward instruction by instruction to see how things change

(gdb) break 10
Breakpoint 1 at 0x1148: file hello-world.c, line 10.
(gdb) break 11
Breakpoint 2 at 0x1154: file hello-world.c, line 11.

then I run it and indeed it stops at the first breakpoint:

(gdb) run
Starting program: /home/pete/shiznat/middlebury/classes/315-f18/lectures/11/hello-world 

Breakpoint 1, main (argc=1, argv=0x7fffffffe778) at hello-world.c:10
10      puts("hello");

we can ask gdb to disassemble the code for us and, when we do, we see that the program is about to issue the call instruction that executes the puts entry in the PLT:

(gdb) disas
...
=> 0x0000555555555148 <+15>:    lea    rax,[rip+0xeb5]        # 0x555555556004
   0x000055555555514f <+22>:    mov    rdi,rax
   0x0000555555555152 <+25>:    call   0x555555555030 <puts@plt>
...

(the "=>" symbol indicates the instruction that is about to be run)

we can ask gdb to execute those three instructions using the "si" (step instruction) command thrice, at which point we will be about to execute the first instruction of the puts entry in the PLT:

(gdb) si
0x000055555555514f  10      printf("hello\n");
(gdb) si
0x0000555555555152  10      printf("hello\n");
(gdb) si
0x0000555555555030 in puts@plt ()
(gdb) disas
Dump of assembler code for function puts@plt:
=> 0x0000555555555030 <+0>:     jmp    QWORD PTR [rip+0x2fe2]        # 0x555555558018 <puts@got.plt>
   0x0000555555555036 <+6>:     push   0x0
   0x000055555555503b <+11>:    jmp    0x555555555020
End of assembler dump.

now it’s time to talk about what the three instructions in the PLT entry actually do

here’s the first instruction:

jmp    QWORD PTR [rip+0x2fe2]

rip is a register that contains the address of the next instruction to execute (in Computer Architecture, you probably heard it called the "program counter"; this is 100% equivalent—"rip" is an x86-ism)

we can ask gdb for its current contents:

(gdb) print $rip
$1 = (void (*)()) 0x555555555030 <puts@plt>

this makes sense because the first jmp instruction is the next instruction to execute

we can also ask gdb to perform the arithmetic for us:

(gdb) print $rip+0x2fe2
$2 = (void (*)()) 0x555555558012

note that the result here is different than the address gdb calculated for us in the disassembly output ("0x555555558018 <puts@got.plt>")

this is because, when that first jmp instruction is executing, the value of rip will be the address of the following instruction (the push)

since the push instruction lives 6 bytes later, if we add 6 to the unexpected output from gdb, we get the address we expect: 0x555555558018

this is the address of the specific entry within the GOT that pertains to puts

the square brackets enclosing an address cause the data stored at that address to be used

and jmp sets the program counter to its parameter

the net effect is that this single instructions goes to the GOT and grabs the entry for puts (which should now be empty) and then jumps to it


let’s confirm this by looking at the data at that address

(gdb) x/gx 0x555555558018
0x555555558018 <puts@got.plt>:  0x0000555555555036

so the jmp instruction will jump to address 0x0000555555555036

what’s at that address?

it’s the second instruction of the puts PLT entry!

so the net effect of the jump instruction (given the current contents of the GOT) is absolutely nothing—often referred to as a "nop" or "no-op"

so the entry in the GOT is not empty… but it has an equivalent effect


we can confirm by executing that single jmp instruction:

(gdb) si
0x0000555555555036 in puts@plt ()
(gdb) disas
Dump of assembler code for function puts@plt:
   0x0000555555555030 <+0>:     jmp    QWORD PTR [rip+0x2fe2]        # 0x555555558018 <puts@got.plt>
=> 0x0000555555555036 <+6>:     push   0x0
   0x000055555555503b <+11>:    jmp    0x555555555020
End of assembler dump.

and indeed we are now about to execute the second instruction of the puts PLT entry

that instruction stores the value 0 on the stack

and the third jumps… somewhere

the zero is the index of the function being linked

(if there were a bunch of function to be linked, we’d see one PLT entry for each, and they would have successively larger values)

the jump target is the code that performs the dynamic linking

it uses the 0 value to know which entry in the GOT to fix up


we’ll let it do its thing, and then execution will stop at the second call to puts

(gdb) continue
Continuing.
hello

Breakpoint 2, main (argc=1, argv=0x7fffffffe7c8) at hello-world.c:11
11      printf("world\n"

now we can again disassemble to see the instructions about to execute:

=> 0x0000555555555157 <+30>:    lea    rax,[rip+0xeac]        # 0x55555555600a
   0x000055555555515e <+37>:    mov    rdi,rax
   0x0000555555555161 <+40>:    call   0x555555555030 <puts@plt>

and again step forward two instructions to get into the PLT entry for puts

(gdb) si
0x000055555555515e  11      printf("world\n");
(gdb) si
0x0000555555555161  11      printf("world\n");
(gdb) si
0x0000555555555030 in puts@plt ()
(gdb) disas
Dump of assembler code for function puts@plt:
=> 0x0000555555555030 <+0>:     jmp    QWORD PTR [rip+0x2fe2]        # 0x555555558018 <puts@got.plt>
   0x0000555555555036 <+6>:     push   0x0
   0x000055555555503b <+11>:    jmp    0x555555555020
End of assembler dump.

now when we examine the GOT we expect to see something different: instead of an address back in the PLT, we expect to see the real address of puts

(gdb) x/gx 0x555555558018
0x555555558018 <puts@got.plt>:  0x00007ffff7e55ab0

the value is on the right, and gdb helpfully tells us that it is indeed the address of puts@got.plt

and if we step a single instruction more, we are indeed at that address and it’s in puts:

(gdb) si
0x00007ffff7e55ab0 in puts () from /usr/lib/libc.so.6

(gdb can’t give us line numbers because this version of libc has not been compiled with -g)

Last modified: