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.
#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;
}
$ 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:
- La shell parsea la linea: separa por espacios, respeta comillas, expande variables ($VAR), expande globs (*, ?).
- La shell crea un arreglo de cadenas: cada palabra se convierte en una cadena, y se termina con NULL.
- La shell llama a execve: pasa el arreglo como argv, y el entorno como envp.
- 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):
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:
- Mensajes de error: los programás usan argv[0] para identificarse en los mensajes de error (por ejemplo, "ls: cannot access...").
- Comportamiento diferente: algunos programás cambian su comportamiento según argv[0]. Por ejemplo, busybox ejecuta diferentes comandos según como se le invoca (ln -s busybox ls, y busybox se comporta como ls).
- Identificacion en ps: la columna COMMAND de ps muestra argv[0] (o la linea de comandos completa).
$ 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:
| Sintaxis | Resultado |
|---|---|
comando a b c | 3 argumentos: "a", "b", "c" |
comando "a b" c | 2 argumentos: "a b", "c" |
comando 'a b' c | 2 argumentos: "a b", "c" (Sin expansion de variables) |
comando "a $VAR" c | 2 argumentos: "a valor_de_VAR", "c" (con expansion) |
comando a\\ b c | 2 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:
extern char **environ;
En la función main, También se puede recibir el entorno como tercer parámetro:
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().
#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.
$ 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:
- Establecer variables especÃficas: por ejemplo, establecer MI_CONFIG=/etc/mi_app.conf antes de ejecutar un programa.
- Eliminar variables sensibles: por ejemplo, eliminar PASSWORD antes de ejecutar un programa externo.
- modificar PATH: agregar directorios al PATH para que el programa encuentre sus dependencias.
- Establecer LD_LIBRARY_PATH: para que el programa encuentre bibliotecas compartidas en directorios no estandar.
#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:
- Seguridad: ejecutar un programa con un entorno minimo, Sin variables que puedan ser explotadas.
- Aislamiento: ejecutar un programa en un entorno controlado (por ejemplo, en un contenedor).
- Pruebas: ejecutar un programa con un entorno especÃfico para reproducir un problema.
#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;
}
$ 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:
- LD_PRELOAD: permite cargar una biblioteca compartida antes que las demás. Un atacante puede usarla para inyectar código en cualquier programa.
- LD_LIBRARY_PATH: cambia la busqueda de bibliotecas compartidas. Un atacante puede hacer que un programa cargue una biblioteca maliciosa.
- PATH: Sí se modifica, un programa puede ejecutar un comando malicioso en lugar del real.
- IFS: el separador de campos en la shell. modificarlo puede afectar como la shell parsea los comandos.
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.
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:
| Archivo | Que contiene |
|---|---|
| /proc/PID/cmdline | Los argumentos, separados por bytes nulos. Se puede leer con tr "\\0" " ". |
| /proc/PID/environ | Las variables de entorno, separadas por bytes nulos. Se puede leer con tr "\\0" "\\n". |
| /proc/PID/comm | El nombre corto del programa (argv[0] truncado a 15 Carácteres). |
$ 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.
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).
#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;
}
$ 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:
#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
- argc es el número de argumentos, argv es el arreglo de cadenas.
- argv[0] es el nombre del programa. argv[argc] es NULL.
- Los argumentos se separan por espacios en la shell; las comillas agrupan palabras.
- El entorno se accede con environ o envp, y se consulta con getenv().
- El entorno se hereda en exec (salvo execve que permite entorno explicito).
- Se puede modificar el entorno antes de exec con setenv/putenv/unsetenv.
- execve permite pasar un entorno controlado (seguridad, aislamiento).
- Los argumentos y el entorno son visibles en /proc/PID/cmdline y /proc/PID/environ.
- No se deben pasar secretos como argumentos o variables de entorno.
- getopt y getopt_long procesan argumentos de forma estandar.
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
- Compila y ejecuta argc_argv.c con diferentes argumentos, incluyendo comillas y espacios.
- Compila y ejecuta getopt_demo.c. Prueba diferentes combinaciones de opciones.
- Compila y ejecuta args_env.c. Observa los argumentos y el entorno.
- Lee /proc/self/cmdline y /proc/self/environ de un proceso en ejecución.
- Escribe un programa que use execve con un entorno controlado y verifique que las variables del padre no se pasan.
- Implementa un programa que se comporte diferente según argv[0] (Como busybox).