CS 134

The Joy of execv

The execv system call replaces the current process with a new program. It loads the new program into the current process's address space, and starts executing it from the beginning. It's arguably the most important system call in Unix-like systems, because it's how you start new programs.

execv is tricky for a few reasons, including

  • If switching over to the new program fails, execv returns an error code to the original program, which can then decide what to do. Thus you can't be too eager to clean up the old program's resources, because you might need to return to it.
  • execv provides arguments to the new program, but getting those arguments copied over correctly can be a bit of a challenge.

An Example

This example will help show the execv system call in action (it actually uses execve, which includes a pointer for an environment, which we'll ignore for now). Here's a simple program that uses execv to run the ls command:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void
print_args(const char* where, int argc, char *argv[])
{
        if (argc >= 0) {
                printf("Number of arguments: %d\n", argc);
        }
        printf("%s these arguments and their memory locations:\n", where);
        printf("via argv @ %p\n", (void *) argv);
        for (int i = 0; argv[i] != NULL; i++) {
                printf("    argv[%d] = %p: %s\n", i, (void *) argv[i], argv[i]);
        }
        printf("\n");
}

static char prog_name[] = "exec-demo";
static char from_str[] = "from";
static char exec_str[] = "exec";
static char hello_str[] = "Hello";
static char world_str[] = "World";
static char *new_argv[] = {prog_name, hello_str, world_str,
                           from_str, exec_str, NULL};

/*
 * This is actually two programs in one. An "executor" that calls execve to
 * run the "executee" program. It can tell which one it is by the number of
 * arguments it receives (no arguments means it's the executor).
 */
int
main(int argc, char *argv[])
{
        int dummy = 0;
        if (argc == 1) {
                printf("=== Executor ===   (stack at %p)\n\n", (void *) &dummy);
                /* Mix things up a bit */
                new_argv[0] = argv[0];
                new_argv[2] = strdup("Universe");
                print_args("Sending", -1, new_argv);

                printf("Calling execve...\n\n");
                execve(argv[0], new_argv, NULL);

                // If execve returns, it must have failed
                perror("execve");
                exit(1);
        } else {
                printf("=== Executee ===   (stack at %p)\n\n", (void *) &dummy);
                print_args("Received", argc, argv);
        }

        return 0;
}

If you look at the addresses of the arguments from both the executor and the executee, you'll see that they're different in some interesting ways. What do you observe?

  • Hedgehog speaking

    I noticed that while in the executor, the arguments were all scattered around in memory, but in the executee, they were all packed together. We had the actual argv array first, and then all of its elements in a nice neat row.

  • Duck speaking

    I also noticed that they're actually up higher in memory than the stack pointer.

So, the kernel needs to act as a data copier, copying the arguments from the executor's address space to the executee's address space, and packing them all together nicely in one block of memory. We have to find somewhere to put the arguments, and the common convention is to use the stack, pushing the top-of=stack pointer down so that it's underneath all the arguments.

  • Rabbit speaking

    Technically, nothing requires us to put the arguments on the stack. We could put them anywhere in the executee's address space that's safe, as long as we tell the executee where they are.

Why Copying Is a Challenge

Copying data from one user address space to another is a challenge because we can only have one user address space active at a time, which means we can't access both address spaces at the same time, so we can't just do a direct copy. Instead, we have to first copy the data into the kernel, and then copy it back out to the new address space. This limitation is annoying for two reasons:

  1. We don't know ahead of time how much data we'll need to copy.
  2. Kernel memory is fairly precious.
    • In the case of OS/161, kmalloc is fine for small amounts of memory, but is generally not recommended for large amounts of memory. After OS/161 has been running for a bit, the kernel's heap may be fragmented, and kmalloc may not be able to find a large enough contiguous block of memory.

As a result, pretty much all POSIX systems impose a limit, ARG_MAX, on the total size of all the arguments that can be passed to a new program. You can find out what that limit is on a Linux machine or Mac by typing

getconf ARG_MAX

On Linux, it tends to be 2097152 bytes (2 MB), and on a Mac, 1048576 bytes (1 MB).

Doing the Copy in OS/161

For OS/161 we set ARG_MAX to 65536 bytes (64 kB), which is fine for most programs. But even there, 64 kB is a lot of memory to use with kmalloc, so we recommend that you compile a 64 kB buffer into your kernel that you can use for copying arguments, and use appropriate locking to ensure that only one program can use it at a time.

  • Goat speaking

    Meh. I'm just so looking forward to implementing this. Not.

  • PinkRobot speaking

    Well, it is a pain to do the copy, but you can prototype some of your code outside of OS/161, and then move it in when you're ready.

  • Goat speaking

    Meh. Sounds like more work. I'll just make my partner debug it.

(When logged in, completion status appears here.)