Introduction

Hello, and welcome back to CS631 "Advanced Programming in the UNIX Environment". Last time, we started investigating the Executable and Linkable Format, or ELF. We differentiated between a relocatable object file and an executable. In this video, we'll take a closer look at how one becomes the other, what tools are needed, and how our executable is... well, executed. In the process, we'll revisit once more a few earlier lectures. In particular, we'll go back to week 05, where we talked about the compile chain and the job of a linker. This will help us along our mission of understanding shared libraries, another type of file described using ELF. Let's start where we left off in our last video. When we ran the readelf(1) command on an ELF executable, we noted the different program headers, including the PT_INTERP header, which we observed to be pointing to a file, /usr/libexec/ld.elf_so in this case. So... where does that path come from, what does it do, and why do we need it? We didn't specify this pathname when we built the executable, did we? Well, not explicitly, anyway.

Revisiting the compile chain

But let's take a step back and review how we build an executable from our source file. We covered this process in more detail in our Week 05 class, but let's recall that the compilation process is a sequence of individual steps, each executed by the compile chain, possibly invoking different tools. Specifically, we know that in the first stage of the compilation, we invoke the C preprocessor cpp(1) to pull in all our includes, expand macros etc., then perform the actual compilation from C into machine-dependent assembly, then use the assembler to turn this input into an object file, a relocatable ELF file, commonly ending in .o. And only then, in the last stage of the compilation process do we invoke the linker to create an executable by combining a number of objects, including us specifying the "-dynamic-linker" option to pull in the run-time link-editor ld.elf_so the various object files required for the C runtime startup routine, and the object files we created -- crypt.o, or, if we invoked cc(1) and let the compiler chain perform all the steps, thereby creating a temporary file as shown here. Next, we specified that we want to link against 'libc' and 'libcrypto'. Note, however that we did not tell the linker where to find these files -- we'll see in a second how it does that. and finally, the linker requires the C runtime epilogues to bookend the startup routine objects. Now taking all these files and re-arranging a few bits, adding some information about where to find undefined symbols and all that, the linker produces the ELF executable.

ld(1) in action

Let's take a look at a practical example. Here, we have a very simple program. It uses the crypt(3) library function, which we'll call to illustrate the use of a shared library other than the standard C library, libc. First we compile the object file, and then we run the ld(1) command we just explained. We specify the dynamic linker ld.elf_so, the C runtime startup routine objects, our object file, the libraries to link against, and the C runtime bookends. When we run readelf(1), we see that in the PT_INTERP header, the program interpreter is set to the dynamic linker we had specified. So what is this ld.elf_so thing anyway? The manual page tells us that ld.elf_so is the run-time link-editor, a program used to find and load the various shared objects the program needs for execution. That is, when we invoke the program, the kernel passes control to this run-time link-editor, which then uses information from the different ELF sections to find what other objects it needs to load. The manual page also tells us how the link-editor finds the right libraries -- remember, when we invoked ld(1), we only told it "hey, we need libcrypto", but we didn't tell it where that library is found. So the linker and the link-editor must have a way to find the actual files. Let's look at the dynamic section of our executable: Here, we see that there are three shared objects marked as "NEEDED": libc.so.12, libcrypto.so.14 and libcrypt.so.1. So when we run the executable, we now know that this will invoke the run-time link-editor, which will determine where to find the symbols that we didn't stash into the executable, and then allow our program to execute. [pause] How do we verify that ld.elf_so is really executed? Let's use the ktrace(1) utility. ktrace(1) is a tool to inspect what system calls, amongst other things, a program makes. The tracing information is stored in a binary format in a file that you can then inspect using the kdump(1) utility. Here we go: [continue] Ok, so we call execve(2) to invoke the "a.out" binary, and we notice that immediately afterwards, we see a call to ld.elf_so, which proceeds to open the ld.so.conf file, then pulls in libc.so, then libcrypto.so, libcrypt.so before then moving on to actually do what we instructed the program to do. Alright, so far, so good. But what if we didn't specify the dynamic linker? ld(1) still succeeds, and the resulting file is still an ELF executable, but notice that now the INTERP header requested is a different program interpreter: /lib/ld64.so.1. Let's see what happens when we execute that command? We get a failure, saying "no such file or directory". Let's check where this file ld64.so.1 is... Ah, it doesn't exist. And so kdump(1) shows exactly that: the program tried to call the program interpreter, that file doesn't exist, and so execution of our program failed. But maybe we can avoid using _any_ program interpreter? Let's pass "-no-dynamic-linker" to ld(1). Just like before, we still get an executable, still of type ELF, but this time there's no INTERP header. What happens when we execute _this_ binary? Oooh, nice, a segfault! There it is - we try to execute the program, but immediately get a segmentation violation because our process is not set up in memory such that it can execute, as it's missing the various symbols from the shared objects that our link-editor would otherwise have provided for us. If we go back to specifying the correct dynamic linker, then everything works out again.

ld.elf_so(1)

So: ld.elf_so, the run-time link-editor in a way performs the inverse steps from the linker. The linker took the various object files and created an executable, so the run-time link-editor now takes an executable... ...inspects the environment, its own configuration, information found within the executable from the DT_RPATH section (which we'll see in more detail in our next video) and a default location for libraries... where it eventually finds the correct shared libraries to use... ...to then, eventually, produce the correct process image to load in memory.

Recap

Alright, let's take a break here. To recap: The linker combines the object files, the C runtime prologue and epilogue objects as well as any shared libraries to produce the executable, and, as we just saw, the loader handles resolving these symbols at run-time by performing certain lookups and then produces the process image to allow execution. On some systems -- Linux, most notably -- the run-time link-editor is itself an executable, and you can pass it a program file to invoke. Give it a try and build an executable without a program interpreter; it should fail to execute as we've observed here, but if you invoke the loader directly and pass it the executable without the interpreter specified, it should still execute. You don't even need execute permissions on the program in question, which comes as a surprise to some people. In our next video, we'll look more closely at what exactly a shared library is as well as some of the peculiarities of the loading and linking processes. Until the next time - thanks for watching. Cheers!