The exec() family of functions are vital for any serious Linux C programmer. They allow launching new processes and programs in a flexible and controllable manner within a C application.

In this advanced, 2600+ word guide, we will go deep into the various exec() functions, usage best practices, techniques and examples for Linux systems programmers.

Overview

The exec() functions are declared in unistd.h header and provide the following capabilities:

  1. Overlay the calling process with the new program image
  2. Pass command line arguments and environment variables in different ways
  3. Search for programs in PATH
  4. Provide fine grained control over program execution

The main exec() variants are:

execl(), execlp(), execle() 
execv(), execvp(), execve()

The l variants pass arguments individually as a list. The v variants use argument vectors. p and e denote PATH searching and environment variable passing accordingly.

Now let‘s understand each function in more depth with examples focused for Linux C programmers.

execl() Function

int execl(const char *path, const char *arg, ..., NULL);
  • path is the full program path
  • Arguments passed individually terminated by NULL

Basic example:

execl("/usr/bin/top", "top", NULL);

To pass additional arguments:

execl("/bin/ls", "ls", "-l", "/tmp", NULL);

The key things to note with execl():

  • Full program path is required
  • Arguments have to be passed separately
  • Environment of calling process is inherited

Now let‘s look at some best practices with execl().

Handling Errors

It is vital to check for errors after exec() calls:

if(execl() == -1) {
  // handle error 
}

Common errors include:

  • EACCES: Permission denied
  • ENOENT: Command not found
  • ENOEXEC: Invalid executable

Print the exact error string like this:

if(execl() == -1) {
  perror("execl");
}

Passing Complex Arguments

For complex arguments with spaces, quotes etc, use an array:

char *args[] = {"prog", "-opt1 ‘complex argument‘", NULL} ;
execl("/opt/prog", args);

execv() Function

The execv() function passes arguments as a vector:

int execv(const char *path, char *const argv[]);  

Example usage:

char *args[] = {"ls", "-l", "/tmp", NULL};
execv("/bin/ls", args);

This keeps the arguments nicely packaged in an array instead of individual items.

To control the environment variables, execve() variant can be used as we will see next.

execve() Function

The execve() function allows passing environment variable key-value pairs:

int execve(const char *filename, char *const argv[],  
                      char *const envp[]);

For example:

char *args[] = {"script.sh", "arg1", NULL};  

char *env[] = {"KEY1=val1", "KEY2=val2", NULL};

execve("/opt/script.sh", args, env); 

This executes /opt/script.sh passing arguments and environment variables.

Some key capabilities of execve():

  • Set custom environment variables
  • Pass arguments safely using array
  • Avoid shell interpretaton of arguments
  • Does not search PATH, so full path required

As you can see, execve() provides most control which is ideal for systems programming tasks.

Capturing Output of Executed Program

The standard output of programs launched via exec() is not captured. To save program output, we need to handle it manually.

One method is to redirect output to a file.

First fork() to create a separate child process:

pid_t pid = fork();

Then in child:

if (pid == 0) {

  close(STDOUT_FILENO);
  open("./output.txt", O_CREAT|O_WRONLY|O_TRUNC, 0644);

  execl(/usr/bin/ls", "ls", NULL);

  _exit(0); 

}

This redirects STDOUT to output.txt where ls command result will be saved.

Another approach is to use pipe():

int pipefd[2]; 
pipe(pipefd);

if (pid == 0) {

  dup2(pipefd[1], STDOUT_FILENO);  
  close(pipefd[0]);

  execlp("ls", "ls", NULL);

} else {

  close(pipefd[1]);
  char buf[512];
  read(pipefd[0], buf, 512);
}

So in summary – to capture output of any program launched via exec(), use standard Unix techniques like files, pipes, dup2() etc.

Security Best Practices

When executing programs via exec(), you must adopt security best practices:

  1. Do not execute untrusted programs: This can compromise system security in unexpected ways. Thoroughly vet programs before execution.

  2. Drop privileges before exec(): If the main program is running as root, switch to an unprivileged user context before executing untrusted programs. Use setuid() etc.

  3. Enforce resource limits: Set functions like setrlimit() to restrict CPU usage, maximum memory etc.

  4. Change into jail directories: Use chroot() or chdir() into empty temporary directories before execution.

  5. Filter environment variables: Carefully handle environment variables passed to exec to prevent leakage of unauthorized information.

Alternatives to exec() Functions

While exec() functions are convenient, some alternatives exist:

1. system() function – simple way to execute command, but less control

2. popen() – sets up input/output streams easily

3. Unix shell – write shell scripts for more complex tasks

Conclusion

The exec() family of functions provide excellent control over spawning external programs from a C application in Linux environments. They are very useful for systems programming tasks.

However, proper care needs to taken regarding security, correct handling of arguments and environment variables. When used correctly, exec() functions enable building very powerful and extensible C programs on Linux platforms.

Similar Posts