Código Independente de Posição (PIC) em Assembly NASM
A principal característica das bibliotecas compartilhadas é que o loader dinâmico pode carregá-las em qualquer endereço de memória disponível. De fato, essa é uma medida de segurança fundamental nos sistemas Linux modernos, conhecida como ASLR (Address Space Layout Randomization), que randomiza os endereços base da pilha, bibliotecas e do próprio executável para dificultar ataques.
Isso significa que tanto as bibliotecas quanto as aplicações devem ser escritas de uma maneira especial, de forma que possam funcionar corretamente independentemente de onde sejam carregadas na memória. Esse tipo de código é conhecido como código independente de posição, ou PIC (Position-Independent Code).
Para criar um código que seja PIC, precisamos ajustar a forma como ele acessa endereços de memória em três áreas principais:
- Chamadas a funções externas.
- Acesso a dados dentro da própria biblioteca (seção
.data). - Acesso a dados externos (em outras bibliotecas ou no executável principal).
1. Chamadas a Funções Externas (PLT)
As chamadas a funções externas são gerenciadas automaticamente pelo linker e pelo loader dinâmico através das tabelas PLT (Procedure Linkage Table) e GOT (Global Offset Table). Ao escrever o código, indicamos ao montador que a chamada deve ser resolvida via PLT:
call print wrt ..pltA diretiva wrt ..plt (with regard to the PLT) instrui o montador a gerar uma instrução de chamada que salta para uma entrada na PLT, que por sua vez se encarrega de encontrar o endereço real da função print através da GOT.
2. Acesso a Dados Internos (Endereçamento Relativo ao RIP)
Para acessar dados definidos na seção .data da própria biblioteca ou executável, utiliza-se um modo de endereçamento conhecido como endereçamento relativo ao PC (Program Counter), que na arquitetura x86-64 corresponde ao registrador RIP (Instruction Pointer).
Em vez de usar um endereço absoluto, o código referencia os dados como um deslocamento (offset) a partir da instrução atual. Isso garante que, não importa onde o código seja carregado, a distância entre a instrução e o dado será sempre a mesma.
No NASM, isso é feito com o operador rel. Por exemplo, para obter o valor de uma variável:
section .data
count: db 10
section .text
mov al, [rel count] ; Carrega o valor de countE para obter o endereço de uma variável:
section .data
message: db "Hello"
section .text
lea rsi, [rel message] ; Carrega o endereço de message3. Acesso a Dados Externos (GOT)
Quando uma biblioteca precisa acessar uma variável global definida no executável principal (ou em outra biblioteca), o acesso é feito através da GOT. A GOT conterá o endereço real da variável, que será preenchido pelo loader dinâmico.
mov rsi, [rel message wrt ..got] ; Carrega o endereço da variável externa 'message'Criando um Executável Independente de Posição (PIE)
Um executável que utiliza código independente de posição é chamado de PIE (Position-Independent Executable). Vamos criar um exemplo.
Primeiro, a biblioteca print.asm (a mesma do artigo anterior):
global print:function
section .text
print:
mov rdx, rdi
mov rdi, 1
mov rax, 1
syscall
retCompilamos a biblioteca:
$ nasm -felf64 print.asm -o print.o $ ld -shared print.o -o libprint.so
Agora, o aplicativo principal app.asm, escrito como PIC:
global _start
extern print
section .data
message: db "Hello World!", 10
count: db $ - message
section .text
_start:
mov rdi, [rel count] ; Acesso a dados internos relativo ao RIP
lea rsi, [rel message] ; Obtém endereço interno relativo ao RIP
call print wrt ..plt ; Chamada externa via PLT
mov rax, 60
syscallPara compilar um executável PIE, passamos a flag -pie para o linker:
$ nasm -felf64 app.asm -o app.o $ ld --dynamic-linker=/lib64/ld-linux-x86-64.so.2 -pie app.o -lprint -L. -o app $ export LD_LIBRARY_PATH=. $ ./app Hello World!
Podemos verificar que o executável foi criado como PIE com o comando readelf -h app:
$ readelf -h app ELF Header: ... Type: DYN (Position-Independent Executable file) ...
O campo Type confirma que o arquivo é um executável independente de posição.
Bibliotecas Independentes de Posição
A mesma lógica se aplica às bibliotecas. Se uma biblioteca precisa acessar dados de fora, ela deve fazê-lo via GOT. Vamos inverter nosso exemplo: a biblioteca print.so irá imprimir uma mensagem definida no aplicativo principal.
Arquivo print.asm modificado:
global print:function
extern message
extern count
section .text
print:
lea rsi, [rel message wrt ..got] ; Carrega o endereço de 'message' via GOT
mov rdx, [rel count wrt ..got] ; Carrega o endereço de 'count' via GOT
mov rdx, [rdx] ; De-referencia para obter o valor de 'count'
mov rdi, 1
mov rax, 1
syscall
retNote que, como a GOT armazena o endereço da variável count, precisamos de uma segunda instrução para ler o valor contido nesse endereço.
Arquivo app.asm modificado, agora exportando as variáveis:
global _start
global message:data ; Exporta o símbolo 'message' como dado
global count:data ; Exporta o símbolo 'count' como dado
extern print
section .data
message: db "Hello programicio.com!", 10
.end:
count: dq message.end - message
section .text
_start:
call print wrt ..plt
mov rax, 60
syscallA compilação segue os mesmos passos, e o resultado será o mesmo. A diferença é que agora a biblioteca está acessando dados que pertencem ao executável principal.
Resumo
- Código Independente de Posição (PIC) é essencial para criar bibliotecas compartilhadas e executáveis seguros (PIE) que funcionam com ASLR.
- Acesso a dados internos: Usa endereçamento relativo ao
RIPcom a diretivarel. - Chamadas a funções externas: Usam a Tabela de Ligação de Procedimentos (PLT) com a diretiva
wrt ..plt. - Acesso a dados externos: Usa a Tabela de Deslocamento Global (GOT) com a diretiva
wrt ..got. - Para criar um executável PIE, a flag
-piedeve ser passada ao linkerld.