Las direcciones absolutas de 32 bits ya no están permitidas en x86-64 Linux?

Linux de 64 bits usa el modelo de memoria pequeña de forma predeterminada, lo que coloca todos los códigos y datos estáticos por debajo del límite de direcciones de 2 GB. Esto asegura que puede usar direcciones absolutas de 32 bits. Las versiones anteriores de gcc usan direcciones absolutas de 32 bits para matrices estáticas con el fin de guardar una instrucción adicional para el cálculo relativo de la dirección. 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 crear un objeto compartido; recomstackr con -fPIC “. Este mensaje de error es engañoso, por supuesto, porque no estoy haciendo 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 arreglos estáticos, la versión 6.3.0 de gcc 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 recomstackrse y el código de ensamblaje heredado se rompe.

Ahora quiero preguntar: ¿Cuándo se hizo este cambio? Está documentado en alguna parte? ¿Y hay una opción de vinculador que lo hace 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 distros están haciendo eso, en estos días.

En realidad estás haciendo un objeto compartido: los ejecutables de PIE son una especie de truco que usa un objeto compartido con un punto de entrada. El enlazador dynamic ya lo admite, 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 del 2GiB bajo (para direcciones extendidas de 32 bits). Las direcciones absolutas de 64 bits están permitidas, pero generalmente solo quiere eso para las 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 el asm escrito a mano; está escrito para el caso de personas que comstackn con gcc -c y luego intentan vincular con gcc -shared -o foo.so *.o , con un gcc donde -fPIE no es el predeterminado. El mensaje de error probablemente debería cambiar porque muchas personas están experimentando este error al vincular el asm manuscrito.


Use gcc -fno-pie -no-pie para anular esto de nuevo al comportamiento anterior. -no-pie es la opción del enlazador, -fno-pie es la opción de código gen . Con solo -fno-pie , gcc creará código como mov eax, offset .LC0 que no se enlaza con el -pie activado -pie .

( clang también puede tener PIE habilitada por defecto: use clang -fno-pie -nopie . Un parche de julio de 2017 hizo -no-pie un alias para -nopie , para compat con gcc, pero clang4.0.1 no lo tiene. )


Con solo el código generado por el comstackdor -no-pie , (pero aún -fpie ) (de fonts 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á ASLR. “Demasiada PIE es mala para el rendimiento” informa una desaceleración promedio del 3% para x86-64 en SPEC CPU2006 (no tengo una copia del documento, así que IDK qué hardware estaba: /). 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 ejecutables PIE es principalmente para indexar matrices estáticas, como describe Agner en la pregunta, donde el uso de una dirección estática como de 32 bits inmediata o como parte de un modo de direccionamiento [disp32 + index*4] guarda instrucciones y registros frente a un LEA relativo al RIP para obtener una dirección en un registro. También 5-byte mov r32, imm32 lugar 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 aún asume que no hay interposición de símbolos para variables / funciones globales, a diferencia de -fPIC para bibliotecas compartidas que deben pasar por GOT para acceder a globales (que es otra razón más para usar static para cualquier variable que se pueda limitar al scope 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 al RIP no está disponible . Vea algunos ejemplos en el explorador del comstackdor Godbolt . En promedio, -fPIE tiene una desventaja de rendimiento / tamaño de código muy pequeña en el código de 64 bits. El peor caso para un bucle específico podría ser solo un pequeño%. Pero PIE de 32 bits puede ser mucho peor.

Ninguna de estas opciones de código-gen hace ninguna diferencia cuando se trata de vincular, o al ensamblar .S manuscrito. gcc -fno-pie -no-pie -O3 main.c nasm_output.o es un caso en el que desea ambas opciones.


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 comstackción del kernel: https://lkml.org/lkml/2016/10/21/904 ).

Relacionado: Crea kernels x86 comprimidos ya que el 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 anterior, o solo se habilitó para algunos paquetes en Ubuntu anterior antes de que se habilitara en todos los ámbitos.


Tenga en cuenta que ld no cambió su valor predeterminado . Todavía funciona normalmente (al menos en Arch Linux con binutils 2.28). El cambio es que gcc pasa por defecto a pasar -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 un locking 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: crear ejecutables estáticos / dynamics con / sin libc, definiendo _start o main .


Comprobando si un ejecutable existente es PIE o no

file y readelf dicen que las 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, ... 

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


Semirelacionado (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 PLT trampoline.

Es de esperar que Distros comience a habilitarlo pronto, ya que también evita tener que escribir páginas de memoria ejecutables + ejecutables. Es una aceleración significativa para los progtwigs que realizan muchas llamadas a bibliotecas compartidas, por ejemplo, x86-64 clang -O2 -g comstackción de tramp3d va de 41.6s a 36.8s en cualquier hardware probado por el autor del parche . (clang es quizás el peor de los escenarios para compartir llamadas a bibliotecas).

Requiere enlace anticipado en lugar de vinculación dinámica diferida, por lo que es más lento para los grandes progtwigs que salen de inmediato. (por ejemplo, clang --version o comstackción hello.c ). Esta ralentización podría reducirse con prelink, aparentemente.

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


Notas a pie de página 1

De hecho, las direcciones absolutas de 64 bits están permitidas en 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.

Así que mov rdi, qword msg funciona (syntax NASM / YASM para 10-byte mov r64, imm64 , también conocido como AT & T syntax movabs , la única instrucción que puede usar un 64-bit inmediato). Pero eso es más grande y generalmente más lento que lea rdi, [rel msg] , que es lo que deberías usar si decides no desactivar -pie . Un inmediato de 64 bits es más lento de obtener de la memoria caché uop en las CPU de la familia Sandybridge, según el microarch pdf 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 de [rel symbol] . Ver también el formato Mach-O de 64 bits no admite direcciones absolutas de 32 bits. NASM Accessing Array para obtener más información sobre cómo evitar el direccionamiento absoluto de 32 bits. OS X no puede usar direcciones de 32 bits, por lo que el direccionamiento relativo al RIP es la mejor manera que existe.

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