Argumentos y entorno del nuevo programa - Procesos e Hilos
Volver a Procesos e Hilos Bloque 4 - Ejecutar programás con exec - Tema 18 de 43

Tema 18

Argumentos y entorno del nuevo programa

Cuando un proceso ejecuta un nuevo programa con exec, le pasa argumentos (argv) y variables de entorno (envp). Estos datos determinan como se comporta el programa: Que archivos procesa, Que configuración usa, Que opciones activa. En este tema veras como se construyen argc y argv, Como se pasa el entorno, y como controlarlos con execve.

1. argc y argv

Cuando un programa en C se ejecuta, su función main recibe dos parámetros: argc (argument count), el número de argumentos, y argv (argument vector), un arreglo de punteros a cadenas con los argumentos. argv[0] es el nombre del programa, y argv[argc] es NULL (terminador).

Los argumentos son las palabras que se escriben después del nombre del programa en la linea de comandos. Por ejemplo, Sí ejecutas ls -l /tmp, entonces argc=3, argv[0]="ls", argv[1]="-l", argv[2]="/tmp", argv[3]=NULL.

argc_argv.c

#include 

int main(int argc, char *argv[])
{
    printf("argc = %d\n", argc);
    for (int i = 0; i < argc; i++)
        printf("argv[%d] = %s\n", i, argv[i]);
    printf("argv[argc] = %p\n", (void *) argv[argc]);
    return 0;
}
terminal - ejecutar argc_argv

$ gcc -std=c17 -Wall -Wextra -o argc_argv argc_argv.c
$ ./argc_argv hola mundo "tres palabras"
argc = 4
argv[0] = ./argc_argv
argv[1] = hola
argv[2] = mundo
argv[3] = tres palabras
argv[argc] = (nil)

Los argumentos se separan por espacios. Las comillas dobles agrupan varias palabras en un sólo argumento. argv[argc] es NULL, lo que permite iterar sobre argv sin conocer argc.

2. Cómo se construyen argc y argv

Cuando la shell ejecuta un programa, construye el arreglo argv a partir de la linea de comandos. El proceso es:

  1. La shell parsea la linea: separa por espacios, respeta comillas, expande variables ($VAR), expande globs (*, ?).
  2. La shell crea un arreglo de cadenas: cada palabra se convierte en una cadena, y se termina con NULL.
  3. La shell llama a execve: pasa el arreglo como argv, y el entorno como envp.
  4. El núcleo carga el programa: coloca argc y argv en la pila del nuevo proceso, justo antes de llamar a main.

El núcleo coloca argc, argv y envp en la pila del proceso al cargar el ELF. La biblioteca de C (glibc) lee estos valores de la pila y los pasa a main. Por eso, main recibe argc y argv como parámetros.

La pila del proceso al inicio se ve así (de arriba a abajo):

pila inicial del proceso

argc                          (Número de argumentos)
argv[0]  -> "nombre_programa"  (puntero a cadena)
argv[1]  -> "arg1"            (puntero a cadena)
...
argv[argc-1] -> "argN"        (puntero a cadena)
argv[argc] = NULL              (terminador)
envp[0]  -> "VAR1=valor1"     (puntero a cadena)
envp[1]  -> "VAR2=valor2"     (puntero a cadena)
...
envp[N] = NULL                 (terminador)
/* seguido de las cadenas reales */

Todo esto se coloca en la pila por el núcleo antes de que el programa empiece a ejecutarse. La biblioteca de C lee estos valores y los pasa a main.

3. El argumento 0

argv[0] es, por convencion, el nombre del programa. Pero no es obligatorio que sea el nombre real: es lo que el proceso que llamo a exec pase como primer argumento. La shell pasa el nombre del comando, pero un programa puede pasar cualquier cosa.

argv[0] se usa para:

terminal - argv[0] y busybox

$ ls -l /bin/ls /bin/busybox
-rwxr-xr-x 1 root root  142144 ... /bin/ls
-rwxr-xr-x 1 root root  900000 ... /bin/busybox
$ ln -s /bin/busybox /tmp/mi_ls
$ /tmp/mi_ls /tmp
archivo1  archivo2

busybox es un sólo binario que implementa decenas de comandos (ls, cat, grep, etc.). Decide que hacer según argv[0]: Sí se invoca como "ls", se comporta como ls; Sí se invoca como "cat", se comporta como cat. Es un ejemplo clasico de uso de argv[0].

4. Argumentos con espacios y comillas

La shell parsea la linea de comandos y separa los argumentos por espacios. Las comillas dobles y simples permiten incluir espacios en un argumento:

SintaxisResultado
comando a b c3 argumentos: "a", "b", "c"
comando "a b" c2 argumentos: "a b", "c"
comando 'a b' c2 argumentos: "a b", "c" (Sin expansion de variables)
comando "a $VAR" c2 argumentos: "a valor_de_VAR", "c" (con expansion)
comando a\\ b c2 argumentos: "a b", "c" (escape con backslash)

Es importante entender como la shell parsea los argumentos, porque los programás reciben los argumentos ya separados. El programa no necesita parsear la linea de comandos: Sólo recibe el arreglo argv.

Las variables de entorno se expanden dentro de comillas dobles pero no dentro de comillas simples. Los globs (*, ?) se expanden sí no estan entre comillas.

5. La variable environ

El entorno de un proceso es un conjunto de pares "NOMBRE=valor" Que se pasan al programa. En C, se accede a través de la variable global environ, un arreglo de punteros a cadenas terminado en NULL.

La variable environ se declara como:

C - declarar environ

extern char **environ;

En la función main, También se puede recibir el entorno como tercer parámetro:

C - main con envp

int main(int argc, char *argv[], char *envp[])
{
    /* envp es el entorno al arrancar */
    /* environ es la variable global, Siempre disponible */
    return 0;
}

En la práctica, environ y envp apuntan al mismo arreglo al inicio. Pero environ puede cambiar sí se usa setenv/putenv, mientras que envp Sigue apuntando al arreglo original.

Para consultar una variable específica, se usa getenv(). Para modificarla, setenv() o putenv(). Para eliminarla, unsetenv().

leer_entorno.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 

extern char **environ;

int main(int argc, char *argv[], char *envp[])
{
    printf("Variables de entorno:\n");
    for (int i = 0; environ[i] != NULL; i++)
        printf("  %s\n", environ[i]);

    printf("\nPATH = %s\n", getenv("PATH"));
    return 0;
}

6. Cómo se pasa el entorno a exec

Cuando se llama a exec (excepto execve y execvpe), el entorno del padre se hereda automaticamente. El núcleo copia el arreglo environ del padre al nuevo programa. El nuevo programa recibe el mismo entorno que el padre tenia en el momento de exec.

Con execve y execvpe, se puede pasar un entorno explicito. Esto permite controlar exactamente que variables de entorno recibe el nuevo programa. Si se pasa un arreglo vacio o NULL, el programa se ejecuta sin variables de entorno.

La herencia del entorno es importante: un programa que se ejecuta con exec recibe las variables de entorno del padre. Si el padre tiene PATH, HOME, USER, etc., el hijo también las tiene. Esto es lo que permite que los programás encuentren sus configuraciónes y recursos.

terminal - variables de entorno heredadas

$ export MI_VAR=hola
$ ./programa_Que_imprime_entorno
MI_VAR=hola
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/home/ana
...

7. modificar el entorno antes de exec

Antes de llamar a exec, el hijo puede modificar su entorno con setenv, putenv y unsetenv. Los cambios se heredan al nuevo programa. Esto es útil para:

modificar_entorno_exec.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 
#include 

int main(void)
{
    pid_t pid = fork();
    if (pid < 0) { perror("fork"); return 1; }

    if (pid == 0) {
        /* Hijo: modificar el entorno */
        setenv("MI_VAR", "Modificado_por_hijo", 1);
        setenv("NUEVA_VAR", "nueva", 1);
        unsetenv("PATH");  /* eliminar PATH */

        /* Ejecutar un programa que muestra el entorno */
        execlp("env", "env", NULL);
        perror("exec");
        _exit(127);
    }

    wait(NULL);
    printf("Padre: MI_VAR = %s (no cambio)\n", getenv("MI_VAR"));
    return 0;
}

El hijo modifica el entorno y ejecuta env, Que muestra las variables. El padre no se ve afectado por los cambios del hijo.

8. execve con entorno controlado

execve permite pasar un entorno completamente nuevo, Sin heredar las variables del padre. Esto es útil para:

entorno_controlado.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 
#include 

int main(void)
{
    pid_t pid = fork();
    if (pid < 0) { perror("fork"); return 1; }

    if (pid == 0) {
        /* Entorno minimo y controlado */
        char *env[] = {
            "PATH=/usr/bin:/bin",
            "HOME=/tmp",
            "USER=sandbox",
            "LANG=C",
            NULL
        };
        char *args[] = {"env", NULL};

        execve("/usr/bin/env", args, env);
        perror("execve");
        _exit(127);
    }

    wait(NULL);
    return 0;
}
terminal - ejecutar entorno_controlado

$ gcc -std=c17 -Wall -Wextra -o entorno_controlado entorno_controlado.c
$ ./entorno_controlado
PATH=/usr/bin:/bin
HOME=/tmp
USER=sandbox
LANG=C

El programa ejecuta env con un entorno de sólo 4 variables. Las variables del padre (SHELL, TERM, DISPLAY, etc.) no se pasan. Esto es útil para ejecutar programás en un entorno controlado.

9. El entorno y la seguridad

El entorno puede ser un vector de ataque. Un programa malicioso puede establecer variables de entorno que afecten el comportamiento de otros programas:

Por eso, los programás setuid y los servidores deben limpiar el entorno antes de ejecutar otros programás. La tetécnica es usar execve con un entorno controlado, eliminando las variables peligrosas.

Limpiar el entorno en programás setuid

Un programa setuid que ejecuta otros programás debe limpiar el entorno (eliminar LD_PRELOAD, LD_LIBRARY_PATH, etc.) antes de exec. De lo contrario, un atacante puede usar estas variables para inyectar código en el programa ejecutado.

El núcleo también tiene protecciónes: las variables LD_PRELOAD y LD_LIBRARY_PATH se ignoran en programás setuid. Pero es buena práctica limpiar el entorno explicitamente.

10. Argumentos y entorno en /proc

Los argumentos y el entorno de un proceso se exponen en /proc:

ArchivoQue contiene
/proc/PID/cmdlineLos argumentos, separados por bytes nulos. Se puede leer con tr "\\0" " ".
/proc/PID/environLas variables de entorno, separadas por bytes nulos. Se puede leer con tr "\\0" "\\n".
/proc/PID/commEl nombre corto del programa (argv[0] truncado a 15 Carácteres).
terminal - leer argumentos y entorno en /proc

$ cat /proc/self/cmdline | tr "\\0" " "; echo
bash
$ tr "\\0" "\\n" < /proc/self/environ | head -5
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/home/ana
USER=ana
SHELL=/bin/bash
TERM=xterm-256color

Los argumentos y el entorno son visibles para cualquier usuario que pueda leer /proc/PID. Esto es una consideracion de seguridad: no se deben pasar contrasenas ni datos sensibles como argumentos o variables de entorno.

No pasar secretos como argumentos

Los argumentos de un proceso son visibles en /proc/PID/cmdline para cualquier usuario. Las variables de entorno son visibles en /proc/PID/environ. Nunca se deben pasar contrasenas, tokens o datos sensibles como argumentos o variables de entorno. Usar archivos con permisos restringidos o pipes en su lugar.

11. Procesar argumentos: getopt

Cuando un programa tiene muchos argumentos y opciones, parsearlos manualmente es tedioso y propenso a errores. La biblioteca de C ofrece getopt() y getopt_long() para procesar argumentos de forma estandar.

getopt() procesa opciones cortas (Como -a, -b, -c). getopt_long() procesa opciones largas (Como --help, --Version, --output).

getopt_demo.c

#define _GNU_SOURCE
#include 
#include 
#include 

int main(int argc, char *argv[])
{
    int opt;
    int verbose = 0;
    char *output = NULL;

    while ((opt = getopt(argc, argv, "vo:")) != -1) {
        switch (opt) {
            case 'v': verbose = 1; break;
            case 'o': output = optarg; break;
            case '?': fprintf(stderr, "Uso: %s [-v] [-o archivo] [args...]\n", argv[0]); return 1;
        }
    }

    printf("verbose = %d\n", verbose);
    printf("output = %s\n", output ? output : "(ninguno)");
    printf("Argumentos restantes:");
    for (int i = optind; i < argc; i++)
        printf(" %s", argv[i]);
    printf("\n");
    return 0;
}
terminal - ejecutar getopt_demo

$ gcc -std=c17 -Wall -Wextra -o getopt_demo getopt_demo.c
$ ./getopt_demo -v -o salida.txt arg1 arg2
verbose = 1
output = salida.txt
Argumentos restantes: arg1 arg2
$ ./getopt_demo -vo salida.txt arg1
verbose = 1
output = salida.txt
Argumentos restantes: arg1

getopt procesa las opciones y deja los argumentos no-opcionales en argv[optind...argc-1]. Es la forma estandar de procesar argumentos en programás unix.

12. Un programa que demuestra argumentos y entorno

El siguiente programa muestra todos los argumentos y variables de entorno, y permite modificar el entorno:

args_env.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 

extern char **environ;

int main(int argc, char *argv[], char *envp[])
{
    printf("=== Argumentos ===\n");
    for (int i = 0; i < argc; i++)
        printf("  argv[%d] = %s\n", i, argv[i]);

    printf("\n=== Entorno (primeras 5) ===\n");
    for (int i = 0; i < 5 && environ[i]; i++)
        printf("  %s\n", environ[i]);

    printf("\n=== modificar entorno ===\n");
    setenv("DEMO_VAR", "hola", 1);
    printf("DEMO_VAR = %s\n", getenv("DEMO_VAR"));

    printf("\n=== Ejecutar con execve (entorno controlado) ===\n");
    pid_t pid = fork();
    if (pid == 0) {
        char *args[] = {"env", NULL};
        char *env[] = {"PATH=/usr/bin:/bin", "DEMO=controlado", NULL};
        execve("/usr/bin/env", args, env);
        perror("execve");
        _exit(127);
    }
    wait(NULL);
    return 0;
}

El programa muestra los argumentos, el entorno, Modifica una variable, y ejecuta env con un entorno controlado. Demuestra como se pasan argumentos y entorno a un nuevo programa.

13. Resumen

14. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. Qué son argc y argv?

argc es el número de argumentos, argv es el arreglo de punteros a cadenas con los argumentos. argv[0] es el nombre del programa, argv[argc] es NULL.

2. Cómo se construyen argc y argv?

La shell parsea la linea de comandos, separa por espacios (respetando comillas), y llama a execve con el arreglo. El núcleo coloca argc y argv en la pila del nuevo proceso.

3. Para que se usa argv[0]?

Para identificar el programa en mensajes de error, en ps, y para cambiar el comportamiento según el nombre (Como busybox).

4. Qué es la variable environ?

Un arreglo de punteros a cadenas con las variables de entorno, terminado en NULL. Cada cadena tiene el formato "NOMBRE=valor".

5. Cómo se pasa el entorno a exec?

Con execve/execvpe se pasa un entorno explicito. Con las demás variantes, se hereda el entorno del padre.

6. Por que es importante limpiar el entorno en programás setuid?

Porque variables como LD_PRELOAD y LD_LIBRARY_PATH pueden ser usadas para inyectar código. El programa setuid debe eliminarlas antes de exec.

7. Donde se pueden ver los argumentos y el entorno de un proceso?

En /proc/PID/cmdline (argumentos) y /proc/PID/environ (entorno). Son visibles para cualquier usuario.

8. Por que no se deben pasar secretos como argumentos?

Porque son visibles en /proc/PID/cmdline para cualquier usuario. Usar archivos con permisos restringidos o pipes.

9. Qué hace getopt?

Procesa opciones de linea de comandos (Como -a, -b, --help) de forma estandar. Deja los argumentos no-opcionales en argv[optind...argc-1].

10. Qué es busybox y como usa argv[0]?

Es un sólo binario que implementa decenas de comandos. Decide que hacer según argv[0]: Sí se invoca como "ls", se comporta como ls.

Para prácticar

  1. Compila y ejecuta argc_argv.c con diferentes argumentos, incluyendo comillas y espacios.
  2. Compila y ejecuta getopt_demo.c. Prueba diferentes combinaciones de opciones.
  3. Compila y ejecuta args_env.c. Observa los argumentos y el entorno.
  4. Lee /proc/self/cmdline y /proc/self/environ de un proceso en ejecución.
  5. Escribe un programa que use execve con un entorno controlado y verifique que las variables del padre no se pasan.
  6. Implementa un programa que se comporte diferente según argv[0] (Como busybox).