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:
- Overlay the calling process with the new program image
- Pass command line arguments and environment variables in different ways
- Search for programs in PATH
- 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);
pathis 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 deniedENOENT: Command not foundENOEXEC: 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:
-
Do not execute untrusted programs: This can compromise system security in unexpected ways. Thoroughly vet programs before execution.
-
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.
-
Enforce resource limits: Set functions like setrlimit() to restrict CPU usage, maximum memory etc.
-
Change into jail directories: Use chroot() or chdir() into empty temporary directories before execution.
-
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.


