Introduction

Hello, and welcome back to CS631 "Advanced Programming in the UNIX Environment". After we spent some time looking at what a process looks like in memory in our last video, it's now time to consider the whole lifetime of a process from startup time until it exits. We know that a process is brought into memory via one of the 'exec' family of functions, and having written our fair share of C programs, we know that we begin in 'main', right? But we also already saw that 'main' is a bit special: if you type 'man main' in the terminal, you won't get a manual page; if you grep your system headers for a forward declaration of the 'main' function, you won't find it. So... where is 'main' defined?

Program Startup in the C Standard

Ok, so this is where we go to the standards. And this time, it's not POSIX, but we're looking at the C Programming Language standard. The name 'main' and whatever prototype it may have, after all, is a programming language specific thing. And the C standard has _this_ to say: specifically, it says that there are multiple prototypes possible: a prototype without any arguments, and a prototype with an argc and an argv, as we are used to. But then it goes on to say "or in some other implementation-defined manner", which is wonderful, because that means we can have other ways of calling 'main'. Hooray! But let's go with the broad guidance given to us by the standard: we know at program startup this 'main' function is to be called. So. When one of the 'exec' functions is called, the kernel needs to start the program somehow. For that, it calls a special startup routine, which sets up the process space, puts the command-line arguments and the environment into the right place etc. etc. We know that argc is passed to main as the number of command-line arguments, and that argv is an array of pointers to the arguments, including argv[0], the name of the program. We also know that we are guaranteed that this array is null termianted, so we can iterate over it easily.

From '_start' to 'main'

Okay, all those are nice words, but let's observe this in the wild, shall we? Here's a trivial program that prints out the address of 'main'. We're compiling it with '-g' to enable debugging symbols, and using the C89 standard instead of the default GNU C11 standard here. As a result, we get a bunch of warnings, which, unlike at any other time when we write code in this class, we are allowed to pass. Note in particular that we get a warning about our main function not returning a value, even though it's function prototype promised it would return an integer. All we're doing is print the address of 'main', which turns out to be at 400 96A hex. So this must be the entry point for the program. Let's check. We use the readelf(1) utility to print the headers of the executable. Don't worry too much about all the output; we'll revisit the ELF format and the readelf(1) utility in a future lecture, but for now, notice that it has a line noting the "Entry point address", which it notes to be 400 5b0, not 400 96A. Sooo... wait, our program doesn't start at 'main', then? Let's ask our good friend, the debugger, to observe what's going on when we run the program. First, we set a breakpoint at the entry point address, so we can see where we end up. Next, we change our layout to show us both the common registers as well as the assembly of the executable. We run the program and reach our breakpoint inside a routine called "_start" at address 400 5b0. That routine populates a few registers and then calls "___start". Let's single-step. Ok, now we're in ___start, at 400 785. That routine does a whole bunch of things, it seems. At some point, it calls things like 'atexit' and '_libc_init' before... eventually... it calls 'main', down here, calling address 400 96A, as we had observed from the program itself. If we now step, we break again right as we enter 'main'. Here, in register rdi we see the first argument to our function, argc, with a value of '1', and in register rsi, we see the second argument to our function, the argv pointer. The first element in this array -- argv[0] -- is then a pointer to the program string. Now within main, we see the call to 'printf'. Let's break there, too. We step, and... now we're in 'printf'. This function comes from libc, so we don't get all our debugging symbols, but we can print the function arguments given to printf by inspecting the argument registers again. Examining the memory address in register 'rdi' as a string gives us the first argument; printing the second argument's value as hex gives us... 0x40096a - the address of main, which was what we were going to print. Now if we continue, we leave 'printf' and are back in 'main', where we now see the value '20' in the 'rax' register used to hold the return value of the last function call. After returning from 'printf', main now returns itself, and we are back in '___start', which had called 'main' earlier. Continuing from there, we exit the program, and, as we can see, the return value of our program becomes whatever was left in the register - 20, in this case. Ok, so we saw that our program didn't really start at 'main'.

crt0.S and crt0-common.c

It started in something called '_start'. Where do we find that? Let's look in the source tree. There, you will find an architecture-dependent assembly file under lib/csu/ - where 'csu' stands for "C Start Up" - in the file 'crt0.S'. 'crt' stands for "C Runtime", and this file... ...contains the initial entry point of the program, the '_start' assembly routine, which, as we see here, calls the '___start' function. Now _that_ function is a regular C function found in the crt0-common.c file. It takes two arguments, a function pointer, and a struct ps_strings. This struct ps_strings is found at the top of the stack and is used to locate the arguments and environment strings, as we had previously observed when we looked at the process layout in memory. So the ___start function uses these strings to set up the 'extern char **environ' as well as the __progname string, which is used by the getprogname(3) library function. Further down, we see the calls to 'atexit' (which we'll see in our next video lecture) and the _libc_init function before... eventually, finally, it calls 'exit', passing it the return value of 'main', to which it passes the argc, argv, and environment pointers. ...which of course means that 'main' is not defined as being with 'no' parameters, nor as having two parameters, but as an implementation-defined function taking three parameters. We'll get back to this whole 'envp' business in another video.

Calling an alternate entry point

But ok, so our entry point is '_start', not 'main', and '_start' calls 'main'. Can we circumvent 'main'? Here's a program that has a function called 'foo', which returns an 'int', and which obviously isn't called at all. So when we compile this program... we get the same warnings as before, and of course the same behaviour as before, too. But there's a switch to tell the compiler to use a different function as the program entry point. Let's pass '-e foo', thereby telling the compiler and linker to create an executable that jumps straight into 'foo' instead of 'main'. We still get the same warnings, but... ...when we run the program, we don't enter 'main'. Instead, we directly call 'foo'. Only... after that, we segfault. That doesn't look too good. Let's compare the behaviour between the two invocations. With a normal executable, we break at the address of the _start routine, and inspect the frame. We see our instruction pointer at the address of the _start routine. We step into the ___start routine as before, and find the instruction pointer at that address. Next, we step and break again in 'main'. But note that here our 'saved rip' register containing the return address to which we will jump when we are done with this function, is now 400 8ed, a few bytes down from the starting address of the frame for ___start. So now, when 'main' completes, we grab the address from the saved rip and find ourselves back in ___start. After that, we step and exit the program. Ok, now let's compare this to the execution of the program when we use an alternate entry point. Now our entry point is 400 9ba, so let's break there. We run the program, break at 400 9ba and find ourselves immediately in 'foo'. No _start or ___start this time. Our instruction pointer is at 400 9ba, as expected, our saved rip is the uninitialized 0x1. So if we step, we get to the 'printf' after which we reach our 'return' statement. Where do we return to? Let's look at the saved rip register: 0x1 And so, at the end of the function, we are now trying to jump to memory location 00001, which is not a valid address in our process and so we get a segmentation violation. So our program with an alternate entry point segfaults because it tries to _return_ to an invalid address, unlike a program set up with _start, which has everything properly aligned to return from the main program.

Alternate entry point without segfault

Can we figure out a way to jump to an alternate entry point without segfaulting afterwards? Recall that '___start' doesn't just call 'main'; it calls 'exit' with the return value from 'main'. Let's try to 'exit' ourselves. So here's our new function 'bar'. Unlike 'foo', it doesn't _return_, it calls 'exit'. Let's give that a try. Hey - that worked! And we got the correct exit value, too! Let's take a look at how that worked: Here we grab our entry point address... break at 400 9ba but also break in 'exit'. We run it and inspect the frame and find our saved rip register to be 0x1, just as with 'foo' being our entry point. But this time, after printing our message, we don't return, we call 'exit'. Exit's frame looks like so. It has it's own instruction pointer somewhere in libc, and a saved rip pointing to where we were called from inside of 'bar'. So if we now step... ...we exit. Which is exactly what the 'exit' function is supposed to do. That is, we don't _return_ at all, so the contents of the saved rip register don't matter, and which is why we didn't segfault, even though we didn't use the '_start' routine as the entry point. Alright, time for a break here.

Recap

We spent enough time talking about how a program starts. We learned that the entry point is defined by the compiler / linker. Meaning we can change it. We saw that there is a C Startup routine that sets up the environment and moves the command-line arguments into place etc. And we saw why 'main' always returns an int: it passes this value on to 'exit'. So all in all, our program doesn't simply start with 'main', but instead we notice the default entry point '_start', which calls '___start', which calls 'exit' with its argument being the return value of 'main'. That is, we also saw how the program exits and returns, but we'll spend a bit more time on that in our next video segment. Until next time - cheers!