1 Tinguaro Tadeo del Rosario Juan José Muñoz GarcíaLección 9: EXEC Tinguaro Tadeo del Rosario Juan José Muñoz García
2 Introducción Un fork crea un proceso hijo que ejecuta el mismo programa, mientras que con un exec se puede lanzar a ejecutar un nuevo programa. El exec permite crear un proceso que ejecuta un código distinto al del padre. Si no existiera el fork y el exec, siempre estaría ejecutándose el proceso init, porque el resto serían simplemente hijos de él.
3 Familia de funciones Realmente, no existe ninguna función llamada exec. Siempre estaremos haciendo referencia al conjunto de funciones que poseen la misma funcionalidad, pero que se diferencian en el modo en el que se les pasan los argumentos. La familia está formada por: execl, execle, execve, execlp, execvp y execv.
4 Modo de uso En caso de éxito en un exec no hay retorno, porque el proceso que llama, ejecuta ahora un nuevo programa. Para crear nuevos procesos, generalmente se siguen dos pasos: Se crea una copia del proceso padre mediante fork o clone. El hijo lanza a ejecutar, mediante exec el nuevo programa.
5 Modo de uso if (fork()) { // Esto lo ejecuta el padre } else// Esto lo ejecuta el hijo execl(“programa”, “programa”); // Finaliza el proceso hijo
6 Implementación del execEl código fuente de la implementación del exec se encuentra en /fs/exec.c. La función principal que lleva a cabo la implementación es do_execve. Otras funciones importantes son: prepare_binprm: prepara los parámetros del fichero ejecutable. search_binary_handler: busca el manejador para el tipo de ejecutable que se trate.
7 do_execve La funcionalidad básica es la siguiente:Lee de memoria información característica del ejecutable (prepare_binprm): de los primeros 128 bytes. Prepara el entorno y argumentos del nuevo programa. Busca el manejador para el tipo de ejecutable que se esté tratando (search_binary_handler).
8 do_execve - linux_binprmEstructura utilizada para configurar parámetros de ejecución del proceso, antes de lanzarlo a ejecutar struct linux_binprm{ char buf[BINPRM_BUF_SIZE]; // Para leer los primeros 128 bytes struct page *page[MAX_ARG_PAGES]; // Tabla pagina del proceso unsigned long p; // Longitud de la memoria utilizada struct dentry * dentry; // Identificador de fichero del ejecutable int e_uid, e_gid; // uid y gid efectivos int argc, envc; // Numero de argumentos y variables de entorno char * filename; /* Nombre del ejecutable */ };
9 do_execve - linux_binprmEl UID real es el identificador del usuario que ha lanzado el proceso. El EUID es el identificador que usa el sistema para los controles de acceso. Puede ser distinto al del usuario real (bit SETUID) Si el fichero ejecutado tiene activo el bit SETUID, se cambia el e_uid del proceso que hizo la llamada al del propietario del fichero ejecutable. Si no lo tiene activo, el e_uid será el del proceso que hizo la llamada. Estructura utilizada para configurar parámetros de ejecución del proceso, antes de lanzarlo a ejecutar struct linux_binprm{ char buf[BINPRM_BUF_SIZE]; // Para leer los primeros 128 bytes struct page *page[MAX_ARG_PAGES]; // Tabla pagina del proceso unsigned long p; // Longitud de la memoria utilizada struct dentry * dentry; // Identificador de fichero del ejecutable int e_uid, e_gid; // uid y gid efectivos int argc, envc; // Numero de argumentos y variables de entorno char * filename; /* Nombre del ejecutable */ };
10 do_execve - Tabla de páginasNúmero de página Tabla de página Marco en memoria física - Para reducir el tiempo de acceso a memoria física, se utilizan tablas TLB - El registro PTBR apunta a la dirección en memoria principal que contiene la tabla de página
11 do_execve Inicializamos tabla de páginas10079 int do_execve(char * filename, char ** argv, char ** envp, struct pt_regs * regs) 10081 { struct linux_binprm bprm; struct dentry * dentry; int retval; int i; 10086 bprm.p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *); for (i=0 ; i
12 do_execve Count cuenta el número de punterosno-nulos de los vectores argv y envp que son los argumentos y las variables de entorno. bprm.dentry = dentry; bprm.filename = filename; bprm.sh_bang = 0; bprm.java = 0; bprm.loader = 0; bprm.exec = 0; if ((bprm.argc = count(argv)) < 0) { dput(dentry); return bprm.argc; } 10106 if ((bprm.envc = count(envp)) < 0) { dput(dentry); return bprm.envc; } 10111 retval = prepare_binprm(&bprm);
13 do_execve Count cuenta el número de punteros no-nulos de los vectores argv y envp que son los argumentos y las variables de entorno. bprm.dentry = dentry; bprm.filename = filename; bprm.sh_bang = 0; bprm.java = 0; bprm.loader = 0; bprm.exec = 0; if ((bprm.argc = count(argv)) < 0) { dput(dentry); return bprm.argc; } 10106 if ((bprm.envc = count(envp)) < 0) { dput(dentry); return bprm.envc; } 10111 retval = prepare_binprm(&bprm); argv es un array de cadenas de argumentos pasados al nuevo programa. envp es un array de cadenas, que se pasan como entorno al nuevo programa. Tanto argv como envp deben terminar en un puntero nulo.
14 do_execve 10114 if (retval >= 0) {bprm.p = copy_strings(1, &bprm.filename, bprm.page, bprm.p, 2); bprm.exec = bprm.p; bprm.p = copy_strings(bprm.envc,envp,bprm.page, bprm.p,0); bprm.p = copy_strings(bprm.argc,argv,bprm.page, bprm.p,0); if (!bprm.p) retval = -E2BIG; } 10125 if (retval >= 0) retval = search_binary_handler(&bprm,regs); if (retval >= 0) /* execve success */ return retval; Copia al espacio de memoria del proceso, el nombre del fichero, las variables de entorno y los argumentos. Si no queda memoria libre, error
15 do_execve Liberamos memoria del proceso/* Something went wrong, return the inode and free the * argument pages*/ if (bprm.dentry) dput(bprm.dentry); 10136 for (i=0 ; i
16 Cuando encuentra un null saledo_execve - count 9480 static int count(char ** argv) 9481 { int i = 0; if (argv != NULL) { for (;;) { char * p; int error; 9488 error = get_user(p,argv); if (error) return error; if (!p) break; argv++; i++; } } return i; 9499 } Recoge los parámetros Cuando encuentra un null sale Devuelve el número de parámetros, que se le pasan a la función que lanza el exec
17 do_execve - prepare_binprm9832 int prepare_binprm(struct linux_binprm *bprm) 9833 { int mode; int retval,id_change,cap_raised; struct inode * inode = bprm->dentry->d_inode; mode = inode->i_mode; if (!S_ISREG(mode)) return -EACCES; if (!(mode & 0111)) return -EACCES; if (IS_NOEXEC(inode)) return -EACCES; if (!inode->i_sb) return -EACCES; if ((retval = permission(inode, MAY_EXEC)) != 0) return retval; if (inode->i_writecount > 0) return -ETXTBSY; Comprueba que es un fichero regular ejecutable, que tiene permiso de ejecución y que no se está escribiendo
18 do_execve - prepare_binprmbprm->e_uid = current->euid; bprm->e_gid = current->egid; id_change = cap_raised = 0; 9856 /* Set-uid? */ if (mode & S_ISUID) { bprm->e_uid = inode->i_uid; if (bprm->e_uid != current->euid) id_change = 1; } /*Set-gid?*/ if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) { bprm->e_gid = inode->i_gid; if (!in_group_p(bprm->e_gid)) id_change = 1; } Si el setuid y/o setgid están activos, los nuevos procesos se tratarán como usuarios / grupos diferentes
19 do_execve - prepare_binprmreturn read_exec(bprm->dentry,0,bprm->buf,128,1); Finalmente, leemos los primeros 128 bytes del fichero, y lo introducimos en la estructura bprm. Una posible optimización del kernel sería reemplazar 128 por un define. El buffer sirve para identificar el tipo de ejecutable que estamos tratando
20 do_execve - copy_string9519 unsigned long copy_strings( int argc,char ** argv, unsigned long *page, unsigned long p, int from_kmem) 9522 { while (argc-- > 0) { get_user(str, argv+argc); len = strlen_user(str); while (len) { offset = pos % PAGE_SIZE; pag = (char *) page[pos/PAGE_SIZE] = (unsigned long *) get_free_page(GFP_USER)) bytes_to_copy = PAGE_SIZE - offset; copy_from_user(pag + offset, str, bytes_to_copy); len -= bytes_to_copy; } } return p; 9573 } Traemos los argumentos de la memoria de usuario, y calculamos la longitud
21 do_execve - copy_string9519 unsigned long copy_strings( int argc,char ** argv, unsigned long *page, unsigned long p, int from_kmem) 9522 { while (argc-- > 0) { get_user(str, argv+argc); len = strlen_user(str); while (len) { offset = pos % PAGE_SIZE; pag = (char *) page[pos/PAGE_SIZE] = (unsigned long *) get_free_page(GFP_USER)) bytes_to_copy = PAGE_SIZE - offset; copy_from_user(pag + offset, str, bytes_to_copy); len -= bytes_to_copy; } } return p; 9573 } Traemos los argumentos de la memoria de usuario, y calculamos la longitud Calculamos una entrada en la tabla de páginas, le asignamos una dirección de memoria física y copiamos en la memoria física el argumento. (Copiamos de página en página)
22 do_execve - search_binary_handler9996 int search_binary_handler(struct linux_binprm *bprm, struct pt_regs *regs) 9998 { for (try=0; try<2; try++) { for (fmt = formats ; fmt ; fmt = fmt->next) { int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary; if (!fn) continue; retval = fn(bprm, regs); if (retval >= 0) { if (bprm->dentry) dput(bprm->dentry); bprm->dentry = NULL; current->did_exec = 1; return retval; } MANEJADOR BINARIO: Mecanismo del núcleo para tratar la variedad de formatos binarios de manera consistente. No todos los programas se almacenan en el mismo formato: un ejemplo “Java Handler”, “Scripts”. Llamamos a load_binary hasta que retorne no-negativo. Para así poder utilizar las funciones que tratan cada ejecutable.
23 do_execve - search_binary_handlerif (retval != -ENOEXEC) break; /* We don't have the dentry anymore */ if (!bprm->dentry) return retval; } if (retval != -ENOEXEC) { break; } else { sprintf(modname, "binfmt-%04x", *(unsigned short *)(&bprm->buf[2])); request_module(modname); } } return retval; 10075 } Si no se encuentra ningún manejador se hace un segundo intento, obteniendo primero un código de formato nuevo, que indique el posible tipo de fichero binario
24 formatos ejecutables No todos los programas están almacenados en el mismo formato Linux usa manejadores binarios para abstraer las diferencias entre los distintos formatos Para cada formato se usa un manejador específico ELF es el formato nativo de ejecutables Linux
25 formatos ejecutables ELF sustituye a otro formato llamado a.out, que es el que se utilizaba como nativo en versiones anteriores del kernel Los manejadores binarios reconocen cualquier número mágico que se encuentre al comienzo de un fichero
26 formatos ejecutables También pueden realizar el reconocimiento del formato por alguna propiedad del nombre Por ejemplo, los binarios java se identifican por su extensión class o por su número mágico 0xCAFEBADE
27 formatos ejecutables El kernel 2.2 soporta los siguientes manejadores:a.out: antiguo formato nativo de linux. Está en desuso, pero aún se le proporciona soporte ELF: formato actual de los binarios Linux. A pesar de ser el formato nativo, se incluye un manejador para homogeneizar el núcleo, y hacerlo más sencillo
28 formatos ejecutables EM86: empleado para ejecutar binarios Linux en máquinas con arquitectura Alpha Misc: el manejador de este formato puede reconocer gran variedad de formatos por extensión, o por número mágico. La gran ventaja que tiene, es que es configurable en tiempo de ejecución, no solo de compilación, y por lo tanto no es necesario recompilar el núcleo para dar soporte a un nuevo formato. En el futuro, los handlers de Java y EM86 serán de este tipo
29 formatos ejecutables Scripts: utilizado para ejecutar scripts del shell. Trata los ficheros cuyos 2 primeros caracteres sean #!
30 Un ejemplo: java handlerEn fs/binfmt_java.c se encuentran las funciones encargadas de manejar los ficheros java, tanto ejecutables como applets do_execve Search_binary_handler do_load_java
31 java handler - do_load_javastatic int do_load_java(struct linux_binprm *bprm,struct pt_regs *regs) { unsigned char *ucp = (unsigned char *) bprm->buf; if ((ucp[0] != 0xca) || (ucp[1] != 0xfe) || (ucp[2] != 0xba) || (ucp[3] != 0xbe)) return -ENOEXEC; if (bprm->java) bprm->java = 1; dput(bprm->dentry); bprm->dentry = NULL; remove_arg_zero(bprm); len = strlen (bprm->filename); if (len >= 6 && !strcmp (bprm->filename + len - 6, ".class")) bprm->filename[len - 6] = 0; do_load_java: Hace todo lo necesario para la carga de ficheros .class de Java Comprueba el número mágico No se puede llamar recursivamente El fichero java debe terminar en “.class”
32 java handler - do_load_javaif ((i_name = strrchr (bprm->filename, '/')) != NULL) i_name++; else i_name = bprm->filename; bprm->p = copy_strings(1, &i_name, bprm->page, bprm->p, 2); bprm->argc++; i_name = binfmt_java_interpreter; if (!bprm->p) return -E2BIG; Copia el nombre del intérprete y del fichero .class a la zona de memoria del proceso, para posteriormente ejecutarlo
33 java handler - do_load_javabprm->filename = binfmt_java_interpreter; dentry = open_namei(binfmt_java_interpreter, 0, 0); retval = PTR_ERR(dentry); if (IS_ERR(dentry)) return retval; bprm->dentry = dentry; retval = prepare_binprm(bprm); if (retval < 0) return search_binary_handler(bprm,regs); } Se ejecuta el intérprete, como se hacía en do_execve: - Comprueba que exista el ejecutable del intérprete - Prepara los argumentos con prepare_binprm - Se busca el manejador para el intérprete (probablemente sea do_load_elf_binary)
34 java handler - load_javaDe cara al exterior, puede parecer que es la función que realiza la ejecución del .class, pero realmente solo llama a do_load_java static int load_java(struct linux_binprm *bprm,struct pt_regs *regs) { int retval; MOD_INC_USE_COUNT; retval = do_load_java(bprm,regs); MOD_DEC_USE_COUNT; return retval; }
35 FIN