Considere os dois arquivos a seguir em um sistema Linux:
use_message.cpp
#include <iostream>
extern const char* message;
void print_message();
int main() {
std::cout << message << '\n';
print_message();
}
libmessage.cpp
#include <iostream>
const char* message = "Meow!";
void print_message() {
std::cout << message << '\n';
}
Podemos compilar use_message.cpp em um arquivo objeto, compilar libmessage.cpp em uma biblioteca compartilhada e vinculá-los, assim:
$ g++ use_message.cpp -c -pie -o use_message.o
$ g++ libmessage.cpp -fPIC -shared -o libmessage.so
$ g++ use_message.o libmessage.so -o use_message
A definição de message
originalmente reside em libmessage.so . Quando use_message
é executado, o vinculador dinâmico executa realocações que:
- Atualize a
message
definição dentro de libmessage.so com o endereço de carregamento dos dados da string - Copie a definição
message
de libmessage.so para a seção use_message.bss
- Atualize a tabela de deslocamento global em libmessage.so para apontar para a nova versão de
message
dentro de use_message
As realocações relevantes, conforme descartadas por readelf
, são:
usar_mensagem
Offset Info Type Sym. Value Sym. Name + Addend
000000004150 000c00000005 R_X86_64_COPY 0000000000004150 message + 0
Esta é a realocação número 2 na lista que escrevi antes.
libmessage.so
Offset Info Type Sym. Value Sym. Name + Addend
000000004040 000000000008 R_X86_64_RELATIVE 2000
000000003fd8 000b00000006 R_X86_64_GLOB_DAT 0000000000004040 message + 0
Estes são os números de realocação 1 e 3, respectivamente.
Há uma dependência entre os números de realocação 1 e 2: a atualização para a definição de libmessage.somessage
deve acontecer antes que esse valor seja copiado para use_message , caso contrário, use_message não apontará para o local correto.
Minha pergunta é: como é especificada a ordem de aplicação das remanejamentos? Existe algo codificado nos arquivos ELF que especifica isso? Ou na ABI? Ou espera-se apenas que o vinculador dinâmico resolva as dependências entre as realocações e garanta que todas as realocações que gravam em um determinado endereço de memória sejam executadas antes de qualquer realocação que seja lida no mesmo local? O vinculador estático gera apenas realocações de modo que as do executável sempre possam ser processadas após as da biblioteca compartilhada?
Acho que a ordem de resolução da realocação não é especificada por um padrão. Carregadores dinâmicos definem um pedido. Para suportar realocações de cópias, o executável principal é realocado por último. Os vinculadores produzem apenas realocações de cópias para links executáveis (-no-pie/-pie) e estão cientes da semântica do carregador dinâmico.
Citando https://maskray.me/blog/2021-01-18-gnu-indirect-function#relocation-resolving-order :
Existem duas partes: a ordem dentro de um módulo e a ordem entre dois módulos.
glibc rtld processa realocações na ordem de pesquisa reversa (l_initfini invertido) com um caso especial para o próprio rtld. O executável principal precisa ser processado por último para processar R_*_COPY. Se A tiver uma ifunc referenciando B, geralmente B precisa ser realocado antes de A. Sem ifunc, a ordem de resolução dos objetos compartilhados pode ser arbitrária.
Digamos que temos a seguinte árvore de dependências.
l_initfini contém main, dep1.so, dep2.so, dep4.so, dep3.so, libc.so.6, ld.so. A ordem de resolução de realocação é ld.so (bootstrap), libc.so.6, dep3.so, dep4.so, dep2.so, dep1.so, main, ld.so.
Dentro de um módulo, glibc rtld resolve as realocações em ordem. Suponha que DT_RELA (.rela.dyn) e DT_PLTREL (.rela.plt) estejam presentes, a lógica glibc é semelhante à seguinte:
musl
ldso/dynlink.c
tem:O FreeBSD rtld usa uma ordem mais sofisticada, o que torna certos códigos ifunc mais robustos.
BTW,
use_message
(com arquivos relocáveis -fPIE) precisa de relocações de cópia por causa de GCCHAVE_LD_PIE_COPYRELOC
. Para outras arquiteturas Clang e GCC, os modos PIE não levarão a realocações de cópias.