pete > courses > CS 315 Fall 26 > lecture 08: binary organization
Lecture 08: binary organization
Goals
- define symbol
- use objdump to disassemble object files
- use nm to identify exported symbols
- explain what it means for a symbol to be undefined
- describe the purpose of linking
we’re going to start by reviewing the basic sequence of operations that takes us from a source file to an executable program
we’ll review (and add detail to) the various complexities we’ve previously discussed
including looking at some assembly language
you won’t be writing assembly in this course, nor will I expect you to deeply understand what it does, but we will walk through some today and next time
and then we’ll take it further by exploring the things we’ve sort of taken for granted in that area
here’s the Makefile for all of today’s programs
and here’s the stupid, simple motivating program: my_program.c
the simplest way to turn this into a runnable executable is to ask gcc to compile it and directly produce a program:
$ make my_program gcc -g -pedantic -o my_program my_program.c
(what I mean by "directly produce a program" is one of those vague phrases for which you probably have an intuitive sense, but not a concrete one, and we’ll work on that today)
(also, I’m not using the -Wall flag today because I’ve cut some corners in writing these programs to make them more concise, but they do introduce warnings that just clutter up the output)
question: what if some other program desperately needs the functionality of my super duper function? how can another program take advantage of it?
the naive solution is to cut and paste the function prototype and the function definition into another source file
like so: my_other_program.c
and then compile that one
like so:
$ make my_other_program gcc -g -pedantic -o my_other_program my_other_program.c
fairly straightforward, but what are the problems with this?
what if somebody comes up with a better way to implement my_super_duper_function?
unlikely as it seems, this is indeed possible
to incorporate a new version, someone would have to copy and paste the new prototype into the right place within my_other_program.c
and paste the new function definition into the separate right place in the same file
they’d also have to make sure all the old code was removed
all of these steps are error-prone
the more moving parts, the more difficult it is to ensure correctness
the solution, as we discussed in the context of assignment 2, is to extract all the parts of the source file that have to do with my_super_duper_function (ie, the prototype and the definition) and put them in their own file(s)
then, if another program wants to use them, we just have to compile these files unchanged: no fragile cut-and-paste steps, just use the whole file
the function prototype goes in the header file: my_super_duper_library.h
the function definition goes in the corresponding source file: my_super_duper_library.c
and then, in the program that uses it, we just need to #include the header file: my_better_program.c
then, to build the program, we need to compile both source files together:
$ make my_better_program gcc -g -pedantic -o my_better_program my_better_program.c my_super_duper_library.c
nothing terribly exotic, at least superficially, but we will dig in a bit more in a moment
but first, a brief digression on header files and how #include works, that will include an explanation of the weird __MY_SUPER_DUPER_LIBRARY_H stuff in there…
#include is very simple: the compiler finds the named file and replaces the #include line with the entire contents of that file
that’s it
note that the header file above only contains the function prototype, which in the original program took the place of the #include line
so nothing really has changed from the original, we’ve just put the lines of text in different files
now let’s look at the three strange lines in that header file:
#ifndef __MY_SUPER_DUPER_LIBRARY_H #define __MY_SUPER_DUPER_LIBRARY_H ... #endif /* __MY_SUPER_DUPER_LIBRARY_H */
we’re going to defer an in-depth discussion of preprocessor directives (which is what these are)
suffice it to say for now: these prevent errors from circular dependency loops, in which a.h includes b.h and b.h includes a.h
using the double underscores at the beginning is a convention for this purpose
another related convention is found back in the .c file that #includes the header
note that it uses double quotes instead of angle brackets
this is a hint to the compiler that the header file is to be found in the current directory, rather than the directory that contains header files to be shared system-wide
okay, so we’ve got these two different methods to produce what we hope to be the exact same code
it’s worth seeing if it really is the same code
to do that, we have to look at the output file
since it’s compiled code—ie, machine code—ie, object code—the program to look at it is called objdump
recall also our discussion about what is in compiled programs: it’s both machine code and data, so we want to make sure here that we’re looking at the machine code, which is traditionally stored in the "text" section of the file
so here’s what we run:
$ objdump -j .text -d my_better_program
this produces a ton of output that scrolls by very quickly
one way to deal with that is to use another program to let us go through the output page by page—this kind of program is called a pager, and is precisely what shows you manpages
thus:
$ objdump -j .text -d my_better_program | less
the "|" symbol (pronounced "pipe") takes the output of the command on the left and feeds it as the input to the command on the right
the program less is, as I said, the same program used to view manpages, so the same keys work within it: arrows keys to scroll, forward-slash to search, q to quit
back to the output
we see a lot of inscrutable stuff
but way down, we see the assembly code for the main and my_super_duper_function functions
they start with lines like so:
0000000000001119 <main>: ... 0000000000001137 <my_super_duper_function>:
this means that main (ie, the machine code instructions that implement that function’s behavior) starts at address 1119 (hex)
and the instructions for my_super_duper_function start at address 1137 (hex)
the corresponding code is predictably short and not very interesting, but that’s not the point for this lecture
it’s worth looking at the particular instruction that performs the function call:
1128: e8 0a 00 00 00 callq 1137 <my_super_duper_function>
the callq instruction takes as parameter the (relative) address of the function to call, which is the latter four bytes of the five-byte instruction
objdump helpfully tells us its name in angle brackets on the right (though there is no notion of its name in the machine code itself: objdump has inferred it… somehow—we’ll see how in a few minutes)
let’s now look at the same output for the original version that didn’t use two source files:
$ objdump -j .text -d my_program | less
it looks pretty much the same (in fact, diff confirms they are, in fact, exactly identical)
this is interesting, but not too surprising given what we know of source files, compilation, and programs
in the first case, the compiler takes the single source file, translates functions therein to machine code, and stuffs them into a program file
in the second case, the compiler takes the two source files, translates the functions therein to machine code, and stuffs them into a program file
the result is the same, the only difference is where the functions came from in the first place
now, to ask the same question as before: what’s fragile or delicate or inefficient or inconvenient about this setup?
what if, again, somebody (inexplicably) comes up with a better way to implement my_super_duper_function?
we’d presumably ask the compiler to take the one source file that has changed (my_super_duper_library.c) and the one that hasn’t changed (my_better_program.c) and again compile them together to produce an executable
note that compiling my_better_program.c is wasted work: it hasn’t actually changed, so the machine code will be exactly the same
in a simple situation like this, with two source files, this isn’t a big deal
but imagine a big program with a few hundred source files, or a huge program with a few thousand
recompiling all of them just because a single source file changes is a huge waste of effort
the solution is to separate the "compile" step and the "combine" step
so compile my_better_program.c and save the result in what we will call a corresponding object file: my_better_program.o
separately, we’ll compile my_super_duper_library.c and save the result in a different object file: my_super_duper_library.o
then combine the two .o to produce the program
like so:
$ make my_better_program_done_right gcc -g -pedantic -c -o my_better_program.o my_better_program.c gcc -g -pedantic -c -o my_super_duper_library.o my_super_duper_library.c gcc -o my_better_program_done_right my_better_program.o my_super_duper_library.o
(note that the final step does not use CFLAGS: this is because it’s not performing compilation, and thus they do not apply—we’ll see which flags do apply another day)
now, if somebody (again, utterly inexplicably) updates my_super_duper_library.c, all we need to do is recompile it to produce a new my_super_duper_library.o
and then combine that new .o file with the previously-compiled my_better_program.o file (whose corresponding source code has not changed) to produce the executable
we can mimic this situation by updating the modification time on my_super_duper_library.c, which will give make the impression that its contents have changed and thus needs to be recompiled:
$ touch my_super_duper_library.c $ make my_better_program_done_right gcc -g -pedantic -c -o my_super_duper_library.o my_super_duper_library.c gcc -o my_better_program_done_right my_better_program.o my_super_duper_library.o
note that I used the exact same make command as before, but only one .c file was compiled
but two .o files (the new one and the old one) were combined to produce the program
again, not a huge win for a two-file program
but it is a potentially huge win for a 100-file program
let’s turn objdump on this thing
$ objdump -j .text -d my_better_program_done_right | less
again: no change! still the exact same contents as the previous two programs
which is what we expect, given the desired behavior, but it’s nice to see our expectations validated
but what about these .o files? they contain machine code, so they’re probably worth looking at with objdump
first the library:
$ objdump -j .text -d my_super_duper_library.o
the output is short enough that we don’t need less
it’s also unsurprising: it contains the machine code for the single function in the corresponding .c file
now the main program source file
$ objdump -j .text -d my_better_program.o
the output is also short and not terribly surprising
except this part:
f: e8 00 00 00 00 callq 14 <main+0x14>
compare this to the output from the last time we objdump’ed the whole program
the target address (the last four bytes) is empty!
this makes sense, though: that function is contained in a completely different file, and if the compiler only has my_better_program.c to work with, it can’t know the address of my_super_duper_function
the implication is important: one part of the "combine .o files to produce a program" task has to be to fill in the address
now for some vocabulary
a symbol is a name with an associated address
so main is a symbol and my_super_duper_function is a symbol, because they both refer to the addresses at which those functions start
additionally, the call to my_super_duper_function is a reference to that symbol: it uses the address as the target of its jump
furthermore, in the excerpted objdump output above, the compiler wasn’t able to figure out the address of that symbol
which makes it an undefined reference
ie, it refers to something, but we don’t know the address of it (yet)
you might ask yourself whether we can learn anything about symbols in object files
and the answer is yes, using the nm program
nm lists each symbol: its address, its status (more in a bit), and its name
(more precisely, it shows the contents of the symbol table, which is another part of the still unfolding complexity of the program file)
thus:
$ nm my_super_duper_library.o 0000000000000000 T my_super_duper_function
this output tells us that this object file defines a single symbol
unsurprisingly named my_super_duper_function
it’s located at address zero within the file
and it is in the text (T) section (ie, executable code, which we expect)
the more interesting collection of symbols is in the main file:
$ nm my_better_program.o
0000000000000000 T main
U my_super_duper_function
main is in the text section, at offset zero
but the my_super_duper_function symbol is undefined (U), which we expect
next vocabulary word: linking is the task of combining these object files and resolving references between them
so before linking we’ve got two object files, one with an undefined reference
after linking, we’ve got the contents of the two object files saved in one program file, and no undefined references
final observation: the two object files we just looked at contain the machine code for exactly two functions
but the programs we looked at contain machine code for many more functions
where do they come from?
the answer is that these are the infrastructural functions that support the running of C programs
they are collectively referred to as the "C runtime"
and we can find object files somewhere on the machine with filenames containing the string "crt"
(recall the strace output showed a lot of stuff happening before the "meat" of the programs took over: those steps are performed by the C runtime—we’ll look at them in more detail over the coming weeks)