Noções básicas sobre modelos de código de arquitetura x64

"Qual modelo de código devo usar?" - uma pergunta que surge com frequência, mas com pouca frequência ao escrever código para a arquitetura x64. No entanto, esse é um problema bastante interessante e é útil ter uma idéia dos modelos de código para entender o código de máquina x64 gerado pelos compiladores. Além disso, para aqueles que estão preocupados com o desempenho até as menores instruções, a escolha do modelo de código também afeta a otimização.



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 mainacessa 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;
}


gccutiliza o modelo de código como o valor da opção -mcmodel. Além disso, uma -fpiccompilaçã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 movpara que corresponda a 0x3s. Estamos interessados ​​na segunda realocação, R_X86_64_PC32que aponta para o operando movno endereço 0x38e 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 01significa "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_arraqui 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_arrpode 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 callqtambém é relativo ao RIP, a realocação R_X86_64_PC32funciona 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 gccum 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 maincompilado 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) + 01dentro de eax.



Então, vamos focar a equipe no 0x3d, movabsversão de 64 bits absoluta movna 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_64para 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,raxconterá 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 é movabsseguido por um comando callque 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 .datae .bsscriar seções especiais: .ldatae .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 _bigpassa 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_arrslot reservado no GOT é corrigido no comando . Assim, o endereço real é colocado raxno comando por 0x36endereço global_arr. Esta etapa é seguida por uma redefinição da referência ao endereço global_arrmais 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 callqendereç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, callqdeve 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_arrglobal_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 0x1ecarrega 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 0x2fadiciona 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 rbxagora 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_arre 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_arrum 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_GOT64para movabssó informa a função de colocar a mudança no GOT onde o raxendereço está localizado global_arr. O comando no endereço 0x5fpega o endereço global_arrdo 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_funcendereço de entrada PLT menos o endereço GOT. O resultado é colocado rdxonde é colocado rbx(endereço GOT absoluto). Como resultado, obtemos o endereço PLT de entrada para global_funcat 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_funccompletamente semelhante ao mesmo código no modelo PIC pequeno, bem como para pequenas matrizes de dados static_arre 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_GOTPC32o 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_bigser 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_bigna 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









All Articles