Page 1 of 1

Calling Global Constructors in UEFI + Clang

Posted: Wed Jan 31, 2024 5:31 pm
by Geometrian
Hi,

Consider the following simple example, "main.cpp":

Code: Select all

#include <Uefi.h>
#include <Library/UefiLib.h>

struct GlobalType {
	int var;
	GlobalType () { var=1; }
};
GlobalType foo, bar, baz;

EFI_STATUS EFIAPI efi_main( IN EFI_HANDLE /*image_handle*/, IN EFI_SYSTEM_TABLE* sys_table ) {
	sys_table->ConOut->OutputString( sys_table->ConOut, (CHAR16*)L"Var is: \"" );
	CHAR16 str[3+1] = { (CHAR16)(foo.var+'0'), (CHAR16)(bar.var+'0'), (CHAR16)(baz.var+'0'), '\0' };
	sys_table->ConOut->OutputString( sys_table->ConOut, str );
	sys_table->ConOut->OutputString( sys_table->ConOut, (CHAR16*)L"\"" );
	while (1) asm("hlt");
	return 0;
}
For simplicity, it can be directly compiled with:
#Path to EDK II (https://github.com/tianocore/edk2) root "edk2/" directory, for UEFI headers
UEFI_ROOT=/path/to/edk2/
clang -I${UEFI_ROOT}MdePkg/Include/ -I${UEFI_ROOT}MdePkg/Include/X64 -ffreestanding -fshort-wchar -target x86_64-pc-win32-coff -c main.cpp
lld-link -subsystem:efi_application -dll -entry:efi_main -nodefaultlib -filealign:16 -out:bootx64.efi main.o
When you boot with the resulting "bootx64.efi", you'll get a screen that cheerfully says
Var is: "000"
I.e., the global constructors for `foo`, `bar`, and `baz`did not run. That's not particularly surprising, since we didn't call them. I want to know how.

The wiki has a page Calling Global Constructors which is pretty out-of-date, and GCC-centric anyway. I also found a blog post and more practically a previous thread where it was explained that one should walk through symbols `__init_array_start` to `__init_array_end`, but all of this seems to be for "-target i386-elf", which is not usable for a UEFI application. At any rate, I tried various things in these directions without really knowing what I was doing, and either failed to get it to link or it didn't work. How is it supposed to work?

Re: Calling Global Constructors in UEFI + Clang

Posted: Wed Jan 31, 2024 10:12 pm
by Octocontrabass
Geometrian wrote:The wiki has a page Calling Global Constructors which is pretty out-of-date, and GCC-centric anyway.
GCC and Clang both call global constructors the same way, it's just that a lot of targets have switched from using .init/.fini/.ctors/.dtors to using .init_array/.fini_array, and different parts of the page were written at different times.
Geometrian wrote:I also found a blog post and more practically a previous thread where it was explained that one should walk through symbols `__init_array_start` to `__init_array_end`, but all of this seems to be for "-target i386-elf", which is not usable for a UEFI application.
It could be usable for a UEFI application if you choose a target that uses .init_array for constructors. (GNU-EFI does this, but GNU-EFI is an ugly hack and I wouldn't recommend using it as an example.)
Geometrian wrote:How is it supposed to work?
Good question! It depends on which runtime Clang thinks you're using. For Microsoft's runtime, it's basically the same as .init_array, except you might need to define symbols to find the start and end of the list For MinGW, it works more like the backwards .ctors, and I think the linker defines all the symbols you need. You can use llvm-objdump to examine main.o and see which runtime Clang expects.

Re: Calling Global Constructors in UEFI + Clang

Posted: Wed Jan 31, 2024 10:39 pm
by nullplan
I'm also no longer sure if the code I had provided back then isn't technically undefined behavior, since I am doing pointer arithmetic between two different arrays. Although I am comparing with !=, so that may work out. But it still might be better to do something like

Code: Select all

typedef void initfunc_t(void);
extern initfunc_t *__init_array_start[], *__init_array_end[];

static void handle_init_array(void) {
  size_t nfunc = ((uintptr_t)__init_array_end - (uintptr_t)__init_array_start)/sizeof (initfunc_t *);
  for (initfunc_t **p = __init_array_start; p < __init_array_start + nfunc; p++)
    (*p)();
}
This way, nfunc is calculated using integer arithmetic, and the loop is definitely defined behavior, and I still avoid the casting orgy the musl initialization code has.

But yeah, this code is not i386 specific, but it might be ELF specific. Which might work out for you if ELF is an intermediate target in building your PE UEFI application.