Guía Hardware

Cómo funciona un programa: ejecución paso a paso

Actualizado a: 19 de enero de 2024

Saber cómo funciona un programa o software es fundamental para conocer qué es lo que ocurre en un ordenador cuando estás ejecutando uno. Por eso, en este tutorial vamos a ver todo lo que deberías conocer al respecto.

Quizás también te interese conocer:

Antes de comenzar, vamos a ver algunos términos y conceptos que deberías conocer…

¿Qué es un software o programa?

Un programa o software es un conjunto de instrucciones y datos que se ejecutan en un ordenador o dispositivo electrónico para realizar una tarea específica. Puede ser cualquier tipo de aplicación, desde programas de procesamiento de texto y hojas de cálculo hasta aplicaciones de diseño gráfico, juegos, navegadores web, hasta software de sistema como los propios sistemas operativos y mucho más.

En realidad, estos programas no son más que algoritmos para realizar alguna función y que se escriben usando un lenguaje de programación para su descripción. Luego se compilan para pasarlos a binario o lenguaje máquina, para que sean comprensibles por la CPU. Por ejemplo, aquí tenemos un pseudocódigo del algoritmo de un programa simple para sumar dos números y el código fuente en C:

#include <stdio.h>

int main() {
   int num1, num2, suma;

   printf("Introduce el primer número: ");
   scanf("%d", &num1);

   printf("Introduce el segundo número: ");
   scanf("%d", &num2);

   suma = num1 + num2;

   printf("La suma de los dos números es: %d\n", suma);

   return 0;
}

El código fuente es un conjunto de instrucciones escritas por un programador utilizando un lenguaje de programación. Representa el texto legible por los humanos que define cómo se debe comportar un programa o software en particular. El código fuente es la base para crear un programa ejecutable, ya sea mediante su compilación o interpretación, y contiene las instrucciones que indican al ordenador qué tareas debe realizar. Es el medio a través del cual los programadores expresan su lógica y algoritmos para resolver problemas y crear aplicaciones y sistemas de software.

1. Solicitar al usuario que introduzca el primer número.
2. Leer el primer número desde la entrada estándar y guardarlo en una variable.
3. Solicitar al usuario que introduzca el segundo número.
4. Leer el segundo número desde la entrada estándar y guardarlo en otra variable.
5. Sumar los dos números y guardar el resultado en una variable.
6. Imprimir el resultado de la suma en la salida estándar.
7. Finalizar el programa.

Mientras que el ensamblador (usando mnemónicos de las instrucciones de la ISA de la CPU, en este caso de x86) sería algo así:

section .data
    num1 db 0
    num2 db 0
    suma db 0

section .text
    global _start

_start:
    ; Leer el primer número
    mov eax, 3
    mov ebx, 0
    mov ecx, num1
    mov edx, 2
    int 0x80

    ; Leer el segundo número
    mov eax, 3
    mov ebx, 0
    mov ecx, num2
    mov edx, 2
    int 0x80

    ; Sumar los números
    mov al, [num1]
    add al, [num2]
    mov [suma], al

    ; Mostrar el resultado
    mov eax, 4
    mov ebx, 1
    mov ecx, suma
    mov edx, 1
    int 0x80

    ; Salir del programa
    mov eax, 1
    xor ebx, ebx
    int 0x80

Y el código máquina en hexadecimal y con cada instrucción representada en binario:

; Código máquina en hexadecimal correspondiente al ensamblador x86

section .data
    num1 db 0
    num2 db 0
    suma db 0

section .text
    global _start

_start:
    ; Leer el primer número
    b8 03 00 00 00       ; mov eax, 3
    bb 00 00 00 00       ; mov ebx, 0
    ba num1 00 00 00     ; mov ecx, num1
    ba 02 00 00 00       ; mov edx, 2
    cd 80                ; int 0x80

    ; Leer el segundo número
    b8 03 00 00 00       ; mov eax, 3
    bb 00 00 00 00       ; mov ebx, 0
    ba num2 00 00 00     ; mov ecx, num2
    ba 02 00 00 00       ; mov edx, 2
    cd 80                ; int 0x80

    ; Sumar los números
    8a 04 0e             ; mov al, [num1]
    02 04 16             ; add al, [num2]
    88 05 suma 00 00 00  ; mov [suma], al

    ; Mostrar el resultado
    b8 04 00 00 00       ; mov eax, 4
    bb 01 00 00 00       ; mov ebx, 1
    ba suma 00 00 00     ; mov ecx, suma
    ba 01 00 00 00       ; mov edx, 1
    cd 80                ; int 0x80

    ; Salir del programa
    b8 01 00 00 00       ; mov eax, 1
    31 db                ; xor ebx, ebx
    cd 80                ; int 0x80

Lenguajes de programación

Un lenguaje de programación es un conjunto de reglas y símbolos utilizados para escribir instrucciones que un ordenador puede entender y ejecutar. Sirve como una herramienta para que los programadores puedan comunicarse con los ordenadores y crear software y aplicaciones. Los lenguajes de programación proporcionan estructuras, sintaxis y semántica específicas para expresar algoritmos y soluciones a problemas, permitiendo la creación de programas que automatizan tareas y realizan diversas funciones. Cada lenguaje de programación tiene sus propias características y propósitos, y los programadores eligen el lenguaje adecuado según las necesidades del proyecto.

Tipos de lenguajes de programación

Los lenguajes de programación se pueden clasificar en dos categorías principales: alto nivel y bajo nivel:

  • Lenguajes de alto nivel: están diseñados para ser más fáciles de entender y de utilizar por los programadores, ya que utilizan una sintaxis más cercana al lenguaje humano. Proporcionan abstracciones de alto nivel y suelen contar con bibliotecas y frameworks que facilitan el desarrollo de aplicaciones. Ejemplos de lenguajes de alto nivel incluyen Python, Java, C++, C#, Ruby y JavaScript.
  • Lenguajes de bajo nivel: están más cerca del lenguaje máquina y son más difíciles de entender y de utilizar directamente por los programadores. Proporcionan un mayor control sobre el hardware y permiten una optimización más precisa del rendimiento. Ejemplos de lenguajes de bajo nivel incluyen el lenguaje ensamblador (o ASM) y el lenguaje de máquina.

Por otro lado, podemos tener:

  • Lenguajes compilados: requieren de un compilador que traduce el código fuente escrito por el programador a un código de bajo nivel que la máquina puede ejecutar directamente. El código compilado resultante suele ser más rápido en la ejecución, pero el proceso de compilación debe realizarse antes de poder ejecutar el programa. Ejemplos de lenguajes compilados son C, C++, Rust y Go.
  • Lenguajes interpretados: no requieren de un proceso de compilación previo. Utilizan un intérprete que lee y ejecuta el código fuente línea por línea en tiempo real. Esto permite una mayor flexibilidad y facilita el desarrollo y la depuración, pero puede resultar en una ejecución más lenta en comparación con los lenguajes compilados. Ejemplos de lenguajes interpretados son Python, JavaScript y Ruby.

A nivel de compilación: del código fuente al binario ejecutable

La compilación de un código fuente en C implica una serie de etapas que permiten transformar el código legible para los programadores en un programa ejecutable. Estas etapas incluyen la compilación, el enlace y la carga. Veamos qué implica cada una de ellas:

  1. El preprocesador es una etapa clave en el proceso de compilación de C y otros lenguajes de programación. Se lleva a cabo antes de la compilación y tiene la tarea de realizar modificaciones en el código fuente original para prepararlo antes de que sea procesado por el compilador. El preprocesador interpreta y maneja las directivas de preprocesador que se encuentran en el código fuente y comienzan con el símbolo ‘#’. Estas directivas permiten realizar diversas acciones, como la inclusión de archivos de encabezado, la definición de constantes simbólicas mediante macros, la condicionalización del código en función de condiciones específicas y la eliminación de comentarios. Algunas de las directivas de preprocesador más utilizadas incluyen ‘#include’ para incluir archivos de encabezado, ‘#define’ para definir macros, ‘#ifdef’ y ‘#ifndef’ para realizar condicionalización del código y ‘#pragma’ para controlar el comportamiento del compilador.
  2. Compilación: es la etapa inicial y esencial del proceso de traducción del código fuente escrito en lenguaje C a un formato ejecutable en lenguaje de máquina. En esta fase, se utiliza un compilador específico para el lenguaje C, conocido como «compilador C». Durante el proceso de compilación, el compilador verifica la estructura sintáctica y semántica del código fuente y lo convierte en instrucciones en lenguaje de máquina que pueden ser interpretadas y ejecutadas por el hardware del ordenador. El resultado de la compilación es la generación de un archivo objeto, por ejemplo, con extensión «.obj» o «.o», que contiene el código de máquina correspondiente al programa, pero aún no es ejecutable por sí mismo. En otras palabras, se ha logrado transformar el código fuente en un formato binario comprensible por el hardware del ordenador, sentando las bases para las etapas posteriores del proceso de compilación.
  3. Enlace: la etapa de enlace es la siguiente fase del proceso de compilación y es llevada a cabo por el «enlazador» o «linker». En esta etapa, se combinan múltiples archivos objeto, junto con las bibliotecas necesarias, para formar un programa ejecutable completo. El enlazador se encarga de resolver las referencias a funciones y variables externas, y asignar direcciones de memoria a las diferentes partes del programa. Durante el enlace, se establecen las conexiones necesarias entre los diferentes módulos del programa y se resuelven las dependencias entre ellos. Esto incluye la resolución de llamadas a funciones externas y la asignación de direcciones de memoria para las variables utilizadas en el programa. El resultado de esta etapa es la generación de un archivo ejecutable, por ejemplo, con extensión «.exe», que contiene toda la información necesaria para que el programa sea ejecutado correctamente por el sistema operativo. Este archivo ejecutable puede ser ejecutado directamente por el sistema operativo para llevar a cabo las instrucciones y funcionalidades definidas en el código fuente original.
  4. Carga: la etapa final del proceso es llevada a cabo por el «cargador» o «loader». Su función es cargar el programa ejecutable en la memoria del sistema y prepararlo para su ejecución. Durante esta etapa, se reserva espacio en la memoria para almacenar las variables y las instrucciones del programa. Además, se resuelven las referencias simbólicas a direcciones de memoria y se configuran los registros y estructuras de datos necesarios para la ejecución del programa. Una vez que el programa ha sido cargado en la memoria, el control es transferido al sistema operativo para iniciar la ejecución del programa. Es en este punto donde el programa comienza a funcionar y llevar a cabo las operaciones y tareas definidas en el código fuente original.

A nivel de sistema operativo: planificador

En este artículo, abordaremos el proceso de carga y ejecución de un programa por parte de un sistema operativo. Antes de que un programa pueda ser ejecutado, se requiere que sea cargado en la memoria. Esta tarea es llevada a cabo por una herramienta llamada cargador de programas.

Una vez que el programa ha sido cargado en la memoria, el sistema operativo debe informar a la CPU sobre el punto de entrada del programa. Este punto de entrada es la dirección desde la cual el programa iniciará su ejecución.

A continuación se muestra una lista de los pasos del proceso de ejecución desde el lado del sistema operativo:

  1. El sistema operativo realiza una búsqueda del nombre del programa en el directorio actual del disco. En caso de no encontrarlo, busca en una serie de directorios predeterminados conocidos como rutas. Si el nombre del programa no es encontrado, se muestra un mensaje de error.
  2. Si el archivo del programa es encontrado, el sistema operativo obtiene información básica sobre el archivo desde el directorio del disco, como su tamaño y ubicación física en la unidad de disco.
  3. A continuación, el sistema operativo busca un espacio disponible en la memoria y carga el archivo binario del programa en dicho espacio. Se asigna un bloque de memoria al programa y se registra información sobre su tamaño y ubicación en una tabla de descriptores. Además, el sistema operativo puede ajustar los punteros dentro del programa para que contengan las direcciones de datos correspondientes.
  4. Una vez cargado, el sistema operativo inicia la ejecución del programa a partir de su primera instrucción, conocida como punto de entrada. En este momento, el programa se convierte en un proceso en ejecución.
  5. El sistema operativo asigna un número de identificación único, conocido como ID de proceso, al nuevo proceso para realizar su seguimiento mientras se ejecuta.
  6. El proceso se ejecuta de forma independiente. Es responsabilidad del sistema operativo supervisar la ejecución del proceso y responder a las solicitudes de recursos del sistema.
  7. Cuando el proceso finaliza su ejecución, se elimina de la memoria para liberar el espacio ocupado.

Entre el software y el hardware: ISA

Es importante comprender que el binario del programa generado es una representación de datos e instrucciones que la CPU puede interpretar. Estas instrucciones, conocidas como mnemónicos en lenguaje ensamblador, representan operaciones aritméticas o lógicas que se realizan en los datos u operandos.

Por ejemplo, en el código binario 11101010101011100101011100010100, corresponde a una instrucción cualquiera de una arquitectura de 32 bits. Está compuesta por diferentes campos de unos y ceros. Por ejemplo, imagina que los ocho primeros bits, 11101010, corresponden al op-code, que indica la operación aritmética específica, que podría ser la instrucción MOV de movimiento de datos. Luego, el resto de bits indicarían el origen y el destino del movimiento, es decir, las direcciones de registros entre los que se tiene que hacer el movimiento de bits.

Considerando que los programas consisten en múltiples instrucciones y datos, la CPU ejecutará estas instrucciones de forma secuencial hasta completar la ejecución del programa. Sin embargo, en las CPUs modernas, esto no se realiza de manera estrictamente secuencial, ya que se emplean técnicas de paralelismo para mejorar el rendimiento. Para una comprensión más sencilla, es útil pensar en cómo las CPUs antiguas ejecutaban las instrucciones de forma secuencial.

A nivel de hardware: CPU

A nivel de la CPU, cuando se ejecuta un programa, sucede lo siguiente:

  1. Fetch: En el ciclo de ejecución, la CPU obtiene una instrucción de la memoria principal o RAM. La instrucción actual en el contador de programa (PC) se recupera y se almacena en el registro de instrucciones (IR) para que la CPU sepa dónde encontrar la primera instrucción que dará inicio al programa.
  2. Decodificación: Durante este ciclo, el decodificador interpreta la instrucción codificada presente en el registro de instrucciones (IR). La unidad de control, a través de su microcódigo, traduce la instrucción en microoperaciones o señales que indicarán qué acciones deben realizar las unidades funcionales o de ejecución. Por ejemplo, si la instrucción es una suma, se envía una señal a la ALU para que se configure en modo de suma. También se determinan los operandos o datos sobre los que se operará y su ubicación.
  3. Ejecución: En esta etapa, la Unidad Aritmético Lógica (ALU) realiza las operaciones entre los operandos indicados por la instrucción. Por ejemplo, si la instrucción es una suma, la ALU llevará a cabo la operación de suma. La ALU toma dos valores de entrada y produce un resultado de salida, que es el resultado de la operación.
  4. Acceso a la memoria: Solo hay 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 desde un registro a la memoria. Otras instrucciones no requieren este paso.
  5. Actualización del archivo de registros: En esta etapa, el resultado o salida de la ALU se escribe nuevamente en el archivo de registros para actualizarlo. El resultado también puede ser el resultado de una carga desde la memoria. Algunas instrucciones no generan un resultado para almacenar. Por ejemplo, las instrucciones de BRANCH (salto) y JUMP (salto incondicional) no tienen un resultado para almacenar.
  6. Actualización del contador de programa (PC): Finalmente, al terminar la ejecución de la instrucción actual, se actualiza el contador de programa (PC) con la dirección de la próxima instrucción. Esto se logra incrementando en uno la dirección actual del PC, lo que nos permite regresar al paso 1 para obtener la siguiente instrucción. Sin embargo, el contador de programa puede establecerse en una dirección de memoria diferente a la siguiente instrucción si la instrucción anterior fue un salto condicional o incondicional. No entraré en más detalles para evitar complicaciones adicionales.

Jaime Herrera

Jaime Herrera

Técnico electrónico y experto en el sector de los semiconductores y el hardware. Con una amplia y sólida trayectoria en el campo de la electrónica, he acumulado una extensa experiencia. Mi pasión por la tecnología y la informática me ha impulsado a dedicar décadas de mi vida al estudio y desarrollo de soluciones en este fascinante sector. Como técnico electrónico, he tenido el privilegio de trabajar en una variedad de proyectos y desafíos, lo que me ha permitido adquirir un profundo conocimiento y experiencia en la creación, diseño y mantenimiento de dispositivos electrónicos.

>
Guía Hardware
Logo