¿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?