¿Ya no se permiten direcciones absolutas de 32 bits en Linux x86-64?
Linux de 64 bits utiliza el modelo de memoria pequeña de forma predeterminada, lo que coloca todo el código y los datos estáticos por debajo del límite de direcciones de 2 GB. Esto garantiza que pueda utilizar direcciones absolutas de 32 bits. Las versiones anteriores de gcc utilizan direcciones absolutas de 32 bits para matrices estáticas con el fin de guardar una instrucción adicional para el cálculo de direcciones relativas. Sin embargo, esto ya no funciona. Si intento crear una dirección absoluta de 32 bits en el ensamblado, aparece el error del vinculador: "la reubicación R_X86_64_32S contra `.data' no se puede utilizar al crear un objeto compartido; vuelva a compilar con -fPIC". Este mensaje de error es engañoso, por supuesto, porque no estoy creando un objeto compartido y -fPIC no ayuda. Lo que he descubierto hasta ahora es esto: la versión 4.8.5 de gcc usa direcciones absolutas de 32 bits para matrices estáticas, la versión 6.3.0 de gcc no. La versión 5 probablemente tampoco. El vinculador en binutils 2.24 permite direcciones absolutas de 32 bits, la versión 2.28 no.
La consecuencia de este cambio es que las bibliotecas antiguas deben recompilarse y el código ensamblador heredado se rompe.
Ahora quiero preguntar: ¿Cuándo se hizo este cambio? ¿Está documentado en alguna parte? ¿Y existe una opción de vinculación que le permita aceptar direcciones absolutas de 32 bits?
Su distribución configuró gcc con --enable-default-pie
, por lo que está creando ejecutables independientes de la posición de forma predeterminada (lo que permite ASLR del ejecutable y de las bibliotecas). La mayoría de las distribuciones están haciendo eso en estos días.
En realidad estás creando un objeto compartido: los ejecutables PIE son una especie de truco que utiliza un objeto compartido con un punto de entrada. El enlazador dinámico ya admitía esto, y ASLR es bueno para la seguridad, por lo que esta fue la forma más fácil de implementar ASLR para ejecutables.
No se permiten reubicaciones absolutas de 32 bits en un objeto compartido ELF; eso evitaría que se cargaran fuera de los 2 GiB bajos (para direcciones de 32 bits con signo extendido). Se permiten direcciones absolutas de 64 bits, pero generalmente solo las desea para tablas de salto u otros datos estáticos, no como parte de instrucciones. 1
La recompile with -fPIC
parte del mensaje de error es falsa para un conjunto escrito a mano; está escrito para el caso de personas que compilan gcc -c
y luego intentan vincularse gcc -shared -o foo.so *.o
con un gcc donde no-fPIE
es el predeterminado. El mensaje de error probablemente debería cambiar porque muchas personas se encuentran con este error al vincular un conjunto escrito a mano.
La forma correcta de interpretar recompile with -fPIC
es " crear un nuevo conjunto que sea compatible con PIE ", ya sea con un compilador de código fuente C o manualmente si su código fuente es conjunto.
Cómo utilizar el direccionamiento relativo a RIP: conceptos básicos
Utilice siempre el direccionamiento relativo a RIP para casos sencillos en los que no haya inconvenientes. Consulte también la nota a pie de página 1 a continuación y esta respuesta para conocer la sintaxis . Considere el uso de direccionamiento absoluto solo cuando sea realmente útil para el tamaño del código en lugar de perjudicial. por ejemplo, NASMdefault rel
en la parte superior de su archivo.
AT&Tfoo(%rip)
o en .intel_syntax noprefix
uso de GAS [rip + foo]
.
Desactive el modo PIE para que funcione el direccionamiento absoluto de 32 bits
Úselo gcc -fno-pie -no-pie
para anular esto y volver al comportamiento anterior. -no-pie
es la opción del vinculador, -fno-pie
es la opción de generación de código . Con solo -fno-pie
, gcc hará que un código como mov eax, offset .LC0
ese no se vincule con el archivo -pie
.
( clang también puede tener PIE habilitado de forma predeterminada: use clang -fno-pie -nopie
. Un parche de julio de 2017 creó -no-pie
un alias para -nopie
, para compatibilidad con gcc, pero clang4.0.1 no lo tiene).
Costo de rendimiento de PIE para código de 64 bits (menor) o de 32 bits (mayor)
Con solo -no-pie
, (pero aún así -fpie
) el código generado por el compilador (de fuentes C o C++) será un poco más lento y más grande de lo necesario , pero seguirá vinculado a un ejecutable dependiente de la posición que no se beneficiará de ASLR. "Demasiado PIE es malo para el rendimiento" informa una desaceleración promedio del 3% para x86-64 en SPEC CPU2006 (no tengo una copia del documento, así que no sé en qué hardware estaba :/). Pero en código de 32 bits, la desaceleración promedio es del 10%, en el peor de los casos, del 25% (en SPEC CPU2006).
La penalización para los ejecutables PIE es principalmente por cosas como indexar matrices estáticas, como describe Agner en la pregunta, donde el uso de una dirección estática como inmediata de 32 bits o como parte de un [disp32 + index*4]
modo de direccionamiento guarda instrucciones y registros frente a un LEA relativo a RIP. para obtener una dirección en un registro. Además, 5 bytes mov r32, imm32
en lugar de 7 bytes lea r64, [rel symbol]
para obtener una dirección estática en un registro son buenos para pasar la dirección de una cadena literal u otros datos estáticos a una función.
-fPIE
todavía no asume ninguna interposición de símbolos para variables/funciones globales, a diferencia -fPIC
de las bibliotecas compartidas que tienen que pasar por GOT para acceder a las globales (lo cual es otra razón más para usar static
cualquier variable que pueda limitarse al alcance del archivo en lugar de global). Consulte El lamentable estado de las bibliotecas dinámicas en Linux .
Por lo tanto, -fPIE
es mucho menos malo que -fPIC
para el código de 64 bits, pero sigue siendo malo para el de 32 bits porque el direccionamiento relativo a RIP no está disponible . Vea algunos ejemplos en el explorador del compilador Godbolt . En promedio, -fPIE
tiene una desventaja muy pequeña de rendimiento/tamaño de código en código de 64 bits. El peor de los casos para un bucle específico podría ser solo un pequeño porcentaje. Pero el PIE de 32 bits puede ser mucho peor.
Ninguna de estas -f
opciones de generación de código hace ninguna diferencia cuando simplemente se vincula o cuando se ensambla .S
un conjunto escrito a mano. gcc -fno-pie -no-pie -O3 main.c nasm_output.o
Es un caso en el que quieres ambas opciones.
Comprobando su configuración de GCC
Si su GCC se configuró de esta manera, gcc -v |& grep -o -e '[^ ]*pie'
imprime--enable-default-pie
. La compatibilidad con esta opción de configuración se agregó a gcc a principios de 2015 . Ubuntu lo habilitó en 16.10 y Debian casi al mismo tiempo en gcc 6.2.0-7
(lo que genera errores de compilación del kernel: https://lkml.org/lkml/2016/10/21/904 ).
Relacionado: La compilación de kernels x86 comprimidos ya que PIE también se vio afectada por el cambio predeterminado.
¿Por qué Linux no aleatoriza la dirección del segmento de código ejecutable? es una pregunta anterior sobre por qué no era el valor predeterminado antes, o solo estaba habilitado para algunos paquetes en Ubuntu anterior antes de que se habilitara en todos los ámbitos.
Tenga en cuenta que ld
en sí mismo no cambió su valor predeterminado . Todavía funciona normalmente (al menos en Arch Linux con binutils 2.28). El cambio es que gcc
de forma predeterminada se pasa -pie
como una opción de vinculación, a menos que uses explícitamente -static
o -no-pie
.
En un archivo fuente NASM, solía a32 mov eax, [abs buf]
obtener una dirección absoluta. (Estaba probando si la forma de 6 bytes para codificar pequeñas direcciones absolutas (tamaño de dirección + mov eax,moffs:) 67 a1 40 f1 60 00
tiene un bloqueo de LCP en las CPU Intel. Lo tiene .)
nasm -felf64 -Worphan-labels -g -Fdwarf testloop.asm &&
ld -o testloop testloop.o # works: static executable
gcc -v -nostdlib testloop.o # doesn't work
...
..../collect2 ... -pie ...
/usr/bin/ld: testloop.o: relocation R_X86_64_32 against `.bss' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: final link failed: Nonrepresentable section on output
collect2: error: ld returned 1 exit status
gcc -v -no-pie -nostdlib testloop.o # works
gcc -v -static -nostdlib testloop.o # also works: -static implies -no-pie
GCC también puede hacer un "PIE estático" con -static-pie
; ASLR no cuenta con bibliotecas dinámicas ni intérprete ELF. No es lo mismo que -static -pie
esos entran en conflicto entre sí (obtienes un PIE estático), aunque es posible que se cambie .
relacionado: construir ejecutables estáticos/dinámicos con/sin libc, definir _start
omain
.
Comprobar si un ejecutable existente es PIE o no
Esto también se preguntó en: ¿ Cómo probar si un binario de Linux se compiló como código independiente de la posición?
file
y readelf
decir que los PIE son "objetos compartidos", no ejecutables ELF. El EXEC de tipo ELF no puede ser PIE.
$ gcc -fno-pie -no-pie -O3 hello.c
$ file a.out
a.out: ELF 64-bit LSB executable, ...
$ gcc -O3 hello.c
$ file a.out
a.out: ELF 64-bit LSB shared object, ...
## Or with a more recent version of file:
a.out: ELF 64-bit LSB pie executable, ...
gcc -static-pie
es algo especial que GCC no hace de forma predeterminada, incluso con -nostdlib
. Aparece como LSB pie executable
, dynamically linked
con las versiones actuales de file
. (Consulte ¿Cuál es la diferencia entre "vinculado estáticamente" y "no un ejecutable dinámico" de Linux ldd? ). Tiene DYN tipo ELF, pero readelf
no muestra .interp
y ldd
le indicará que está vinculado estáticamente. GDB starti
y /proc/maps
confirma que la ejecución comienza en la parte superior de su archivo _start
, no en un intérprete ELF.
Semi-relacionado (pero no realmente): otra característica reciente de gcc es gcc -fno-plt
. Finalmente, las llamadas a bibliotecas compartidas pueden ser solo call [rip + symbol@GOTPCREL]
(AT&T call *puts@GOTPCREL(%rip)
), sin trampolín PLT.
La versión NASM de esto es call [rel puts wrt ..got]
una alternativa a call puts wrt ..plt
. Consulte No se puede llamar a la función de biblioteca estándar C en Linux de 64 bits desde código ensamblador (yasm) . Esto funciona en un PIE o no, y evita que el vinculador cree un código auxiliar PLT para usted.
Algunas distribuciones han comenzado a habilitarlo. También evita la necesidad de páginas de memoria ejecutables y grabables, por lo que es bueno para la seguridad contra la inyección de código. (Creo que las implementaciones PLT modernas tampoco necesitan eso, simplemente actualizar un puntero GOT sin reescribir una jmp rel32
instrucción, por lo que es posible que no haya una diferencia de seguridad).
It's a significant speedup for programs that make a lot of shared-library calls, e.g. x86-64 clang -O2 -g
compiling tramp3d goes from 41.6s to 36.8s on whatever hardware the patch author tested on. (clang is maybe a worst-case scenario for shared library calls, making lots of calls to small LLVM library functions.)
It does require early binding instead of lazy dynamic linking, so it's slower for big programs that exit right away. (e.g. clang --version
or compiling hello.c
). This slowdown could be reduced with prelink, apparently.
This doesn't remove the GOT overhead for external variables in shared library PIC code, though. (See the godbolt link above).
Footnote 1: 64-bit absolute
64-bit absolute addresses actually are allowed in Linux ELF shared objects, with text relocations to allow loading at different addresses (ASLR and shared libraries). This allows you to have jump tables in section .rodata
, or static const int *foo = &bar;
without a runtime initializer.
So mov rdi, qword msg
works (NASM/YASM syntax for 10-byte mov r64, imm64
, aka AT&T syntax movabs
, the only instruction which can use a 64-bit immediate). But that's larger and usually slower than lea rdi, [rel msg]
, which is what you should use if you decide not to disable -pie
. A 64-bit immediate is slower to fetch from the uop cache on Sandybridge-family CPUs, according to Agner Fog's microarch pdf. (Yes, the same person who asked this question. :)
You can use NASM's default rel
instead of specifying it in every [rel symbol]
addressing mode. See also Mach-O 64-bit format does not support 32-bit absolute addresses. NASM Accessing Array for some more description of avoiding 32-bit absolute addressing. OS X can't use 32-bit addresses at all, so RIP-relative addressing is the best way there, too.
In position-dependent code (-no-pie
), you should use mov edi, msg
when you want an address in a register; 5-byte mov r32, imm32
is even smaller than RIP-relative LEA, and more execution ports can run it.