As informações sobre esse tópico na web ou em outros lugares são raras. O mais importante dos recursos disponíveis é a ABI oficial x64, você pode baixá-la aqui (daqui em diante será chamada de "ABI"). Algumas informações também podem ser encontradas na página
man
.gcc
. O objetivo deste artigo é fornecer recomendações acessíveis sobre o tópico, discutir questões relacionadas e também demonstrar alguns conceitos por meio do código usado no trabalho, com bons exemplos.
Nota importante: Este artigo não é um tutorial para iniciantes. Antes do conhecimento, é recomendável ter um forte comando de C e assembler, além de um conhecimento básico da arquitetura x64.
Consulte também nossa postagem anterior sobre um tópico relacionado : Como x86_x64 trata de memória
Modelos de código. Parte motivacional
Na arquitetura x64, o código e os dados são enviados por meio de modelos de endereçamento relativos ao comando (ou, usando o jargão x64, relativo ao RIP). Nesses comandos, a mudança do RIP é limitada a 32 bits, no entanto, pode haver casos em que um comando, ao tentar endereçar parte da memória ou dados, simplesmente não possui mudança suficiente de 32 bits, por exemplo, ao trabalhar com programas com mais de dois gigabytes.
Uma maneira de resolver esse problema é abandonar completamente o modo de endereçamento relativo ao RIP em favor de uma mudança completa de 64 bits para todas as referências de dados e código. No entanto, esta etapa será muito cara: para cobrir o caso (bastante raro) de programas e bibliotecas incrivelmente grandes, mesmo as operações mais simples de todo o código exigirão um número maior de comandos do que o normal.
Assim, os modelos de código se tornam um trade-off. [1] Um modelo de código é um acordo formal entre o programador e o compilador, no qual o programador especifica suas intenções sobre o tamanho do (s) programa (s) esperado (s) em que o módulo de objeto compilado atualmente se enquadra. [2] Os modelos de código são necessários para que o programador possa dizer ao compilador: "Não se preocupe, este módulo de objeto entrará apenas em pequenos programas, para que você possa usar modos de endereçamento relativos a RIP rápidos". Por outro lado, pode dizer ao compilador o seguinte: "vamos vincular este módulo a programas grandes; portanto, use os modos de endereçamento absoluto de lazer e segurança com turno total de 64 bits".
Sobre o que este artigo falará
Falaremos sobre os dois cenários descritos acima, o modelo de código pequeno e o modelo de código grande: o primeiro modelo informa ao compilador que um deslocamento relativo de 32 bits deve ser suficiente para todas as referências de código e dados na unidade de objeto; o segundo insiste em que o compilador use modos absolutos de endereçamento de 64 bits. Além disso, há também uma versão intermediária, o chamado modelo de código do meio .
Cada um desses modelos de código é apresentado em variações independentes de PIC e não PIC, e falaremos sobre cada um dos seis.
Exemplo C original
Para demonstrar os conceitos discutidos neste artigo, usarei o programa C abaixo e o compilaremos com vários modelos de código. Como você pode ver, a função
main
acessa quatro matrizes globais diferentes e uma função global. As matrizes diferem em dois parâmetros: tamanho e visibilidade. O tamanho é importante para explicar o modelo de código médio e não será necessário ao trabalhar com modelos pequenos e grandes. A visibilidade é importante para a operação dos modelos de código PIC e pode ser estática (visibilidade apenas no arquivo de origem) ou global (visibilidade para todos os objetos compilados no programa).
int global_arr[100] = {2, 3};
static int static_arr[100] = {9, 7};
int global_arr_big[50000] = {5, 6};
static int static_arr_big[50000] = {10, 20};
int global_func(int param)
{
return param * 10;
}
int main(int argc, const char* argv[])
{
int t = global_func(argc);
t += global_arr[7];
t += static_arr[7];
t += global_arr_big[7];
t += static_arr_big[7];
return t;
}
gcc
utiliza o modelo de código como o valor da opção -mcmodel
. Além disso, uma -fpic
compilação PIC pode ser definida com um sinalizador .
Um exemplo de compilação para um módulo de objeto por meio de um modelo de código grande usando PIC:
> gcc -g -O0 -c codemodel1.c -fpic -mcmodel=large -o codemodel1_large_pic.o
Modelo de código pequeno
Tradução de uma citação de man gcc sobre o assunto de um modelo de código pequeno:
-mcmodel = pequeno
Geração de código para um modelo pequeno: o programa e seus símbolos devem estar vinculados nos dois gigabytes inferiores do espaço de endereço. O tamanho dos ponteiros é de 64 bits. Os programas podem ser vinculados estaticamente ou dinamicamente. Este é o modelo de código básico.
Em outras palavras, o compilador pode assumir com segurança que o código e os dados são acessíveis por meio de um deslocamento relativo de RIP de 32 bits a partir de qualquer comando no código. Vamos dar uma olhada em um exemplo desmontado de um programa C que compilamos por meio de um modelo de código pequeno não-PIC:
> objdump -dS codemodel1_small.o
[...]
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
3c: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
3f: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
45: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
48: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
4e: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
51: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
57: 01 45 fc add %eax,-0x4(%rbp)
return t;
5a: 8b 45 fc mov -0x4(%rbp),%eax
}
5d: c9 leaveq
5e: c3 retq
Como você pode ver, o acesso a todas as matrizes é organizado da mesma maneira - usando o turno relativo ao RIP. No entanto, no código, o deslocamento é 0, pois o compilador não sabe onde o segmento de dados será colocado; portanto, para cada acesso, ele cria uma realocação:
> readelf -r codemodel1_small.o
Relocation section '.rela.text' at offset 0x62bd8 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000002f 001500000002 R_X86_64_PC32 0000000000000000 global_func - 4
000000000038 001100000002 R_X86_64_PC32 0000000000000000 global_arr + 18
000000000041 000300000002 R_X86_64_PC32 0000000000000000 .data + 1b8
00000000004a 001200000002 R_X86_64_PC32 0000000000000340 global_arr_big + 18
000000000053 000300000002 R_X86_64_PC32 0000000000000000 .data + 31098
Vamos decodificar completamente o acesso ao
global_arr
. O segmento desmontado de seu interesse:
t += global_arr[7];
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
3c: 01 45 fc add %eax,-0x4(%rbp)
O endereçamento relativo ao RIP é relativo ao próximo comando; portanto, o turno deve ser corrigido para o comando
mov
para que corresponda a 0x3s. Estamos interessados na segunda realocação, R_X86_64_PC32
que aponta para o operando mov
no endereço 0x38
e significa o seguinte: pegamos o valor do símbolo, adicionamos o termo e subtraímos o turno indicado pela realocação. Se você calculou tudo corretamente, verá como o resultado fará uma mudança relativa entre o próximo comando e global_arr
, mais 01
. Como 01
significa "o sétimo int na matriz" (na arquitetura x64, o tamanho de cada um int
é de 4 bytes), precisamos dessa mudança relativa. Assim, usando o endereçamento relativo ao RIP, o comando faz referência corretamente global_arr[7]
.
Também é interessante observar o seguinte: embora os comandos de acesso
static_arr
aqui sejam semelhantes, seu redirecionamento usa um símbolo diferente, apontando assim para uma seção em vez de um símbolo específico .data
. Isso ocorre devido às ações do vinculador, ele coloca uma matriz estática em um local conhecido na seção e, portanto, a matriz não pode ser usada em conjunto com outras bibliotecas compartilhadas. Como resultado, o vinculador resolverá a situação com essa realocação. Por outro lado, como global_arr
pode ser usado (ou substituído) por outra biblioteca compartilhada, o carregador já dinâmico precisará descobrir o link para global_arr
. [3]
Finalmente, vamos dar uma olhada na referência a
global_func
:
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
Como o operando
callq
também é relativo ao RIP, a realocação R_X86_64_PC32
funciona aqui da mesma maneira que coloca o deslocamento relativo real para global_func no operando.
Concluindo, observamos que, devido ao modelo de código pequeno, o compilador percebe todos os dados e códigos do programa futuro como acessíveis por meio de uma mudança de 32 bits e, assim, cria código simples e eficiente para acessar todos os tipos de objetos.
Modelo de código grande
Tradução de uma citação de
man
gcc
um modelo de código grande:
-mcmodel = grande
Gerando código para um modelo grande: este modelo não faz suposições sobre os endereços e tamanhos de seção.
Um exemplo de código desmontado
main
compilado usando um modelo grande não PIC:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
35: 00 00 00
38: ff d2 callq *%rdx
3a: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
3d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
44: 00 00 00
47: 8b 40 1c mov 0x1c(%rax),%eax
4a: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
4d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
54: 00 00 00
57: 8b 40 1c mov 0x1c(%rax),%eax
5a: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
5d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
64: 00 00 00
67: 8b 40 1c mov 0x1c(%rax),%eax
6a: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
6d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
74: 00 00 00
77: 8b 40 1c mov 0x1c(%rax),%eax
7a: 01 45 fc add %eax,-0x4(%rbp)
return t;
7d: 8b 45 fc mov -0x4(%rbp),%eax
}
80: c9 leaveq
81: c3 retq
Mais uma vez, é útil observar as realocações:
Relocation section '.rela.text' at offset 0x62c18 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000030 001500000001 R_X86_64_64 0000000000000000 global_func + 0
00000000003f 001100000001 R_X86_64_64 0000000000000000 global_arr + 0
00000000004f 000300000001 R_X86_64_64 0000000000000000 .data + 1a0
00000000005f 001200000001 R_X86_64_64 0000000000000340 global_arr_big + 0
00000000006f 000300000001 R_X86_64_64 0000000000000000 .data + 31080
Como não há necessidade de fazer suposições sobre o tamanho das seções e dos dados do código, o modelo de código grande é bastante unificado e identifica o acesso a todos os dados da mesma maneira. Vamos dar uma outra olhada em
global_arr
:
t += global_arr[7];
3d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
44: 00 00 00
47: 8b 40 1c mov 0x1c(%rax),%eax
4a: 01 45 fc add %eax,-0x4(%rbp)
Dois comandos precisam obter o valor desejado da matriz. O primeiro comando coloca um endereço absoluto de 64 bits
rax
, que, como veremos em breve, acaba sendo um endereço global_arr
, enquanto o segundo comando carrega uma palavra de (rax) + 01
dentro de eax
.
Então, vamos focar a equipe no
0x3d
, movabs
versão de 64 bits absoluta mov
na arquitetura x64. Ele pode soltar a constante completa de 64 bits diretamente no registrador e, como em nosso código desmontado o valor dessa constante é zero, teremos que consultar a tabela de realocação para obter a resposta. Nele, encontraremos a realocação absoluta R_X86_64_64
para o operando no endereço 0x3f
, com o seguinte valor: colocar o valor do símbolo mais a soma de volta no turno. Em outras palavras,rax
conterá um endereço absoluto global_arr
.
E a função de chamada?
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
35: 00 00 00
38: ff d2 callq *%rdx
3a: 89 45 fc mov %eax,-0x4(%rbp)
O que já sabemos é
movabs
seguido por um comando call
que chama uma função no endereço em rdx
. Basta olhar para a realocação correspondente para entender como é semelhante ao acesso a dados.
Como você pode ver, o modelo de código grande não faz suposições sobre o tamanho das seções de código e dados, bem como sobre o local final dos caracteres, ele simplesmente se refere a caracteres através de etapas absolutas de 64 bits, uma espécie de "caminho seguro". No entanto, observe como, comparado a um modelo de código pequeno, um modelo grande é forçado a usar um comando adicional ao acessar cada caractere. Este é o preço da segurança.
Portanto, encontramos dois modelos completamente opostos: enquanto o modelo de código pequeno supõe que tudo se encaixa nos dois gigabytes de memória inferiores, o modelo grande supõe que nada é impossível e que qualquer caractere pode estar em qualquer lugar na sua totalidade. pouco espaço de endereço. O trade-off entre os dois modelos é o modelo do código do meio.
Modelo de código médio
Como antes, vamos dar uma olhada na tradução da citação de
man
gcc
:
-mcmodel=medium
: . . , -mlarge-data-threshold, bss . , .
Semelhante ao modelo de código pequeno, o modelo do meio pressupõe que o código inteiro esteja organizado em dois gigabytes inferiores. No entanto, os dados são divididos em supostamente organizados nos dois gigabytes inferiores de "dados pequenos" e ilimitados no espaço de memória "dados grandes". Os dados se enquadram na categoria grande quando excedem o limite, por definição, igual a 64 kilobytes.
Também é importante observar que, ao trabalhar com o modelo de código médio para dados grandes, semelhantes às seções
.data
e .bss
criar seções especiais: .ldata
e .lbss
. Isso não é tão importante no prisma do tópico do artigo atual, mas vou me afastar um pouco dele. Mais detalhes sobre esse assunto podem ser encontrados na ABI.
Agora fica claro por que essas matrizes apareceram no exemplo
_big
: eles são necessários pelo modelo médio para interpretar os "big data" que são, com 200 kilobytes cada. Abaixo você pode ver o resultado da desmontagem:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
3c: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
3f: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
45: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
48: 48 b8 00 00 00 00 00 movabs $0x0,%rax
4f: 00 00 00
52: 8b 40 1c mov 0x1c(%rax),%eax
55: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
58: 48 b8 00 00 00 00 00 movabs $0x0,%rax
5f: 00 00 00
62: 8b 40 1c mov 0x1c(%rax),%eax
65: 01 45 fc add %eax,-0x4(%rbp)
return t;
68: 8b 45 fc mov -0x4(%rbp),%eax
}
6b: c9 leaveq
6c: c3 retq
Preste atenção em como o acesso é feito a matrizes: o acesso a matrizes
_big
passa por métodos de um modelo de código grande, enquanto o acesso a outras matrizes passa por métodos de um modelo pequeno. A função também é chamada usando o método de modelo de código pequeno, e as realocações são tão semelhantes aos exemplos anteriores que eu nem as demonstrarei.
O modelo de código médio é uma troca hábil entre modelos grandes e pequenos. É improvável que o código do programa seja muito grande [4], portanto, apenas grandes pedaços de dados estaticamente vinculados a ele podem movê-lo além do limite de dois gigabytes, talvez como parte de algum tipo de pesquisa de tabela volumosa. Como o modelo de código médio filtra grandes quantidades de dados e os processa de maneira especial, as chamadas para funções e caracteres pequenos pelo código serão tão eficazes quanto no modelo de código pequeno. Somente acessos a símbolos grandes, por analogia com o modelo grande, exigirão que o código use o método completo de 64 bits do modelo grande.
Modelo de código PIC pequeno
Agora, vejamos as opções de modelo de código PIC e, como antes, começaremos com um modelo pequeno. [5] Abaixo você pode ver um exemplo do código compilado através do pequeno modelo PIC:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
36: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
3d: 8b 40 1c mov 0x1c(%rax),%eax
40: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
43: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
49: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
4c: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
53: 8b 40 1c mov 0x1c(%rax),%eax
56: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
59: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
5f: 01 45 fc add %eax,-0x4(%rbp)
return t;
62: 8b 45 fc mov -0x4(%rbp),%eax
}
65: c9 leaveq
66: c3 retq
Realocações:
Relocation section '.rela.text' at offset 0x62ce8 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000002f 001600000004 R_X86_64_PLT32 0000000000000000 global_func - 4
000000000039 001100000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4
000000000045 000300000002 R_X86_64_PC32 0000000000000000 .data + 1b8
00000000004f 001200000009 R_X86_64_GOTPCREL 0000000000000340 global_arr_big - 4
00000000005b 000300000002 R_X86_64_PC32 0000000000000000 .data + 31098
Como as diferenças entre dados grandes e pequenos não desempenham nenhum papel no modelo de código pequeno, focaremos nos pontos importantes ao gerar código através do PIC: as diferenças entre símbolos locais (estáticos) e globais.
Como você pode ver, não há diferença entre o código gerado para matrizes estáticas e o código no caso não PIC. Essa é uma das vantagens da arquitetura x64: graças ao acesso relativo aos IP aos dados, obtemos um PIC como bônus, pelo menos até que o acesso externo aos caracteres seja necessário. Todos os comandos e realocações permanecem os mesmos, portanto, não há necessidade de processá-los novamente.
É interessante prestar atenção às matrizes globais: vale lembrar que no PIC, os dados globais devem passar pelo GOT, já que em algum momento eles podem ser armazenados ou usados por bibliotecas compartilhadas [6]. Abaixo você pode ver o código para acessar
global_arr
:
t += global_arr[7];
36: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
3d: 8b 40 1c mov 0x1c(%rax),%eax
40: 01 45 fc add %eax,-0x4(%rbp)
A realocação de interesse para nós é
R_X86_64_GOTPCREL
: a posição da entrada do símbolo no GOT mais o termo, menos o turno para aplicar a realocação. Em outras palavras, o deslocamento relativo entre o RIP (próxima instrução) e o global_arr
slot reservado no GOT é corrigido no comando . Assim, o endereço real é colocado rax
no comando por 0x36
endereço global_arr
. Esta etapa é seguida por uma redefinição da referência ao endereço global_arr
mais um deslocamento para o sétimo elemento em eax
.
Agora vamos dar uma olhada na chamada de função:
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
Ele tem uma deslocalização do operando
callq
endereço 0x2e
, R_X86_64_PLT32
: endereço de entrada PLT para o deslocamento negativo símbolo de mais prazo para a aplicação de deslocalização. Em outras palavras, callq
deve chamar corretamente o trampolim PLT global_func
.
Observe as suposições implícitas que o compilador faz: que o GOT e o PLT podem ser acessados por meio de endereçamento relativo ao RIP. Isso será importante ao comparar este modelo com outras variantes PIC de modelos de código.
Modelo de código PIC grande
Desmontagem:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 53 push %rbx
1a: 48 83 ec 28 sub $0x28,%rsp
1e: 48 8d 1d f9 ff ff ff lea -0x7(%rip),%rbx
25: 49 bb 00 00 00 00 00 movabs $0x0,%r11
2c: 00 00 00
2f: 4c 01 db add %r11,%rbx
32: 89 7d dc mov %edi,-0x24(%rbp)
35: 48 89 75 d0 mov %rsi,-0x30(%rbp)
int t = global_func(argc);
39: 8b 45 dc mov -0x24(%rbp),%eax
3c: 89 c7 mov %eax,%edi
3e: b8 00 00 00 00 mov $0x0,%eax
43: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
4a: 00 00 00
4d: 48 01 da add %rbx,%rdx
50: ff d2 callq *%rdx
52: 89 45 ec mov %eax,-0x14(%rbp)
t += global_arr[7];
55: 48 b8 00 00 00 00 00 movabs $0x0,%rax
5c: 00 00 00
5f: 48 8b 04 03 mov (%rbx,%rax,1),%rax
63: 8b 40 1c mov 0x1c(%rax),%eax
66: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr[7];
69: 48 b8 00 00 00 00 00 movabs $0x0,%rax
70: 00 00 00
73: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
77: 01 45 ec add %eax,-0x14(%rbp)
t += global_arr_big[7];
7a: 48 b8 00 00 00 00 00 movabs $0x0,%rax
81: 00 00 00
84: 48 8b 04 03 mov (%rbx,%rax,1),%rax
88: 8b 40 1c mov 0x1c(%rax),%eax
8b: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr_big[7];
8e: 48 b8 00 00 00 00 00 movabs $0x0,%rax
95: 00 00 00
98: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
9c: 01 45 ec add %eax,-0x14(%rbp)
return t;
9f: 8b 45 ec mov -0x14(%rbp),%eax
}
a2: 48 83 c4 28 add $0x28,%rsp
a6: 5b pop %rbx
a7: c9 leaveq
a8: c3 retq
Realocações: Desta vez , as diferenças entre grandes e pequenos dados ainda não importam, portanto, focaremos e . Mas primeiro, você precisa prestar atenção ao prólogo deste código, não encontramos anteriormente algo como isto:
Relocation section '.rela.text' at offset 0x62c70 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000027 00150000001d R_X86_64_GOTPC64 0000000000000000 _GLOBAL_OFFSET_TABLE_ + 9
000000000045 00160000001f R_X86_64_PLTOFF64 0000000000000000 global_func + 0
000000000057 00110000001b R_X86_64_GOT64 0000000000000000 global_arr + 0
00000000006b 000800000019 R_X86_64_GOTOFF64 00000000000001a0 static_arr + 0
00000000007c 00120000001b R_X86_64_GOT64 0000000000000340 global_arr_big + 0
000000000090 000900000019 R_X86_64_GOTOFF64 0000000000031080 static_arr_big + 0
static_arr
global_arr
1e: 48 8d 1d f9 ff ff ff lea -0x7(%rip),%rbx
25: 49 bb 00 00 00 00 00 movabs $0x0,%r11
2c: 00 00 00
2f: 4c 01 db add %r11,%rbx
Abaixo, você pode ler a tradução da citação relacionada da ABI:
( GOT) AMD64 IP- . GOT . GOT , AMD64 ISA 32 .
Vamos dar uma olhada em como o prólogo descrito acima calcula o endereço GOT. Primeiro, a equipe de endereços
0x1e
carrega seu próprio endereço rbx
. Em seguida, juntamente com a realocação, R_X86_64_GOTPC64
é executada uma etapa absoluta de 64 bits r11
. Essa realocação significa o seguinte: pegue o endereço do GOT, subtraia o turno alternado e adicione o termo. Finalmente, o comando no endereço 0x2f
adiciona os dois resultados. O resultado é o endereço absoluto do GOT rbx
. [7]
Por que se preocupar em calcular o endereço GOT? Primeiramente, como observado na citação, em um modelo de código grande, não podemos assumir que um deslocamento relativo de RIP de 32 bits seja suficiente para o endereçamento GOT, e é por isso que precisamos de um endereço de 64 bits completo. Em segundo lugar, ainda queremos trabalhar com a variação do PIC, portanto, não podemos simplesmente colocar o endereço absoluto no registro. Em vez disso, o próprio endereço deve ser calculado em relação ao RIP. É para isso que serve o prólogo: ele executa uma computação relativa ao RIP de 64 bits.
De qualquer forma, como
rbx
agora temos um endereço GOT, vejamos como acessar static_arr
:
t += static_arr[7];
69: 48 b8 00 00 00 00 00 movabs $0x0,%rax
70: 00 00 00
73: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
77: 01 45 ec add %eax,-0x14(%rbp)
A realocação do primeiro comando é
R_X86_64_GOTOFF64
: símbolo mais termo menos GOT. No nosso caso, essa é uma mudança relativa entre o endereço static_arr
e o endereço GOT. A instrução a seguir adiciona o resultado a rbx
(endereço GOT absoluto) e redefine o link com um deslocamento de 0x1c
. Para facilitar a visualização desse cálculo, abaixo você pode ver o exemplo do pseudo-C:
// char* static_arr
// char* GOT
rax = static_arr + 0 - GOT; // rax now contains an offset
eax = *(rbx + rax + 0x1c); // rbx == GOT, so eax now contains
// *(GOT + static_arr - GOT + 0x1c) or
// *(static_arr + 0x1c)
Preste atenção a um ponto interessante: o endereço GOT é usado como uma ligação para
static_arr
. Normalmente, um GOT não contém um endereço de símbolo e, como não é static_arr
um símbolo externo, não há motivo para armazená-lo dentro de um GOT. No entanto, nesse caso, o GOT é usado como uma ligação ao endereço de símbolo relativo da seção de dados. Esse endereço, que, entre outras coisas, é independente da localização, pode ser encontrado com um turno completo de 64 bits. O vinculador é capaz de lidar com essa realocação, portanto, não há necessidade de modificar a seção do código no tempo de carregamento.
Mas que tal
global_arr
?
t += global_arr[7];
55: 48 b8 00 00 00 00 00 movabs $0x0,%rax
5c: 00 00 00
5f: 48 8b 04 03 mov (%rbx,%rax,1),%rax
63: 8b 40 1c mov 0x1c(%rax),%eax
66: 01 45 ec add %eax,-0x14(%rbp)
Esse código é um pouco mais longo e a realocação é diferente da usual. Em essência, GOT é utilizado aqui de uma forma mais tradicional: deslocalização
R_X86_64_GOT64
para movabs
só informa a função de colocar a mudança no GOT onde o rax
endereço está localizado global_arr
. O comando no endereço 0x5f
pega o endereço global_arr
do GOT e o coloca rax
. O seguinte comando repõe a referência ao global_arr[7]
e coloca o valor em eax
.
Agora vamos dar uma olhada no link do código para
global_func
. Lembre-se de que em um modelo de código grande, não podemos fazer suposições sobre o tamanho das seções de código; portanto, devemos assumir que, mesmo para acessar o PLT, precisamos de um endereço absoluto de 64 bits:
int t = global_func(argc);
39: 8b 45 dc mov -0x24(%rbp),%eax
3c: 89 c7 mov %eax,%edi
3e: b8 00 00 00 00 mov $0x0,%eax
43: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
4a: 00 00 00
4d: 48 01 da add %rbx,%rdx
50: ff d2 callq *%rdx
52: 89 45 ec mov %eax,-0x14(%rbp)
A realocação em que estamos interessados é
R_X86_64_PLTOFF64
: o global_func
endereço de entrada PLT menos o endereço GOT. O resultado é colocado rdx
onde é colocado rbx
(endereço GOT absoluto). Como resultado, obtemos o endereço PLT de entrada para global_func
at rdx
.
Observe que novamente o GOT é usado como uma âncora, desta vez para fornecer uma referência independente de endereço ao deslocamento da entrada PLT.
Modelo de código PIC médio
Por fim, detalharemos o código gerado para o modelo PIC médio:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 53 push %rbx
1a: 48 83 ec 28 sub $0x28,%rsp
1e: 48 8d 1d 00 00 00 00 lea 0x0(%rip),%rbx
25: 89 7d dc mov %edi,-0x24(%rbp)
28: 48 89 75 d0 mov %rsi,-0x30(%rbp)
int t = global_func(argc);
2c: 8b 45 dc mov -0x24(%rbp),%eax
2f: 89 c7 mov %eax,%edi
31: b8 00 00 00 00 mov $0x0,%eax
36: e8 00 00 00 00 callq 3b <main+0x26>
3b: 89 45 ec mov %eax,-0x14(%rbp)
t += global_arr[7];
3e: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
45: 8b 40 1c mov 0x1c(%rax),%eax
48: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr[7];
4b: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
51: 01 45 ec add %eax,-0x14(%rbp)
t += global_arr_big[7];
54: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
5b: 8b 40 1c mov 0x1c(%rax),%eax
5e: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr_big[7];
61: 48 b8 00 00 00 00 00 movabs $0x0,%rax
68: 00 00 00
6b: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
6f: 01 45 ec add %eax,-0x14(%rbp)
return t;
72: 8b 45 ec mov -0x14(%rbp),%eax
}
75: 48 83 c4 28 add $0x28,%rsp
79: 5b pop %rbx
7a: c9 leaveq
7b: c3 retq
Realocações:
Relocation section '.rela.text' at offset 0x62d60 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000021 00160000001a R_X86_64_GOTPC32 0000000000000000 _GLOBAL_OFFSET_TABLE_ - 4
000000000037 001700000004 R_X86_64_PLT32 0000000000000000 global_func - 4
000000000041 001200000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4
00000000004d 000300000002 R_X86_64_PC32 0000000000000000 .data + 1b8
000000000057 001300000009 R_X86_64_GOTPCREL 0000000000000000 global_arr_big - 4
000000000063 000a00000019 R_X86_64_GOTOFF64 0000000000030d40 static_arr_big + 0
Primeiro, vamos remover a chamada de função. Semelhante ao modelo pequeno, no modelo intermediário, assumimos que as referências de código não excedem os limites do turno RIP de 32 bits; portanto, o código para a chamada é
global_func
completamente semelhante ao mesmo código no modelo PIC pequeno, bem como para pequenas matrizes de dados static_arr
e global_arr
. Portanto, vamos nos concentrar em matrizes de big data, mas primeiro vamos falar sobre o prólogo: aqui ele difere do prólogo do modelo de big data.
1e: 48 8d 1d 00 00 00 00 lea 0x0(%rip),%rbx
Este é o prólogo inteiro: foram necessários apenas um comando para realocar
R_X86_64_GOTPC32
o endereço GOT rbx
(comparado a três no modelo grande). Qual é a diferença? O fato é que, como no modelo intermediário, o GOT não faz parte das "seções de big data", assumimos sua disponibilidade no turno de 32 bits. No modelo grande, não podíamos fazer tais suposições e tivemos que usar a mudança completa de 64 bits.
De interesse é o fato de o código de acesso
global_arr_big
ser semelhante ao mesmo código no modelo PIC pequeno. Isso acontece pela mesma razão que o prólogo do modelo intermediário é mais curto que o prólogo do modelo grande: consideramos a disponibilidade do GOT como parte do endereçamento relativo ao RIP de 32 bits. De fato, para mimglobal_arr_big
é impossível obter esse acesso, mas esse caso ainda cobre o GOT, pois ele está, global_arr_big
na verdade, na forma de um endereço completo de 64 bits.
A situação, no entanto, é diferente para
static_arr_big
:
t += static_arr_big[7];
61: 48 b8 00 00 00 00 00 movabs $0x0,%rax
68: 00 00 00
6b: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
6f: 01 45 ec add %eax,-0x14(%rbp)
Este caso é semelhante ao grande modelo de código PIC, pois aqui ainda temos o endereço absoluto do símbolo, que não está no próprio GOT. Como esse é um símbolo grande, que não se pode supor estar nos dois gigabytes inferiores, nós, como no modelo grande, exigimos uma mudança de PIC de 64 bits.
Notas:
[1] Não confunda modelos de código com modelos de dados de 64 bits e modelos de memória Intel , todos esses são tópicos diferentes.
[2] É importante lembrar: os comandos reais são criados pelo compilador e os modos de endereçamento são corrigidos exatamente nesta etapa. O compilador não pode saber em quais programas ou bibliotecas compartilhadas o módulo de objetos se enquadra; alguns podem ser pequenos, enquanto outros podem ser grandes. O vinculador sabe o tamanho do programa final, mas é tarde demais: o vinculador só pode corrigir a troca de comandos com a realocação e não alterar os próprios comandos. Portanto, a "convenção" do modelo de código deve ser "assinada" pelo programador em tempo de compilação.
[3] Se algo permanecer incerto, confira o próximo artigo .
[4] No entanto, os volumes estão aumentando gradualmente. Quando eu verifiquei pela última vez a compilação Debug + Asserts do Clang, ele quase atingiu um gigabyte, graças em grande parte ao código gerado automaticamente.
[5] Se você ainda não sabe como o PIC funciona (em geral e em particular para a arquitetura x64), é hora de ler os seguintes artigos sobre o assunto: um e dois .
[6] Portanto, o vinculador não pode resolver os links por conta própria e precisa mudar o processamento GOT para o carregador dinâmico.
[7] 0x25 - 0x7 + GOT - 0x27 + 0x9 = GOT