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: }
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;
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.