A diario ejecutamos multitud de apps o programas de software. Pero ¿sabes cómo se ejecuta un programa realmente? Si sientes curiosidad y la respuesta a esta pregunta es negativa, te va a gustar este artículo donde explicaré todo lo que debes saber paso a paso y con todo detalle para que sepas qué ocurre…
Índice de contenidos
Para comprender mejor este artículo debes leer previamente estos otros:
- Partes de un programa
- Microarquitectura de CPU
- Tipos de microarquitectura
- ASM o ensamblador
- ¿Qué son las syscalls?
Compilación: binario ejecutable
El proceso de compilación de un código fuente en C consta de varias etapas, que incluyen la compilación, el enlace y la carga. A continuación, te explicaré brevemente cada una de estas etapas:
- Pre-procesador: es una fase del proceso de compilación en C (y en otros lenguajes de programación) que ocurre antes de la etapa de compilación propiamente dicha. Su función principal es realizar transformaciones en el código fuente original antes de que sea procesado por el compilador. El preprocesador se encarga de interpretar y manipular las directivas de preprocesador que comienzan con el carácter ‘#’ en el código fuente. Algunas de las directivas de preprocesador más comunes incluyen:
-
- Inclusión de archivos de encabezado: con la directiva #include, se puede insertar el contenido de un archivo de encabezado (header file) en el código fuente en el punto donde se encuentra la directiva. Los archivos de encabezado suelen contener declaraciones de funciones, definiciones de constantes y otras declaraciones compartidas por varios archivos fuente.
- Definición de constantes simbólicas: mediante la directiva #define, se pueden establecer constantes simbólicas que se reemplazarán por su valor correspondiente en todo el código fuente. Esto permite definir macros, como por ejemplo #define PI 3.14159, que sustituiría todas las apariciones de PI por el valor 3.14159.
- Condiciones de compilación condicional: con las directivas #ifdef, #ifndef, #if, #else y #endif, se pueden crear bloques de código que se compilan o excluyen según condiciones específicas. Por ejemplo, se puede utilizar #ifdef DEBUG para compilar cierto código solo si se ha definido previamente la macro DEBUG.
- Supresión de comentarios: mediante la directiva #pragma, se pueden utilizar opciones específicas del compilador para controlar el comportamiento del preprocesador. Por ejemplo, #pragma warning(disable: 1234) deshabilitaría el mensaje de advertencia con el código 1234 emitido por el compilador.
-
- Compilación: es la primera etapa del proceso realmente, y se encarga de traducir el código fuente escrito en lenguaje C a un código objeto en lenguaje de máquina. El compilador utilizado para este propósito se conoce como «compilador C». Durante la compilación, el compilador verifica la sintaxis y la semántica del código fuente y lo traduce a instrucciones en lenguaje de máquina que pueden ser entendidas y ejecutadas por la computadora. El resultado de esta etapa es un archivo objeto (por ejemplo, archivo.obj o archivo.o) que contiene código de máquina pero aún no es ejecutable. Es decir, hemos conseguido pasar de un código fuente hasta un binario comprensible por el hardware.
- Enlace: la etapa de enlace tiene lugar después de la compilación y es llevada a cabo por el «enlazador» o «linker». En esta etapa, se combinan varios archivos objeto, junto con cualquier biblioteca necesaria, para formar un programa ejecutable completo. El enlazador se encarga de resolver las referencias a funciones y variables externas, así como de asignar direcciones de memoria a las distintas partes del programa. El resultado de esta etapa es un archivo ejecutable (por ejemplo, archivo.exe) que puede ser ejecutado por el sistema operativo.
- Carga: la carga es la etapa final del proceso y se lleva a cabo por el «cargador» o «loader». El cargador se encarga de cargar el programa ejecutable en la memoria del sistema y prepararlo para su ejecución. Durante esta etapa, se asigna espacio en la memoria para las variables y las instrucciones del programa. También se resuelven las referencias simbólicas a direcciones de memoria y se configuran los registros y las estructuras de datos necesarias para ejecutar el programa. Una vez que el programa ha sido cargado en la memoria, el control se transfiere al sistema operativo para que comience la ejecución del programa.
Planificador del sistema operativo
En este artículo, explicamos cómo un sistema operativo carga y ejecuta un programa.
Antes de que un programa pueda ejecutarse, debe ser cargado en la memoria por una utilidad conocida como cargador de programas.
Después de cargarlo, el sistema operativo debe indicar a la CPU el punto de entrada del programa, que es la dirección desde la cual el programa comenzará a ejecutarse.
A continuación se muestra una lista de los pasos del proceso:
- El sistema operativo busca el nombre del programa en el directorio del disco actual. Si no encuentra el nombre allí, busca en una lista predeterminada de directorios (llamados rutas) el nombre del archivo. Si el sistema operativo no encuentra el nombre del programa, emite un mensaje de error.
- Si se encuentra el archivo del programa, el sistema operativo obtiene la información básica sobre el archivo del programa desde el directorio del disco, incluyendo el tamaño del archivo y su ubicación física en la unidad de disco.
- El sistema operativo determina la siguiente ubicación disponible en la memoria y carga el archivo binario del programa en la memoria. Asigna un bloque de memoria al programa y registra información sobre el tamaño y la ubicación del programa en una tabla (a veces llamada tabla de descriptores). Además, el sistema operativo puede ajustar los valores de los punteros dentro del programa para que contengan direcciones de datos del programa.
- El sistema operativo comienza la ejecución de la primera instrucción del programa (su punto de entrada). Tan pronto como el programa comienza a ejecutarse, se le llama proceso.
- El sistema operativo asigna al proceso un número de identificación (ID de proceso), que se utiliza para realizar un seguimiento mientras se ejecuta.
- El proceso se ejecuta por sí mismo. Es responsabilidad del sistema operativo realizar un seguimiento de la ejecución del proceso y responder a las solicitudes de recursos del sistema.
Cuando el proceso finaliza, se elimina de la memoria.
Ensamblador e ISA
Por otro lado, hay que entender que ese binario del programa que se ha creado no es más que una serie de datos e instrucciones que la CPU es capaz de entender. A las instrucciones se les denomina como mnemónicos en lenguaje ensamblador, y representan operaciones aritméticas o lógicas realizadas sobre los datos u operandos.
Por ejemplo, en la imagen anterior tenemos el código binario de una instrucción de la ISA MIPS32, es decir, la MIPS de 32-bit. Y, como ves, se compone de varios campos de unos y ceros. La primera parte (rojo) es el op-code, es decir, el código de operación, el que indica de qué operación aritmeticológica se trata, en este caso es una suma de enteros (addi). Luego tenemos una parte violeta que corresponde al primer registro que se emplea para almacenar el resultado de la operación, el segundo registro (r2) es el primer operando y 350 es una constante que se sumará al valor que contenga el registro r2, es decir, es lo que aparece en amarillo, el valor inmediato.
Si tenemos en cuenta que los programas están compuestos por muchas de estas instrucciones y datos, hay que decir que la CPU irá ejecutando estas instrucciones una a una de forma secuencial hasta completar la ejecución del programa. No obstante, esto no es del todo así en las modernas CPUs, ya que no ejecutan de forma secuencial, y es que para ganar rendimiento lo hacen de forma paralela, pero para entenderlo es mejor suponer cómo lo hacían las antiguas CPUs, que es más sencillo.
A nivel de hardware
A nivel de la CPU, cuando se ejecuta un programa, lo que sucede es lo siguiente:
- Fetch: el ciclo de ejecución comienza con la obtención de una instrucción desde la memoria principal o RAM. La instrucción en el contador de programa actual (PC) se obtendrá y se almacenará en el registro de instrucciones (IR) para que la CPU sepa dónde localizar esta primera instrucción que dará comienzo al programa.
- Decodificación: durante este ciclo, la instrucción codificada presente en el registro de instrucciones (IR) es interpretada por el decodificador. Es decir, la unidad de control, a través del microcódigo que tiene, traducirá la instrucción en microoperaciones o señales que determinarán qué es lo que las unidades funcionales o de ejecución deben hacer. Por ejemplo, si se trata de una instrucción suma se le envía una señal a la ALU para que se ponga en modo suma… También se determinará en este ciclo los operandos o datos sobre los que se operará y su localización.
- Ejecución: la Unidad Aritmético Lógica (ALU) es donde se realizan las operaciones entre dos operandos indicados por el operador en las instrucciones. Por ejemplo, si la instrucción es sumar dos números, aquí se llevará a cabo la suma. La ALU toma dos valores y produce uno de salida, que es el resultado de la operación.
- Acceso a la memoria: hay solo dos tipos de instrucciones que acceden a la memoria: LOAD (cargar) y STORE (almacenar). LOAD copia un valor desde la memoria a un registro, mientras que STORE copia un valor de un registro a la memoria. Otras instrucciones omiten este paso.
- Actualizar el archivo de registros: en este paso, el resultado/salida de la ALU se escribe de vuelta en el archivo de registros para actualizarlo. El resultado también puede ser el resultado de una carga desde la memoria. Algunas instrucciones no tienen resultados para almacenar. Por ejemplo, las instrucciones BRANCH (salto) y JUMP (salto incondicional) no tienen resultados para almacenar.
- Actualizar el contador de programa (PC): finalmente, al finalizar la ejecución de la instrucción actual, debemos actualizar el registro contador de programa (PC) a la dirección de la próxima instrucción, de modo que podamos volver al paso 1 donde la CPU obtendrá la siguiente instrucción. Esto se consigue sumando +1 a la dirección actual del PC, hasta terminar la secuencia de instrucciones del programa. Sin embargo, el contador de programa puede necesitar establecerse en una dirección de memoria diferente a la siguiente si la instrucción fue una instrucción BRANCH o JUMP, aunque no entraré en esto para no complicarlo todo más…
Ahora ya sabes cómo se ejecuta un programa, no olvides comentar con tus dudas o sugerencias…