GNU/Linux >> Tutoriales Linux >  >> Linux

Segmentación de memoria de Linux

Sí, Linux usa paginación para que todas las direcciones sean siempre virtuales. (Para acceder a la memoria en una dirección física conocida, Linux mantiene toda la memoria física asignada 1:1 a un rango de espacio de direcciones virtuales del kernel, por lo que simplemente puede indexar en esa "matriz" usando la dirección física como compensación. Complicaciones del módulo para 32 -bit kernels en sistemas con más RAM física que espacio de direcciones del kernel).

Este espacio de direcciones lineales constituido por páginas, se divide en cuatro segmentos

No, Linux usa un modelo de memoria plana. La base y el límite para los 4 de esos descriptores de segmento son 0 y -1 (ilimitado). es decir, todos se superponen por completo, cubriendo todo el espacio de direcciones lineales virtuales de 32 bits.

Entonces la parte roja consta de dos segmentos __KERNEL_CS y __KERNEL_DS

No, aquí es donde te equivocaste. los registros de segmento x86 no utilizado para la segmentación; son equipaje heredado x86 que solo se usa para el modo CPU y la selección de nivel de privilegio en x86-64 . En lugar de agregar nuevos mecanismos para eso y eliminar segmentos por completo para el modo largo, AMD simplemente neutralizó la segmentación en modo largo (la base se fijó en 0 como todos los que usan en modo de 32 bits de todos modos) y siguió usando segmentos solo para fines de configuración de la máquina que no son particularmente interesante a menos que realmente estés escribiendo código que cambia al modo de 32 bits o lo que sea.

(Excepto que puede establecer una base distinta de cero para FS y/o GS, y Linux lo hace para el almacenamiento local de subprocesos. Pero esto no tiene nada que ver con cómo copy_from_user() está implementado, o cualquier cosa. Solo tiene que comprobar ese valor de puntero, no con referencia a ningún segmento ni al CPL/RPL de un descriptor de segmento.)

En el modo heredado de 32 bits, es posible escribir un kernel que use un modelo de memoria segmentada, pero ninguno de los sistemas operativos convencionales hizo eso. Sin embargo, algunas personas desearían que eso se hubiera convertido en una cosa, p. vea esta respuesta lamentando que x86-64 haga imposible un sistema operativo de estilo Multics. Pero esto no cómo funciona Linux.

Linux es un https://wiki.osdev.org/Higher_Half_Kernel, donde los punteros del kernel tienen un rango de valores (la parte roja) y las direcciones de espacio de usuario están en la parte verde. El kernel puede simplemente desreferenciar direcciones de espacio de usuario si se asignan las tablas de páginas de espacio de usuario correctas, no necesita traducirlas ni hacer nada con los segmentos; esto es lo que significa tener un modelo de memoria plano . (El núcleo puede usar entradas de tabla de página de "usuario", pero no viceversa). Para x86-64 específicamente, consulte https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt para ver el mapa de memoria real.

La única razón por la que esas 4 entradas GDT deben estar separadas es por razones de nivel de privilegio y porque los descriptores de segmentos de datos frente a códigos tienen formatos diferentes. (Una entrada GDT contiene más que solo la base/límite; esas son las partes que deben ser diferentes. Consulte https://wiki.osdev.org/Global_Descriptor_Table)

Y especialmente https://wiki.osdev.org/Segmentation#Notes_Regarding_C, que describe cómo y por qué un sistema operativo "normal" suele utilizar la GDT para crear un modelo de memoria plana, con un par de códigos y descriptores de datos para cada nivel de privilegio. .

Para un kernel de Linux de 32 bits, solo gs obtiene una base distinta de cero para el almacenamiento local de subprocesos (por lo que los modos de direccionamiento como [gs: 0x10] accederá a una dirección lineal que depende del hilo que la ejecuta). O en un kernel de 64 bits (y un espacio de usuario de 64 bits), Linux usa fs . (Porque x86-64 hizo especial a GS con el swapgs instrucción, diseñada para usarse con syscall para que el núcleo encuentre la pila del núcleo.)

Pero de todos modos, la base distinta de cero para FS o GS no proviene de una entrada GDT, se configuran con el wrgsbase instrucción. (O en CPU que no lo admiten, con una escritura en un MSR).

pero ¿cuáles son esas banderas, a saber, 0xc09b , 0xa09b y así ? Tiendo a creer que son los selectores de segmentos

No, los selectores de segmentos son índices en la GDT. El núcleo está definiendo el GDT como una matriz C, utilizando la sintaxis de inicializador designado como [GDT_ENTRY_KERNEL32_CS] = initializer_for_that_selector .

(En realidad, los 2 bits inferiores de un selector, es decir, el valor del registro de segmento, son el nivel de privilegio actual. Por lo tanto, GDT_ENTRY_DEFAULT_USER_CS debería ser `__USER_CS>> 2.)

mov ds, eax activa el hardware para indexar la GDT, ¡no para realizar búsquedas lineales de datos coincidentes en la memoria!

Formato de datos GDT:

Está viendo el código fuente de Linux x86-64, por lo que el kernel estará en modo largo, no en modo protegido. Podemos saberlo porque hay entradas separadas para USER_CS y USER32_CS . El descriptor de segmento de código de 32 bits tendrá su L poco aclarado. La descripción del segmento CS actual es lo que pone una CPU x86-64 en modo de compatibilidad de 32 bits frente al modo largo de 64 bits. Para ingresar al espacio de usuario de 32 bits, un iret o sysret establecerá CS:RIP en un selector de segmento de 32 bits en modo de usuario.

yo creo también puede tener la CPU en modo de compatibilidad de 16 bits (como el modo de compatibilidad, no el modo real, pero el tamaño predeterminado del operando y el tamaño de la dirección son 16). Sin embargo, Linux no hace esto.

De todos modos, como se explica en https://wiki.osdev.org/Global_Descriptor_Table and Segmentation,

Cada descriptor de segmento contiene la siguiente información:

  • La dirección base del segmento
  • El tamaño de operación predeterminado en el segmento (16 bits/32 bits)
  • El nivel de privilegio del descriptor (Anillo 0 -> Anillo 3)
  • La granularidad (el límite de segmento está en unidades de byte/4kb)
  • El límite del segmento (la compensación legal máxima dentro del segmento)
  • La presencia del segmento (¿Está presente o no?)
  • El tipo de descriptor (0 =sistema; 1 =código/datos)
  • El tipo de segmento (Código/Datos/Lectura/Escritura/Accedido/Conforme/No conforme/Expandir hacia arriba/Expandir hacia abajo)

Estos son los bits adicionales. No estoy particularmente interesado en qué bits son cuáles porque (creo que) entiendo la imagen de alto nivel de para qué sirven las diferentes entradas GDT y qué hacen, sin entrar en los detalles de cómo se codifica realmente.

Pero si consulta los manuales x86 o la wiki de osdev, y las definiciones de esas macros de inicio, debería encontrar que dan como resultado una entrada GDT con el L bit establecido para segmentos de código de 64 bits, borrado para segmentos de código de 32 bits. Y, obviamente, el tipo (código frente a datos) y el nivel de privilegio difieren.


Descargo de responsabilidad

Estoy publicando esta respuesta para aclarar este tema de cualquier concepto erróneo (como lo señaló @PeterCordes).

Paginación

La gestión de la memoria en Linux (modo protegido x86) utiliza la paginación para asignar las direcciones físicas a un plano virtualizado. espacio de direcciones lineal, desde 0x00000000 a 0xFFFFFFFF (en 32 bits), conocido como modelo de memoria plana . Linux, junto con la MMU (Unidad de gestión de memoria) de la CPU, mantendrá todas las direcciones virtuales y lógicas asignadas 1:1 a la dirección física correspondiente. La memoria física suele dividirse en páginas de 4 KiB, para permitir una gestión más sencilla de la memoria.

Las direcciones virtuales del núcleo puede ser núcleo contiguo lógico direcciones asignadas directamente a páginas físicas contiguas; otras direcciones virtuales del kernel son totalmente direcciones virtuales mapeadas en páginas físicas no contiguas utilizadas para grandes asignaciones de búfer (que superan el área contigua en sistemas de memoria pequeña) y/o memoria PAE (solo 32 bits). Los puertos MMIO (E/S asignadas a la memoria) también se asignan mediante direcciones virtuales del kernel.

Cada dirección desreferenciada debe ser una dirección virtual. Ya sea que se trate de una dirección lógica o totalmente virtual, la RAM física y los puertos MMIO se asignan en el espacio de direcciones virtuales antes de su uso.

El kernel obtiene una parte de la memoria virtual usando kmalloc() , apuntado por una dirección virtual, pero más importante, que es también una dirección lógica del kernel, lo que significa que tiene un mapeo directo a contiguo páginas físicas (por lo tanto adecuado para DMA). Por otro lado, el vmalloc() la rutina devolverá una parte de totalmente memoria virtual, apuntada por una dirección virtual, pero solo contigua en el espacio de direcciones virtuales y asignada a páginas físicas no contiguas.

Las direcciones lógicas del núcleo utilizan una asignación fija entre el espacio de direcciones físico y virtual. Esto significa que las regiones virtualmente contiguas son, por naturaleza, también físicamente contiguas. Este no es el caso de las direcciones totalmente virtuales, que apuntan a páginas físicas no contiguas.

Las direcciones virtuales de los usuarios - a diferencia de las direcciones lógicas del núcleo - no utilice un mapeo fijo entre las direcciones virtuales y físicas, los procesos de la zona de usuario hacen un uso completo de la MMU:

  • Solo se asignan las partes utilizadas de la memoria física;
  • La memoria no es contigua;
  • La memoria puede intercambiarse;
  • La memoria se puede mover;

En más detalles, las páginas de memoria física de 4KiB se asignan a direcciones virtuales en la tabla de páginas del sistema operativo, cada asignación se conoce como PTE (entrada de tabla de página). La MMU de la CPU mantendrá un caché de cada PTE utilizado recientemente de la tabla de páginas del sistema operativo. Esta área de almacenamiento en caché se conoce como TLB (Translation Lookaside Buffer). El cr3 El registro se utiliza para ubicar la tabla de páginas del sistema operativo.

Siempre que sea necesario traducir una dirección virtual a una física, se buscará el TLB. Si se encuentra una coincidencia (TLB hit ), se devuelve y se accede a la dirección física. Sin embargo, si no hay ninguna coincidencia (TLB miss ), el controlador de fallas de TLB buscará en la tabla de páginas para ver si existe una asignación (página caminar ). Si existe, se vuelve a escribir en la TLB y se reinicia la instrucción de error, esta traducción posterior encontrará un hit de TLB. y el acceso a la memoria continuará. Esto se conoce como menor error de página.

A veces, el sistema operativo puede necesitar aumentar el tamaño de la memoria RAM física moviendo páginas al disco duro. Si una dirección virtual se resuelve en una página asignada en el disco duro, la página debe cargarse en la memoria RAM física antes de acceder a ella. Esto se conoce como major fallo de página. El controlador de fallas de la página del sistema operativo necesitará encontrar una página libre en la memoria.

El proceso de traducción puede fallar si no hay una asignación disponible para la dirección virtual, lo que significa que la dirección virtual no es válida. Esto se conoce como inválido excepción de error de página y segfault será emitido al proceso por el controlador de fallas de la página del sistema operativo.

Segmentación de memoria

Modo real

El modo real todavía usa un espacio de direcciones de memoria segmentado de 20 bits, con 1 MiB de memoria direccionable (0x00000 - 0xFFFFF ) y acceso de software directo ilimitado a toda la memoria direccionable, direcciones de bus, puertos PMIO (E/S asignadas a puertos) y hardware periférico. El modo real no ofrece protección de memoria , sin niveles de privilegio y sin direcciones virtualizadas. Por lo general, un registro de segmento contiene el valor del selector de segmento y el operando de memoria es un valor de desplazamiento relativo a la base del segmento.

Para evitar la segmentación (los compiladores de C generalmente solo admiten el modelo de memoria plana), los compiladores de C usaron el far no oficial tipo de puntero para representar una dirección física con un segment:offset notación de dirección lógica. Por ejemplo, la dirección lógica 0x5555:0x0005 , después de calcular 0x5555 * 16 + 0x0005 produce la dirección física de 20 bits 0x55555 , utilizable en un puntero lejano como se muestra a continuación:

char far    *ptr;           /* declare a far pointer */
ptr = (char far *)0x55555;  /* initialize a far pointer */

A día de hoy, la mayoría de las CPU x86 modernas siguen arrancando en modo real por motivos de compatibilidad con versiones anteriores y, a partir de entonces, cambian al modo protegido.

Modo protegido

En modo protegido, con el modelo de memoria plana , la segmentación no se utiliza . Los cuatro segmentos, a saber, __KERNEL_CS , __KERNEL_DS , __USER_CS , __USER_DS todos tienen sus direcciones base configuradas en 0. Estos segmentos son solo el equipaje heredado del modelo x86 anterior donde se usaba la administración de memoria segmentada. En el modo protegido, dado que todas las direcciones base de los segmentos se establecen en 0, las direcciones lógicas son equivalentes a direcciones lineales.

El modo protegido con el modelo de memoria plana significa que no hay segmentación. La única excepción en la que un segmento tiene su dirección base establecida en un valor distinto de 0 es cuando se trata de almacenamiento local de subprocesos. El FS (y GS en 64 bits) se utilizan registros de segmento para este fin.

Sin embargo, los registros de segmentos como SS (registro de segmento de pila), DS (registro de segmento de datos) o CS (registro de segmento de código) todavía están presentes y se utilizan para almacenar selectores de segmento de 16 bits , que contienen índices para segmentar descriptores en la LDT y GDT (tabla de descriptores locales y globales).

Cada instrucción que toca la memoria implícitamente utiliza un registro de segmento. Dependiendo del contexto, se usa un registro de segmento particular. Por ejemplo, el JMP la instrucción usa CS mientras que PUSH usa SS . Los selectores se pueden cargar en registros con instrucciones como MOV , siendo la única excepción el CS registro que solo es modificado por instrucciones que afectan el flujo de ejecución , como CALL o JMP .

El CS register es particularmente útil porque realiza un seguimiento del CPL (Nivel de privilegio actual) en su selector de segmento, conservando así el nivel de privilegio para el segmento actual. Este valor de CPL de 2 bits es siempre equivalente al nivel de privilegio actual de la CPU.

Protección de memoria

Paginación

El nivel de privilegio de la CPU, también conocido como bit de modo o anillo de protección , de 0 a 3, restringe algunas instrucciones que pueden subvertir el mecanismo de protección o causar caos si están permitidas en modo usuario, por lo que quedan reservadas al kernel. Un intento de ejecutarlos fuera del anillo 0 provoca una protección general excepción de falla, el mismo escenario cuando ocurre un error de acceso de segmento no válido (privilegio, tipo, límite, derechos de lectura/escritura). Del mismo modo, cualquier acceso a la memoria y a los dispositivos MMIO está restringido según el nivel de privilegio y cada intento de acceder a una página protegida sin el nivel de privilegio requerido provocará una excepción de fallo de página.

El bit de modo cambiará automáticamente del modo de usuario al modo de supervisor cada vez que se presente una solicitud de interrupción. (IRQ), ya sea software (es decir, syscall ) o hardware, ocurre.

En un sistema de 32 bits, solo se pueden direccionar de manera efectiva 4 GiB de memoria y la memoria se divide en un formato de 3 GiB/1 GiB. Linux (con la paginación habilitada) utiliza un esquema de protección conocido como medio núcleo superior donde el espacio de direccionamiento plano se divide en dos rangos de direcciones virtuales:

  • Direcciones en el rango 0xC0000000 - 0xFFFFFFFF son direcciones virtuales del núcleo (área roja). El rango de 896MiB 0xC0000000 - 0xF7FFFFFF mapea directamente las direcciones lógicas del kernel 1:1 con las direcciones físicas del kernel en el low-memory contiguo páginas (usando el __pa() y __va() macros). El rango restante de 128 MiB 0xF8000000 - 0xFFFFFFFF luego se utiliza para asignar direcciones virtuales para grandes asignaciones de búfer, puertos MMIO (E/S asignadas a la memoria) y/o memoria PAE en la alta memoria no contigua páginas (usando ioremap() y iounmap() ).

  • Direcciones en el rango 0x00000000 - 0xBFFFFFFF son direcciones virtuales de usuario (área verde), donde residen el código de usuario, los datos y las bibliotecas. La asignación puede estar en páginas no contiguas de memoria baja y memoria alta.

La memoria alta solo está presente en sistemas de 32 bits. Toda la memoria asignada con kmalloc() tiene un lógico dirección virtual (con un mapeo físico directo); memoria asignada por vmalloc() tiene un totalmente dirección virtual (pero sin mapeo físico directo). Los sistemas de 64 bits tienen una gran capacidad de direccionamiento, por lo que no necesitan mucha memoria, ya que cada página de RAM física se puede direccionar de manera efectiva.

El límite la dirección entre la mitad superior del supervisor y la mitad inferior del usuario se conoce como TASK_SIZE_MAX en el núcleo de Linux. El núcleo comprobará que todas las direcciones virtuales a las que se accede desde cualquier proceso de la zona de usuario residen por debajo de ese límite, como se ve en el siguiente código:

static int fault_in_kernel_space(unsigned long address)
{
    /*
     * On 64-bit systems, the vsyscall page is at an address above
     * TASK_SIZE_MAX, but is not considered part of the kernel
     * address space.
     */
    if (IS_ENABLED(CONFIG_X86_64) && is_vsyscall_vaddr(address))
        return false;

    return address >= TASK_SIZE_MAX;
}

Si un proceso de espacio de usuario intenta acceder a una dirección de memoria superior a TASK_SIZE_MAX , el do_kern_addr_fault() la rutina llamará al __bad_area_nosemaphore() rutina, eventualmente señalando la tarea fallida con un SIGSEGV (usando get_current() para obtener el task_struct ):

/*
 * To avoid leaking information about the kernel page table
 * layout, pretend that user-mode accesses to kernel addresses
 * are always protection faults.
 */
if (address >= TASK_SIZE_MAX)
    error_code |= X86_PF_PROT;

force_sig_fault(SIGSEGV, si_code, (void __user *)address, tsk); /* Kill the process */

Las páginas también tienen un bit de privilegio, conocido como U. Indicador ser/Supervisor, utilizado para SMAP (Prevención de acceso en modo supervisor) además de R Indicador de lectura/escritura que utiliza SMEP (Prevención de ejecución en modo supervisor).

Segmentación

Las arquitecturas más antiguas que usan segmentación generalmente realizan la verificación de acceso al segmento usando el bit de privilegio GDT para cada segmento solicitado. El bit de privilegio del segmento solicitado, conocido como DPL (Descriptor Privilege Level), se compara con el CPL del segmento actual, lo que garantiza que CPL <= DPL . Si es verdadero, se permite el acceso a la memoria al segmento solicitado.


Linux
  1. Linux – ¿Modo de teclado sin formato?

  2. Linux:¿un núcleo corrupto en Linux?

  3. Linux:¿los diferentes kernels de Linux/unix son intercambiables?

  4. Controlador de dispositivo del kernel de Linux a DMA desde un dispositivo a la memoria del espacio del usuario

  5. ¿Qué son la memoria alta y la memoria baja en Linux?

Comando Dmesg en Linux

Comando Modprobe en Linux

Comando Sysctl en Linux

¿Linux es un sistema operativo o un kernel?

Núcleo de Linux vs. Núcleo de Mac

Memoria inactiva de Linux