Calling global constructors from the bootloader

1. ELF Format

In gcc we can mark functions as constructor to be executed before main, also global objects need to call the constructors before main too (etc).

ELF stores pointer to constructor function in .init, .ctors and .init_array.

Here we can see why in alma we have to use .ctors instead of .init_array

Creating a function in the kernel as a constructor creates a new pointer in the .ctors:

kernel.elf:     formato del fichero elf64-x86-64

Contenido de la sección .ctors:
 23b0 a0230000 00000000                    .#.....
__attribute__((constructor)) void
foo()
{
    // garbage to evade optimization
    char *a = (char *)0x990;
    *a      = 0;
}
kernel.elf:     formato del fichero elf64-x86-64

Contenido de la sección .ctors:
 23c0 a0230000 00000000 b0230000 00000000  .#.......#......

We can check how we have to read it by doing:

[ecomaikgolf@desktop ../alma/qemu-fs]$ nm -A kernel.elf | grep foo
kernel.elf:00000000000023a0 T _Z3foov

2. Bootloader

2.1. Find .ctors

 1: void
 2: call_ctors(Elf64_Ehdr *elf)
 3: {
 4:     /* e->shentsize is the index of the string table section */
 5:     Elf64_Shdr *str_table_hdr =
 6:       (Elf64_Shdr *)((char *)elf + elf->e_shoff + (elf->e_shentsize * elf->e_shstrndx));
 7: 
 8:     char *str_table = ((char *)elf + str_table_hdr->sh_offset);
 9: 
10:     /* Iterate over section headers, skip the first as it's the NULL section header */
11:     for (int i = 1; i < elf->e_shnum; i++) {
12:         Elf64_Shdr *header = (Elf64_Shdr *)((char *)elf + elf->e_shoff + elf->e_shentsize * i);
13: 
14:         /* http://www.sco.com/developers/gabi/2003-12-17/ch4.strtab.html */
15:         char *section_name = str_table + header->sh_name;
16: 
17:         if (strncmp(section_name, ".ctors", 7) == 0) {
18:            // do things
19:         }
20:     }
21: }

From the in-heap elf pointer add the starting section header offset and then add the offset of (size-header * string-header-index) to get the string table section header.

The get the table by adding the content offset.

The iterate each section to get the “.ctors” section header name.

2.2. Get address

1: if (strncmp(section_name, ".ctors", 7) == 0) {
2:     uint64_t *ctors = (uint64_t *)((char *)elf + header->sh_offset);
3:     info("%p", *ctors);
4:     info("%p", *(ctors + 1));
5: }

HcmpHB.png

We can see that they are the same we saw before in nm

2.3. Get all addresses

 1: if (strncmp(section_name, ".ctors", 7) == 0) {
 2:     uint64_t *ctors = (uint64_t *)((char *)elf + header->sh_offset);
 3: 
 4:     /* / uint64_t as 64bit systems have 64bit addr */
 5:     uint64_t num_functions = (header->sh_size / sizeof(uint64_t));
 6: 
 7:     for (uint64_t i = 0; i < num_functions; i++) {
 8:         info("%p", *ctors);
 9:         ctors++;
10:     }
11: }

2.4. Execute all addresses

 1: if (strncmp(section_name, ".ctors", 7) == 0) {
 2:     uint64_t *ctors = (uint64_t *)((char *)elf + header->sh_offset);
 3: 
 4:     /* / uint64_t as 64bit systems have 64bit addr */
 5:     uint64_t num_functions = (header->sh_size / sizeof(uint64_t));
 6: 
 7:     for (uint64_t i = 0; i < num_functions; i++) {
 8:         /* sysv_abi as we are on a PE executable */
 9:         void (*func)() = ((__attribute__((sysv_abi)) void (*)(void)) * ctors);
10:         func();
11:         ctors++;
12:     }
13: }

3. Result

//kernel.cpp
screen::psf1_renderer kernel::tty;

fMnQZl.png

This doesn’t work without calling the global constructor as draw() it’s a virtual method and needs it’s vtable filled. Yes, I lost a lot of time finding that strange bug, the vtable pointer wasn’t initialized properly.