Endereços absolutos de 32 bits não são mais permitidos no Linux x86-64?

O Linux de 64 bits usa o modelo de memory pequena por padrão, o que coloca todos os códigos e dados estáticos abaixo do limite de endereços de 2 GB. Isso garante que você pode usar endereços absolutos de 32 bits. Versões mais antigas do gcc usam endereços absolutos de 32 bits para matrizes estáticas para salvar uma instrução extra para o cálculo do endereço relativo. No entanto, isso não funciona mais. Se eu tentar fazer um endereço absoluto de 32 bits na assembly, recebo o erro do linker: “a relocação R_X86_64_32S contra` .data ‘não pode ser usada ao criar um object compartilhado; recompilar com -fPIC “. Essa mensagem de erro é enganosa, é claro, porque não estou fazendo um object compartilhado e o -fPIC não ajuda. O que eu descobri até agora é o seguinte: o gcc versão 4.8.5 usa endereços absolutos de 32 bits para matrizes estáticas, o gcc versão 6.3.0 não. a versão 5 provavelmente também não. O vinculador no binutils 2.24 permite endereços absolutos de 32 bits, ou seja, 2,28 não.

A conseqüência dessa alteração é que as bibliotecas antigas precisam ser recompiladas e o código de assembly herdado é interrompido.

Agora quero perguntar: quando foi feita essa mudança? Está documentado em algum lugar? E existe uma opção de vinculador que aceita endereços absolutos de 32 bits?

Sua distro configurou o gcc com --enable-default-pie , portanto, está tornando os executáveis ​​independentes de posição por padrão (permitindo o ASLR do executável, bem como das bibliotecas). A maioria das distros está fazendo isso hoje em dia.

Você realmente está fazendo um object compartilhado: executáveis ​​PIE são uma espécie de hack usando um object compartilhado com um ponto de input. O vinculador dynamic já suportava isso, e o ASLR é legal para segurança, então essa era a maneira mais fácil de implementar o ASLR para executáveis.

A realocação absoluta de 32 bits não é permitida em um object compartilhado ELF; isso impediria que eles fossem carregados fora do 2GiB baixo (para endereços de 32 bits estendidos por sinal). Endereços absolutos de 64 bits são permitidos, mas geralmente você só deseja isso para tabelas de salto ou outros dados estáticos, não como parte das instruções. 1

A recompile with -fPIC parte recompile with -fPIC da mensagem de erro é falsa para asm escrita à mão; é escrito para o caso de pessoas compilando com o gcc -c e então tentando linkar com gcc -shared -o foo.so *.o , com um gcc em que -fPIE não é o padrão. A mensagem de erro provavelmente deve mudar porque muitas pessoas estão incorrendo nesse erro ao vincular o ASM escrito à mão.


Use gcc -fno-pie -no-pie para replace isso de volta ao comportamento antigo. -no-pie é a opção de vinculador, -fno-pie é a opção code-gen . Com apenas -fno-pie , o gcc fará código como mov eax, offset .LC0 que não se liga ao ainda ativado -pie .

(o clang também pode ter o PIE ativado por padrão: use clang -fno-pie -nopie . Um patch feito em julho de 2017 -no-pie um alias para -nopie , para compat com gcc, mas o clang4.0.1 não o possui. )


Com apenas -no-pie , (mas ainda -fpie ) código gerado pelo compilador (a partir de fonts C ou C ++) será um pouco mais lento e maior que o necessário , mas ainda será vinculado a um executável dependente de posição que não se beneficiará ASLR. “Muito PIE é ruim para o desempenho” relata uma desaceleração média de 3% para x86-64 na SPEC CPU2006 (não tenho uma cópia do documento, então IDK em qual hardware estava: /). Mas no código de 32 bits, a desaceleração média é de 10%, o pior caso é de 25% (no SPEC CPU2006).

A penalidade para executáveis ​​PIE é principalmente para coisas como indexação de matrizes estáticas, como Agner descreve na questão, onde usar um endereço estático como imediato de 32 bits ou como parte de um modo de endereçamento [disp32 + index*4] salva instruções e registros vs. um LEA relativo ao RIP para obter um endereço em um registrador. Também 5-byte mov r32, imm32 vez de 7-byte lea r64, [rel symbol] para obter um endereço estático em um registrador é bom para passar o endereço de uma string literal ou outros dados estáticos para uma function.

-fPIE ainda não assume interposição de símbolos para variables ​​/ funções globais, ao contrário de -fPIC para bibliotecas compartilhadas que precisam passar pelo GOT para acessar globais (o que é outro motivo para usar static para qualquer variável que possa ser limitada ao escopo de arquivo de global). Veja O estado lastimável de bibliotecas dinâmicas no Linux .

Portanto, -fPIE é muito menos ruim que -fPIC para código de 64 bits, mas ainda é ruim para 32 bits porque o endereçamento relativo a RIP não está disponível . Veja alguns exemplos no explorador do compilador Godbolt . Em média, -fPIE tem um desempenho muito pequeno / tamanho do código em 64 bits. O pior caso para um loop específico pode ser apenas alguns%. Mas o PIE de 32 bits pode ser muito pior.

Nenhuma dessas opções -f code-gen faz alguma diferença quando se liga, ou quando se monta o .S escrito à mão. gcc -fno-pie -no-pie -O3 main.c nasm_output.o é um caso onde você quer as duas opções.


Se o seu GCC foi configurado dessa maneira, gcc -v |& grep -o -e '[^ ]*pie' imprime --enable-default-pie . O suporte para esta opção de configuração foi adicionado ao gcc no início de 2015 . O Ubuntu habilitou em 16.10, e o Debian na mesma época no gcc 6.2.0-7 (levando a erros de compilation do kernel: https://lkml.org/lkml/2016/10/21/904 ).

Relacionado: Crie kernels x86 compactados, já que o PIE também foi afetado pelo padrão alterado.

Por que o Linux não randomiza o endereço do segmento de código executável? é uma questão antiga sobre por que não era o padrão anterior, ou só estava habilitado para alguns pacotes no Ubuntu antigo antes de ser habilitado em toda a linha.


Note que o próprio ld não alterou seu padrão . Ele ainda funciona normalmente (pelo menos no Arch Linux com binutils 2.28). A mudança é que o padrão do gcc é passar -pie como uma opção de vinculador, a menos que você use explicitamente -static ou -no-pie .

Em um arquivo de origem NASM, usei a32 mov eax, [abs buf] para obter um endereço absoluto. (Eu estava testando se o caminho de 6 bytes para codificar endereços absolutos pequenos (endereço-tamanho + mov eax, moffs: 67 a1 40 f1 60 00 ) tem um barramento de LCP nos processadores 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 

related: construindo executáveis ​​estáticos / dynamics com / sem libc, definindo _start ou main .


Verificando se um executável existente é PIE ou não

file e readelf dizem que PIEs são “objects compartilhados”, não executáveis ​​ELF. Os executáveis ​​estáticos não podem ser TORTA.

 $ 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, ... 

Isso também foi perguntado em: Como testar se um binário do Linux foi compilado como código independente de posição?


Semi-relacionado (mas não realmente): outro recurso recente do gcc é gcc -fno-plt . Finalmente, as chamadas para bibliotecas compartilhadas podem ser call [rip + symbol@GOTPCREL] apenas de call [rip + symbol@GOTPCREL] ( call *puts@GOTPCREL(%rip) AT & T call *puts@GOTPCREL(%rip) ), sem trampolim PLT.

As distribuições começarão a ativá-lo em breve, porque também evita a necessidade de páginas de memory executáveis ​​+ executáveis. É uma aceleração significativa para programas que fazem muitas chamadas de biblioteca compartilhada, por exemplo, x86-64 clang -O2 -g compilation tramp3d vai de 41,6s para 36.8s em qualquer hardware testado pelo autor do patch . (O clang é talvez o pior cenário para as chamadas da biblioteca compartilhada.)

Ele exige vinculação antecipada em vez de vinculação dinâmica preguiçosa, por isso é mais lento para programas grandes que saem imediatamente. (por exemplo, clang --version ou compiling hello.c ). Esta desaceleração poderia ser reduzida com prelink, aparentemente.

Isso não remove a sobrecarga do GOT para variables ​​externas no código PIC da biblioteca compartilhada, no entanto. (Veja o link do godbolt acima).


Notas de rodapé 1

Endereços absolutos de 64 bits, na verdade, são permitidos em objects compartilhados Linux ELF, com realocações de texto para permitir o carregamento em endereços diferentes (ASLR e bibliotecas compartilhadas). Isso permite que você tenha tabelas de salto na section .rodata ou static const int *foo = &bar; sem um inicializador de tempo de execução.

Então, mov rdi, qword msg funciona (syntax NASM / YASM para movabs de syntax AT & T de 10 bytes mov r64, imm64 , também conhecida como AT & T, a única instrução que pode usar um imediato de 64 bits). Mas isso é maior e geralmente mais lento que lea rdi, [rel msg] , que é o que você deve usar se decidir não desabilitar -pie . Um imediato de 64 bits é mais lento para buscar a partir do cache uop em CPUs da família Sandybridge, de acordo com o pdf microar de Agner Fog . (Sim, a mesma pessoa que fez esta pergunta 🙂

Você pode usar o default rel do NASM em vez de especificá-lo em cada modo de endereçamento [rel symbol] . Veja também Mach-O 64-bit format não suporta endereços absolutos de 32 bits. NASM Array de Acesso para mais algumas descrições de como evitar endereçamento absoluto de 32 bits. O OS X não pode usar endereços de 32 bits, portanto, o endereçamento relativo a RIP também é o melhor caminho.

No código dependente da posição ( -no-pie ), você deve usar mov edi, msg quando quiser um endereço em um registrador; O 5-byte mov r32, imm32 é ainda menor que o LEA relativo ao RIP, e mais portas de execução podem executá-lo.