execve()
Beginner
3 Programs
8 Questions
What is execve()?
Imagine your Linux process is like a person running a program. The execve() system call is like that person picking up a completely new script, throwing away the old one, and starting fresh โ in the same body (same process ID).
When you call execve(), the kernel loads a brand new program into the current process’s memory. The old program code, stack, heap, and data โ all gone. The new program starts running from its own main().
Key point: The process ID does NOT change. Only the running program changes.
How execve() Works โ Step by Step
| Step | What Happens | Before execve() | After execve() |
|---|---|---|---|
| 1 | Process identity | PID = 1234 | PID = 1234 (unchanged) |
| 2 | Program code | Old program text | New program loaded |
| 3 | Stack & Heap | Old process memory | Replaced completely |
| 4 | Execution starts | Old main() running | New main() starts |
| 5 | Return value | โ | Never returns on success |
Function Signature
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
/* Returns: never on success, -1 on error */
| Parameter | Type | Purpose | Example |
|---|---|---|---|
pathname |
const char * | Path to the new program to run | "/bin/ls" |
argv[] |
char *const [] | Command-line arguments (NULL-terminated) | {"ls", "-l", NULL} |
envp[] |
char *const [] | Environment variables (NULL-terminated) | {"HOME=/root", NULL} |
โ ๏ธ Important Rules About execve()
- argv[0] is the program name by convention โ usually the basename of the executable
- Both argv[] and envp[] must end with a NULL pointer
- execve() never returns if it succeeds โ your old code is gone!
- If it does return, it means an error occurred โ check
errno - The process ID remains the same after exec
Example 1: Basic execve() โ Run /bin/ls
This is the simplest possible use of execve(). We launch the ls command from a C program.
/* example1_execve_basic.c
* Run: gcc -o example1 example1_execve_basic.c
* ./example1
*/
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(void)
{
/* argv[0] = program name, argv[1] = flag, NULL = end of list */
char *argv[] = { "ls", "-l", "/tmp", NULL };
/* Simple environment: just one variable */
char *envp[] = { "HOME=/root", "PATH=/bin:/usr/bin", NULL };
printf("Before execve: PID = %d\n", getpid());
printf("About to exec /bin/ls ...\n");
/* Replace this process with /bin/ls */
execve("/bin/ls", argv, envp);
/* If we reach here, execve() failed */
printf("execve failed: %s\n", strerror(errno));
return 1;
}
Before execve: PID = 4321About to exec /bin/ls ...(ls output of /tmp directory)
Notice: the “After execve” message never prints โ because execve() replaced the process!
Example 2: execve() with fork() โ Classic Pattern
The most common real-world use: fork() first, then exec() in the child. This way the parent process keeps running.
/* example2_fork_execve.c
* Run: gcc -o example2 example2_fork_execve.c
* ./example2
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>
int main(void)
{
pid_t child_pid;
int status;
printf("Parent PID: %d โ starting fork\n", getpid());
child_pid = fork();
if (child_pid == -1) {
perror("fork failed");
exit(1);
}
if (child_pid == 0) {
/* ---- CHILD PROCESS ---- */
printf("Child PID: %d โ about to exec /bin/date\n", getpid());
char *argv[] = { "date", NULL };
char *envp[] = { "TZ=Asia/Kolkata", NULL }; /* IST timezone */
execve("/bin/date", argv, envp);
/* Only reached if exec fails */
fprintf(stderr, "exec failed: %s\n", strerror(errno));
exit(127);
}
/* ---- PARENT PROCESS ---- */
printf("Parent: waiting for child %d to finish...\n", child_pid);
if (waitpid(child_pid, &status, 0) == -1) {
perror("waitpid failed");
exit(1);
}
if (WIFEXITED(status))
printf("Parent: child exited with status %d\n", WEXITSTATUS(status));
printf("Parent: continuing after child done\n");
return 0;
}
Parent PID: 1000 โ starting forkChild PID: 1001 โ about to exec /bin/dateThu Jun 5 10:30:00 IST 2025Parent: waiting for child 1001 to finish...Parent: child exited with status 0Parent: continuing after child doneExample 3: execve() Environment Variables
Here we show how the child program receives and can read the environment variables we pass via envp.
/* example3_exec_env.c
* TWO files: the launcher and the receiver
* File 1: launcher.c โ calls execve() with custom env
* File 2: show_env.c โ prints its env and argv
*/
/* ===== FILE 1: launcher.c ===== */
/* gcc -o launcher launcher.c && gcc -o show_env show_env.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "Usage: %s <path-to-show_env>\n", argv[0]);
exit(1);
}
/* Build argument vector */
char *new_argv[] = {
"show_env", /* argv[0]: program name */
"hello", /* argv[1] */
"world", /* argv[2] */
NULL /* Must end with NULL */
};
/* Build environment vector โ "name=value" pairs */
char *new_envp[] = {
"GREETING=Namaste",
"PLACE=Hyderabad",
"LANG=en_IN",
NULL /* Must end with NULL */
};
printf("Launcher PID: %d\n", getpid());
printf("Launching: %s\n", argv[1]);
execve(argv[1], new_argv, new_envp);
/* Should not reach here */
fprintf(stderr, "execve failed: %s\n", strerror(errno));
exit(1);
}
/* ===== FILE 2: show_env.c ===== */
#include <stdio.h>
#include <stdlib.h>
extern char **environ; /* Global environment pointer */
int main(int argc, char *argv[])
{
int i;
char **ep;
printf("=== show_env: PID = %d ===\n", getpid());
printf("\n--- Command-line arguments ---\n");
for (i = 0; i < argc; i++)
printf(" argv[%d] = %s\n", i, argv[i]);
printf("\n--- Environment variables ---\n");
for (ep = environ; *ep != NULL; ep++)
printf(" %s\n", *ep);
return 0;
}
Launcher PID: 2000Launching: ./show_env=== show_env: PID = 2000 === โ Same PID!--- Command-line arguments --- argv[0] = show_env argv[1] = hello argv[2] = world--- Environment variables --- GREETING=Namaste PLACE=Hyderabad LANG=en_INCommon Errors from execve()
| errno | Meaning | Common Cause |
|---|---|---|
EACCES |
Permission denied | File has no execute permission, or filesystem mounted noexec |
ENOENT |
File not found | Wrong path, typo in pathname |
ENOEXEC |
Not a valid executable | Script without #! line, corrupt binary |
ETXTBSY |
File busy | Another process has the file open for writing |
E2BIG |
Argument list too large | argv + envp total exceeds system limit |
What Changes vs What Stays After execve()
| Attribute | After execve() | Notes |
|---|---|---|
| Process ID (PID) | โ Unchanged | Same process, just different program |
| Parent PID (PPID) | โ Unchanged | Still the same parent |
| Open file descriptors | โ Unchanged (unless FD_CLOEXEC set) | See Chapter 27.4 |
| Program code (text) | โ Replaced | New program’s code loaded |
| Stack & Heap | โ Replaced | Fresh memory for new program |
| Signal handlers | โ Reset to default | Handler functions are gone with old code |
| Effective UID/GID | โ ๏ธ May change | If new file has setUID/setGID bit |
๐ setUID and execve()
If the file being exec’d has the set-user-ID (SUID) bit set, the process’s effective user ID changes to the file owner’s UID during exec. This is how programs like passwd run with root privileges even when called by a normal user.
/* Check setUID bit before exec */
#include <sys/stat.h>
struct stat sb;
stat("/usr/bin/passwd", &sb);
if (sb.st_mode & S_ISUID)
printf("passwd has SUID bit set โ runs as root!\n");
โ Interview Questions โ execve()
Answer: It replaces the current process image (code, stack, heap, data) with a new program. The process ID stays the same. execve() does not return on success โ the new program starts running from its own main().
Answer: Both are arrays of string pointers. The kernel needs to know where the list ends. A NULL pointer at the end acts as a sentinel/terminator โ without it, the kernel would read past the end of the array into garbage memory.
Answer: When you want the parent to continue running after the child execs a new program. fork() creates a copy of the process; the child then calls execve() to replace itself. The parent waits or continues independently.
Answer: The file exists and has execute permission, but the kernel cannot recognize its format as a valid executable. This often means it’s a shell script without a #!/bin/sh line, or a binary for a different architecture.
Answer: Yes, by default all open file descriptors (stdin, stdout, stderr, and any others) remain open across execve(). You can prevent this with the FD_CLOEXEC flag on individual descriptors.
Answer: Signal handlers are reset to SIG_DFL (default action). This is because the handler function lives in the old program’s code, which is gone after exec. Signals set to SIG_IGN remain ignored.
Answer: It is a symbolic link in the Linux /proc filesystem that points to the absolute path of the executable file currently being run by that process. For example, /proc/1234/exe โ /usr/bin/python3.
Answer: argv[0] should be the program name โ typically the basename of the pathname being executed. This is just a convention (the kernel doesn’t enforce it), but programs use argv[0] to display their own name in error messages and usage output.
Next: exec() Library Functions
Learn about execl(), execlp(), execvp() and how they simplify execve()
