pete > courses > CS 315 Fall 26 > lecture 09: linking
Lecture 09: linking
Goals
- describe the contents of the Standard C Library (libc)
- describe the purpose and effect of static linking (and cause gcc to do it)
- describe the purpose and effect of dynamic linking (and cause gcc to do it)
- describe the purpose of ELF (and use readelf to examine its structure)
- describe the purpose and contents of the procedure linkage table (PLT)
- describe the purpose and contents of the global offset table (GOT)
- use gdb to examine and control a process’ execution state
- observe dynamic linking in action
let’s start with this sample program today:
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…
- the program code will be copied into memory
- the necessary libraries will be copied into memory
- each function call in the program that points to an undefined symbol gets fixed to point to the correct memory address in the correct library (ie, the beginning of that function in the library’s instructions)
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:
- it finds the entry for the do_stuff function in the aforementioned table; if that entry is empty, it calls the linker, which looks up the address of the do_stuff function, puts it into the table, and then calls it
- otherwise, the table contains the address of the do_stuff function, that function is called, and we’re good
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:
- instructions
- data
- list of libraries that need to be linked at runtime
- GOT
- PLT
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:
- the global offset table (GOT), which is comprised of one record for every external function called (note that it is not one record for every undefined reference); when the program starts, all these records are "empty", meaning they are all still undefined
- the procedure linkage table (PLT), which also is comprised of one record for every external function called; each record consists of code that checks the GOT for a valid entry, jumps to the function if the reference is defined, or calls the linker if it’s not
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)