Flujo de ejecución de una llamada al sistema en xv6 con RISC-V

Este documento analiza el mecanismo completo de una llamada al sistema (syscall) en el sistema operativo xv6, tomando como referencia la función write(). Se desrcibe el recorrido desde el espacio de usuario hasta el núcleo del sistema y el retorno.

Fase 1: Invocación desde el espacio de usuario

Cuando un programa llama a write(), se ejecuta una rutina ensamblador que prepara los registros y dispara el mecanismo de transición al modo supervisor.


#include "kernel/syscall.h"
write:
 li a7, SYS_write
 ecall
 ret

La constante SYS_write está definida en kernel/syscall.h con el valor 16. Este identificador numérico se carga en el registro a7 para indicar al kernel qué operación se solicita.

La instrucción ecall genera una excepción síncrona que provoca los siguientes efectos:

  1. El procesador cambia del modo usuario al modo supervisor.
  2. La dirección de retorno (valor del contador de programa) se almacena en el registro especail sepc.
  3. La ejecución salta a la dirección almacenada en el registro stvec (Supervisor Trap Vector).

En xv6, el registro stvec apunta al punto de entrada uservec dentro de trampoline.S. La instrucción ecall por sí sola es insuficiente: es necesario preservar los 31 registros de propósito general del proceso, conmutar la tabla de páginas del usuario a la del kernel, obtener una pila del kernel y transferir el control a la lógica de tratamiento de traps.


# Al ejecutar ecall, el tipo de syscall se indica en a7
# y los parámetros se colocan en los registros a0 a a5

Fase 2: Rutina uservec en trampoline.S

Esta rutina se encarga de salvar el estado completo del contexto del usuario antes de proseguir con la lógica del kernel.


uservec:
  # Se intercambia a0 con sscratch para poder usar a0 como puntero
  csrw sscratch, a0

  # Cargar dirección base del trapframe en a0
  li a0, TRAPFRAME

  # Persistir registros de propósito general en el trapframe
  sd ra, 40(a0)
  sd sp, 48(a0)
  sd gp, 56(a0)
  sd tp, 64(a0)
  ...

  # Recuperar el valor original de a0 desde sscratch y guardarlo
  csrr t0, sscratch
  sd t0, 112(a0)

  # Cargar la pila del kernel desde el trapframe
  ld sp, 8(a0)

  # Cargar el identificador del hart (CPU) actual
  ld tp, 32(a0)

  # Cargar dirección de la función usertrap()
  ld t0, 16(a0)

  # Cargar la tabla de páginas del kernel
  ld t1, 0(a0)

  # Barrera de memoria antes de cambiar la tabla de páginas
  sfence.vma zero, zero

  # Conmutar a la tabla de páginas del kernel escribiendo en satp
  csrw satp, t1
  sfence.vma zero, zero

  # Transferir control a usertrap()
  jr t0

Fase 3: Función usertrap() en trap.c

Una vez en el kernel, usertrap() determina la causa del trap y ejecuta el tratamiento adecuado.


void usertrap(void) {
  int dispositivo = 0;

  // Verificar que el trap provino del modo usuario
  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: trap no originado en modo usuario");

  // Redirigir stvec al manejador de traps del kernel
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();

  // Conservar el valor de sepc en el trapframe del proceso
  p->trapframe->epc = r_sepc();

  uint64 causa = r_scause();

  if(causa == 8){
    // Es una llamada al sistema

    if(killed(p))
      exit(-1);

    // Avanzar el PC a la instrucción siguiente a ecall
    p->trapframe->epc += 4;

    // Habilitar interrupciones durante el procesamiento
    intr_on();

    // Despachar la syscall según el código en a7
    syscall();
  } else if((dispositivo = devintr()) != 0){
    // Interrupción de dispositivo manejada
  } else {
    printf("usertrap(): scause inesperado %p pid=%d\n", causa, p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    setkilled(p);
  }

  if(killed(p))
    exit(-1);

  // Ceder la CPU si se trató de una interrupción de temporizador
  if(dispositivo == 2)
    yield();

  usertrapret();
}

¿Por qué guardar el valor de sepc en el trapframe? Durante la ejecución en el kernel, el planificador puede cambiar de proceso. Si otro proceso ejecuta su propia syscall, el registro sepc se sobrescribiría. Almacenar el valor en el trapframe garantiza que cada proceso pueda restaurar su propio punto de reanudación.

El campo scause es establecido automáticamente por el hardware cuando ocurre una excepción síncrona como ecall.

Fase 4: Preparación del retorno con usertrapret()

Antes de devolver el control al usuario, se restauran los parámetros necesarios para el próximo trap.


void usertrapret(void)
{
  struct proc *p = myproc();

  // Desactivar interrupciones mientras se reconfigura stvec
  intr_off();

  // Restaurar stvec para que apunte a uservec
  uint64 destino_uservec = TRAMPOLINE + (uservec - trampoline);
  w_stvec(destino_uservec);

  // Guardar información del kernel en el trapframe
  p->trapframe->kernel_satp = r_satp();
  p->trapframe->kernel_sp = p->kstack + PGSIZE;
  p->trapframe->kernel_trap = (uint64)usertrap;
  p->trapframe->kernel_hartid = r_tp();

  unsigned long sreg = r_sstatus();

  // Limpiar bit SPP para retornar a modo usuario con sret
  sreg &= ~SSTATUS_SPP;

  // Activar bit SPIE para que las interrupciones se habiliten tras sret
  sreg |= SSTATUS_SPIE;
  w_sstatus(sreg);

  // Restaurar la dirección de reanudación del usuario
  w_sepc(p->trapframe->epc);

  // Construir el valor de satp para la tabla de páginas del usuario
  uint64 satp_usuario = MAKE_SATP(p->pagetable);

  // Calcular la dirección de userret dentro del trampolín
  uint64 destino_userret = TRAMPOLINE + (userret - trampoline);

  // Invocar userret pasando la tabla de páginas como argumento
  ((void (*)(uint64))destino_userret)(satp_usuario);
}

Fase 5: Restauración final con userret()

La rutina ensamblador userret conmuta a la tabla de páginas del usuario, restaura todos los registros y ejecuta sret para volver al modo usuario.


userret:
  # a0 contiene la dirección de la tabla de páginas del usuario

  # Conmutar la tabla de páginas
  sfence.vma zero, zero
  csrw satp, a0
  sfence.vma zero, zero

  # Establecer a0 como puntero al trapframe
  li a0, TRAPFRAME

  # Restaurar registros de propósito general desde el trapframe
  ld ra, 40(a0)
  ld sp, 48(a0)
  ld gp, 56(a0)
  ld tp, 64(a0)
  ...

  # Restaurar el registro a0 original
  ld a0, 112(a0)

  # sret realiza tres acciones:
  # 1. Cambia el procesador a modo usuario
  # 2. Copia sepc al contador de programa (pc)
  # 3. Habilita las interrupciones si SPIE estaba activo
  sret

En este punto, la llamada al sistema ha concluido y el programa continúa su ejecución en el espacio de usuario justo después de la instrucción ecall.

Etiquetas: xv6 RISC-V syscalls ecall trampoline

Publicado el 6-14 18:05