linux gcc x86-64 linker-errors relocation

¿Ya no se permiten direcciones absolutas de 32 bits en Linux x86-64?



gcc linker-errors (1)

Su distribución configuró gcc con --enable-default-pie , por lo que está haciendo ejecutables independientes de la posición de forma predeterminada (permitiendo ASLR del ejecutable y las bibliotecas). La mayoría de las distribuciones están haciendo eso, en estos días.

Realmente está creando un objeto compartido: los ejecutables PIE son una especie de truco que utiliza un objeto compartido con un punto de entrada. El vinculador dinámico ya era compatible con esto, y ASLR es bueno para la seguridad, por lo que esta fue la forma más fácil de implementar ASLR para ejecutables.

La reubicación absoluta de 32 bits no está permitida en un objeto compartido ELF; eso evitaría que se carguen fuera de los 2GiB bajos (para direcciones de 32 bits con signo extendido). Se permiten direcciones absolutas de 64 bits, pero generalmente solo se desea para tablas de salto u otros datos estáticos, no como parte de las instrucciones. 1

La recompile with -fPIC parte recompile with -fPIC del mensaje de error es falsa para asm escrito a mano; está escrito para el caso de personas que compilan con gcc -c y luego intentan vincularse con gcc -shared -o foo.so *.o , con un gcc donde -fPIE no es el valor predeterminado. El mensaje de error probablemente debería cambiar porque muchas personas se encuentran con este error al vincular asm escritos a mano.

Cómo usar el direccionamiento relativo a RIP: conceptos básicos

Utilice siempre el direccionamiento relativo a RIP para casos simples en los que no haya inconvenientes. Vea también la nota 1 a continuación y esta respuesta para la sintaxis . Solo considere usar el direccionamiento absoluto de 32 bits cuando sea realmente útil para el tamaño del código en lugar de perjudicial. por ejemplo, NASM default rel en la parte superior de su archivo.

AT&T foo(%rip) o en GAS .intel_syntax noprefix use [rip + foo] .

Deshabilite el modo PIE para que el direccionamiento absoluto de 32 bits funcione

Use gcc -fno-pie -no-pie para anular esto al comportamiento anterior. -no-pie es la opción del vinculador, -fno-pie es la opción code-gen . Con solo -fno-pie , gcc creará código como mov eax, offset .LC0 que no se vincula con el -pie aún habilitado.

( clang también puede tener PIE habilitado por defecto: use clang -fno-pie -nopie . Un parche de julio de 2017 hizo que -no-pie un alias para -nopie , para compat 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 -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 aún estará 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é qué hardware estaba en: /). Pero en el 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 para cosas como indexar matrices estáticas, como describe Agner en la pregunta, donde el uso de una dirección estática como un modo de direccionamiento inmediato de 32 bits o como parte de un modo de direccionamiento [disp32 + index*4] guarda instrucciones y registros frente a una LEA relativa a RIP para obtener una dirección en un registro. También mov r32, imm32 de 5 bytes mov r32, imm32 lugar de lea r64, [rel symbol] de 7 bytes lea r64, [rel symbol] para obtener una dirección estática en un registro es bueno para pasar la dirección de un literal de cadena u otros datos estáticos a una función.

-fPIE todavía no asume ninguna interposición de símbolos para las variables / funciones globales, a diferencia de -fPIC para las bibliotecas compartidas que tienen que pasar por el GOT para acceder a los globales (que es otra razón más para usar static para cualquier variable que pueda limitarse al alcance del archivo) de global). Consulte El lamentable estado de las bibliotecas dinámicas en Linux .

Por -fPIE tanto, -fPIE es mucho menos malo que -fPIC para el -fPIC de 64 bits, pero sigue siendo malo para 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 -fPIE muy pequeña de rendimiento / tamaño de código en el código de 64 bits. El peor de los casos para un bucle específico podría ser solo un pequeño%. Pero el PIE de 32 bits puede ser mucho peor.

Ninguna de estas opciones de -f code-gen hace ninguna diferencia al vincular o al ensamblar .S escrito a mano asm. gcc -fno-pie -no-pie -O3 main.c nasm_output.o es un caso en el que desea 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 . El soporte para 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 lleva a errores de compilación del núcleo: https://lkml.org/lkml/2016/10/21/904 ).

Relacionado: Crear núcleos x86 comprimidos ya que PIE también se vio afectado 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 anteriormente, o solo estaba habilitado para algunos paquetes en Ubuntu anterior antes de que se habilitara en todos los ámbitos.

Tenga en cuenta que ld sí no cambió su valor predeterminado . Todavía funciona normalmente (al menos en Arch Linux con binutils 2.28). El cambio es que gcc defecto pasa -pie como una opción de enlazador, a menos que use explícitamente -static o -no-pie .

En un archivo fuente NASM, utilicé a32 mov eax, [abs buf] para obtener una dirección absoluta. (Estaba probando si la forma de 6 bytes para codificar direcciones absolutas pequeñas (tamaño de dirección + mov eax, moffs: 67 a1 40 f1 60 00 ) tiene una pérdida de LCP en las CPU de Intel.

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

relacionado: construir ejecutables estáticos / dinámicos con / sin libc, definiendo _start o main .

Comprobando si un ejecutable existente es PIE o no

file y readelf dicen que los PIE son "objetos compartidos", no ejecutables ELF. Los ejecutables estáticos no pueden 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, ...

Esto también se ha preguntado en: ¿Cómo probar si un binario de Linux se compiló como código de posición independiente?

Semi-relacionado (pero no realmente): otra característica reciente de gcc es gcc -fno-plt . Finalmente, las llamadas a bibliotecas compartidas pueden ser simplemente call [rip + symbol@GOTPCREL] (AT&T call *puts@GOTPCREL(%rip) ), sin trampolín PLT.

Con suerte, Distros comenzará a habilitarlo pronto, porque también evita la necesidad de páginas de memoria escribibles + ejecutables. Es una aceleración significativa para los programas que realizan muchas llamadas a bibliotecas compartidas, por ejemplo, x86-64 clang -O2 -g compilando tramp3d va de 41.6s a 36.8s en cualquier hardware que el autor del parche haya probado . (Clang es quizás el peor de los casos para las llamadas a la biblioteca compartida).

Requiere un enlace temprano en lugar de un enlace dinámico lento, por lo que es más lento para los programas grandes que salen de inmediato. (por ejemplo, clang --version o compilando hello.c ). Esta desaceleración podría reducirse con el preenlace, aparentemente.

Sin embargo, esto no elimina la sobrecarga GOT para las variables externas en el código PIC de la biblioteca compartida. (Ver el enlace godbolt arriba).

Notas al pie 1

Las direcciones absolutas de 64 bits están permitidas en los objetos compartidos ELF de Linux, con reubicaciones de texto para permitir la carga en diferentes direcciones (ASLR y bibliotecas compartidas). Esto le permite tener tablas de salto en la section .rodata , o static const int *foo = &bar; sin un inicializador de tiempo de ejecución.

Entonces mov rdi, qword msg funciona (sintaxis NASM / YASM para mov r64, imm64 , también conocido como AT&T movabs , la única instrucción que puede usar un inmediato de 64 bits). Pero eso es más grande y generalmente más lento que lea rdi, [rel msg] , que es lo que debe usar si decide no deshabilitar -pie . Un inmediato de 64 bits es más lento para obtener de la caché uop en las CPU de la familia Sandybridge, según el pdf de microarchivo de Agner Fog . (Sí, la misma persona que hizo esta pregunta. :)

Puede usar el default rel de NASM en lugar de especificarlo en cada modo de direccionamiento [rel symbol] . Consulte también el formato Mach-O de 64 bits no admite direcciones absolutas de 32 bits. NASM Accessing Array para obtener una descripción más detallada de cómo evitar el direccionamiento absoluto de 32 bits. OS X no puede usar direcciones de 32 bits en absoluto, por lo que el direccionamiento relativo a RIP también es la mejor manera.

En el código dependiente de la posición ( -no-pie ), debe usar mov edi, msg cuando desee una dirección en un registro; mov r32, imm32 5 bytes mov r32, imm32 es incluso más pequeño que LEA relativo a RIP, y más puertos de ejecución pueden ejecutarlo.

Linux de 64 bits utiliza el modelo de memoria pequeña de forma predeterminada, que coloca todo el código y los datos estáticos por debajo del límite de dirección de 2 GB. Esto asegura que pueda usar direcciones absolutas de 32 bits. Las versiones anteriores de gcc usan direcciones absolutas de 32 bits para matrices estáticas para guardar una instrucción adicional para el cálculo de la dirección relativa. Sin embargo, esto ya no funciona. Si trato de hacer una dirección absoluta de 32 bits en el ensamblado, aparece el error del enlazador: "la reubicación R_X86_64_32S contra ''.data'' no se puede usar al hacer un objeto compartido; recompilar 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: gcc versión 4.8.5 utiliza direcciones absolutas de 32 bits para matrices estáticas, gcc versión 6.3.0 no. la versión 5 probablemente tampoco. El enlazador en binutils 2.24 permite direcciones absolutas de 32 bits, verson 2.28 no.

La consecuencia de este cambio es que las bibliotecas antiguas deben volver a compilarse y el código de ensamblaje heredado no funciona.

Ahora quiero preguntar: ¿Cuándo se realizó este cambio? ¿Está documentado en alguna parte? ¿Y hay una opción de enlace que lo haga aceptar direcciones absolutas de 32 bits?