CAPÍTULO 16 · PARTE II

Kernel, llamadas al sistema, excepciones e interrupciones

Cómo el kernel media la entrada privilegiada, cómo una llamada al sistema difiere de una excepción y cómo las interrupciones asíncronas sostienen dispositivos, temporizadores y replanificación.

Nivel N2–N3 · Estado published

Una frontera que cambia quién puede decidir

Un proceso en espacio de usuario puede pedir abrir un archivo, enviar un paquete o crear otro proceso, pero no debería escribir arbitrariamente en la memoria del kernel ni programar directamente el controlador de un disco. La pregunta importante no es sólo qué instrucción ejecuta la CPU. Es quién obtiene autoridad para interpretar la petición, sobre qué estado, con qué identidad y durante cuánto tiempo.

El kernel es la parte privilegiada del sistema operativo que media recursos y ofrece servicios a los procesos. «Privilegiada» no significa que sea correcta por definición: significa que la arquitectura le permite ejecutar operaciones que un proceso ordinario no puede ejecutar y acceder a estructuras protegidas. Una llamada al sistema (system call) es la interfaz controlada mediante la cual un programa solicita una operación del kernel. Una excepción es un evento síncrono asociado a la ejecución de una instrucción. Una interrupción es una notificación asíncrona, habitualmente originada por hardware, que desvía temporalmente la ejecución.

Estas rutas se parecen porque todas pueden guardar un contexto y transferir control a código privilegiado, pero no son intercambiables. Una llamada expresa una solicitud; una excepción informa de una condición ligada a la instrucción; una interrupción anuncia un evento externo. Confundirlas oculta la causa y dificulta razonar sobre autorización y latencia.

El capítulo usa x86-64 sobre Linux como caso técnico. La secuencia conceptual sirve para comparar implementaciones, pero una tabla de vectores, una instrucción de entrada o un contexto de interrupción deben atribuirse a la plataforma concreta.

Qué protege la frontera del kernel

Los capítulos anteriores distinguieron privilegios, direcciones y procesos. Aquí se conectan esas ideas. El procesador ejecuta instrucciones con un contexto que incluye contador de programa, registros, estado de interrupciones y, según la arquitectura, nivel de privilegio. El sistema operativo configura qué entradas pueden provocar una transición y qué código recibe cada una. En x86, el Interrupt Descriptor Table (IDT, tabla de descriptores de interrupción) relaciona vectores con puertas de entrada; en otros procesadores existen mecanismos equivalentes, pero no necesariamente la misma tabla.

El kernel no es una función ordinaria que el proceso pueda invocar con una llamada de lenguaje. La transición cambia el contexto autorizado y conserva estado para volver al punto correcto. El código de entrada establece un marco de registros antes de pasar a C de bajo nivel. Linux kernel, «Entry/exit handling for exceptions, interrupts, syscalls and KVM», sección «Syscall entry»

Este mecanismo limita una clase de abuso, no elimina la necesidad de comprobaciones. El kernel debe tratar la memoria, los números de recursos, los descriptores y las credenciales recibidos desde usuario como entradas no confiables. Si una ruta privilegiada acepta una dirección de usuario como si fuese un puntero propio, puede leer datos equivocados, provocar un fallo o escribir fuera del objeto previsto. Si comprueba una condición y usa el objeto después de que otro hilo lo cambie, aparece una carrera. El privilegio amplifica tanto los errores de validación como sus consecuencias.

Del wrapper a la system call

Desde C, read(fd, buffer, count) parece una función ordinaria. En Linux, la mayoría de las llamadas se invocan mediante wrappers de la biblioteca C; el wrapper adapta argumentos, coloca el número de llamada donde exige el ABI (Application Binary Interface) y transforma un error del kernel en el contrato que observa el programa. La página syscalls(2) señala que la system call es la interfaz fundamental entre aplicación y kernel, pero que normalmente no se invoca directamente. Linux man-pages, syscalls(2), «System calls and library wrapper functions»

El recorrido conceptual de una llamada es el siguiente:

  1. El programa construye argumentos en su espacio de direcciones y llama a una función de biblioteca.
  2. El wrapper prepara el número de operación y los argumentos conforme al ABI de la arquitectura. En x86-64 Linux, la instrucción de entrada habitual es syscall; otra arquitectura puede usar otra instrucción o convención.
  3. La CPU transfiere control a una entrada privilegiada y conserva el estado necesario para continuar después.
  4. El código de entrada del kernel salva y normaliza registros, identifica la llamada y aplica comprobaciones de arquitectura y de seguridad.
  5. La ruta concreta valida los argumentos y precondiciones que exige su contrato. Según la llamada, esto puede incluir descriptores, longitudes, punteros de usuario, credenciales o estado del recurso; no todas las syscalls reciben ni vuelven a comprobar cada categoría. Cuando transfiere datos entre dominios, usa primitivas apropiadas y no trata un puntero de usuario como una dirección interna ya confiable.
  6. La operación puede terminar de inmediato, devolver un error o quedar bloqueada esperando un recurso. Si se bloquea, el scheduler puede ejecutar otro proceso antes de que la llamada continúe.
  7. El kernel escribe un resultado en el marco de retorno, restaura el contexto y vuelve a usuario. La biblioteca traduce el contrato de error, por ejemplo estableciendo errno y devolviendo -1.

No toda interfaz pasa siempre por una entrada al kernel: algunas bibliotecas usan mecanismos como vDSO para operaciones concretas o hacen trabajo adicional. Una system call define un punto de entrada; una función con el mismo nombre no prueba por sí sola que cada invocación haya ejecutado una transición privilegiada.

El uso directo de syscall(2) hace visible la dependencia del ABI: recibe un número y argumentos según la arquitectura, y deja los errores en errno. Linux man-pages, syscall(2), «Description» y «Architecture-specific requirements» Esto ayuda a entender la frontera, pero no convierte el número en una API estable entre arquitecturas o versiones. La interfaz documentada incluye argumentos, estados, errores y garantías; la tabla interna de dispatch es un detalle de implementación.

El ejemplo de read no es una copia de memoria

Supongamos un proceso que pide read sobre un descriptor de archivo. La autoridad para operar sobre el objeto se obtuvo normalmente cuando el proceso abrió, recibió o heredó ese descriptor; read no repite de manera universal toda la autorización basada en pathname. En esta llamada el kernel resuelve el descriptor vigente, comprueba que admita lectura y valida el count y el rango de memoria de usuario necesario para la transferencia. Si el objeto es un archivo regular, puede obtener datos de la caché o iniciar I/O; si es un pipe o socket, el proceso puede quedar dormido hasta que otro productor entregue datos. Cambios de política, credenciales y tipo de objeto pueden introducir reglas adicionales, por lo que la evidencia debe identificar la ruta real.

Cuando llegan bytes, el kernel copia sólo la cantidad que el contrato permite y devuelve un resultado que el programa debe interpretar. Un retorno menor que la longitud solicitada puede ser correcto; no autoriza al programa a asumir que recibió el mensaje completo. Un error puede indicar una condición temporal, una interrupción por señal o un fallo permanente. La system call media tres fronteras a la vez: autoridad sobre el descriptor, memoria del proceso y estado del dispositivo o del objeto lógico.

El error frecuente es describir read como «la CPU trae bytes del disco». El dispositivo, el controlador, la caché, la memoria y el scheduler pueden intervenir. La propiedad de seguridad está en la combinación de validación de argumentos, permisos, aislamiento, estado del objeto y controles del driver.

Recorrido de read desde wrapper y ABI hasta entrada al kernel, validación concreta y retorno de bytes o error.
Una system call cruza una frontera con contrato de entrada, validación y retorno.

El descriptor ya representa autoridad sobre un objeto abierto; read no vuelve a autorizar universalmente el pathname original.

Bloqueo, espera y retorno diferido

Una llamada al sistema puede tener una semántica bloqueante. «Bloquear» aquí significa que el proceso deja de ser elegible para ejecutar hasta que ocurra una condición, no que la CPU quede detenida. El kernel registra una espera, puede cambiar el estado del proceso y entrega el procesador a otra tarea. Cuando el evento despierta la espera, la ruta reanuda la operación o devuelve un resultado que obliga a la aplicación a reintentar.

Este detalle cambia el análisis de una autorización. La comprobación inicial de permisos no convierte en atómico todo lo que ocurre durante una llamada larga. Entre comprobar un objeto y usarlo pueden cambiar el descriptor, el montaje, la identidad efectiva o el estado del recurso. Las interfaces diseñadas para ser atómicas reducen ciertas ventanas, pero el programa debe leer el contrato concreto. Un timeout tampoco prueba que el kernel haya cancelado toda actividad subyacente: sólo describe lo que esa interfaz garantiza al solicitante.

Las señales pueden interrumpir una espera. POSIX contempla entrega al proceso o al hilo; Linux documenta disposiciones por proceso, máscaras por hilo y acciones por defecto, ignoradas o manejadas. The Open Group, POSIX Base Specifications Issue 8, «Signal Concepts» Linux man-pages, signal(7), «Signal dispositions» Una llamada puede devolver EINTR, reiniciarse o terminar con resultado parcial según el contrato; una señal no cancela siempre la llamada.

Excepciones: el evento pertenece al flujo de instrucciones

Una excepción es síncrona: la CPU puede asociarla con la instrucción que estaba ejecutando. División por cero, instrucción inválida, breakpoint y ciertos fallos de acceso a memoria son ejemplos de familias distintas. En x86, el procesador clasifica excepciones según si la instrucción puede reiniciarse, si ya produjo efectos y si el estado guardado permite reanudar. El manual de Intel dedica su guía de programación de sistema al entorno de soporte del sistema operativo, incluida la gestión de interrupciones y excepciones. Intel, 64 and IA-32 Architectures Software Developer’s Manual, Vol. 3A, revisión 093, §6

Una distinción útil es:

La terminología y la clasificación exactas dependen de la arquitectura. Lo importante para seguridad es la causalidad: una excepción no es un mensaje arbitrario de otro proceso, sino una entrada generada por el estado de ejecución. El handler debe conservar un marco coherente, decidir si la condición pertenece a usuario o kernel y evitar interpretar datos no validados como autoridad.

Page fault: el mismo vector, conclusiones diferentes

Un acceso de usuario a una página que no está mapeada puede producir un page fault recuperable. El kernel examina la dirección, los permisos y el estado de la región; puede cargar una página, materializar una página compartida o concluir que el acceso no está permitido. En el último caso, entrega una señal al proceso o termina su ejecución según la política. Un SIGSEGV observado por la aplicación no identifica por sí solo un exploit: también puede ser un puntero nulo, una región sin permiso o una incompatibilidad de ABI.

La misma clase de excepción durante ejecución de kernel es distinta. Si el kernel toca una dirección que no puede corregir, el sistema puede registrar el contexto y detener esa ruta o el sistema completo. Por eso el diagnóstico necesita el modo de privilegio, la instrucción, la dirección, el estado de mapas y la procedencia del contexto. «Hubo un page fault» es una observación; «un atacante ejecutó código» es una inferencia que exige evidencia adicional.

Interrupciones: el mundo exterior reclama atención

Una interrupción es asíncrona respecto de la instrucción que ejecutaba el procesador. Un controlador de red puede señalar que un paquete llegó, un temporizador puede indicar que venció un intervalo o un CPU puede recibir una petición de trabajo de otro CPU. El dispositivo no elige arbitrariamente una función de usuario: activa una línea o mensaje que el controlador de interrupciones traduce a un vector. El kernel instala el handler y decide qué trabajo puede hacerse en ese contexto.

En Linux, la capa genérica de IRQ abstrae distintos controladores y modos de disparo. La documentación describe flujos para interrupciones por nivel, por flanco, fast-EOI, simples y por CPU, y muestra que el handler puede reconocer la fuente, atenderla y enviar el fin de interrupción al chip cuando corresponde. Linux kernel, «Linux generic IRQ handling», «High-level IRQ flow handlers» La forma concreta de acknowledge, mask, unmask y EOI pertenece al controlador; no debe presentarse como una secuencia universal de todas las máquinas.

El trabajo en contexto hardirq debe ser corto y respetar restricciones fuertes: no puede dormir como si fuese un proceso ordinario y debe proteger los datos que comparte con el resto del kernel. Por eso un driver suele hacer un trabajo mínimo —leer el estado del dispositivo, confirmar la fuente, capturar un descriptor o marcar trabajo pendiente— y diferir operaciones costosas a un threaded interrupt, softirq, workqueue u otro contexto permitido. La división no es estética. Reduce el tiempo durante el cual se bloquean otras entradas y hace explícito qué estado debe sincronizarse.

Un error frecuente es confundir «interrupción recibida» con «paquete procesado por la aplicación». La IRQ puede despertar una cola, el stack de red puede procesar el paquete más tarde y la aplicación puede no leerlo nunca. De forma análoga, un timer interrupt puede hacer posible la contabilidad de tiempo o la replanificación sin ser la causa completa de un cambio de proceso. La interrupción inicia una ruta; no describe por sí sola toda la política posterior.

Las interrupciones pueden compartir una línea y llegar mientras un handler anterior aún trabaja. La documentación de IRQ advierte que un flujo por flanco puede registrar eventos pendientes y repetir el manejo hasta vaciarlos. El driver debe distinguir su dispositivo de una interrupción espuria y ordenar inicialización, reconocimiento y habilitación para no perder eventos. Un bug en ese orden puede producir pérdida de disponibilidad sin que exista una vulnerabilidad explotable por red.

Los NMI (non-maskable interrupts) y excepciones similares exigen más cautela porque pueden aparecer en contextos donde el código normal no puede asumir sus invariantes. Linux indica que machine checks, double faults y debug interrupts pueden impactar cualquier contexto y que el tratamiento cambia según ocurra en usuario o kernel. Linux kernel, «Entry/exit handling», «NMI and NMI-like exceptions» NMI no significa «más importante en todos los sentidos» ni es una vía ordinaria para servicios de usuario; describe restricciones de enmascaramiento y de contexto.

Cadena conceptual desde IRQ y trabajo hardirq mínimo a trabajo diferido, cola del subsistema y proceso esperando; la IRQ no despierta siempre una aplicación.
La IRQ inicia una ruta; el trabajo diferido y las colas median lo que observa el proceso.

La secuencia es conceptual: EOI, tipo de disparo y contexto diferido dependen del controlador y del subsistema.

Señales: representación de alto nivel, no sinónimo de IRQ

Una señal es una notificación que el kernel entrega a un proceso o hilo. Puede originarse en una excepción de usuario, en un timer, en otro proceso que usa kill(2) o en una condición del kernel. Al entregar una señal, Linux puede aplicar la disposición por defecto, ignorarla o invocar un handler instalado por el programa; SIGKILL y SIGSTOP son excepciones que no pueden capturarse ni ignorarse. La señal no conserva necesariamente la causalidad completa de la entrada de hardware: SIGIO, SIGSEGV o SIGTERM son interfaces de proceso con semánticas distintas.

Las señales estándar pendientes no son una cola de eventos idéntica a una secuencia de IRQ; las señales de tiempo real tienen reglas de orden y cola diferentes. La disposición es por proceso, mientras que la máscara y ciertos estados de entrega se relacionan con el hilo. Un handler que modifica estructuras compartidas sin respetar funciones async-signal-safe puede introducir corrupción incluso si la señal se originó legítimamente. Para diagnosticar se deben registrar señal, hilo, máscara, punto de entrega y relación con la llamada interrumpida.

Así se separan tres niveles: la CPU puede generar una excepción; el kernel puede traducirla a una señal; la aplicación puede observar un handler o un retorno con error. Cada transición pierde o transforma información. El profesional no debe saltar desde la señal visible a una causa única sin examinar el contexto de kernel y los registros preservados.

Comparación de system call, excepción, IRQ y señal por actor, sincronía y semántica; SIGKILL y SIGSTOP no pueden capturarse ni ignorarse.
Las cuatro entradas tienen causalidades distintas aunque compartan una frontera privilegiada.

Una excepción puede terminar representada como señal, pero esa transformación no conserva toda la causa original.

El retorno también es una decisión del kernel

Salir del kernel no consiste sólo en ejecutar una instrucción inversa. Antes del retorno, el kernel puede procesar señales pendientes, comprobar si debe replanificar, restaurar registros y seleccionar si vuelve al mismo proceso, a otro hilo o a otro nivel de privilegio. Una interrupción puede haber actualizado contadores, despertado tareas o marcado trabajo diferido; una system call puede haber cambiado credenciales, descriptores o mapas de memoria.

Este punto es relevante para telemetría. Un log de entrada a openat no demuestra que el archivo se haya abierto; un contador de IRQ no prueba que el driver consumiera el evento; un SIGSEGV no identifica la causa raíz. La evidencia debe conservar el enlace temporal entre entrada, decisión y retorno, sin tratar los eventos como una única transacción atómica.

Un recorrido completo: llegada de datos a un proceso

Consideremos un servicio que espera datos en un socket. La aplicación llama a una función de biblioteca que termina solicitando una operación de lectura. El kernel valida el descriptor, la dirección de destino y la autorización del proceso. Si no hay datos, el hilo queda esperando; no mantiene la CPU ocupada.

Más tarde, el adaptador de red recibe una trama y comunica el evento mediante una IRQ. El controlador reconoce que la interrupción pertenece al dispositivo, captura el estado mínimo y agenda trabajo posterior. El stack de red valida y clasifica los datos, actualiza la cola del socket y despierta a los hilos que esperan. El scheduler decide cuándo volver a ejecutar el servicio. La ruta de system call reanuda, copia los datos permitidos a la memoria de usuario y retorna una cantidad de bytes o un error.

Este ejemplo reúne cuatro hechos que no deben colapsarse. La llamada al sistema es una petición explícita del proceso. La IRQ es un evento externo. El trabajo diferido es una decisión de diseño del kernel y el driver. El retorno de read es el contrato observable, no una prueba de que la red sea confiable ni de que el contenido de aplicación sea válido. Una pérdida en cualquiera de las colas puede degradar disponibilidad; una validación defectuosa en la copia puede afectar aislamiento; una política de socket puede rechazar correctamente los datos aun cuando el hardware funcione.

Qué revisar profesionalmente en una frontera de entrada

Al revisar una system call o un handler de interrupción, conviene formular preguntas concretas:

Los controles no se evalúan por presencia nominal. Un filtro de system calls puede reducir superficie, pero no reemplaza la autorización interna. Deshabilitar una IRQ puede evitar una tormenta temporal y también perder eventos. Registrar el vector aporta trazabilidad, no demuestra que el handler aplicara el acknowledge correcto. La conclusión debe unir mecanismo, precondición, cobertura y límite.

Errores de modelo que conviene descartar

«Toda llamada de biblioteca es una system call». Hay wrappers, funciones puramente de usuario y rutas como vDSO. Se debe distinguir la API de biblioteca de la entrada privilegiada que finalmente usa.

«Excepción significa ataque». Las excepciones son eventos de CPU; muchas proceden de errores de programa, paginación legítima o depuración. La intención adversaria requiere evidencia independiente.

«Interrupción es lo mismo que una señal». Una IRQ entra en el kernel desde hardware o controlador; una señal es una semántica de proceso que puede ser consecuencia, causa o una ruta independiente.

«El handler puede hacer cualquier cosa porque ya está en kernel». El contexto de hardirq restringe sueño, locks y acceso a recursos. El privilegio no elimina las reglas de concurrencia.

«Volver de la system call vuelve al mismo estado». La llamada puede cambiar memoria, descriptores, credenciales, señales pendientes y planificación. El retorno restaura un contexto compatible, no un estado idéntico.

Síntesis

El kernel convierte peticiones y eventos de bajo nivel en decisiones sobre recursos, procesos y dispositivos. Una system call es una frontera de servicio invocada por software, con un ABI, validación y un contrato de resultados. Una excepción es síncrona y pertenece a la ejecución de una instrucción; puede recuperarse, transformarse en señal o revelar un fallo no recuperable. Una interrupción es asíncrona y reclama atención desde el exterior del flujo actual; su handler debe reconocer la fuente, respetar el contexto y diferir el trabajo que no cabe en una entrada inmediata.

La misma arquitectura de entrada sirve para contener privilegio y para crear superficie de ataque. La seguridad depende de que el kernel preserve aislamiento, valide datos y sincronice estados; la disponibilidad depende también de colas, latencias, afinidad, recuperación y drivers. Ningún contador de system calls, vector de excepción o IRQ aislado explica por sí mismo el comportamiento del sistema. La explicación profesional sigue la causalidad completa: qué inició la transición, qué contexto se guardó, qué autoridad se comprobó, qué trabajo ocurrió y qué evidencia justifica el resultado.

Comprobación de comprensión

  1. ¿Qué diferencia semántica existe entre la función de biblioteca read y la system call que puede ejecutar detrás?
  2. Reconstruye los pasos que cruzan una petición desde un proceso hasta el dispatch del kernel e identifica dos validaciones que el contrato de esa llamada exige.
  3. Compara una fault de página recuperable con un SIGSEGV que termina el proceso. ¿Qué observaciones adicionales necesitas para adjudicar la causa?
  4. ¿Por qué una IRQ de red no demuestra que una aplicación haya recibido un mensaje completo?
  5. Explica por qué el trabajo de un handler hardirq suele dividirse entre una parte inmediata y otra diferida.
  6. Un proceso recibe EINTR durante una lectura. ¿Qué afirmaciones son válidas y cuáles requieren consultar el contrato concreto de la llamada?
  7. Distingue excepción, señal e interrupción usando un ejemplo en el que una excepción termine siendo observable como señal.
  8. En una revisión de driver, el contador de IRQ aumenta pero la cola de aplicación permanece vacía. Propón hipótesis ordenadas por la cadena causal y la evidencia que las separaría.

Respuestas modelo y límites

  1. Respuesta: El wrapper prepara número y argumentos según el ABI; la entrada conserva el marco; read resuelve el descriptor ya abierto, valida el buffer y count que su contrato exige, puede esperar o devolver progreso parcial, y la biblioteca adapta el resultado y errno. Límite: no toda llamada valida todas esas categorías ni reautoriza el pathname.
  2. Respuesta: La system call es una petición explícita de software; la excepción es síncrona con el estado o instrucción; la IRQ es asíncrona y normalmente proviene de hardware/controlador. Límite: comparten mecanismos de entrada, pero no causa ni contrato.
  3. Respuesta: SIGSEGV permite afirmar una señal asociada a una condición de acceso inválido bajo la política observada. Límite: no prueba explotación ni causa raíz sin instrucción, mapas, permisos y contexto.
  4. Respuesta: hardirq tiene restricciones de sueño y sincronización; reconocer la fuente y diferir lo costoso reduce esa ventana. Límite: la división concreta depende del driver y del contexto permitido.
  5. Respuesta: Hay que consultar contrato de llamada, disposición/máscara, progreso parcial y política de reinicio; EINTR no implica siempre cero bytes ni reintento incondicional. Límite: no se decide sólo por el nombre del error.
  6. Respuesta: Revisaría reconocimiento del driver, colas/trabajo diferido y cola del socket/despertar/retorno, correlacionando trazas y errores. Límite: un contador de IRQ no adjudica por sí solo la causa.
  7. Respuesta: El número y registros dependen de arquitectura y ABI; las tablas de compatibilidad suelen conservar números existentes y las versiones agregan llamadas, pero no hay una cifra portable aislada. Límite: la API es el contrato documentado, no el dispatch interno.
  8. Respuesta: Compararía inicialización y registro, reconocimiento/limpieza, tipo de disparo y EOI, afinidad/CPU y trabajo diferido. Límite: sólo con esa evidencia se separan configuración, hardware y carrera de apagado.

Problema de transferencia

Un servicio aislado en un contenedor muestra muchos EFAULT al escribir respuestas, un aumento de IRQ del adaptador virtual y varios reinicios del proceso. Formula un plan de análisis que mantenga separadas la entrada de system call, la validación de memoria, la entrega de interrupciones, la señal o excepción observada y la decisión del supervisor de reiniciar. Para cada hipótesis indica qué registro, métrica o prueba la apoyaría y qué conclusión seguiría fuera de alcance.

Fuentes principales