Introduction

Hello, and welcome to CS631 "Advanced Programming in the UNIX Environment". Last time, we talked about program startup and how we enter the startup routine before calling main. This time, we'll look at program termination, even though we touched on that a little bit in the last segment.

Return codes by example

Remember how we started out last time? We just printed the address of 'main' and then... fell off main, without returning an explicit value. With our default compiler options, the return value was 0. But if we compiled the program with '-std=c89', we got a warning about the missing return value and observed our program to return a mysterious value of 20. ...which just so happens to be the length of the message printed by printf(3). Let's compare what the difference is between the instructions generated when using C89 and when using C11. For that, we use the objdump(1) utility to disassemble the executable. Let's look for the 'main' function. Here, we see the call to 'printf', followed by us returning from the function without any further action. Now, let's compile it using the default GNU C11 standard. The difference between the code generated is minimal. The lines starting with a '+' are the C89 code, the ones starting with a '-' are C11. They are almost identical, but in the C11 code, we see an explicit statement to move the value zero into the return value register 'eax' before returning. Ok, so it seems that the C programming language was changed to default to a return value of zero if no value was explicitly specified. After all, we didn't really do anything with the return value of our 'printf' call. Let's change that. What happens if we assign the return value to a variable? Let's see... When we compile the code, we now get a warning about the integer 'n' being set, but not being used. Smart little compiler! Our return value, though, remains unchanged. Let's again disassemble the code... ...and then compare to C89 code. There, too, we get the same warning, but we also see again the latter warning about an explicit return value missing. Let's look at the C89 assembly. Here, inside of 'main', we see that we are using the return value of 'printf' and move it into a general purpose register - a step representing the assignment of the value to our integer 'n' -- before we return from the function. In comparison, in the C11 code... ...we see the exact same step taking place, but this time it is followed again by the explicit assignment of zero to the return value register. [pause] Ok, so assigning the value to a variable by itself doesn't change much: the compiler appears to be smart enough to realize that 'n' isn't really used -- hence the warning we got when compiling -- and then still explicitly assigns 0 as the return value as the C11 standard mandates. Now let's compare the code if we _do_ use the returned value: Note that we no longer get a warning, as now we are using the variable. Same behavior for our standard C11 compilation. And when we disassemble both, we find that the resulting code is now identical... ...and looks like so. We call 'printf', move the return value into the register, then, per the 'return' statement, move the value from that register into the return value register.... ...yielding the expected return code of 20, the total number of characters printed by 'printf'.

Process Termination

Ok, so with this and our previous video, we've already seen multiple ways of how a process can terminate. On the one hand, there's normal - i.e., expected -- termination. We've seen: implicit return from 'main', which may yield a different return value depending on the C standard in question. explicit return from main and a call to the exit(3) library function, which can happen from within 'main' or from any other function. But there's also the "_exit(2)" syscall, as well as, oddly, an upper-case variation thereof. We got _Exit(2) as a gift of discrepancies between the C language standard, which requires _Exit(2) and POSIX, which requires _exit(2). Yay, standards! two ways that only apply to multi-threaded programs, which we'll skip over here. Aside from that, we also have "abnormal" ways to terminate a process: We can call abort(3), we may be terminated by a signal - such as SIGSEGV or SIGKILL and again, if we're multi-threaded, we might terminate upon a cancellation request.

exit(3)

So let's take a look at the exit(3) function: exit(3) is a library function, so you an already speculate that it wraps the _exit(2) syscall and provides additional features. As we know, exit(3) takes as argument an integer value that then becomes the exit status of the program. Note that of course exit(3) does not itself return -- it can't: it leads to the termination of the process, as we just saw in our code exploration. Before exiting, it performs these steps: It calls any functions previously registered as exit handlers via atexit(3), which we'll see in a minute. It flushes all open output streams and then closes them. It unlinks any files that were created by the process via the tmpfile(3) function. And finally, it calls _exit(2).

_exit(2)

_exit(2), a syscall, can be called directly, and would then terminate the process immediately without doing any of those things. There are still a few things that happen when you exit a process via this call, but we'll revisit those considerations in future lectures when we talk about interprocess communications and signals.

atexit(3)

So as noted, the exit(3) library function may call any exit handlers that were previously registered via the atexit(3) function, so let's take a look at it. atexit(3) takes as argument a function pointer for a simple function that takes no arguments and has no return type. Upon success, atexit(3) pushes the function pointer on a stack that exit(3) in turn will pop function pointers off, meaning the order in which they are executed at process termination time is the reverse order in which they were registered. There's no reason why we can't have the same function get called multiple times - we'd just have to register it multiple times. All of this is useful to ensure you perform certain tasks not only when you return from main, but at any time you exit.

atexit(3) example

Let's look at an example: Here we have a few trivial functions that simply print a message. The function 'func' prints a message and then, depending on the number of arguments given, may either exit(3), _exit(2), or abort(3) the program In 'main', we register these functions above as exit handlers via atexit(3). Remember that the functions will be popped off the stack in the reverse order than the one we're registering them, so we expect the order at exit time to be my_exit1, my_exit1, my_exit2. We then call 'func' and, upon return from that function, print a message here in 'main' before we return from 'main'. Let's run it. Ok, the order looks as expected. We called 'func', then returned back into 'main', then returned from 'main'. Our exit handlers are called by 'exit' from the startup routine after 'main' returned. If we pass an argument, then we will have 'func' call exit(3) itself, and so we observe that we do not return to 'main', but that our registered exit handlers are executed nevertheless as promised. If we pass a second argument, then 'func' will call _exit(2), which will terminate the process immediately without triggering the calls to the exit handlers. Finally, with a third argument passed, 'func' will call abort(3), and we exit without calling the exit handlers, but with a core dump. Inspecting the coredump, we can see the backtrace and find the exact location in the code where the program was terminated by the SIGABRT signal.

Lifetime of a Unix Process

Alright, so let's look at the lifetime of a Unix process in this illustration: The new process comes to life via 'exec' from the kernel. It then enters the C startup routine in user space, which ends up calling 'main'. 'main' may call any number of functions which ultimately return to 'main'. After 'main' returns, we're back in the C startup routine, which calls exit(3), from where we leave user space and terimante the process via _exit(2). However, we can also call exit(3) ourselves from within 'main'... ...or any function, really. exit(3) may call any exit handlers that were previously registered via atexit(3) and then perform some cleanup of I/O streams or temporary files before calling _exit(2). But we also have the option of calling _exit(2) ourselves from within 'main' or any other function.

Recap

Ok, time for a recap: We've seen a number of ways of exiting from a program. Returning from main either explicitly or implicitly will lead to a call to exit(3) via the C startup routine. The return value depends on whether or not you passed one in as well as on the C standard version you're following. We can exit(3) or _exit(2) ourselves at any time. If we have certain tasks that we'd like to be performed whenever our program terminates, we can register exit handlers via atexit(3). These exit handlers are circumvented by calling _exit(2) or abort(3). What happens to other processes when our program terminates, finally, is something we'll discuss in a future lecture. And with that, it's time to exit this video segment. Thanks for watching - cheers!