Linking
Compilação separada
A partição dos programas por vários ficheiros é uma prática necessária na produção, na manutenção e na reutilização de software.
Em programas extensos, alterações de código fonte num ficheiro não implica a necessidade de recompilar a totalidade dos ficheiros do projeto.
Unidade de tradução
O compilador compila um ficheiro fonte de cada vez. Neste contexo o ficheiro é designado por unidade de tradução. Uma unidade de tradução é formada por uma sequência de definições externas – variáveis ou funções. Externas porque são globalmente acessíveis, podem ser usadas por funções definidas nesta ou noutras unidades de tradução.
As variáveis definidas dentro de funções são internas.
Uma parte de um programa, numa unidade de tradução, interage com outra parte do programa, noutra unidade de tradução, através de variáveis ou funções. As variáveis ou funções são identificadas por símbolos (o nome da variável ou da função).
O resultado da tradução é um ficheiro objeto relocalizável.
Declaração
Declarar uma variável ou função consiste em anunciar as suas caraterísticas.
extern int z; – declara que z é uma variável do tipo int de âmbito externo.
size_t strlen(const char *str); – declara que strlen é uma função
que recebe como argumento um ponteiros para char e devolve um valor do tipo size_t.
As suas definições encontram-se noutro local. Pode ser noutro ou no mesmo ficheiro.
Definição
Definir uma variável ou função significa anunciar a sua exitência e implica ocupar espaço de memória para a alojar.
No caso das variáveis, a definição pode vir acompanhada da definição do valor inicial. Implica reservar memória da dimensão adequada ao tipo.
Por exemplo a seguinte definição reserva quatro bytes na secção .data,
preencidos com os valores 0x21 0x00 0x00 0x00.
int z = 33;
No caso das funções, a definição provoca a geração do código máquina que determina o seu comportamento e a respetiva reserva de espaço de memória.
Por exemplo, a seguinte definição da função strlen implica reservar na secção .text
uma porção de 28 bytes preenchidos com o código binário das instruções.
#include <stdlib.h>
size_t strlen(const char *str) {
size_t length = 0;
while (*str++)
++length;
return length;
}
|
Uma definição também é uma declaração.
Âmbito
O âmbito dos símbolos – que identificam as variáveis e as funções – pode ser bloco, função, unidade de tradução (ficheiro) ou global (de todo o programa).
A utilização do mesmo símbolo numa declaração mais interior esconde uma declaração mais exterior desse mesmo símbolo.
A uma declaração está associada um storage class: automático ou estático.
O storage class depende dos especificadores static, extern ou do local da declaração.
Uma declaração diz-se externa se estiver fora de uma função (linha 1)
ou for precedida do especificador extern (linha 4).
1int i; 2 3int main() { 4 extern int j; 5}
Numa declaração interna o atributo static significa que a variável vai ser alojada num local permanente da memória (linha 2).
1int main() { 2 static int i; 3}
Visibilidade
Para que um símbolo, definido num módulo, possa ser referenciado noutro módulo, é necessário que seja classificado como globalmente visível.
Na linguagem C, por omissão, uma definição externa produz um símbolo globalmente visível (símbolo global).
Para restringir a visibilidade de um símbolo ao ficheiro onde é definido,
usa-se o especificador static (linha 2).
1int c; 2static int j; 3int main() { 4 int b; 5}
Na linguagem assembly GNU, por omissão, um símbolo é visível apenas no ficheiro onde é definido.
Para o tornar globalmente visível é necessário explicitar através da diretiva
.global como no seguinte exemplo:
1 .global main 2main:
Um símbolo global precisa ser conhecido no ficheiro onde é referenciado. O AS (nome do assembler GNU) assume que um símbolo referenciado e não definido no presente ficheiro é global e está definido noutro ficheiro.
As declarações são escritas em ficheiros com extensão h, que por sua vez são intercalados pela diretiva #include nos ficheiros fonte, a fim de dar a conhecer as propriedades dos objetos.
Secções de dados
O compilador gcc da GNU aloja as variáveis externas, ou locais com atributo static,
nas secções .bss, .data ou .rodata, de acordo com certas propriedades.
.bss– Variáveis inicializadas com zero.
.data– Variáveis inicializadas com valores diferentes de zero.
.rodata– Variáveis com atributoconste strings literais.1int a; .bss - 4 bytes 2static int b; .bss - 4 bytes 3int x = 20; .data - 4 bytes 4static int y = 24; .data - 4 bytes 5int array[5]; .bss - 20 bytes 6int table[] = {1, 2, 3, 4, 5}; .data - 20 bytes 7char matrix[100][20]; .bss - 2000 bytes 8char n[] = "Manuel"; .data - 7 bytes 9const char m[] = "Joaquim"; .rodata - 8 bytes 10const char *d = "Rita"; .data - 8 bytes .rodata - 5 bytes 11const char *const f = "Ezequiel"; .rodata - 8 + 9 bytes 12 13void function() 14{ 15 int j; registo ou *stack* 16 int k = 34; registo ou *stack* 17 register int l; preferencialmente registo ou *stack* 18 const int m = 20; registo ou *stack* 19 static const int n = 20; .rodata 20 static int o = 55; .data 21}
Processo de geração de programas
Considere-se como exemplo um programa constituído pelos módulos: main.c e calculate.c.
#include <stdio.h>
#include "calculate.h"
int a = 3, b = 5;
int x = 45;
int main() {
calculate();
}
|
#include "calculate.h"
#define X 1000
extern int a, b;
static const int x = X;
int y = 33;
static int *pa = &a;
static int *pb;
int res;
static int multiply(int a, int b) {
return a * b;
}
int calculate() {
pb = &b;
res = *pa + *pb + multiply(x, y);
return res;
}
|
Numa única invocação, o gcc pode processar vários ficheiros fonte e produzir o executável.
$ gcc main.c add.c -o main
Efetivamente, o programa gcc não é o compilador de linguagem C, é um compiler driver. No processo de geração do executável o gcc invoca sucessivamente, para cada ficheiro fonte:
O pré-processador
$ cpp <outros argumentos> -c main.c -o main.i
O compilador de linguagem C
$ cc <outros argumentos> -S main.i -o main.s
O assembler
$ as <outros argumentos> -S main.s -o main.o
No final invoca o linker com todos os ficheiros objeto relocalizáveis.
$ ld <outros argumentos> crt1.o *.o main.o add.o -o main
O método designado por compilação separada consiste em invocar
os tradutores individualmente (cpp, cc, as) para cada ficheiro fonte,
produzindo o respetivo ficheiro objeto relocalizável (com a extensão *.o).
Figura 13 Ilustração do processo de compilação e ligação
Em projetos reais são utilizadas ferramentas para controlar o processo de geração dos programas. A ferramenta Make é uma das mais utilizadas. Abaixo apresenta-se um ficheiro makefile para geração do programa executável tratado nesta secção.
main: main.o calculate.o
gcc main.o calculate.o -o main
main.o: main.c
gcc -c main.c
calculate.o: calculate.c
gcc -c calculate.c
Ficheiro objeto relocalizável
Em última instância, o ficheiro objeto relocalizável é produzido pelo assembler
e é-lhe dada a extensão o. Pode ter tido origem num programa fonte em linguagem C ou em linguagel assembly.
Nos sistemas Linux atuais os ficheiros objeto relocalizáveis e os objetos executáveis, utilizam o formato ELF [1]. O seu conteúdo é essencialmente composto por um cabeçalho e um conjunto de blocos que se designam por secções que contêm o binário do programa e outras informações.
Secções
$ readelf -S calculate.o
There are 16 section headers, starting at offset 0x490:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000006b 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000318
00000000000000a8 0000000000000018 I 13 1 8
[ 3] .data PROGBITS 0000000000000000 000000ac
0000000000000004 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000000b0
0000000000000010 0000000000000000 WA 0 0 8
[ 5] .rodata PROGBITS 0000000000000000 000000b0
0000000000000004 0000000000000000 A 0 0 4
[ 6] .data.rel PROGBITS 0000000000000000 000000b8
0000000000000008 0000000000000000 WA 0 0 8
[ 7] .rela.data.rel RELA 0000000000000000 000003c0
0000000000000018 0000000000000018 I 13 6 8
[ 8] .comment PROGBITS 0000000000000000 000000c0
0000000000000027 0000000000000001 MS 0 0 1
[ 9] .note.GNU-stack PROGBITS 0000000000000000 000000e7
0000000000000000 0000000000000000 0 0 1
[10] .note.gnu.pr[...] NOTE 0000000000000000 000000e8
0000000000000050 0000000000000000 A 0 0 8
[11] .eh_frame PROGBITS 0000000000000000 00000138
0000000000000060 0000000000000000 A 0 0 8
[12] .rela.eh_frame RELA 0000000000000000 000003d8
0000000000000030 0000000000000018 I 13 11 8
[13] .symtab SYMTAB 0000000000000000 00000198
0000000000000150 0000000000000018 14 9 8
[14] .strtab STRTAB 0000000000000000 000002e8
000000000000002c 0000000000000000 0 0 1
[15] .shstrtab STRTAB 0000000000000000 00000408
0000000000000083 0000000000000000 0 0 1
.text binário das instruções assembly;
.rela.text locais na secção .text que precisam ser atualizadas na ligação;
.data variáveis com valor inicial definido e determinável em compilação;
.data.rel variáveis com valor inicial definido determinado em ligação;
.rela.data.rel locais na secção .data.rel que precisam ser atualizadas na ligação;
.bss variáveis com valor inicial zero;
.rodata dados apenas de leitura – constantes, strings;
.symtab tabela de símbolos – nome das funções e variáveis, definidas e invocadas;
.strtab tabela de strings com o nome dos símbolos;
.shstrtab tabela de strings usadas nos cabeçalhos de secções.
Símbolos
$ readelf -s calculate.o Symbol table '.symtab' contains 14 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS calculate.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text 3: 0000000000000000 0 SECTION LOCAL DEFAULT 4 .bss 4: 0000000000000000 4 OBJECT LOCAL DEFAULT 5 x 5: 0000000000000000 0 SECTION LOCAL DEFAULT 6 .data.rel 6: 0000000000000000 8 OBJECT LOCAL DEFAULT 6 pa 7: 0000000000000008 8 OBJECT LOCAL DEFAULT 4 pb 8: 0000000000000000 23 FUNC LOCAL DEFAULT 1 multiply 9: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 y 10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND a 11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 res 12: 0000000000000017 84 FUNC GLOBAL DEFAULT 1 calculate 13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND b
Detalhe dos símbolos: posição relativa ao início da secção a que pertencem (Value),
dimensão (Size); tipo função (FUNC) ou variável (OBJECT);
âmbito local ou global (Bind), secção a que pertence (Ndx),
e nome do símbolo (Name).
Outra forma de visualizar os símbolos de um módulo:
$ nm calculate.o U a U b 0000000000000017 T calculate 0000000000000000 t multiply 0000000000000000 d pa 0000000000000008 b pb 0000000000000000 B res 0000000000000000 r x 0000000000000000 D y
Símbolos locais definidos e invocados apenas neste ficheiro – multiply, pa, pb, x.
Símbolos globais definidos em calculate.c e eventualmente referidos de outros ficheiros – calculate, res, y.
Símbolos globais referidos em calculate.c – a, b.
Os símbolos são representados no ficheiro objeto pela estrutura Elf64_Sym.
typedef struct {
Elf64_Word st_name; Índice na tabela de strings com o nome dos símbolos
unsigned char st_info; Bits 0 a 3 - âmbito local ou global; bit 4 a 7 - tipo OBJECT, FUNC
unsigned char st_other; Reservado
Elf64_Half st_shndx; Secção onde é definido (há três casos especiais ABS, COM , UND)
Elf64_Addr st_value; Valor associado ao símbolo (endereço de memória ou offset)
Elf64_Xword st_size; Dimensão de memória que ocupa
} Elf64_Sym;
Visualização em hexadecimal da tabela de símbolos.
Os dados entre as posições 0xc0 e 0xd7 correspondem ao símbolo multiply.
$ readelf -x .symtab calculate.o Hex dump of section '.symtab': 0x00000000 00000000 00000000 00000000 00000000 ................ 0x00000010 00000000 00000000 01000000 0400f1ff ................ 0x00000020 00000000 00000000 00000000 00000000 ................ 0x00000030 00000000 03000100 00000000 00000000 ................ 0x00000040 00000000 00000000 00000000 03000400 ................ 0x00000050 00000000 00000000 00000000 00000000 ................ 0x00000060 0d000000 01000500 00000000 00000000 ................ 0x00000070 04000000 00000000 00000000 03000600 ................ 0x00000080 00000000 00000000 00000000 00000000 ................ 0x00000090 0f000000 01000600 00000000 00000000 ................ 0x000000a0 08000000 00000000 12000000 01000400 ................ 0x000000b0 08000000 00000000 08000000 00000000 ................ 0x000000c0 15000000 02000100 00000000 00000000 ................ 0x000000d0 17000000 00000000 1c000000 11000300 ................ 0x000000e0 00000000 00000000 04000000 00000000 ................ 0x000000f0 10000000 10000000 00000000 00000000 ................ 0x00000100 00000000 00000000 1e000000 11000400 ................ 0x00000110 00000000 00000000 04000000 00000000 ................ 0x00000120 22000000 12000100 17000000 00000000 "............... 0x00000130 54000000 00000000 13000000 10000000 T............... 0x00000140 00000000 00000000 00000000 00000000 ................
$ readelf -x .strtab calculate.o
Hex dump of section '.strtab':
0x00000000 0063616c 63756c61 74652e63 00780070 .calculate.c.x.p
0x00000010 61007062 006d756c 7469706c 79007265 a.pb.multiply.re
0x00000020 73006361 6c63756c 61746500 s.calculate.
Ligação (linking)
A operação de ligação é realizada em duas fase:
resolução de símbolos – a cada referência corresponde apenas uma definição;
relocalização – calcula endereços dos símbolos e completa o código binário.
Resolução de símbolos
Consiste em encontrar a única definição para cada símbolo.
Nos símbolos locais isso é realizado pelo compilador – símbolos com ligação interna.
Nas resolução de símbolos de ligação externa, pode acontecer a definição do símbolo não existir ou existirem múltiplas definições.
Neste segundo caso a resolução poderá ter êxito se:
existir apenas uma definição forte devendo as outras ser fracas;
haver só definições fracas – neste caso escolher uma qualquer.
Relocalização
A relocalização começa depois de se conhecerem todas as definições de símbolos e engloba as seguintes operações:
agregar todas as secções do mesmo tipo definidas nos ficheiros numa única secção de saída;
calcular e associar a cada símbolo o endereço de execução;
modificar o conteúdo das secções nos locais indicados na respetiva tabela de relocalização.
Quando o assembler codifica uma instrução com referência a um símbolo, não sabe a localização final dessa instrução, nem a localização do símbolo referido. Nessa altura gera uma relocation entrie.
$ objdump -d -r calculate.o
calculate.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <multiply>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 89 75 f8 mov %esi,-0x8(%rbp)
e: 8b 45 fc mov -0x4(%rbp),%eax
11: 0f af 45 f8 imul -0x8(%rbp),%eax
15: 5d pop %rbp
16: c3 ret
0000000000000017 <calculate>:
17: f3 0f 1e fa endbr64
1b: 55 push %rbp
1c: 48 89 e5 mov %rsp,%rbp
1f: 53 push %rbx
20: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 27 <calculate+0x10>
23: R_X86_64_PC32 b-0x4
27: 48 89 05 00 00 00 00 mov %rax,0x0(%rip) # 2e <calculate+0x17>
2a: R_X86_64_PC32 .bss+0x4
2e: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # 35 <calculate+0x1e>
31: R_X86_64_PC32 .data.rel-0x4
35: 8b 10 mov (%rax),%edx
37: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # 3e <calculate+0x27>
3a: R_X86_64_PC32 .bss+0x4
3e: 8b 00 mov (%rax),%eax
40: 8d 1c 02 lea (%rdx,%rax,1),%ebx
43: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 49 <calculate+0x32>
45: R_X86_64_PC32 y-0x4
49: ba e8 03 00 00 mov $0x3e8,%edx
4e: 89 c6 mov %eax,%esi
50: 89 d7 mov %edx,%edi
52: e8 a9 ff ff ff call 0 <multiply>
57: 01 d8 add %ebx,%eax
59: 89 05 00 00 00 00 mov %eax,0x0(%rip) # 5f <calculate+0x48>
5b: R_X86_64_PC32 res-0x4
5f: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 65 <calculate+0x4e>
61: R_X86_64_PC32 res-0x4
65: 48 8b 5d f8 mov -0x8(%rbp),%rbx
69: c9 leave
6a: c3 ret
A instrução localizada no endereço 20 lea 0x0(%rip),%rax tem associada uma relocalização
para atualizar o offset relativo ao PC (R_X86_64_PC32) para acesso à variável b.
A instrução localizada no endereço 43 mov 0x0(%rip),%eax tem associada uma relocalização
para atualizar o offset relativo ao PC (R_X86_64_PC32) para acesso à variável y.
Algoritmo de relocalização
foreach section s {
foreach relocation entry r {
ptr = s + r.offset local do ficheiro onde é preciso afetar
if (r.type == R_X86_64_PC32) {
address = address(s) + r.offset; endereço de execução
*ptr = adress(r.symbol) + r.addend – address;
}
if (r.type == R_X86_64_64)
*ptr = adress(r.symbol) + r.addend;
} }
A referência ao símbolo multiply é resolvida pelo próprio compilador.
Como se trata de um símbolo interno, com endereçamento relativo,
a distância do ponto de referência (endereço 0x57) ao endereço da função (0x0)
é -87 (0xffffffa9).
Também pode ser confirmado utilizando os endereços finais:
Endereço de referência = 0x11be
Endereço da função
multiply= 0x1167Diferença de endereços = 0x11be - 0x1167 = 0x57
Ficheiro objeto executável
O ficheiro objeto executável contém toda a informação necessária para carregar o programa em memória antes de ser executado. As secções e os símbolos têm agora posições definidas e as referências a símbolos estão preenchidas (foram preenchidas através das relocations).
$ objdump -h a.out
a.out: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 0000001c 0000000000000318 0000000000000318 00000318 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.gnu.property 00000030 0000000000000338 0000000000000338 00000338 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 0000000000000368 0000000000000368 00000368 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .note.ABI-tag 00000020 000000000000038c 000000000000038c 0000038c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .gnu.hash 00000024 00000000000003b0 00000000000003b0 000003b0 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynsym 000000a8 00000000000003d8 00000000000003d8 000003d8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .dynstr 00000090 0000000000000480 0000000000000480 00000480 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .gnu.version 0000000e 0000000000000510 0000000000000510 00000510 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .gnu.version_r 00000030 0000000000000520 0000000000000520 00000520 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .rela.dyn 000000d8 0000000000000550 0000000000000550 00000550 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
10 .rela.plt 00000018 0000000000000628 0000000000000628 00000628 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
11 .init 0000001b 0000000000001000 0000000000001000 00001000 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .plt 00000020 0000000000001020 0000000000001020 00001020 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .plt.got 00000010 0000000000001040 0000000000001040 00001040 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .plt.sec 00000010 0000000000001050 0000000000001050 00001050 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
15 .text 00000172 0000000000001060 0000000000001060 00001060 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
16 .fini 0000000d 00000000000011d4 00000000000011d4 000011d4 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
17 .rodata 00000008 0000000000002000 0000000000002000 00002000 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
18 .eh_frame_hdr 00000044 0000000000002008 0000000000002008 00002008 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
19 .eh_frame 000000f0 0000000000002050 0000000000002050 00002050 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
20 .init_array 00000008 0000000000003db8 0000000000003db8 00002db8 2**3
CONTENTS, ALLOC, LOAD, DATA
21 .fini_array 00000008 0000000000003dc0 0000000000003dc0 00002dc0 2**3
CONTENTS, ALLOC, LOAD, DATA
22 .dynamic 000001f0 0000000000003dc8 0000000000003dc8 00002dc8 2**3
CONTENTS, ALLOC, LOAD, DATA
23 .got 00000048 0000000000003fb8 0000000000003fb8 00002fb8 2**3
CONTENTS, ALLOC, LOAD, DATA
24 .data 00000028 0000000000004000 0000000000004000 00003000 2**3
CONTENTS, ALLOC, LOAD, DATA
25 .bss 00000018 0000000000004028 0000000000004028 00003028 2**3
ALLOC
26 .comment 00000026 0000000000000000 0000000000000000 00003028 2**0
CONTENTS, READONLY
Execução de programas em Linux
Na arquitetura x86_64 os endereços de memória são definidos com 64 bits. As implementações atuais desta arquitetura, dos 64 bits de endereço, apenas utilizam 48 bits em endereço virtual e cerca de 40 bits em endereço físico.
No sistema operativo Linux,
a primeira metade do espaço de endereçamento virtual 0000 0000 0000 0000 – 7fff ffff ffff
é usada para processos e a segunda metade
ffff 8000 0000 0000 – ffff ffff ffff ffff é usada pelo kernel.
Ao lançar um processo, o Linux faz a seguinte ocupação do espaço de endereçamento virtual:
$ cat /proc/self/maps
652deccb2000-652deccb4000 r--p 00000000 103:02 30933476 /usr/bin/cat
652deccb4000-652deccb9000 r-xp 00002000 103:02 30933476 /usr/bin/cat
652deccb9000-652deccbb000 r--p 00007000 103:02 30933476 /usr/bin/cat
652deccbb000-652deccbc000 r--p 00008000 103:02 30933476 /usr/bin/cat
652deccbc000-652deccbd000 rw-p 00009000 103:02 30933476 /usr/bin/cat
652ded06f000-652ded090000 rw-p 00000000 00:00 0 [heap]
79d41ba00000-79d41bf75000 r--p 00000000 103:02 30934874 /usr/lib/locale/locale-archive
79d41c000000-79d41c028000 r--p 00000000 103:02 30939484 /usr/lib/x86_64-linux-gnu/libc.so.6
79d41c028000-79d41c1b0000 r-xp 00028000 103:02 30939484 /usr/lib/x86_64-linux-gnu/libc.so.6
79d41c1b0000-79d41c1ff000 r--p 001b0000 103:02 30939484 /usr/lib/x86_64-linux-gnu/libc.so.6
79d41c1ff000-79d41c203000 r--p 001fe000 103:02 30939484 /usr/lib/x86_64-linux-gnu/libc.so.6
79d41c203000-79d41c205000 rw-p 00202000 103:02 30939484 /usr/lib/x86_64-linux-gnu/libc.so.6
79d41c205000-79d41c212000 rw-p 00000000 00:00 0
79d41c235000-79d41c25a000 rw-p 00000000 00:00 0
79d41c26e000-79d41c270000 rw-p 00000000 00:00 0
79d41c270000-79d41c271000 r--p 00000000 103:02 30939481 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
79d41c271000-79d41c29c000 r-xp 00001000 103:02 30939481 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
79d41c29c000-79d41c2a6000 r--p 0002c000 103:02 30939481 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
79d41c2a6000-79d41c2a8000 r--p 00036000 103:02 30939481 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
79d41c2a8000-79d41c2aa000 rw-p 00038000 103:02 30939481 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffe94ba8000-7ffe94bc9000 rw-p 00000000 00:00 0 [stack]
7ffe94bd0000-7ffe94bd4000 r--p 00000000 00:00 0 [vvar]
7ffe94bd4000-7ffe94bd6000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
Os endereços de execução são atribuídos aleatoriamente no momento do carregamento do programa em memória.
O Insight atribui endereços fixos.
Os endereços atribuido a um processo podem ser observados assim:
$ ps -x
$ cat /proc/<pid>/maps/
Momentos importantes na vida de um programa:
compilação (compile time) – tradução de linguagem de programação para código máquina.
ligação (link time) – composicão de programa com base em código máquina de várias proveniências.
carregamento (load time) – criação do ambiente de execução de um programa.
execução (run time) – programa em execuçã.
Carregamento no arranque da aplicação
O loader carrega o programa e verifica se existe uma secção .interp com a indicação do linker dinâmico:
$ readelf -x .interp main
Hex dump of section '.interp':
0x00000318 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
0x00000328 7838362d 363
O linker dinâmico realiza as seguintes operações:
Mapeia as secções de dados e de código das bibliotecas dinâmicas que vão ser utilizadas no espaço de memória do processo;
Resolve as referências existentes no programa para dados e código das bibliotecas. Esta operação não utiliza o processo das relocations porque isso implicaria alterações de código. É resolvido via PLT/GOT.
Código independente da posição (PIC)
Considere-se o ficheiro main.c como o código fonte do programa executável
e o ficheiro libpic.c como o código fonte da biblioteca.
Nestas experiências foram utilizados o gcc 13.2.0 e o binutils 2.40.50.20230331.
extern int lib_var;
void lib_func1();
void lib_func2();
int prog_var = 4;
int prog_func() {
return prog_var + lib_var;
}
int main() {
lib_func1();
lib_func2();
return prog_func();
}
|
extern int prog_var;
int prog_func();
int lib_var = 3;
void lib_func1() {
lib_var = prog_var;
prog_var = prog_func();
}
void lib_func2() {
}
|
Os ficheiros objeto são produzidos sob o controlo dos seguintes makefiles:
CFLAGS = -c -Wall -std=c2x -g -Og
all: main
main.o: main.c
gcc $(CFLAGS) main.c -o main.o
main: main.o
gcc main.o -L. -lpic -o main
clean:
rm -rf *.o *.i main
|
CFLAGS = -c -Wall -std=c2x -g -Og
all: libpic.so
libpic.o: libpic.c
gcc $(CFLAGS) -fPIC libpic.c -o libpic.o
libpic.so: libpic.o
gcc -shared -o libpic.so libpic.o
|
Acesso a variáveis
Os acessos que interessa analisar são a variáveis globais.
Acesso a variáveis a partir da aplicação
As variáveis globais, as definidas no executável (prog_var´´)
e as definidas na biblioteca (``lib_var),
são mapeadas na secção .data do executável.
Os acessos são realizados com endereçamento relativo ao RIP e o seu endereço é determinado em tempo de compilação (compile time).
$ objdump -d main
0000000000001169 <prog_func>:
1169: f3 0f 1e fa endbr64
116d: 8b 05 a5 2e 00 00 mov 0x2ea5(%rip),%eax # 4018 <lib_var@@Base>
1173: 03 05 97 2e 00 00 add 0x2e97(%rip),%eax # 4010 <prog_var>
1179: c3 ret
Acesso a variáveis a partir da biblioteca
A posição das variáveis, relativamente ao código da biblioteca, só é determinável em tempo de carregamento load time.
O acesso a estas variáveis é feito via tabela GOT (Global Offset Table) quer sejam definidas no executável quer sejam definidas na biblioteca.
Observar o código de acesso às variáveis globais prog_var e lib_var (instruções de 111e a 112e):
$ objdump -d libpic.so
0000000000001119 <lib_func1>:
1119: f3 0f 1e fa endbr64
111d: 53 push %rbx
111e: 48 8b 1d b3 2e 00 00 mov 0x2eb3(%rip),%rbx # 3fd8 <prog_var>
1125: 8b 13 mov (%rbx),%edx
1127: 48 8b 05 8a 2e 00 00 mov 0x2e8a(%rip),%rax # 3fb8 <lib_var-0x58>
112e: 89 10 mov %edx,(%rax)
1130: e8 1b ff ff ff call 1050 <prog_func@plt>
1135: 89 03 mov %eax,(%rbx)
1137: 5b pop %rbx
1138: c3
A tabela GOT tem uma entrada por cada variável externa referida.
Cada módulo em biblioteca possui uma secção .got onde é alojada a tabela GOT.
Esta secção é localizada a uma distância fixa em relação à secção .text,
determinada em tempo de compilação.
O acesso ao conteúdo da tabela GOT é realizado com endereçamento relativo ao RIP, como às variáveis nas secções de dados.
Durante o carregamento do programa, as entradas da tabela GOT são preenchidas pelo linker dinâmico, na fase de relocalização, com o endereço absoluto das variáveis.
A instrução mov 0x2ebe(%rip),%rbx coloca em RBX o conteúdo da entrada da GOT
relativa à variável prog_var, que é o endereço absoluto desta variável.
A instrução mov (%rbx), %edx carrega o conteúdo de ``prog_var em EDX.
Invocação de funções
Tal como nas variáveis, o endereço das funções da biblioteca só é determinado em tempo de carregamento. O acesso ao endereço das funções para chamadas entre executável e biblioteca é diferente do utilizado para as variáveis. Mas poderia ser igual.
O objetivo é manter o código de invocação de função igual, quer na ligação estática quer na ligação dinâmica, isto é, não é necessário gerar o código de utilizador diferente para cada tipo de ligação. No caso de ligação estática chama a função diretamente, no caso de ligação dinâmica chama a PLT.
Invocação de funções da biblioteca a partir do programa
$ objdump -d main
000000000000117a <main>:
117a: f3 0f 1e fa endbr64
117e: 48 83 ec 08 sub $0x8,%rsp
1182: e8 e9 fe ff ff call 1070 <lib_func1@plt>
1187: e8 d4 fe ff ff call 1060 <lib_func2@plt>
118c: e8 d8 ff ff ff call 1169 <prog_func>
1191: 48 83 c4 08 add $0x8,%rsp
1195: c3
A chamada a funções de biblioteca é feita por via da PLT (Procedure Linkage Table).
A partir da versão 8 do compilador GCC, passou a haver uma secção PLT primária .plt
e uma secção PLT secundária .plt.sec.
$ objdump -d main --section=.plt
0000000000001020 <.plt>:
1020: ff 35 92 2f 00 00 push 0x2f92(%rip) # 3fb8 <_GLOBAL_OFFSET_TABLE_+0x8>
1026: ff 25 94 2f 00 00 jmp *0x2f94(%rip) # 3fc0 <_GLOBAL_OFFSET_TABLE_+0x10>
102c: 0f 1f 40 00 nopl 0x0(%rax)
1030: f3 0f 1e fa endbr64
1034: 68 00 00 00 00 push $0x0
1039: e9 e2 ff ff ff jmp 1020 <_init+0x20>
103e: 66 90 xchg %ax,%ax
1040: f3 0f 1e fa endbr64
1044: 68 01 00 00 00 push $0x1
1049: e9 d2 ff ff ff jmp 1020 <_init+0x20>
104e: 66 90 xchg %ax,%ax
$ objdump -d main --section=.plt.sec
0000000000001060 <lib_func2@plt>:
1060: f3 0f 1e fa endbr64
1064: ff 25 5e 2f 00 00 jmp *0x2f5e(%rip) # 3fc8 <lib_func2@Base>
106a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
0000000000001070 <lib_func1@plt>:
1070: f3 0f 1e fa endbr64
1074: ff 25 56 2f 00 00 jmp *0x2f56(%rip) # 3fd0 <lib_func1@Base>
107a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
Em cada entrada de .plt.sec existe um jump indireto sobre uma entrada da tabela GOT.
Por exemplo, na chamada à função lib_func1, a instrução jmpq *0x2f56(%rip)
acede à entrada GOT de endereço 0x3fd0 onde se encontra o endereço absoluto da função lib_func1.
O linker aplica um procedimento designado por lazy binding que consiste em protelar a atualização da entrada GOT para o momento da primeira chamada. Até lá, todas as entradas da tabela GOT apontam para a PLT primária.
Essa secção contém um array em que cada entrada corresponde a uma função e contém uma sequência push/jmp. A instrução push carrega no stack um argumento que corresponde ao número identificador da função e em seguida a instrução jmp salta para a PLT[0] que por sua vez invoca o linker dinâmico apontado por GOT[2].
.plt [0] 1020 ff 35 92 2f 00 00 push 0x2f92(%rip) # 3fb8 <GOT+0x8>
1026 ff 25 94 2f 00 00 jmp *0x2f94(%rip) # 3fc0 <GOT+0x10>
102c 0f 1f 40 00 nopl 0x0(%rax)
[1] 1030 f3 0f 1e fa endbr64
1034 68 00 00 00 00 push $0x0
1039 e9 e2 ff ff ff jmp 1020 <_init+0x20>
103e 66 90 xchg %ax,%ax
[2] 1040 f3 0f 1e fa endbr64
1044 68 01 00 00 00 push $0x1
1049 e9 d2 ff ff ff jmp 1020 <_init+0x20>
104e 66 90 xchg %ax,%ax
.plt.sec [0] 1060 f3 0f 1e fa endbr64
1064 ff 25 5e 2f 00 00 jmp *0x2f5e(%rip) # 3fc8 <lib_func2@Base>
106a 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) 1060 endbr64
[1] 1070 f3 0f 1e fa endbr64
1074 ff 25 56 2f 00 00 jmp *0x2f56(%rip) # 3fd0 <lib_func1@Base>
107a 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) 1060 endbr64
.text main: 117a f3 0f 1e fa endbr64
117e 48 83 ec 08 sub $0x8,%rsp
1182 e8 e9 fe ff ff call 1070 <lib_func1@plt>
1187 e8 d4 fe ff ff call 1060 <lib_func2@plt>
118c e8 d8 ff ff ff call 1169 <prog_func>
1191 48 83 c4 08 add $0x8,%rsp
1195 c3
ret
.got [0] 3fb0 address of .dynamic 0x3db0
[1] 3fb8 address of reloc entries
[2] 3fc0 address of dynamic linker
[3] 3fc8 address of lib_func2
[4] 3fd0 address of lib_func1
[5] 3fd8
[6] 3fe0
[7] 3fe8
[8] 3ff0
[9] 3ff8
.data 4000
4008
prog_var: 4010
.bss lib_var: 4018
Chamada à função lib_func1 – primeira vez
(RIP = 1182) Chamar a função
lib_func1a partir da funçãomain– salta para.plt.sec[1].(RIP = 1070) Na
.plt.sec[1]salta para o endereço contido na GOT[4]. Da primeira esse endereço é PLT[1].(RIP = 1030) Empilhar o identificador da função
lib_func1(pushq 0x0) e saltar para PLT[0].(RIP = 1020) Em PLT[0], depois de empilhar GOT[1], salta para GOT[2] que é o endereço do linker dinâmico. O linker, baseado nos parâmetros recebidos, substitui o conteúdo de GOT[4] pelo endereço de execução de
lib_func1.
Chamada à função lib_func1 – vezes seguintes
(RIP = 1182) Chamar a função
lib_func1a partir da funçãomain– salta para.plt.sec[1].(RIP = 1070) Na
.plt.sec[1]salta para o endereço contido na GOT[4]. Esse endereço agora é o endereço delib_func1.
Invocação de funções do programa a partir da biblioteca
A operação de chamada a função a partir da biblioteca é idêntica à do sentido contrário. (Serve apenas como exercício.)
$ objdump -d libpic.so
0000000000001119 <lib_func1>:
1119: f3 0f 1e fa endbr64
111d: 53 push %rbx
111e: 48 8b 1d b3 2e 00 00 mov 0x2eb3(%rip),%rbx # 3fd8 <prog_var>
1125: 8b 13 mov (%rbx),%edx
1127: 48 8b 05 8a 2e 00 00 mov 0x2e8a(%rip),%rax # 3fb8 <lib_var-0x58>
112e: 89 10 mov %edx,(%rax)
1130: e8 1b ff ff ff call 1050 <prog_func@plt>
1135: 89 03 mov %eax,(%rbx)
1137: 5b pop %rbx
1138: c3 ret
$ objdump -d libpic.so --section=.plt
0000000000001020 <.plt>:
1020: ff 35 ca 2f 00 00 push 0x2fca(%rip) # 3ff0 <_GLOBAL_OFFSET_TABLE_+0x8>
1026: ff 25 cc 2f 00 00 jmp *0x2fcc(%rip) # 3ff8 <_GLOBAL_OFFSET_TABLE_+0x10>
102c: 0f 1f 40 00 nopl 0x0(%rax)
1030: f3 0f 1e fa endbr64
1034: 68 00 00 00 00 push $0x0
1039: e9 e2 ff ff ff jmp 1020 <_init+0x20>
103e: 66 90 xchg %ax,%ax
$ objdump -d libpic.so --section=.plt.sec
0000000000001050 <prog_func@plt>:
1050: f3 0f 1e fa endbr64
1054: ff 25 a6 2f 00 00 jmp *0x2fa6(%rip) # 4000 <prog_func>
105a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
Referências
Notas de rodapé