Most of our computer science students have been through the famous "Hello World" program at least once. When compared to a typical application program ---almost always featuring a web-aware graphical user interface, "Hello World" turns into an very uninteresting fragment of code. Nevertheless, many computer science students still didn't get the real story behind it. The goal of this exercise is to cast some light in the subject by snooping in the "Hello World" life-cycle.
Let's begin with Hello World's source code:
Line 1 instructs the compiler to include the declarations needed to invoke the printf C library (libc) function.
Line 3 declares function main, which is believed to be our program entry point (it is not, as we will see later). It is declared as a function that takes no parameter (we disregard command line arguments in this program) and returns an integer to the parent process --- the shell, in our case. By the way, the shell dictates a convention by which a child process must return an 8-bit number representing it status: 0 for normal termination, 0 > n < 128 for process detected abnormal termination, and n > 128 for signal induced termination.
Line 4 through 8 comprise the definition of function main, which invokes the printf C library function to output the "Hello World!\n" string and returns 0 to the parent process.
Simple, very simple!
Now let's take a look at the compilation process for "Hello World". For the upcoming discussion, we'll take the widely-used GNU compiler (gcc) and its associated tools (binutils). We can compile the program as follows:# gcc -Os -c hello.c
This produces the object file hello.o. More specifically,# file hello.o
tells us hello.o is a relocatable object file, compiled for the IA-32 architecture (I used a standard PC for this study), stored in the Executable and Linking Format (ELF), that contains a symbol table (not stripped).
By the way,# objdump -hrt hello.o
tells us hello.o has 5 sections:
It also shows us a symbol table with symbol main bound to address 00000000 and symbol puts undefined. Moreover, the relocation table tells us how to relocate the references to external sections made in section .text. The first relocatable symbol corresponds to the "Hello World!\n" string contained in section .rodata. The second relocatable symbol, puts, designates a libc function which was generated as a result of invoking printf. To better understand the contents of hello.o, let's take a look at the assembly code:
||# gcc -Os -S hello.c -o -
From the assembly code, it becomes clear where the ELF section flags come from. For instance, section .text is to be 32-bit aligned (line 7). It also reveals where the .comment section comes from (line 20). Since printf was called to print a single string, and we requested our nice compiler to optimize the generated code (-Os), puts was generated instead. Unfortunately, we'll see later that our libc implementation will render the compiler effort useless.
And what about the assembly code produced? No surprises here: a simple call to function puts with the string addressed by .LC0 as argument.
Now let's take a look at the process of transforming hello.o into an executable. One might think the following command would do:# ld -o hello hello.o -lc
But what's that warning? Try running it!
Yes, it doesn't work. So let's go back to that warning: it tells the linker couldn't find our program's entry point _start. But wasn't it main our entry point? To be short here, main is the start point of a C program from the programmer's perspective. In fact, before calling main, a process has already executed a bulk of code to "clean up the room for execution". We usually get this surrounding code transparently from the compiler/OS provider.
So let's try this:# ld -static -o hello -L`gcc -print-file-name=` /usr/lib/crt1.o /usr/lib/crti.o hello.o /usr/lib/crtn.o -lc -lgcc
Now we should have a real executable. Static linking was used for two reasons: first, I don't want to go into the discussion of how dynamic libraries work here; second, I'd like to show you how much unnecessary code comes into "Hello World" due to the way libraries (libc and libgcc) are implemented. Try the following:# find hello.c hello.o hello -printf "%f\t%s\n"
You can also try "nm hello" or "objdump -d hello" to get an idea of what got linked into the executable.
In a POSIX OS, loading a program for execution is accomplished by having the father process to invoke the fork system call to replicates itself and having the just-created child process to invoke the execve system call to load and start the desired program. This procedure is carried out, for instance, by the shell whenever you type an external command. You can confirm this with truss or strace:# strace -i hello > /dev/null
Besides the execve system call, the output shows the call to write that results from puts, and the call to exit with the argument returned by function main (0).
To understand the details behind the loading procedure carried out by execve, let's take a look at our ELF executable:# readelf -l hello
The output shows the overall structure of hello. The first program header corresponds to the process' code segment, which will be loaded from file at offset 0x000000 into a memory region that will be mapped into the process' address space at address 0x08048000. The code segment will be 0x55dac bytes large and must be page-aligned (0x1000). This segment will comprise the .text and .rodata ELF segments discussed earlier, plus additional segments generated during the linking procedure. As expected, it's flagged read-only (R) and executable (X), but not writable (W).
The second program header corresponds to the process' data segment. Loading this segment follows the same steps mentioned above. However, note that the segment size is 0x01df4 on file and 0x03240 in memory. This is due to the .bss section, which is to be zeroed and therefore doesn't need to be present in the file. The data segment will also be page-aligned (0x1000) and will contain the .data and .bss ELF segments. It will be flogged readable and writable (RW). The third program header results from the linking procedure and is irrelevant for this discussion.
If you have a proc file system, you can check this, as long as you get "Hello World" to run long enough (hint: gdb), with the following command:# cat /proc/`ps -C hello -o pid=`/maps
The first mapped region is the process' code segment, the second and third build up the data segment (data + bss + heap), and the fourth, which has no correspondent in the ELF file, is the stack. Additional information about the running hello process can be obtained with GNU time, ps, and /proc/pid/stat.
When "Hello World" executes the return statement in main function, it passes a parameter to the surrounding functions discussed in section linking. One of these functions invokes the exit system call passing by the return argument. The exit system call hands over that value to the parent process, which is currently blocked on the wait system call. Moreover, it conducts a clean process termination, with resources being returned to the system. This procedure can be partially traced with the following:# strace -e trace=process -f sh -c "hello; echo $?" > /dev/null
The intention of this exercise is to call attention of new computer science students to the fact that a Java applet doesn't get run by magic: there's a lot of system software behind even the simplest program. If consider it useful and have any suggestion to improve it, please e-mail me.