Docly Child

4.4.2. Sistemas Operativos: Definición y fundamentos de funcionamiento

Tabla de contenidos:

Fundamentos de Sistemas Operativos

La definición de Sistema Operativo (SO en adelante) puede realizarse por sus funciones principales. Entre estas funciones estarían:

Función de interfaz entre el usuario y la computadora: Actúa como intermediario entre la ejecución del software de los programas informáticos y los componentes de hardware. Entre los elementos de hardware que componen la computadora están:

    1. Procesador (CPU): Es la unidad central de procesamiento. Los procesadores funcionan mediante instrucciones en lenguaje de bajo nivel en lenguaje ensamblador (ya presentado en la introducción). Las instrucciones admitidas varían en función del tipo y marca del procesador.
    2. Memoria principal (RAM): Almacena información de los diferentes procesos así como también es la ubicación donde se cargan las instrucciones del software que está en ejecución.
    3. Dispositivos de Entrada/Salida (E/S): Son el conjunto de dispositivos que conforman la computadora. Entre ellos están los discos duros magnéticos, la tarjeta de red, la tarjeta gráfica, el teclado, el ratón, etc. Pueden ser usados por los programa en ejecución y para ello pueden recibir y enviar datos. Para su funcionamiento es fundamental el uso de controladores o drivers. Los controladores pueden considerarse como programas y verse afectados por vulnerabilidades de software.

Función de gestor de recursos: Gestiona los recursos de hardware y software a través de procesos que son ejecutados por estos últimos. La noción de proceso y otros para comprender como funciona el SO serían:

    1. Programa o aplicación informática: Está compuesto por una serie de instrucciones que deben ejecutarse en el procesador. Normalmente un programa se escribe en lenguaje de alto nivel (Java, Python, C++…) y se compila para generar unos ficheros que contienen las instrucciones en lenguaje ensamblador.
    2. Proceso: Un proceso es el código de un software ejecutándose. La ejecución del código no se efectúa de forma lineal sino que puede haber varios flujos asociados a este proceso (hilos o threads). El flujo genera una secuencia de datos y además consume unos determinados recursos que el SO operativo debe ser capaz de proveer. En las computadoras actuales se pueden ejecutar varios procesos a la vez.
    3. Fichero: Un fichero es una estructura de datos que almacena información (un conjunto de bytes). Un sistema operativo está en condiciones de tratar al fichero como una unidad lógica, interpretando y presentando la información correspondiente al usuario indicando el nombre, el tipo de fichero (extensión) y un modo de acceder a él.

A continuación un gráfico que relaciona estos elementos:

Funcionamiento de Sistema Operativo

Como puede verse en el gráfico, se menciona las utilidades (utilities). Las utilidades son un conjunto de características que definen los sistemas operativos. Estas características puede ampliarse en el material bibliográfico que se han presentado en el punto anterior pero entre estas cabe esperar de forma muy resumida:

  • Gestión de los recursos de forma eficiente indicando que procesos deben estar en ejecución.
  • Abstracción del hardware para permitir que las diferentes aplicaciones puedan hacer uso de los recursos. Es decir, que un mismo software puedan funcionar para diferentes elementos de hardware.
  • Interfaces de usuario final, creando un entorno para que este pueda interactuar con los diferentes recursos, siendo el más directo el abanico de aplicaciones y programas informáticos con los que interactúa el usuario.

En cuanto a los tipos (types) de SO existen diferentes clasificaciones según el número de aplicaciones (procesos) que es capaz de ejecutar simultáneamente y la forma en que se presta el servicio (servidor, sistema operativo web, ordenador personal, embebido…). Solo mencionar que la mayoría de SO implementados actualmente son multiprogramables, es decir, permiten la ejecución de varios procesos y se tienen varias aplicaciones cargadas a la vez en la memoria principal.

En lo que respecta a los componentes básicos (basic components) de un SO existe una clasificación académica que suele exponerse en la mayoría de cursos y que se corresponden con el núcleo del Sistema Operativo o kernel en inglés. Vinculadas con las funciones que se han presentado al principio de este texto y yendo desde la capa más física a la más abstracta:

  • Interfaz con el hardware: Permitir a los diferentes procesos hacer uso de los principales recursos de la computadora y los dispositivos de E/S.
  • Gestión de la memoria: Permite distribuir y gestionar el espacio de memoria principal (RAM) y memoria virtual para los procesos en ejecución.
  • Gestión de procesos: Incluye la planificación, mecanismos de comunicación entre procesos y mecanismos de sincronización.
  • Administración de ficheros: Permite la gestión de información que se encuentra en formatos de fichero en los dispositivos de almacenamiento del sistema (discos duros, etc.).
Componentes básicos de un SO

Además de estos componentes, el SO debe ofrecer un proceso de arranque del sistema (no se verá aquí como funciona, varía en función del SO), un sistema de creación y gestión de usuarios, almacenamiento de datos del sistema (hora, etc.) y una interfaz de usuario para iniciar los programas. Otro componente fundamental de los SO para llevar a cabo sus funciones son las llamadas al sistema. Para comprender bien que son las llamadas a sistema es preciso comprender otras nociones como son los modos de operación de la CPU, relación entre procesos y CPU, etc. (a continuación).

4.4.2.1. Modos de operación de la CPU: instrucciones, registros e interrupciones

Los procesadores (CPU) operan mediante lenguaje ensamblador. Como ya se ha comentado anteriormente, el lenguaje ensamblador indica las operaciones que debe realizar la CPU cuando se ejecuta un programa. El lenguaje ensamblador es el resultado del proceso de compilar un código de un software en lenguaje de alto nivel (Java, C++, etc.). Para compilar un programa hay que tener en cuenta el sistema operativo en el que se va ejecutar el programa y la marca y tipo de procesador (el compilador también es un programa que tiene en cuenta estos factores a la hora de efectuar sus tareas).

Así pues, para un mismo programa que utiliza un mismo código fuente en un determinado lenguaje de programación, el equivalente en ensamblador puede ser bastante diferente cuando se compila para ser ejecutado en Windows o Linux o en una CPU que opere con registros de 32 o 64 bits. En el siguiente ejemplo de código ensamblador se señalan dos elementos significativos que son las instrucciones y los registros:

Instrucciones y registros en lenguaje ensamblador

Una definición de cada elemento: 

  • Instrucciones: Las instrucciones son comandos específicos que se ejecutan en el procesador para realizar operaciones. Se utilizan mnemotécnicos como mov (move), xor (bitwise XOR), push (push onto stack), entre otros muchos más, para representar estas instrucciones. Cada instrucción realiza una operación específica, como mover datos entre registros y memoria (mov), realizar una operación xor lógica entre dos valores (xor) o empujar datos en la pila (push).
  • Registros: Son componentes del procesador que almacenan datos de forma temporal y se utilizan para realizar operaciones. Los que se muestran en el ejemplo: rax, rdi, rdx son registros de propósito general en la versión de 64 bits de la arquitectura x86_64. Se utilizan para almacenar datos, direcciones de memoria y resultados de operaciones.

El ejemplo de código ensamblador mostrado proviene de una función básica (escribir en pantalla Hola Mundo) en lenguaje de programación C++ para ejecutarse en Linux en una arquitectura de computadora de 64 bits o x86_64 desarrollada por la marca de creación de procesadores Intel. Pues bien, este código ensamblador puede ser muy diferente si se trata de otra marca de procesadores o arquitectura y lo define el fabricante. Es decir, el juego de instrucciones y operaciones para cada procesador lo define el fabricante.

Así por ejemplo cuando se habla de que un procesador es de 32 bits o 64 bits, se refiere en parte al tamaño de los registros (rax, rdi, rdx…) de propósito general que el procesador utiliza para manejar datos y realizar operaciones aritméticas. Un registro de 32 bits puede almacenar y procesar datos de hasta 32 bits de longitud en una sola operación, mientras que un registro de 64 bits puede manejar datos de hasta 64 bits en una sola operación. Así por ejemplo para la familia de procesadores de la marca Intel de 32 bits (x86) los registros eax, ebx, ecx tienen como equivalente en la arquitectura de 64 bits (x86_64) los registros rax, rbx, rcx.

4.4.2.1.1. CPU: Modo usuario vs modo privilegiado e interrupciones

Los fabricantes de procesadores además de definir un conjunto de instrucciones y registros para sus productos, también admiten al menos dos modos de funcionamiento para este hardware. Estos dos modos son: (NOTA: No confundir con los usuarios estándar o root, no tienen nada que ver)

  • Modo no privilegiado o modo usuario: El procesador ejecuta instrucciones que provienen de una aplicación estándard. Son instrucciones como las que se han visto anteriormente como mov, xor… a las que hay que añadir otras como add, sub, or, jmp, etc. Estas instrucciones representan secuencias que definen las funciones estándares de una aplicación. Representan un subconjunto del total de instrucciones permitidas por el procesador.
  • Modo privilegiado o modo supervisor: El procesador puede ejecutar todas las instrucciones en este modo, incluyendo las de modo usuario. Sin embargo en este modo destacan el conjunto de instrucciones que dan acceso a las funciones especiales para acceder a los recursos del sistema. Más adelante se va a definir este concepto, pero el modo privilegiado del procesador es el modo en que se ejecuta el núcleo del Sistema Operativo.

Nota: En esta sección no se muestran ejemplos de instrucciones en modo supervisor. En las prácticas de ingeniería inversa para descubrir vulnerabilidades se suele acudir a depuradores de código ensamblador para el software de aplicación y por lo tanto las instrucciones que habitualmente se examinan incluyen instrucciones para modo usuario.

El cambio de modo de usuario a supervisor en el procesador se efectúa mediante interrupciones. Explicar en detalle el funcionamiento de las interrupciones no es el objetivo de esta publicación y se puede ampliar en la bibliografía, sin embargo se anotan los siguientes puntos sobre las interrupciones:

  • Una interrupción definida en sentido amplio es un evento que se produce en el sistema informático de una computadora. En ella algún dispositivo de hardware (E/S) o proceso solicitan detener temporalmente la ejecución normal de un programa para solicitar atención o notificar una condición o evento importante al SO o al procesador.
  • Cuando se produce una interrupción, el procesador suspende temporalmente la ejecución del programa actual y pasa a ejecutar un código específico para manejar la interrupción. Este código puede ser proporcionado por el SO (núcleo) o por controladores de dispositivos de E/S (drivers). El SO dispone de suficientes elementos para tratar de forma eficiente las interrupciones como son una Tabla de Interrupciones y una Rutina de tratamiento o el Bloque de Control de Procesos para almacenar el estado de un proceso actual antes de cambiar de estado y atender la interrupción.
  • Existen diferentes tipos de interrupciones. Las interrupciones por software son aquí las más destacadas para la explicación dada hasta el momento y son aquellas que se producen cuando un proceso de una aplicación que se está ejecutando en modo usuario requiere de la intervención SO para una realizar una operación especial mediante una llamada al sistema. En este caso:
    1. El SO guarda el estado de ejecución del proceso.
    2. El procesador cambia a modo supervisor.
    3. Se ejecutan las instrucciones en modo privilegiado del Sistema Operativo en cuestión. En otras palabras, se transfiere el control al núcleo del SO.
    4. Se devuelve el control al proceso en cuestión y conmuta el estado del procesador a modo usuario.

De esta manera, las interrupciones se utilizan como mecanismo para permitir que los programas de usuario soliciten servicios del Sistema Operativo de manera controlada y segura sin riesgo de alterar a otros procesos.

Control de Interrupciones

4.4.2.2. Llamadas a sistema (System Calls)

Tal y como se ha explicado en el anterior punto una interrupción se puede producir por una llamada al sistema de un proceso que requiere acceder a unos determinados recursos que solo puede efectuar el Sistema Operativo en nombre del proceso con el modo privilegiado. Dentro del código ensamblador se pueden encontrar instrucciones referidas a una llamada al sistema como se verá a continuación.

Con las system call el SO proporciona a las aplicaciones un conjunto de abstracciones para hacer posible que el proceso pueda ejecutarse a través del hardware del sistema. En otras palabras, el SO ofrece una interfaz de programación (API) que la aplicación pueda usar en un momento determinado para solicitar recursos gestionados por el SO. A nivel académico las llamadas a sistema se clasifican por categorías asociadas a sus funciones (sin entrar en detalle, se pueden interpretar): gestión de procesos, mantenimiento de información, gestión de dispositivos, gestión de archivos

Llamadas al Sistema: Categorías estándares

Mediante las llamadas al sistema se accede al núcleo (kernel) del Sistema Operativo. Dos de las API para llamadas a sistema más estandarizadas son POSIX para los SO Unix/Linux y WIN32/WIN64 para los SO Windows. Para hacerse una idea, estas API proporcionan alrededor de unas 350 tipos de llamadas al sistema, cada una con sus funciones específicas. Algunas de las llamadas a sistema se pueden invocar a través de funciones en un lenguaje de programación de alto nivel como serían open, read, fork, nmap, time, etc. en lenguaje C/C++.

A continuación se muestra un ejemplo en lenguaje C++ con algunas funciones de llamadas a sistema. La función fork() se utiliza para crear un nuevo proceso. En el siguiente punto se verá, pero todos los procesos en un SO tienen un proceso padre y pueden tener procesos hijos. En el ejemplo, después de la llamada a fork(), el programa tiene dos procesos: el proceso padre y el proceso hijo. El proceso hijo imprime «Hola desde el proceso hijo!» y el proceso padre imprime «Hola desde el proceso padre!». El código también incluye un ejemplo de llamada al sistema execv(), que se utiliza para reemplazar la imagen del proceso actual con una nueva imagen. En el ejemplo, se llama a execv() para ejecutar el comando ls -l en el directorio /bin en un entorno Unix/Linux:

				
					#include <iostream>
#include <unistd.h>

int main() {
    // Ejemplo de llamada al sistema fork()
    pid_t pid = fork();

    if (pid == -1) {
        std::cerr << "Error en la llamada a fork()" << std::endl;
        return 1;
    } else if (pid == 0) {
        // Proceso hijo
        std::cout << "Hola desde el proceso hijo!" << std::endl;
    } else {
        // Proceso padre
        std::cout << "Hola desde el proceso padre!" << std::endl;
    }

    // Ejemplo de llamada al sistema exec()
    const char* programa = "/bin/ls";
    char* const argumentos[] = {"/bin/ls", "-l", nullptr};

    execv(programa, argumentos);
    std::cerr << "Error en la llamada a exec()" << std::endl;

    return 0;
}

				
			

Una parte del código anterior en lenguaje ensamblador para Unix/Linux en arquitectura x86_64 (64 bits) sería:

				
					section .data
    mensaje_hijo db "Hola desde el proceso hijo!", 0
    mensaje_padre db "Hola desde el proceso padre!", 0
    programa db "/bin/ls", 0
    argumentos db "/bin/ls", "-l", 0

section .text
    global _start

_start:
    ; Llamada al sistema fork()
    mov eax, 0x39     ; Número de la llamada al sistema fork()
    syscall

    cmp rax, -1       ; Comprobar si hubo un error en la llamada a fork()
    je error_fork

    test rax, rax
    jz proceso_hijo   ; Salta a la etiqueta proceso_hijo si el valor de rax es cero

proceso_padre:
    ; Escribir mensaje del proceso padre
    mov edi, 1        ; Descriptor de archivo (stdout)
    mov rsi, mensaje_padre
    mov edx, 25       ; Longitud del mensaje
    mov eax, 1        ; Número de la llamada al sistema write()
    syscall

    jmp fin_programa

				
			

Como puede apreciarse en esta parte del código ensamblador de la función de ejemplo, se puede observar que instrucciones determinan las llamadas a sistema y para qué tipo. Si se hubiera compilado para un arquitectura de 32 bits la instrucción para invocar la llamada al sistema sería int 0x80.

4.4.2.3. Núcleo (KERNEL) del Sistema Operativo

Aunque ya se podía intuir por lo descrito hasta ahora, es el momento de definir el concepto de kernel o núcleo del sistema operativo. El núcleo del Sistema Operativo es el código diseñado para que sea ejecutado mientras el procesador está en modo núcleo. En otras palabras: es la parte del código del SO que se ejecuta en modo privilegiado del procesador. El núcleo no debería considerarse un programa sino más bien una rutina de biblioteca, en la que uno o más procedimientos en ella ejecutan siguiendo una interrupción o llamada al sistema.

Núcleo del Sistema Operativo

En el siguiente punto se va a proponer una serie de prácticas para obtener información sobre la CPU y trazar la ejecución de una llamada al sistema de una aplicación. Además de ampliar la información respecto a los procesos, la memoria y el almacenamiento desde una vertiente práctica