Olá pessoal! A tradução do artigo foi preparada especialmente para os alunos do curso "Desenvolvedor C ++" . É interessante desenvolver nessa direção? Fique on-line em 13 de dezembro às 20:00, horário de Moscou. para a aula principal "Pratique usando o Google Test Framework" !

Neste artigo, veremos como o clang implementa vtables (tabelas de métodos virtuais) e RTTI (identificação do tipo de tempo de execução). Na primeira parte, começamos com as classes base e, em seguida, analisamos a herança múltipla e virtual.
Observe que, neste artigo, precisamos investigar a representação binária gerada para várias partes do nosso código usando gdb. Este é um nível bastante baixo, mas farei todo o trabalho duro por você. Não acho que a maioria dos posts futuros descreva os detalhes de um nível tão baixo.
Isenção de responsabilidade : tudo o que está escrito aqui depende da implementação, pode mudar em qualquer versão futura, portanto você não deve confiar nela. Consideramos isso apenas para fins educacionais.

excelente, então vamos começar.
Parte 1 - vtables - noções básicas
Vejamos o seguinte código:
#include <iostream> using namespace std; class NonVirtualClass { public: void foo() {} }; class VirtualClass { public: virtual void foo() {} }; int main() { cout << "Size of NonVirtualClass: " << sizeof(NonVirtualClass) << endl; cout << "Size of VirtualClass: " << sizeof(VirtualClass) << endl; }
$ # main.cpp $ clang++ main.cpp && ./a.out Size of NonVirtualClass: 1 Size of VirtualClass: 8
NonVirtualClass
tem um tamanho de 1 byte, porque nas classes C ++ não pode ter um tamanho zero. No entanto, isso não é importante agora.
VirtualClass
tem 8 bytes em uma máquina de 64 bits. Porque Porque lá dentro existe um ponteiro oculto apontando para uma tabela. vtables são tabelas de conversão estáticas criadas para cada classe virtual. Este artigo fala sobre seu conteúdo e como eles são usados.
Para entender melhor como é o vtables, vejamos o seguinte código com o gdb para descobrir como a memória é alocada:
#include <iostream> class Parent { public: virtual void Foo() {} virtual void FooNotOverridden() {} }; class Derived : public Parent { public: void Foo() override {} }; int main() { Parent p1, p2; Derived d1, d2; std::cout << "done" << std::endl; }
$ # , gdb $ clang++ -std=c++14 -stdlib=libc++ -g main.cpp && gdb ./a.out ... (gdb) # gdb - C++ (gdb) set print asm-demangle on (gdb) set print demangle on (gdb) # main (gdb) b main Breakpoint 1 at 0x4009ac: file main.cpp, line 15. (gdb) run Starting program: /home/shmike/cpp/a.out Breakpoint 1, main () at main.cpp:15 15 Parent p1, p2; (gdb) # (gdb) n 16 Derived d1, d2; (gdb) # (gdb) n 18 std::cout << "done" << std::endl; (gdb) # p1, p2, d1, d2 - , (gdb) p p1 $1 = {_vptr$Parent = 0x400bb8 <vtable for Parent+16>} (gdb) p p2 $2 = {_vptr$Parent = 0x400bb8 <vtable for Parent+16>} (gdb) p d1 $3 = {<Parent> = {_vptr$Parent = 0x400b50 <vtable for Derived+16>}, <No data fields>} (gdb) p d2 $4 = {<Parent> = {_vptr$Parent = 0x400b50 <vtable for Derived+16>}, <No data fields>}
Aqui está o que aprendemos acima:
- Apesar de as classes não terem membros de dados, há um ponteiro oculto para a vtable;
- vtable para p1 e p2 é o mesmo. vtables são dados estáticos para cada tipo;
- d1 e d2 herdam o ponteiro vtable do pai, que aponta para vtable Derived;
- Todas as vtables indicam um deslocamento de 16 (0x10) bytes na vtable. Também discutiremos isso mais tarde.
Vamos continuar nossa sessão gdb para ver o conteúdo de vtables. Vou usar o comando x, que exibe a memória na tela. Vamos gerar 300 bytes em hexadecimal, começando com 0x400b40. Por que exatamente esse endereço? Como vimos acima, o ponteiro vtable aponta para 0x400b50 e o símbolo desse endereço é vtable for Derived+16 (16 == 0x10)
.
(gdb) x/300xb 0x400b40 0x400b40 <vtable for Derived>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b48 <vtable for Derived+8>: 0x90 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400b50 <vtable for Derived+16>: 0x80 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b58 <vtable for Derived+24>: 0x90 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b60 <typeinfo name for Derived>: 0x37 0x44 0x65 0x72 0x69 0x76 0x65 0x64 0x400b68 <typeinfo name for Derived+8>: 0x00 0x36 0x50 0x61 0x72 0x65 0x6e 0x74 0x400b70 <typeinfo name for Parent+7>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b78 <typeinfo for Parent>: 0x90 0x20 0x60 0x00 0x00 0x00 0x00 0x00 0x400b80 <typeinfo for Parent+8>: 0x69 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400b88: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b90 <typeinfo for Derived>: 0x10 0x22 0x60 0x00 0x00 0x00 0x00 0x00 0x400b98 <typeinfo for Derived+8>: 0x60 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400ba0 <typeinfo for Derived+16>: 0x78 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400ba8 <vtable for Parent>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400bb0 <vtable for Parent+8>: 0x78 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400bb8 <vtable for Parent+16>: 0xa0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400bc0 <vtable for Parent+24>: 0x90 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 ...
Nota: olhamos para caracteres não decorados (desmontados). Se você estiver realmente interessado, _ZTV é o prefixo da vtable, _ZTS é o prefixo da string de tipo (nome) e _ZTI para typeinfo.
Aqui está a estrutura vtable Parent
da vtable Parent
:
Aqui está a estrutura vtable Derived
da vtable Derived
:
1:
(gdb) # , 0x400aa0 (gdb) info symbol 0x400aa0 Parent::Foo() in section .text of a.out
2:
(gdb) info symbol 0x400a90 Parent::FooNotOverridden() in section .text of a.out
3:
(gdb) info symbol 0x400a80 Derived::Foo() in section .text of a.out
Lembre-se de que o ponteiro da vtable em Derived apontou para um deslocamento de +16 bytes na vtable? O terceiro ponteiro é o endereço do ponteiro do primeiro método. Quer um terceiro método? Não tem problema - adicione 2 sizeof (vazio ) ao ponteiro da tabela. Quer um registro typeinfo? vá para o ponteiro na frente dele.
Seguindo em frente - e a estrutura de registros typeinfo?
Parent
:
E aqui está a entrada typeinfo Derived
:
1:
(gdb) info symbol 0x602090 vtable for __cxxabiv1::__class_type_info@@CXXABI_1.3 + 16 in section .bss of a.out
2:
(gdb) x/s 0x400b69 0x400b69 <typeinfo name for Parent>: "6Parent"
3:
(gdb) info symbol 0x602210 vtable for __cxxabiv1::__si_class_type_info@@CXXABI_1.3 + 16 in section .bss of a.out
4:
(gdb) x/s 0x400b60 0x400b60 <typeinfo name for Derived>: "7Derived"
Se você quiser saber mais sobre __si_class_type_info, pode encontrar algumas informações aqui e também aqui .
Isso esgota minhas habilidades com o gdb e também completa esta parte. Sugiro que algumas pessoas achem isso muito baixo, ou talvez simplesmente não tenha valor prático. Nesse caso, eu recomendaria pular as partes 2 e 3, indo direto para a parte 4 .
Parte 2 - Herança Múltipla
O mundo das hierarquias de herança única é mais fácil para o compilador. Como vimos na primeira parte, cada classe filho estende a vtable pai adicionando entradas para cada novo método virtual.
Vejamos a herança múltipla, o que complica a situação, mesmo quando a herança é implementada apenas puramente a partir de interfaces.
Vejamos o seguinte trecho de código:
class Mother { public: virtual void MotherMethod() {} int mother_data; }; class Father { public: virtual void FatherMethod() {} int father_data; }; class Child : public Mother, public Father { public: virtual void ChildMethod() {} int child_data; };
Observe que existem 2 ponteiros da tabela. Intuitivamente, eu esperaria 1 ou 3 ponteiros (mãe, pai e filho). De fato, é impossível ter um ponteiro (mais sobre isso mais adiante), e o compilador é inteligente o suficiente para combinar as entradas do child vtable Child como uma continuação do vtable Mother, economizando 1 ponteiro.
Por que uma criança não pode ter um ponteiro de tabela para todos os três tipos? Lembre-se de que um ponteiro filho pode ser passado para uma função que aceita um ponteiro mãe ou pai, e ambos esperam que esse ponteiro contenha os dados corretos nos deslocamentos corretos. Essas funções não precisam saber sobre Criança, e você definitivamente não deve assumir que Criança é realmente o que está sob o ponteiro Mãe / Pai com o qual elas operam.
(1) Não é relevante para este tópico, mas, no entanto, é interessante que os dados child_ sejam realmente colocados no preenchimento do Pai. Isso é chamado de preenchimento da cauda e pode ser o assunto de uma publicação futura.
Aqui está a estrutura vtable
:
Neste exemplo, a instância Child terá o mesmo ponteiro ao converter para o ponteiro Mother. Porém, ao converter para o ponteiro Pai, o compilador calcula o deslocamento desse ponteiro para apontar para a parte _vptr $ Father da Criança (terceiro campo na estrutura Criança, veja a tabela acima).
Em outras palavras, para um determinado filho c;: (vazio ) & c! = (Vazio ) static_cast <Pai *> (& c). Algumas pessoas não esperam isso, e talvez um dia essas informações economizem tempo na depuração.
Eu achei isso útil mais de uma vez. Mas espere, isso não é tudo.
E se Child decidisse substituir um dos métodos do Pai? Considere este código:
class Mother { public: virtual void MotherFoo() {} }; class Father { public: virtual void FatherFoo() {} }; class Child : public Mother, public Father { public: void FatherFoo() override {} };
A situação está ficando mais difícil. A função pode pegar o argumento Pai * e chamar PaiFoo () para ele. Mas se você passar a instância Child, espera-se chamar o método Child substituído com o ponteiro correto. No entanto, o chamador não sabe que ele realmente contém Criança. Ele tem um ponteiro para o deslocamento da Criança, onde está a localização do Pai. Alguém tem que deslocar o ponteiro deste, mas como fazê-lo? Que mágica faz o compilador para fazer isso funcionar?
Antes de respondermos, observe que a substituição de um dos métodos Mãe não é muito complicada, pois o ponteiro este é o mesmo. A criança sabe o que ler depois da vtable Mother e espera que os métodos da criança estejam logo após.
Aqui está a solução: o compilador cria um método thunk que corrige o ponteiro this e depois chama o método "real". O endereço do método do adaptador estará sob o vtable Pai, enquanto o método "real" estará sob o vtable Filho.
Aqui está o vtable Child
:
0x4008e8 <vtable for Child>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4008f0 <vtable for Child+8>: 0x60 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4008f8 <vtable for Child+16>: 0x00 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400900 <vtable for Child+24>: 0x10 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400908 <vtable for Child+32>: 0xf8 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400910 <vtable for Child+40>: 0x60 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x400918 <vtable for Child+48>: 0x20 0x08 0x40 0x00 0x00 0x00 0x00 0x00
O que isso significa:
Explicação: como vimos anteriormente, a Criança tem duas tabelas de jogos - uma é usada para Mãe e Filho e a outra para Pai. No vtable Father, FatherFoo () aponta para um "adaptador" e no vtable Child aponta diretamente para Child :: FatherFoo ().
E o que há nesse "adaptador", você pergunta?
(gdb) disas /m 0x400820, 0x400850 Dump of assembler code from 0x400820 to 0x400850: 15 void FatherFoo() override {} 0x0000000000400820 <non-virtual thunk to Child::FatherFoo()+0>: push %rbp 0x0000000000400821 <non-virtual thunk to Child::FatherFoo()+1>: mov %rsp,%rbp 0x0000000000400824 <non-virtual thunk to Child::FatherFoo()+4>: sub $0x10,%rsp 0x0000000000400828 <non-virtual thunk to Child::FatherFoo()+8>: mov %rdi,-0x8(%rbp) 0x000000000040082c <non-virtual thunk to Child::FatherFoo()+12>: mov -0x8(%rbp),%rdi 0x0000000000400830 <non-virtual thunk to Child::FatherFoo()+16>: add $0xfffffffffffffff8,%rdi 0x0000000000400837 <non-virtual thunk to Child::FatherFoo()+23>: callq 0x400810 <Child::FatherFoo()> 0x000000000040083c <non-virtual thunk to Child::FatherFoo()+28>: add $0x10,%rsp 0x0000000000400840 <non-virtual thunk to Child::FatherFoo()+32>: pop %rbp 0x0000000000400841 <non-virtual thunk to Child::FatherFoo()+33>: retq 0x0000000000400842: nopw %cs:0x0(%rax,%rax,1) 0x000000000040084c: nopl 0x0(%rax)
Como já discutimos, isso é compensado e o FatherFoo () é chamado. E quanto devemos mudar isso para obter Child? top_offset!
Observe que, pessoalmente, acho o nome de thunk não virtual extremamente confuso, pois é uma entrada de tabela virtual para uma função virtual. Não tenho certeza de que não seja virtual, mas essa é apenas minha opinião.
É tudo por agora, em um futuro próximo, traduziremos 3 e 4 partes. Acompanhe as novidades!