是否有 64 位 UEFI ELF 引导加载程序?

Is there a 64 bit UEFI ELF bootloader?

我有 ELF 内核。所以我需要一个引导加载程序来加载我的 64 位 ELF 文件。我不需要过时的 Legacy BIOS 引导加载程序,我需要 UEFI 引导加载程序 with/without GUI。

I have ELF kernel. So I need a bootloader that will load my 64 bit ELF file.

您有一个 ELF 内核;所以你可能需要一个引导加载程序:

  • 加载内核的ELF文件

  • 告诉内核内存映射

  • 告诉内核有关硬件的各种信息(“扁平设备树”或 ACPI 表或...),可能包括帧缓冲区详细信息。

  • 同时加载其他文件(例如初始RAM盘);因为(即使对于“模块化单片”内核)内核在尚未从磁盘加载磁盘驱动程序时也无法从磁盘加载磁盘驱动程序。

  • 告诉内核某种内核配置(可以是另一个文件,可以是“内核命令行参数”)。

  • 谁知道还有什么(例如,我希望我的引导加载程序在其最终虚拟地址处设置分页和映射内核;当磁盘 IO 缓慢时,解压缩值得考虑以改善引导时间;做理智在相信它有意义之前检查内核文件是否被篡改,...)。

换句话说;您需要有关 any/all 这些事情如何发生的完整详细说明(例如,内核如何从引导加载程序检索内存映射,它采用哪种格式,如果有“内存映射中的 2 个或更多条目不会描述same/overlapping 内存区域", 等等);其中引导加载程序(或所有引导加载程序)和内核(或所有内核)都符合详细规范。

“内核是 ELF”只是那个详细规范的一小部分。

这给您留下了 2 个选择:

  • 找一个别人设计的详细规范恰好有“内核是ELF”(或者至少是“boot loader必须支持ELF”),采纳他们的规范,然后放了解他们所有的设计决策,无论它们是否对您的 OS 有意义。这里唯一的选择(据我所知)是多重引导规范。

  • 为您的 OS 创建您自己的详细规格;然后要么写你自己的引导loader/s,要么让其他人根据你的规范来写。这几乎是每个众所周知的内核(Windows、Linux、FreeBSD 等)所做的。

注意 1:通常它不是“一个引导加载程序”,而更像是一组引导程序(一个用于从“GPT 分区硬盘”引导,一个用于从网络引导,另一个用于从可移动媒体引导,. ..).可以通过将“引导加载程序”分成两半来解决这个问题(许多不同的“第一阶段”处理差异,加上一个共同的“第二阶段”处理相似之处)。

注2:对于UEFI,您可以将UEFI用作“其他人设计的详细规范”而不必费心使用引导加载程序。在那种情况下,您必须将 ELF 转换为 PE 格式;或将您的 ELF 文件“按原样”插入 PE 文件中作为数据(PE 文件中有一些代码可以解压缩其中包含的 ELF)。

注3:理论上它真的是关于环境的;其中“引导加载程序”从一个环境(例如从 UEFI)更改为另一个环境(例如“您的内核期望的环境”);并且这些“改变环境的代码片段”可以分层(例如,可能是“BIOS -> 多启动 -> UEFI 模拟器 -> 其他 -> 你的内核所期望的”)。

注4:对于最后的“你的内核期望的环境”;可能值得考虑诸如“kexec()”之类的事情,其中​​您的内核的前一个实例用于启动您的内核的下一个实例。这有其自身的实际好处(例如更快的内核更新),但考虑它的主要原因是改进“内核期望的环境”的设计(以帮助确定内核实际需要什么并避免从中引入包袱其他环境提供的并不是您的内核真正想要的)。

TLDR: 您可能最终会使用 Multiboot2(和 GRUB)。您可以在此处找到 Multiboot2 的规范:https://www.gnu.org/software/grub/manual/multiboot2/multiboot.html

不久前我在 Whosebug 上问了一个问题,我在其中提供了我的 x64 UEFI 引导加载程序代码。参考代码如下:

#include <Uefi.h>
#include <Protocol/SimpleFileSystem.h>
#include <Protocol/GraphicsOutput.h>
#include <Library/BaseLib.h>

#define EFI_ACPI_TABLE_GUID { 0xeb9d2d30, 0x2d88, 0x11d3, {0x9a, 0x16, 0x0, 0x90, 0x27, 0x3f, 0xc1, 0x4d }}
#define EFI_ACPI_20_TABLE_GUID { 0x8868e871, 0xe4f1, 0x11d3, {0xbc, 0x22, 0x0, 0x80, 0xc7, 0x3c, 0x88, 0x81 }} 

typedef struct {
        CHAR8   Signature[8];
        UINT8   Checksum;
        UINT8   OemId[6];
        UINT8   Revision;
    UINT32  RsdtAddress;
        UINT32  Length;
        UINT64  XsdtAddress;
        UINT8   ExtendedChecksum;
        UINT8   Reserved[3];
} RSDP;

typedef struct {
    UINT64 Address;
    UINT64 Size;
    UINT64 HorizontalResolution;
    UINT64 VerticalResolution;
    UINT64 PixelsPerScanLine;   
} FrameBuffer;

BOOLEAN CompareGUID(EFI_GUID rguid1, EFI_GUID rguid2){
    return 
   rguid1.Data1 == rguid2.Data1 &&
   rguid1.Data2 == rguid2.Data2 &&
   rguid1.Data3 == rguid2.Data3 &&
   rguid1.Data4[0] == rguid2.Data4[0] &&
   rguid1.Data4[1] == rguid2.Data4[1] &&
   rguid1.Data4[2] == rguid2.Data4[2] &&
   rguid1.Data4[3] == rguid2.Data4[3] &&
   rguid1.Data4[4] == rguid2.Data4[4] &&
   rguid1.Data4[5] == rguid2.Data4[5] &&
   rguid1.Data4[6] == rguid2.Data4[6] &&
   rguid1.Data4[7] == rguid2.Data4[7];
}

void MemCpy(void *dest, void *src, UINTN n){  
    CHAR8* cdest = (CHAR8*) dest;
    CHAR8* csrc = (CHAR8*) src;  
    for (UINTN i = 0; i < n; i++) 
        cdest[i] = csrc[i]; 
} 

EFI_STATUS EFIAPI UefiMain (IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE  *SystemTable){
    SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Hello World!\n");
    
    /*Locate RSDP table*/
    RSDP* rsdpPointer = NULL;
    RSDP rsdp;
    EFI_GUID AcpiGuid = EFI_ACPI_20_TABLE_GUID;
    for (UINTN i = 0; i < SystemTable->NumberOfTableEntries; i++){
        if (CompareGUID(SystemTable->ConfigurationTable[i].VendorGuid, AcpiGuid)){
                CHAR8* TablePointer = (CHAR8*) SystemTable->ConfigurationTable[i].VendorTable;
                if (TablePointer[0] == 'R' && TablePointer[1] == 'S' && TablePointer[2] == 'D' && TablePointer[3] == ' ' && 
                TablePointer[4] == 'P' && TablePointer[5] == 'T' && TablePointer[6] == 'R' && TablePointer[7] == ' '){
                    rsdpPointer = (RSDP*)SystemTable->ConfigurationTable[i].VendorTable;
                    rsdp = *rsdpPointer;
                }   
            }
    }
    if (rsdpPointer == NULL){
        SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Kernel Panic!!: Could not locate the RSDP.\n");
        goto DONE;
    }else{
        SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Found the RSDP.\n");
    }
    RSDP* RSDPTable = (RSDP*)0x350000;
    *RSDPTable = rsdp;
    
    
    /*Get the kernel's file*/
    SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Booting the kernel.\n");
    
    EFI_STATUS Status;
    EFI_BOOT_SERVICES* BS = SystemTable->BootServices;
    EFI_GUID FSPGuid = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID;
    EFI_HANDLE* Handles = NULL;   
    UINTN HandleCount = 0;
    Status = BS->LocateHandleBuffer(ByProtocol, &FSPGuid, NULL, &HandleCount, &Handles);
    if (EFI_ERROR(Status)){
        SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Kernel Panic!!: Could not locate the handle buffer.\n");
        goto DONE;
    }else{
        SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Located the handle buffer for file system protocol.\n");
    }
    EFI_SIMPLE_FILE_SYSTEM_PROTOCOL* FS = NULL; 
    EFI_FILE_PROTOCOL* Root = NULL;
    EFI_FILE_PROTOCOL* Token = NULL;
    for (UINTN index = 0; index < (UINTN)HandleCount; index++)
    {
            Status = BS->HandleProtocol(Handles[index], &FSPGuid, (void**)&FS);
        Status = FS->OpenVolume(FS, &Root);
        Status = Root->Open(Root, &Token, L"kernel.elf", EFI_FILE_MODE_READ, EFI_FILE_MODE_WRITE);
        if(!EFI_ERROR(Status))
            break;
    }
    UINTN BufferSize = 100000;
    CHAR8 KernelBuffer[100000];
    Status = Token->Read(Token, &BufferSize, KernelBuffer);
    if(EFI_ERROR(Status)){
        SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Kernel Panic!!: Located the kernel, but could not read from it.\n");
        goto DONE;
    }else
        SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Could read the Kernel properly now jumping to it's entry point.\n");
    
    /*Get the frame buffer info*/
    EFI_GUID GOPGuid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
    EFI_GRAPHICS_OUTPUT_PROTOCOL* GOP;
    Status = BS->LocateProtocol(&GOPGuid, NULL, (void**)&GOP);
    //GOP->SetMode(GOP, GOP->Mode->MaxMode - 1);
        FrameBuffer Video = {GOP->Mode->FrameBufferBase, GOP->Mode->FrameBufferSize, GOP->Mode->Info->HorizontalResolution,
            GOP->Mode->Info->VerticalResolution, GOP->Mode->Info->PixelsPerScanLine};
        FrameBuffer* VideoPointer = (FrameBuffer*)0x351000;
        *VideoPointer = Video;
    
    /*
    Got the kernel's file in a buffer, now map it
    in memory and jump to its entry point
    */
    UINT64 EntryPoint;
    MemCpy(&EntryPoint, KernelBuffer + 24, 8);
    UINT64 ProgramHeaderPosition;
    MemCpy(&ProgramHeaderPosition, KernelBuffer + 32, 8);
    UINT16 NumberOfEntriesInProgramHeader;
    MemCpy(&NumberOfEntriesInProgramHeader, KernelBuffer + 56, 2);
    for (UINTN i = 0; i < NumberOfEntriesInProgramHeader; i++){
        UINT32 SegmentType;
        UINT64 SegmentDataPosition;
        UINT64 SegmentVirtualAddress;
        UINT64 SegmentSizeInFile;
        UINT64 SegmentSizeInMemory;
        MemCpy(&SegmentType, KernelBuffer + ProgramHeaderPosition, 4);
        if (SegmentType == 1){
            MemCpy(&SegmentDataPosition, KernelBuffer + ProgramHeaderPosition + 8, 8);
            MemCpy(&SegmentVirtualAddress, KernelBuffer + ProgramHeaderPosition + 16, 8);
            MemCpy(&SegmentSizeInFile, KernelBuffer + ProgramHeaderPosition + 32, 8);
            MemCpy(&SegmentSizeInMemory, KernelBuffer + ProgramHeaderPosition + 40, 8);
            CHAR8* VirtualAddress = (CHAR8*)SegmentVirtualAddress;
            for (UINT64 i = 0; i < SegmentSizeInMemory; i++){
                if (i < SegmentSizeInFile){
                    *(VirtualAddress + i) = *(KernelBuffer + SegmentDataPosition + i);  
                }else{
                    *(VirtualAddress + i) = 0;
                }
            }
        }
        ProgramHeaderPosition += 56;
    }
    
    /*Final jump to the entry point*/
    SystemTable->BootServices->ExitBootServices(ImageHandle, 0);
    BASE_LIBRARY_JUMP_BUFFER JumpBuffer;
    SetJump(&JumpBuffer);
    JumpBuffer.Rip = EntryPoint;
    LongJump(&JumpBuffer, 1);
  
    DONE:
    return EFI_SUCCESS;
}

该代码会将 RSDP 放置在 0x350000 处,并将帧缓冲区上的信息放置在 0x351000 处。这只是我为简化将数据传递到 RAM 中静态位置的内核所做的设计选择。

该代码为内核分配了 100k 字节的静态数据,这对您的内核来说可能不够用。它将引导任何静态和独立的 ELF 文件,并在您的硬盘的 ESP 分区的根目录中调用 kernel.elf。

您可能需要使用 AllocatePages 来分配内核缓冲区,而不是使用 UEFI 分配器。我发现,当内核达到一定大小后,静态分配会使UEFI应用程序崩溃。

如果您想了解有关如何编译代码的信息,请在此处查看我的回答:Build edk2 in linux

此外,如果您决定将引导加载程序和 运行 编译成任何问题,请随时直接问我。我可以帮助解决任何问题。