Есть ли 64-битный загрузчик UEFI ELF?

У меня ядро ​​ELF. Поэтому мне нужен загрузчик, который загрузит мой 64-битный файл ELF. Мне не нужны устаревшие загрузчики Legacy BIOS, мне нужен загрузчик UEFI с графическим интерфейсом или без него.


person Daniel    schedule 18.07.2021    source источник


Ответы (2)


У меня ядро ​​ELF. Поэтому мне нужен загрузчик, который загрузит мой 64-битный файл ELF.

У вас есть ядро ​​ELF; так что вам, вероятно, понадобится загрузчик, который:

  • загрузить файл ELF ядра

  • сообщить ядру о карте памяти

  • сообщать ядру различные вещи об оборудовании (сплющенное дерево устройств или таблицы ACPI или ...), вероятно, включая детали буфера кадра.

  • также загрузить другие файлы (например, начальный RAM-диск); потому что (даже для модульного монолитного ядра) ядро ​​не может загрузить драйвер диска с диска, если оно еще не загрузило драйвер диска с диска.

  • сообщить ядру какую-то конфигурацию ядра (может быть другой файл, может быть аргумент командной строки ядра).

  • кто знает, что еще (например, я ожидаю, что мои загрузчики настроят разбиение на страницы и сопоставят ядро ​​по его окончательному виртуальному адресу; стоит подумать о декомпрессии, чтобы улучшить время загрузки при медленном вводе-выводе диска; выполнение проверок работоспособности, чтобы увидеть, не был ли изменен файл ядра прежде чем доверять, это имеет смысл ...).

Другими словами; вам нужна полная подробная спецификация о том, как все это происходит (например, как ядро ​​извлекает карту памяти из загрузчика, в каком формате она используется, если есть гарантии, что 2 или более записей в карте памяти не будут описывать одно и то же / перекрытие область памяти и т. д.); где и загрузчик (или все загрузчики), и ядро ​​(или все ядра) соответствуют подробной спецификации.

Ядро ELF - это лишь небольшая часть этой подробной спецификации.

Это оставляет вам 2 варианта:

  • найти подробную спецификацию, разработанную кем-то другим, которая включает ядро ​​в формате ELF (или, по крайней мере, загрузчик должен поддерживать ELF), и принять их спецификацию, а затем смириться со всеми их дизайнерскими решениями, имеют ли они смысл для вашей ОС или не. Единственный выбор здесь (о котором я знаю) - это мультизагрузочная спецификация.

  • создать собственную подробную спецификацию, предназначенную для вашей ОС; затем либо напишите свой собственный загрузчик / ы, либо позвольте другим людям писать их из вашей спецификации. Это то, что делает почти каждое известное ядро ​​(Windows, Linux, FreeBSD, ...).

Примечание 1: обычно это не один загрузчик, а скорее их набор (один для загрузки с жесткого диска, разделенного на GPT, один для загрузки по сети, другой для загрузки со съемного носителя и т. Д.). Это можно обойти, разделив загрузчик на две части (много разных первых этапов, которые обрабатывают различия, плюс общий второй этап, который обрабатывает сходства).

Примечание 2: для UEFI вы можете просто использовать UEFI как подробную спецификацию, разработанную кем-то другим, и не беспокоиться о загрузчике. В этом случае вам придется либо конвертировать ваш ELF в формат PE; или вставьте ваш ELF-файл, как он находится внутри PE-файла в качестве данных (где PE-файл имеет некоторый код для распаковки ELF, содержащегося внутри него).

Примечание 3: Теоретически это действительно касается окружающей среды; где загрузчик переходит из одной среды (например, из UEFI) в другую (например, в среду, ожидаемую вашим ядром); и эти фрагменты кода, которые изменяют среду, могут быть многоуровневыми (например, возможно, BIOS - ›мультизагрузка -› эмулятор UEFI - ›что-то еще -› то, что ожидает ваше ядро).

Примечание 4: Что касается окончательной среды, ожидаемой вашим ядром; возможно, стоит рассмотреть такие вещи, как kexec (), когда предыдущий экземпляр вашего ядра используется для запуска следующего экземпляра вашего ядра. Это само по себе имеет практические преимущества (например, более быстрое обновление ядра), но основная причина подумать об этом - это усовершенствовать дизайн среды, которую ожидает ваше ядро ​​(чтобы помочь определить, что на самом деле хочет ядро, и избежать внесения багажа из других сред. предоставить это не совсем то, что действительно нужно вашему ядру).

TL; DR: вы, вероятно, в конечном итоге будете использовать Multiboot2 (и GRUB). Вы можете найти спецификацию для Multiboot2 здесь: https://www.gnu.org/software/grub/manual/multiboot2/multiboot.html.

person Brendan    schedule 18.07.2021

Не так давно я задал вопрос о StackOverflow, где я даю свой код загрузчика 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. Это просто выбор дизайна, который я сделал, чтобы упростить передачу данных в мое ядро ​​в статических позициях в ОЗУ.

Код выделяет ядру статические 100 Кбайт данных, которых может быть недостаточно для вашего. Он загрузит любой статический и автономный файл ELF с именем kernel.elf в корневом каталоге раздела ESP на вашем жестком диске.

Вам может потребоваться выделить буфер ядра с помощью AllocatePages вместо использования распределителя UEFI. Я обнаружил, что после того, как ядро ​​достигнет определенного размера, статическое выделение приведет к сбою приложения UEFI.

Если вам нужна информация о том, как скомпилировать код, посмотрите мой ответ здесь: Build edk2 in linux

Кроме того, если вы все же решите скомпилировать загрузчик и столкнетесь с какими-либо проблемами, не стесняйтесь спрашивать меня напрямую. Я могу помочь исправить любую проблему.

person user123    schedule 19.07.2021