La dirección de inicio generalmente se establece mediante un script de vinculación.
Por ejemplo, en GNU/Linux, mirando /usr/lib/ldscripts/elf_x86_64.x
vemos:
...
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); \
. = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
El valor 0x400000
es el valor predeterminado para el SEGMENT_START()
funcionan en esta plataforma.
Puede obtener más información sobre las secuencias de comandos del enlazador consultando el manual del enlazador:
% info ld Scripts
ld
La secuencia de comandos del enlazador predeterminado tiene ese 0x400000
valor integrado para ejecutables no PIE.
Los PIE (ejecutables independientes de la posición) no tienen una dirección base predeterminada; siempre son reubicados por el kernel, con el kernel's el valor predeterminado es 0x0000555...
más alguna compensación de ASLR a menos que ASLR esté deshabilitado para este proceso o en todo el sistema. ld
no tiene control sobre esto. Tenga en cuenta que la mayoría de los sistemas modernos configuran GCC para usar -fPIE -pie
por defecto, pasa -pie
a ld
, y convierte C en asm que es independiente de la posición. El asm escrito a mano debe seguir las mismas reglas si lo vincula de esa manera.
Pero qué hace 0x400000
(4 MiB) ¿un buen valor predeterminado?
Tiene que estar por encima de mmap_min_addr
=65536 =64K por defecto.
Y estar bastante lejos de 0 da mucho más espacio para protegerse contra NULL deref con una lectura compensada .text
o .data
/.bss
memoria (array[i]
donde array
es nulo). Incluso sin aumentar mmap_min_addr
(para lo cual deja espacio sin romper los ejecutables), generalmente mmap
elige al azar direcciones altas, por lo que en la práctica tenemos al menos 4MiB de protección contra la desref. NULL.
2M-alineado es bueno
Esto lo coloca al comienzo de un directorio de páginas en el siguiente nivel de las tablas de páginas, lo que significa que la misma cantidad de entradas de tablas de páginas de 4K se dividirá en menos entradas de directorio de páginas de 2M, ahorrando memoria de tablas de páginas del núcleo y página de ayuda. -Andar caché de hardware mejor. Para arreglos estáticos grandes, también es bueno estar cerca del comienzo de un subárbol 1G del siguiente nivel.
IDK por qué 4MiB en lugar de 2MiB, o cuál fue el razonamiento de los desarrolladores. 4MiB es el tamaño de página grande de 32 bits sin PAE (PTE de 4 bytes, es decir, 10 bits por nivel en lugar de 9), pero una CPU debe usar tablas de página x86-64 para estar en modo de 64 bits.
Una dirección de inicio baja permite casi 2 GiB de arreglos estáticos
(Sin usar un modelo de código más grande, donde al menos los arreglos grandes deben abordarse de maneras que a veces son menos eficientes. Consulte la sección 3.5.1 Restricciones arquitectónicas en el documento x86-64 System V ABI para obtener detalles sobre los modelos de código).
El modelo de código predeterminado para ejecutables que no son PIE ("pequeños") permite que los programas supongan que cualquier dirección estática se encuentra en los 2GiB bajos del espacio de direcciones virtuales. Así que cualquier dirección absoluta en .text
/.rodata
, .data
, .bss
se puede usar como un signo inmediato extendido de 32 bits en el código de máquina donde es más eficiente.
(Este no es el caso en un PIE o una biblioteca compartida:consulte ¿Las direcciones absolutas de 32 bits ya no se permiten en Linux x86-64? por las cosas que usted/el compilador no puede hacer en x86-64 asm como resultado, en particular addss xmm0, [foo + rdi*4]
en su lugar, requiere un LEA relativo a RIP para obtener la dirección de inicio de la matriz en un registro. El único modo de direccionamiento relativo al RIP de x86-64 es [RIP+rel32], sin ningún registro de propósito general).
Comenzar las secciones/segmentos del ejecutable cerca de la parte inferior del espacio de direcciones virtuales deja casi todos los 2GiB disponibles para que text+data+bss sea tan grande. (Podría haber sido posible tener un valor predeterminado más alto y hacer que los ejecutables grandes hicieran que ld eligiera una dirección más baja para que encajaran, pero eso sería un script de vinculación más complicado).
Esto incluye matrices inicializadas en cero en el .bss que no forman el archivo ejecutable enorme, solo la imagen del proceso en la memoria. En la práctica, los programadores de Fortran tienden a encontrarse con esto más que con C y C++, ya que las matrices estáticas son populares allí. Por ejemplo gfortran para dummies:¿Qué hace exactamente mcmodel=medium? tiene una buena explicación de un error de compilación con el small
predeterminado modelo y la diferencia asm x86-64 resultante para medium
(donde no se supone que los objetos por encima de un cierto umbral de tamaño estén en el 2G bajo o dentro de +-2G del código. Pero el código y los datos estáticos más pequeños aún lo están, por lo que la penalización de velocidad es menor).
Por ejemplo static float arr[1UL<<28];
es una matriz de 1 GiB. Si tuvieras 3 de ellos, no todos podrían comenzar dentro de los 2 GiB bajos (que pueden ser todo lo que necesita para un asm escrito a mano), y mucho menos tener accesible cada elemento.
gcc -fno-pie
espera poder compilar float *p = &arr[size-1];
a mov $arr+1073741820, %edi
, un mov $imm32
de 5 bytes . RIP-relative tampoco funcionará si la dirección de destino está a más de 2 GiB del código que genera la dirección (o se carga desde él con movss arr+1073741820(%rip), %xmm0
; RIP-relative es la forma normal de cargar/almacenar datos estáticos incluso en un no-PIE, cuando no hay un índice variable en tiempo de ejecución). Es por eso que el modelo de PIC pequeño también tiene un límite de tamaño de 2GiB en texto+datos+bss (más espacios entre segmentos):todos los datos estáticos y el código deben estar dentro de los 2 GiB de cualquier otro que pueda querer alcanzarlos.
Si su código solo accede a elementos altos o sus direcciones a través de índices variables en tiempo de ejecución, solo necesita que el inicio de cada matriz, el símbolo en sí, esté en los 2 GiB bajos. Olvidé si el enlazador impone tener el final de bss dentro de los 2GiB bajos; podría, ya que la secuencia de comandos del enlazador coloca un símbolo allí al que podría hacer referencia algún código de inicio de CRT.
Nota al pie 1 :No hay tamaños más pequeños útiles para un modelo de código de menos de 2GiB. El código de máquina x86-64 usa 8 o 32 bits para el modo inmediato y de direccionamiento. 8 bits (256 bytes) es demasiado pequeño para ser utilizable, y muchas instrucciones importantes como call rel32
, mov r32, imm32
y [rip+rel32]
direccionamiento, solo están disponibles con constantes de 4 bytes, no de 1 byte de todos modos.
Limitar a los 2 GiB bajos (en lugar de 4) significa que las direcciones pueden extenderse a cero de manera segura como con mov edi, OFFSET arr
, o signo extendido, como con mov eax, [arr + rdi*4]
. Recuerda que las direcciones no son el único caso de uso para [reg + disp32]
modos de direccionamiento; [rbp - 256]
a menudo puede tener sentido, por lo que es bueno que el código de máquina x86-64 sign-extienda disp8 y disp32 a 64 bits, no cero extensiones.
La extensión cero implícita a 64 bits ocurre cuando se escribe un registro de 32 bits, como con mov
-inmediato para poner una dirección en un registro, donde el tamaño del operando de 32 bits es una instrucción de código de máquina más pequeña que el tamaño del operando de 64 bits. Consulte Cómo cargar la dirección de función o etiqueta en el registro (que también cubre LEA relativo a RIP).
Relacionado con Windows de 32 bits
Raymond Chen escribió un artículo sobre por qué el mismo 0x400000
la dirección base es la predeterminada para Windows de 32 bits .
Menciona que las DLL se cargan en direcciones altas de forma predeterminada, y una dirección baja está lejos de eso. Los objetos compartidos x86-64 SysV se pueden cargar en cualquier lugar donde haya una brecha lo suficientemente grande de espacio de direcciones, con el kernel predeterminado cerca de la parte superior del espacio de direcciones virtuales del espacio de usuario, es decir, la parte superior del rango canónico. Pero se requiere que los objetos compartidos ELF sean completamente reubicables, por lo que funcionarían bien en cualquier lugar.
La elección de 4MiB para Windows de 32 bits también estuvo motivada por evitar los 64 K bajos (desref. NULL) y por elegir el inicio de un directorio de páginas para las tablas de páginas heredadas de 32 bits. (Donde el tamaño de "página grande" es 4M, no 2M para x86-64 o PAE). Con un montón de Win95 y Win3.1 heredados, las razones del mapa de memoria por las que al menos 1MiB o 4MiB eran parcialmente necesarias, y cosas como trabajar con CPU bichos.