Hello, and welcome back to CS631 "Advanced Programming in the UNIX Environment". In our last video, we talked in some detail about the processes involved in creating executables and then executing them. We discussed the steps involved in the linking process, where multiple object files are combined, and we saw that in an ELF executable the PT_INTERP program header includes the pathname for the program interpreter or run-time link editor, which has the job to resolve any undefined symbols at run-time via shared libraries. In this video, we'll cover shared libraries themselves. We'll discuss just what exactly a shared library is, how you create one, and how we can influence the behavior of dynamically linked executables due to the way the link-editor works. --- So let's look again at how the linker creates the executable. We recall that it inserts the program interpreter, /usr/libexec/ld.elf_so, and then combines the object files with the shared libraries that we told it to link with to create the terribly named a.out ELF binary. But we really only specified the names of the shared libraries and nothing else. So how does the linker use them? --- And what exactly is a shared library anyway? When we discussed the compile chain in week 05, we described how the C preprocessor pulls in the various C headers, and we also saw that those headers only provide macros, constants, and the forward declarations or function prototypes, but not the actual implementation of the functions. - That is, when you "include ", you are only providing the forward declarations of, say, fprintf(3) etc., but _not_ their implementation. The full code to implement fprintf(3) has to come from somewhere else -- the standard C _library_ in this case. In other words, a library contains actual code that can be used by your program, and in fact any code that you did not write yourself _has to_ come from some library. Now to be able to take this code and load it into memory for any program executing, - requires that it uses position independent code. Your compiler may already generate this position independent code by default, or you may need to specify a specific flag, as we'll see in a minute. - Libraries may be static or dynamic, and we'll take a look at the difference between the two in this video. - Dynamically shared libraries can be loaded at execution time by the loader, or at will as part of the program. But perhaps it's best to illustrate all that by example... --- Here's our program crypt.c from the last video, although we added another function to illustrate how definitions in our code are identified in the ELF file. Anyway, we make use of some standard library functions, such as fprintf(3), which we know has a function prototype defined in the stdio.h header file, while the crypt(3) library function is declared in the unistd.h header file. The implementation of fprintf(3) is provided as part of the standard C library, libc, while crypt(3) is implemented within the libcrypt library. If we compile the code into an object file, everything works without a problem, as the compiler finds the forward declarations and then produces the appropriate .o file, but if we try to link the .o file into an executable, we get an error: the linker does not find the implementation of the 'crypt' function and then throws this error about an undefined reference. Fortunately, the manual page tells us which library we need to get the implementation of the crypt(3) function -- we need to link with '-lcrypt'. So if we do that, the linker succeeds and produces an executable. Looking at the symbol table via readelf(1), we can see which symbols are defined in our executable: 'main' is of type 'function' and is defined, as is the function 'printCrypt'. But 'crypt(3)' itself is still undefined, as are fprintf(3) and exit(3)! And all the addresses for these functions are 0, too! How could we create an executable with undefined symbols? Let's try to run the linker ourselves. If we invoke ld(1) with just our object file, we get a whole bunch of errors about unresolved symbols - many more than we had earlier, including those about the functions from the standard C library. So let's try to tell the linker to use libc. Ok, that's better. Now we only get the error about crypt(3) not being defined, like before. That is, our compile chain by default will automatically link against libc, which seems like a reasonable thing to do. Next, let's add -lcrypt to our command. There, now everything succeeded, no more undefined references. The symbol table for the crypt.o object file contains the undefined references, but what about the executable? readelf -s a.out shows us a large number of undefined references here, too! But now we have a bunch of things that _are_ defined. For example, we have the _start routine, which we know to be the entry point of our executable at the hex address 400550, and 'main' is found as a defined function, too. But again, crypt(3), exit(3) and fprintf(3) remain undefined. Let's look at the dynamic section of the ELF executable. Now this is interesting: here, we see that the executable includes information about what libraries it needs: libc.so.12 and libcrypt.so.1. And when we execute the program, everything works out. So the information included here in the executable informs the loader that it needs to look for these two libraries, libc and libcrypt. The ldd(1) command can also be used to show effectively what the loader would be doing as far as the shared libraries are concerned: it shows that the a.out executable requires /usr/lib/libc.so.12 and /usr/lib/libcrypt.so.1, having found the absolute paths of the libraries the names of which were contained in the executable as being required. --- So just as we described in the last video, we saw here that ld.elf_so had to find the information about the shared libraries from the executable, and then use some logic to map it to the actual pathnames before loading those to resolve the undefined references _at run-time_. --- In other words, the linker and loader again have to work together. - At link time, the linker resolves the various undefined symbols and makes a note of which libraries are required. It provides this information in the executable, but does not pull all the actual code into the executable. - The actual code we wrote, everything we have in our object files that we compiled is pulled into the executable, however. If we had used a static library, all that code would also have been pulled into the executable. We'll see an example of that in a minute. - But the contents of the dynamic libraries such as libc or libcrypt in our example are _not_ pulled into the executable. Instead, we only include information about which libraries we need in the executable at link time, and the dynamic linker will, at program execution time, perform the inverse operation and then load the shared libraries specified in the program executable. - The run-time link-editor, however, is not the only way for you to load shared libraries -- you can also make an explicit call to the syscalls by which this is implemented yourself. We'll have an example of this towards the end of this video as well. But first, let's perhaps create a more simple example to better illustrate how a shared library is created. --- Let us consider this simple program. 'main' only calls three functions -- ldtest0, ldtest1, and ldtest2. That's all. The file main.c does not include the implementation of these functions, but does provide the function prototype necessary to let the compiler create an object file. This is equivalent to having a custom include statement here pulling in a file that provides these forward declarations. ldtest1.c is a file that contains the implementation of the ldtest0 and ldtest1 functions. These functions aren't very exciting themselves, but they will serve our purpose. ldtest2.c then contains the implementation of the equally boring ldtest2 function. So now if we compile our main.c program file, we of course fail: we have not provided any code to implement the ldtest functions. But note that if had compiled the program into just an object file, that would have worked because we did provide the function prototypes. So if we now compile the ldtest .c files into object files and then link main.o with those object files, we get a functional executable. Exciting. Let's take a look at the symbol table. This time, we'll use the nm(1) utility for this. Again, we see a number of symbols that are defined in the text segment of the program, some that are undefined because they come from a shared library -- libc -- as well as some things defined in the BSS segment. [pause] But this is all just the usual, boring stuff. We've been building executables like this the entire semester. Let's change this -- let's create a library that provides the ldtest functions instead. For that, we use the ar(1) utility to create an archive that contains the two ldtest object files. An archive is quite simply a file that contains a few other files, together with a table of contents so that you can more easily access the individual files inside the archive. [continue] If we look at the table of contents of this archive, we see that yes, this now contains the two object files. Now let's look at the symbol table for this archive. The index tells us which symbols are defined in which file, and we also identify that these files themselves still include some unresolved references. So what can we do with this archive now? If we compile our 'main.c' file by itself, we know it'll fail, but if we then add the archive as another argument, compilation and linking succeeds. That is, the linker was able to extract the files from the archive. In other words, the archive has become a library, which just happens to be in the current directory. But we can stash it somewhere else. Let's move it here into /tmp/lib. Would be nice if we could now use the "-lldtest" flag, instead of providing a fixed path to the archive, right? Unfortunately the linker of course will tell us that it has no idea where to find such a library. But if we give the linker a hint by specifying the '-L' flag and tell it to look for any libraries it might need in the /tmp/lib directory, then... ...it succeeds, and our executable works as before. Let's now illustrate the difference between a statically linked executable and a dynamically linked one. We rename this executable and then create a new one, this one statically linked. Let's look at the difference. a.out.static is now a statically linked ELF executable, a.out.dyn is a dynamically linked ELF executable. The dynamically linked executable requires a program interpreter -- the statically linked one does not! They both behave the same way, but the two executables are clearly very different. The dynamic one is much smaller! readelf(1) tells us that this executable requires libc.so.12 to run, while a.out.static... does not. The symbol table for a.out.dyn is as before, with various things being defined -- including the ldtest functions we had placed into the archive -- while the things from libc remain undefined and therefore require the loading of libc. The symbol table of a.out.static, however, looks a bit different. It contains a _lot_ more symbols, including those for all our standard I/O functions! Let's compare: a.out.dyn contains 36 symbols, a.out.static... 1075! This explains why the statically linked executable is so much larger: it does in fact contain everything from our standard C library, which is why it does _not_ require a program interpreter or a link loader. Everything it needs is baked into the executable. --- And so what we created here -- the libldtest .a archive -- is a _static_ library. - As we've seen, these libraries -- archives -- are created using the ar(1) utility, - and typically end in .a, though of course this being Unix this is by no means a requirement. - An archive really is nothing but a single file that contains other files, a concatenation of files with a table of contents, if you will, - which of course explains why linking against such an archive produces one big binary that contains all the contents of the archive, and thus does not require a program interpreter or loader to go out and find any other libraries at program execution time. Now let's compare this with a dynamically linked executable using dynamic shared libraries: --- As we said before, in order for a library to be loaded at execution time or even at will during program execution, it needs to use Position Independent Code, so we need to use the compiler's "-fPIC" flag. The position-independent code resolves its addresses through a Game of Thrones, uhm, I mean: the Global Offset Table. So let's use this flag to compile our library files. Note that our object files now contain in the ELF symbol table a Global Offset Table reference. Let's create a shared library. To do that, we use the '-shared' flag for the compiler and pass on to the linker the '-soname' flag to specify the name of the shared object we are creating. Per convention, the shared libraries are named after their Application Binary Interface promise in a major- and minor version number with symbolic links created to allow the user, the linker, and the loader to find the right library. So libldtest.so.1.0 is now a dynamically linked shared object. As before, compiling 'main.c' without specifying the library fails; specifying the library name alone isn't sufficient since the linker doesn't know where the library might be, but if we specify the '-L' flag with the right directory, then off we go. a.out is a dynamically linked executable with /usr/libexec/ld.elf_so as the program interpreter. So now we can run the program and... whoops. Now what? I thought that we had created an executable with the right library, why does it now fail to execute? ldd(1) also can't tell us where the library is. Let's look at the library. In the dynamic section, it tells us that it needs libc -- no surprise there -- and, in the SONAME, that it's named libldtest.so.1, as we had specified. Let's look at the manual page for the link-editor. Here it tells us how it's trying to find the libraries at run-time: it will look at some user-defined paths, a config file, any paths explicitly specified in the executable, and, if all that fails, by looking in /usr/lib. We didn't specify any place to look for at execution time, so it couldn't find it in /usr/lib and failed. Let's set the LD_LIBRARY_PATH environment variable, as that appears to take precedence in the list of locations ld.elf_so looks at. And whaddaya know - ldd(1) now finds the library, and so does the link-editor at execution time. Hooray! [pause] So this is great: we can specify the directory where to look for the shared library to use. But this also means that if we had a shared library somewhere else, we could make the program use that. And if _that_ shared library behaved differently, then our program would behave differently. Here, let's illustrate this. [continue] Here's an alternate implementation of the 'ldtest1' function. Let's create a new library that uses _this_ code. We compile the files as before and create the same set of symbolic links, but this time we'll pretend that this is a minor version increment in the library version. Our LD_LIBRARY_PATH still points to ./lib, so our program behaves as before, but now if we change the LD_LIBRARY_PATH environment variable to point to the new directory ./lib2, then our program immediately picks up the new behavior upon execution. So the thing that's really noteworthy here is that we changed the behavior of the executable without recompiling it, merely by pointing the LD_LIBRARY_PATH variable to a different directory! But so it seems that we really need to have this variable set, which seems a bit annoying and cumbersome. Shouldn't there be a way to execute the binary without having to know where the libraries are stored, even if they are not in the default library path? Fortunately, there is. We can pass a special flag to the linker -- "-rpath" -- to instruct it to look for libraries in a specific directory. Now notice that readelf(1) tells us not only the library names this executable needs, but also the path where to look for libraries, the "rpath". And just like that, executing the program works even though the LD_LIBRARY_PATH variable is not set. However, even with the rpath specified, we can still set the LD_LIBRARY_PATH variable and trigger a different behavior of the executable, as the order of lookup, as we saw in the manual page, grants the environment precedence over the rpath from the binary. But this could actually have some pretty serious consequences: suppose I create a library that implements some of the standard C library functions, such as printf... Here, evil.c does just that. If we can build such a library, and then set the LD_LIBRARY_PATH variable to cause my library to be loaded, will that use my printf function instead of the standard C library function? Let's give it a try. Here, our ldtest library now includes our evil object. And yes, it looks like when we run a.out, it does indeed call our evil version of printf. Now consider what this means if you had a setuid root executable and you could cause it to call a library function under your control... you'd have instant and complete root access! Let's give it a try. Ok, a.out is now setuid root... ...but when we run it, it doesn't call our evil printf, even though our LD_LIBRARY_PATH variable is set... but if it's _not_ setuid, then it does. So this is a security mechanism the system provides: the LD_LIBRARY_PATH variable is only honored if the program is not setuid. Which makes sense: if the program isn't setuid, then it already runs with all your privileges and you can't trick it into doing anything you can't already do, but if you have a different euid from your real UID, then we need to be more careful, and ld.elf_so will not honor LD_LIBRARY_PATH. Phew. --- Ok, so in contrast to static libraries, dynamic libraries... - require the object files to be of position-independent code. We specify the "-fPIC" flag to the compiler to cause it to generate this position-independent code. - Dynamic libraries usually end in .so, for "shared object", and - normally have a number of symbolic links to facilitate backwards compatibility in the Application Binary Interface. - The symbols in a dynamic library are looked up at link time, but not resolved until execution time. This requires the loader to know where to find the libraries, - and as we've seen, the user and the system administrator may influence the behavior of the loader via several settings. But we mentioned earlier that libraries may not only be loaded at execution time, but also at will at any point during the execution of the program. What does that look like? --- Again, as seen before a few times, our crypt.c program requires libcrypt to be linked, and this information is then stored in the executable. But how is this loading of the library at execution time done? Let's look at the manual page for the dlopen(2) function, which is automatically included in every dynamically linked program. This function allows the loading of a position-independent shared object. So let's take a look at the dlopenex.c program, which will illustrate the use of the dlopen(2) call to load symbols and invoke functions from a given dynamic library _without linking against it_. Here, in printCrypt, we now declare a variable _crypt as a function pointer matching the prototype we know the real 'crypt' function to match. We call dlopen(2) and ask it to load "libcrypt.so", then try to resolve the symbol for the 'crypt' function within that library and assign it to our function pointer. If that works out, then we can now call the system crypt(3) function from the libcrypt library via our _crypt name. Here, let's compile it. Note that we now _didn't_ link against libcrypt as we had to before! ldd(1) confirms that our program only requires libc, nothing else. But running it still works! Looking at our symbol table, we also do not see any reference to the "crypt" function, and still we are able to call the function. That is, we've shown that we can load a dynamic library _at will_ during the execution of the program without having to link against it. --- Alright, this brings us to the end of this video. We've covered a lot of ground and learned a fair bit about shared libraries: - We've seen how static libraries are really not much more than archives of object files and that their code is placed into the executable at link time. - Dynamic libraries, on the other hand, provide hints to the loader as to which libraries should be loaded to perform the look-up of the unresolved symbols. - And we've seen how the LD_LIBRARY_PATH environment variable or the "-rpath" flag to the compiler can change the behavior of the executable at run-time. Now before we close out here, please consider spending some time - looking at the other environment variables that are available and make sure to understand their use cases, both valid and those that abuse the feature for nefarious reasons. - On linux systems -- or rather: on systems using GNU libc or on BSD systems if the libraries have been compiled with debugging support -- you can set an additional environment variable -- LD_DEBUG -- with varying results. Play around with that and see what interesting information you can elicit from the execution of a dynamically linked program. - There's a lot more to be learned about the binary formats involved here -- different tools allow you to see different parts. Make sure to compare the different tools we've used in this series, including readelf(1), objdump(1) and nm(1). There's also another exercise listed on the course website to help you practice writing a shared library yourself - give it a go. After this, we'll move on to a bit of a mishmash of topics on "Advanced I/O". Until then - thanks for watching. Cheers!