Depois de muito quebrar a cabeça no post anterior, finalmente consegui fazer os construtores globais funcionar no nosso código. Passei dias no readelf
e objdump
tentando entender o que o compilador fazia (ou não fazia) e o que foi necessário alterar para que isso acontecesse.
Eu alterei o código das classes GDT
e GDTPointer
para que as construíssemos através de construtores. Também adicionei um destrutor na classe GDTPointer
para fins didáticos e de testes, uma vez que tudo estiver funcionando esse destrutor será removido. No momento, ele só chama a função printk
para sabermos se o destrutor está sendo invocado. As classes ficaram assim:
class GDT {
public:
uint16_t limit_bytes_0_1 = 0;
uint16_t base_address_bytes_2_3 = 0;
uint8_t base_address_byte_4 = 0;
uint8_t type_access_byte_5 = 0;
uint8_t limit_flags_byte_6 = 0;
uint8_t base_address_byte_7 = 0;
GDT(uint32_t base_address, uint32_t limit, uint8_t type, uint8_t flags) {
this->limit_bytes_0_1 = limit & 0xffff;
this->base_address_bytes_2_3 = base_address & 0xffff;
this->base_address_byte_4 = (base_address >> 16) & 0xff;
this->type_access_byte_5 = type;
this->limit_flags_byte_6 = (limit >> 16) & 0xf;
this->limit_flags_byte_6 |= flags;
this->base_address_byte_7 = (base_address >> 24) & 0xff;
printk((char *)" GDT() ", YELLOW, BLUE);
}
__attribute__((constructor));
} __attribute__((__packed__));
class GDTPointer {
public:
uint16_t size = 0;
uint32_t gdt_entries_address = 0;
~GDTPointer() { printk((char *)" ~GDTPointer() ", RED, YELLOW); }
GDTPointer(uint16_t size, uint32_t address) {
this->size = size;
this->gdt_entries_address = address;
printk((char *)" GDTPointer() ", RED, YELLOW);
}
__attribute__((constructor));
} __attribute__((__packed__));
A declaração das variáveis globais mudou para usar os construtores no arquivo gdt.cpp
, e a função install_gdt
foi simplificada de tabela. Esse trecho ficou portanto:
GDT gdt_entries[] = {
// Null descriptor
GDT(0,0,0,0),
// Kernel Code Descriptor
GDT(0x00000000, 0xfffff,
GDT_ACCESS_CODE_SEG_READ | GDT_TYPE_CODE_SEG_READ |
GDT_TYPE_CODE_SEG_NOT_CONFIRMING | GDT_TYPE_CODE_SEG |
GDT_TYPE_DESCRIPTOR_CODE_OR_DATA | GDT_RING_00 | GDT_TYPE_PRESENT,
GDT_FLAG_USER_DEFINED | GDT_LONG_MODE_DISABLED | GDT_32BIT_FLAG |
GDT_GRANULARITY_FLAG),
// Kernel Data Descriptor
GDT(0x00000000, 0xfffff,
GDT_ACCESS_DATA_SEG_WRITE | GDT_TYPE_DATA_SEG_WRITE |
GDT_TYPE_DATA_SEG_GROW_UPSIDE | GDT_TYPE_DATA_SEG |
GDT_TYPE_DESCRIPTOR_CODE_OR_DATA | GDT_RING_00 | GDT_TYPE_PRESENT,
GDT_FLAG_USER_DEFINED | GDT_LONG_MODE_DISABLED | GDT_32BIT_FLAG |
GDT_GRANULARITY_FLAG)
};
GDTPointer gdtp((uint16_t)(sizeof(GDT) * GDT_ENTRIES),
(uint32_t)&gdt_entries[0]);
void install_gdt() {
GDTPointer testing(0, 0);
asm("lgdt %0" : : "m"(gdtp));
asm("ljmp %0, $continue_load_gdt_register \n\t"
"continue_load_gdt_register: \n\t" ::"i"(GDT_KCS_SEL * 0x8));
asm("mov %0, %%eax \n\t"
"mov %%ax, %%ds \n\t"
"mov %%ax, %%es \n\t"
"mov %%ax, %%fs \n\t"
"mov %%ax, %%gs \n\t"
"mov %%ax, %%ss \n\t" ::"i"(GDT_KDS_SEL * 0x8));
}
O trecho GDTPointer testing(0, 0);
está ai apenas para testar os construtores e destrutores locais. Uma vez que o teste tenha sido validado, ele será removido no futuro.
Para emitir outro construtor em outro arquivo, para fins de teste também, adicionei a variável global GDT test(0, 0, 0, 0);
no início do arquivo screen.cpp. A compilação desses arquivos resulta nessas funções, que são os construtores das variáveis globais:
$ objdump -D build/kernel | grep '_GLOBAL__sub_I'
000012d0 <_GLOBAL__sub_I_test>:
000012f0 <_GLOBAL__sub_I_gdt_entries>:
Perceba que a construção da variável test
ficou na função _GLOBAL__sub_I_test
(arquivo screen.cpp
), e a construção das variáveis gdt_entries
e gdtp
ficou na função _GLOBAL__sub_I_gdt_entries
(arquivo gdt.cpp
). O comportamento é esse mesmo, a construção de variáveis globais são agrupadas por unidade de compilação, no nosso caso, cada arquivo .cpp
.
Partindo do que foi constatado até o momento, devemos visitar esse artigo AQUI para entender como esses construtores são invocados.
A inicialização de objetos em um compilador GCC/G++ compatível com a ABI do System V utiliza 5 tipos de arquivos especiais: crt0.o
, crti.o
, crtbegin.o
, crtend.o
, and crtn.o
. Esses arquivos possuem códigos que são executados antes da main()
iniciar e depois que ela finaliza. Mas isso ocorre para código de usuários, quando você compila seu programa no gcc
para rodar no seu Linux. Como estamos programando um kernel, há uma matemágica necessária que possamos fazer isso. Por exemplo, nosso kernel funcionará como o arquivo crt0.o
, e portanto é nossa responsabilidade saber como vamos invocar os construtores.
Vale deixar bem claro que o que vou descrever daqui para frente é dependente de arquitetura, versão do compilador etc. No fim das contas o compilador gerará um vetor de ponteiros para função que armazena os endereços dos construtores (e outro vetor para os destrutores). Nas minhas primeiras tentativas meu compilador estava gerando um vetor oculto chamado __frame_dummy_init_array_entry
, mas que deveria ser chamado pela função _init()
e por algum motivo isso não acontecia. Algumas frustrações e muito café depois, lendo ESSE artigo cheguei nesse trecho:
In this case things are slightly different. The system ABI mandates the use of special sections called .init_array and .fini_array, rather than the common .init and .fini sections. This means that crtbegin.o and crtend.o, as provided by your cross-compiler, does not insert instructions into the .init and .fini sections. The result is that if you follow the method from Intel/AMD systems, your _init and _fini functions will do nothing. Your cross-compiler may actually come with default crti.o and crtn.o objects, however they also suffer from this ABI decision, and their _init and _fini functions will also do nothing.1
O trecho acima diz que o procedimento que eu estava tentando utilizar não se aplicava à arquitetura ARM. Oras, mas eu não estou usando ARM, estou usando Intel/AMD…
De toda sorte, tentei o procedimento para ARM e fui bem sucedido! Então é esse que vou explicar aqui.
Primeiro, o compilador fornece os arquivos crtbegin.o
e crtend.o.
Em especial o crtbegin.o
, ele possui especialmente um vetor chamado _init_array_start
(e o _fini_array_start
para os destrutores), que armazenará os endereços dos construtores, e o compilador/linker o irá complementar quando a compilação completa acontecer. Temos portanto um vetor de ponteiros de função (tal qual o __frame_dummy_init_array_entry
dito acima).
Nossa missão é portanto criar os arquivos crti.cpp
e crtn.cpp
, respectivamente o preâmbulo e epílogo dos arquivos crtbegin.o
e crtend.o
. No arquivo crti.cpp
nós dizemos que os símbolos _init_array_start
, _init_array_end
, _fini_array_start
e _fini_array_end
estão definidos em outro lugar, mas que vamos referenciá-los. Nós sabemos a fronteira de tais vetores pois o compilador malandramente sabe onde começa e termina as seções .init_array
(para os construtores) e .fini_array
(para os destrutores) no arquivo ELF, portanto, ao iterar por um vetor de ponteiros para função começando por _init_array_start
, quando nosso iterator alcançar _init_array_end
, sabemos que o vetor terminou. Se não ficou claro, observe o trecho de interesse da saída do comando objdump -D build/kernel
:
Ignore as instruções em assembly do lado direito. Isso é um vetor de ponteiros para função, com três valores, sendo eles: 0x00001150
, 0x000012d0
e 0x000012f0
(agrupe de quatro em quatro bytes, e considere a notação Little Endian - byte menos significativo primeiro). E o que esses valores significam? Isso:
$ objdump -D build/kernel | grep '00001150\|000012d0\|000012f0'
00001150 <frame_dummy>:
000012d0 <_GLOBAL__sub_I_test>:
000012f0 <_GLOBAL__sub_I_gdt_entries>:
AHÁ, olhe nossos construtores ai! A primeira entrada diz respeito a coisas que o compilador colocou, isso é o endereço da função frame_dummy
, que por sua vez chama a função register_tm_clones…
para mais detalhes dessa cadeia de chamadas, veja AQUI. As outras duas entradas dizem respeito aos nossos construtores, ditos anteriormente.
E como a gente acessa esse vetor e invoca essas funções? Ai é que está a mágica do negócio. Pegando como exemplo os construtores (a analogia é a mesma para os destrutores, apenas a seção que é diferente). Primeiro, declaramos o tipo de ponteiro para função func_ptr
.
typedef void (*func_ptr)(void);
Declaramos as variáveis _init_array_start
e _init_array_end
, dizendo que elas são do tipo func_ptr
, ou seja, um array de ponteiros para função, e dizemos que o endereço delas não está nesse arquivo .cpp
(ou seja, são externas).
extern func_ptr _init_array_start[0], _init_array_end[0];
Vetor de tamanho zero? É isso ai jovem, isso indica para o compilador que é um vetor, mas ele não efetivamente “ocupa” espaço na memória, é apenas um marcador para a posição de memória correspondente. Isso é muito útil e usado em estrutura de dados de tamanho variável, como o encapsulamento de dados nas camadas de rede. Para um explicação simples do motivo disso, veja se esse artigo AQUI consegue esclarecer essa artimanha.
Apontamos o _init_array_start
para o início da seção .init_array
:
func_ptr _init_array_start[0] __attribute__((used, section(".init_array"),
aligned(sizeof(func_ptr)))) = {};
e o _init_array_end
para o fim da seção (é a mesma coisa, mas isso está no arquivo crtn.cpp
, o rodapé portanto):
func_ptr _init_array_end[0] __attribute__((used, section(".init_array"),
aligned(sizeof(func_ptr)))) = {};
Agora, no arquivo crti.cpp
, chamar tais construtores é tão simples quanto iterar de _init_array_start
até _init_array_end
, invocando cada um dos ponteiros no processo:
extern "C" void _init(void) {
for (func_ptr *func = _init_array_start; func != _init_array_end; func++)
(*func)();
}
Basta então declarar a função _init()
no arquivo kernel.cpp e invocá-la antes de tudo:
extern "C" void _init(void);
...
extern "C" void _start() {
_init();
...
}
É isso! Difícil? Eu achei.
Ainda não acabou. Para tudo isso funcionar, vocês notaram que os arquivos crti.cpp
e crtn.cpp
trabalham com as seções .init_array
e .fini_array
no arquivo ELF. Então, ao invés de passar parâmetros de seções diretamente no linker, criamos o arquivo linker.ld
com o conteúdo abaixo:
ENTRY(_start)
SECTIONS
{
. = 0x1000;
.text BLOCK(4K) : ALIGN(4K)
{
*(.text)
}
/* Read-only data. */
.rodata BLOCK(4K) : ALIGN(4K)
{
*(.rodata)
}
/* Read-write data (initialized) */
.data BLOCK(4K) : ALIGN(4K)
{
*(.data)
}
/* Read-write data (uninitialized) and stack */
.bss BLOCK(4K) : ALIGN(4K)
{
*(COMMON)
*(.bss)
}
/* The compiler may produce other sections, by default it will put them in
a segment with the same name. Simply add stuff here as needed. */
.preinit_array :
{
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array))
PROVIDE_HIDDEN (__preinit_array_end = .);
}
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
PROVIDE_HIDDEN (__init_array_end = .);
}
.fini_array :
{
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)))
KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors))
PROVIDE_HIDDEN (__fini_array_end = .);
}
}
Descartamos coisas no linker como -Ttext 0x1000
, essa informação está no arquivo linker.ld
agora. A invocação fica assim:
...
kernel: kernel.o $(OBJ_LINK_LIST)
cd $(OUTPUTDIR) && ld -O2 -g -m elf_i386 -T../linker.ld -z noexecstack -o $@ kernel.o $(OBJ_LINK_LIST)
...
A variável OBJ_LINK_LIST
é uma concatenação cuidadosa de nossos arquivos .o, adicionados aos exigidos pelo compilador para geração dos construtores:
OBJS:=screen.o gdt.o
CRTI_OBJ=crti.o
CRTBEGIN_OBJ:=$(shell g++ -m32 $(CFLAGS) -print-file-name=crtbegin.o)
CRTEND_OBJ:=$(shell g++ -m32 $(CFLAGS) -print-file-name=crtend.o)
CRTN_OBJ=crtn.o
OBJ_LINK_LIST:=$(CRTI_OBJ) $(CRTBEGIN_OBJ) $(OBJS) $(CRTEND_OBJ) $(CRTN_OBJ)
E não esqueça de adicionar as flags -fno-exceptions
-fno-use-cxa-atexit
, caso contrário você terá problemas com o g++:
...
%.o: $(SRCDIR)/%.cpp
g++ -g -m32 -c $< -o $(OUTPUTDIR)/$@ -O2 -Wall -Wextra -ffreestanding -fno-rtti -nostdlib -fno-exceptions -fno-use-cxa-atexit
...
É isso. Ao compilar, qual é o resultado? Esse da imagem abaixo:
Note que o construtor da classe GDT
foi invocado quatro vezes, três pelas entradas da variável global gdt_entries
no arquivo gdt.cpp
, e uma pelo teste da variável test
no arquivo screen.cpp
. O construtor da classe GDTPointer
é invocado duas vezes, uma para a variável global gdtp
, outra para a variável local testing
dentro da função install_gdt
. A variável testing
inclusive é a única que está dentro de um contexto que é encerrado, ao fim da função install_gdt
o destrutor é invocado. Por fim a função printk
é invocada dentro da função _start do arquivo kernel.cpp emitindo a mensagem " Hello Protected Mode! "
, como já havia antes.
Depois que você vê o código pronto e funcionando, tudo parece simples e óbvio. Mas foi muito difícil chegar a essa solução, em especial por desconhecer esses detalhes do compilador, e construções como __attribute__((used, section(".init_array"), aligned(sizeof(func_ptr))))
.
Pouco provável que eu conseguisse fazer isso sozinho, ou pelo menos ia precisar de outras soluções e gambiarras criativas para obter o endereço de _init_array_start
. Esse nem seria o problema, mas sim saber o tamanho desse vetor. A artimanha de posicionar códigos de preâmbulo e epílogo na seção .init_array
é bem inteligente, eu teria feito de outra forma muito menos sutil, talvez lendo o arquivo ELF ou outra coisa do tipo.
https://wiki.osdev.org/Calling_Global_Constructors#Using_crti.o,_crtbegin.o,_crtend.o,_and_crtn.o_in_a_Kernel