linux kernel - ¿Por qué Linux en x86 usa diferentes segmentos para los procesos de usuario y el kernel?
linux-kernel memory-segmentation (4)
La arquitectura de administración de memoria x86 utiliza tanto la segmentación como la paginación. En términos generales, un segmento es una partición del espacio de direcciones de un proceso que tiene su propia política de protección. Por lo tanto, en la arquitectura x86, es posible dividir el rango de direcciones de memoria que un proceso ve en múltiples segmentos contiguos, y asignar diferentes modos de protección a cada uno. La paginación es una técnica para asignar regiones pequeñas (generalmente 4KB) del espacio de direcciones de un proceso a trozos de memoria física real. Por lo tanto, la paginación controla cómo las regiones dentro de un segmento se asignan a la RAM física.
Todos los procesos tienen dos segmentos:
un segmento (direcciones 0x00000000 a 0xBFFFFFFF) para datos específicos del proceso a nivel de usuario, como el código del programa, datos estáticos, montón y pila. Cada proceso tiene su propio segmento de usuario independiente.
un segmento (direcciones 0xC0000000 a 0xFFFFFFFF), que contiene datos específicos del kernel como las instrucciones del kernel, datos, algunas pilas en las que se puede ejecutar el código del kernel y, lo que es más interesante, una región en este segmento se asigna directamente a la memoria física, de modo que El kernel puede acceder directamente a las ubicaciones de memoria física sin tener que preocuparse por la traducción de direcciones. El mismo segmento de kernel se asigna a cada proceso, pero los procesos solo pueden acceder a él cuando se ejecutan en modo de kernel protegido.
Por lo tanto, en el modo de usuario, el proceso solo puede acceder a direcciones inferiores a 0xC0000000; Cualquier acceso a una dirección más alta que esto resulta en una falla. Sin embargo, cuando un proceso de modo de usuario comienza a ejecutarse en el kernel (por ejemplo, después de haber realizado una llamada al sistema), el bit de protección en la CPU se cambia al modo de supervisor (y se cambian algunos registros de segmentación), lo que significa que el proceso es por lo tanto, es capaz de acceder a direcciones por encima de 0xC0000000.
Referido desde: HERE
Por lo tanto, sé que Linux utiliza cuatro segmentos predeterminados para un procesador x86 (código de kernel, datos de kernel, código de usuario, datos de usuario), pero todos tienen la misma base y límite (0x00000000 y 0xfffff), lo que significa que cada segmento se asigna a la misma Conjunto de direcciones lineales.
Dado esto, ¿por qué incluso tener segmentos de usuario / núcleo? Entiendo por qué debería haber segmentos separados para el código y los datos (solo debido a cómo el procesador x86 se ocupa de los registros cs y ds), pero ¿por qué no tener un solo segmento de código y un solo segmento de datos? La protección de la memoria se realiza a través de la paginación, y los segmentos de usuario y kernel se asignan a las mismas direcciones lineales de todos modos.
La arquitectura x86 asocia un tipo y un nivel de privilegio con cada descriptor de segmento. El tipo de descriptor permite que los segmentos sean de solo lectura, lectura / escritura, ejecutables, etc., pero la razón principal para que diferentes segmentos tengan la misma base y límite es permitir que se use un nivel de privilegio de descriptor (DPL) diferente.
El DPL es de dos bits, lo que permite que los valores de 0 a 3 se codifiquen. Cuando el nivel de privilegio es 0, se dice que suena 0 , que es el más privilegiado. Los descriptores de segmento para el kernel de Linux son anillo 0 mientras que los descriptores de segmento para espacio de usuario son anillo 3 (menos privilegiados). Esto es cierto para la mayoría de los sistemas operativos segmentados; el núcleo del sistema operativo es el anillo 0 y el resto es el anillo 3.
El kernel de Linux configura, como usted mencionó, cuatro segmentos:
- __KERNEL_CS (segmento de código de Kernel, base = 0, límite = 4GB, tipo = 10, DPL = 0)
- __KERNEL_DS (segmento de datos del núcleo, base = 0, límite = 4GB, tipo = 2, DPL = 0)
- __USER_CS (segmento de código de usuario, base = 0, límite = 4GB, tipo = 10, DPL = 3)
- __USER_DS (segmento de datos del usuario, base = 0, límite = 4GB, tipo = 2, DPL = 3)
La base y el límite de los cuatro son iguales, pero los segmentos del núcleo son DPL 0, los segmentos de usuario son DPL 3, los segmentos de código son ejecutables y legibles (no grabables), y los segmentos de datos son legibles y grabables (no ejecutables) .
Ver también:
La memoria del núcleo no debe ser legible desde los programas que se ejecutan en el espacio de usuario.
Los datos del programa a menudo no son ejecutables (DEP, una característica del procesador, que ayuda a evitar la ejecución de un búfer desbordado y otros ataques maliciosos).
Se trata del control de acceso: los diferentes segmentos tienen diferentes derechos. Es por eso que acceder al segmento incorrecto le dará un "fallo de segmentación".
en X86 - los registros de segmento de Linux se utilizan para la comprobación de desbordamiento del búfer [vea el fragmento de código a continuación que ha definido algunas matrices de caracteres en la pila]:
static void
printint(int xx, int base, int sgn)
{
char digits[] = "0123456789ABCDEF";
char buf[16];
int i, neg;
uint x;
neg = 0;
if(sgn && xx < 0){
neg = 1;
x = -xx;
} else {
x = xx;
}
i = 0;
do{
buf[i++] = digits[x % base];
}while((x /= base) != 0);
if(neg)
buf[i++] = ''-'';
while(--i >= 0)
my_putc(buf[i]);
}
Ahora, si vemos el desmontaje del código generado por gcc.
Volcado del código del ensamblador para la función printint:
0x00000000004005a6 <+0>: push %rbp
0x00000000004005a7 <+1>: mov %rsp,%rbp
0x00000000004005aa <+4>: sub $0x50,%rsp
0x00000000004005ae <+8>: mov %edi,-0x44(%rbp)
0x00000000004005b1 <+11>: mov %esi,-0x48(%rbp)
0x00000000004005b4 <+14>: mov %edx,-0x4c(%rbp)
0x00000000004005b7 <+17>: mov %fs:0x28,%rax ------> obtaining an 8 byte guard from based on a fixed offset from fs segment register [from the descriptor base in the corresponding gdt entry]
0x00000000004005c0 <+26>: mov %rax,-0x8(%rbp) -----> pushing it as the first local variable on to stack
0x00000000004005c4 <+30>: xor %eax,%eax
0x00000000004005c6 <+32>: movl $0x33323130,-0x20(%rbp)
0x00000000004005cd <+39>: movl $0x37363534,-0x1c(%rbp)
0x00000000004005d4 <+46>: movl $0x42413938,-0x18(%rbp)
0x00000000004005db <+53>: movl $0x46454443,-0x14(%rbp)
...
...
// function end
0x0000000000400686 <+224>: jns 0x40066a <printint+196>
0x0000000000400688 <+226>: mov -0x8(%rbp),%rax -------> verifying if the stack was smashed
0x000000000040068c <+230>: xor %fs:0x28,%rax --> checking the value on stack is matching the original one based on fs
0x0000000000400695 <+239>: je 0x40069c <printint+246>
0x0000000000400697 <+241>: callq 0x400460 <__stack_chk_fail@plt>
0x000000000040069c <+246>: leaveq
0x000000000040069d <+247>: retq
Ahora, si eliminamos las matrices basadas en pila de esta función, gcc no generará esta comprobación de protección.
He visto lo mismo generado por gcc incluso para los módulos del kernel. Básicamente, estaba viendo un fallo al bloquear un código del kernel y estaba fallando con la dirección virtual 0x28. Más tarde, pensé que creía haber inicializado correctamente el puntero de la pila y haber cargado el programa correctamente, no tengo las entradas correctas en gdt, lo que traduciría el desplazamiento basado en fs en una dirección virtual válida.
Sin embargo, en el caso del código del kernel simplemente se estaba ignorando, el error en lugar de saltar a algo como __stack_chk_fail @ plt>.
La opción del compilador relevante que agrega esta protección en gcc es -fstack-protector. Creo que esto está habilitado por defecto para compilar una aplicación de usuario.
Para el kernel, podemos habilitar este indicador gcc a través de la opción de configuración CC_STACKPROTECTOR.
config CC_STACKPROTECTOR 699 bool "Enable -fstack-protector buffer overflow detection (EXPERIMENTAL)" 700 depends on SUPERH32 701 help 702 This option turns on the -fstack-protector GCC feature. This 703 feature puts, at the beginning of functions, a canary value on 704 the stack just before the return address, and validates 705 the value just before actually returning. Stack based buffer 706 overflows (that need to overwrite this return address) now also 707 overwrite the canary, which gets detected and the attack is then 708 neutralized via a kernel panic. 709 710 This feature requires gcc version 4.2 or above.
El archivo del kernel relevante donde este gs / fs es linux / arch / x86 / include / asm / stackprotector.h