Operating Systems : simple Operating System from scratch (3) — boot from uefi, part 2
Hello!
在上一篇有提到很簡易的 edk2 開發環境的使用方式,在這邊將其延伸到成為一個簡易的 Boot & Loader 。以下的程式碼都是源自於 田宇 ,一切的版權由 田宇 所有,我只是依照我的需求小改了一下,並加了一點註解而已。
希望我有錯,或有任何問題的話可以在留言區指正 < _ _ >。
目標
利用 edk2 來寫一個簡單的 uefi Boot & Loader ,最後跳到極簡單的 kernel 來繪製螢幕。
一個簡單的 Boot & Loader
有了 uefi 後,就不用使用組合語言來辛辛苦苦地從 real mode 逐漸跳到 long mode 了,但仍需要獲取某些資訊並傳遞給 kernel。我這邊簡單整理幾個在這個 uefi boot&loader 需要實做的功能。
- 載入 kernel.bin 到記憶體
- 獲取顯示器的資訊,並將 frame buffer 映射到 某一段記憶體
- 取得記憶體配置
- 將 protocol 關閉並跳到 kernel 內,開始執行 kernel
完整的程式碼可以參考這裡,以下是對程式碼的一些註解。
0. 一些 structure 的定義
/***************************************************
版权声明
本操作系统名为:MINE
该操作系统未经授权不得以盈利或非盈利为目的进行开发,
只允许个人学习以及公开交流使用
代码最终所有权及解释权归田宇所有
本模块作者: 田宇
EMail: 345538255@qq.com
*****************************************************/struct EFI_GRAPHICS_OUTPUT_INFORMATION
{
unsigned int HorizontalResolution;
unsigned int VerticalResolution;
unsigned int PixelsPerScanLine;
unsigned long FrameBufferBase;
unsigned long FrameBufferSize;
};struct EFI_E820_MEMORY_DESCRIPTOR
{
unsigned long address;
unsigned long length;
unsigned int type;
}__attribute__((packed));truct EFI_E820_MEMORY_DESCRIPTOR_INFORMATION
{
unsigned int E820_Entry_count;
struct EFI_E820_MEMORY_DESCRIPTOR E820_Entry[0];
};struct KERNEL_BOOT_PARAMETER_INFORMATION
{
struct EFI_GRAPHICS_OUTPUT_INFORMATION Graphics_Info;
struct EFI_E820_MEMORY_DESCRIPTOR_INFORMATION E820_Info;
};
- 載入 kernel.bin 到記憶體
EFI_STATUS EFIAPI UefiMain(IN EFI_HANDLE ImageHandle,IN EFI_SYSTEM_TABLE *SystemTable)
{
EFI_LOADED_IMAGE *LoadedImage;
EFI_FILE_IO_INTERFACE *Vol;
EFI_FILE_HANDLE RootFs;
EFI_FILE_HANDLE FileHandle;
int i = 0;
void (*func)(void);
EFI_STATUS status = EFI_SUCCESS;
struct KERNEL_BOOT_PARAMETER_INFORMATION *kernel_boot_para_info = NULL; gBS->HandleProtocol(ImageHandle,&gEfiLoadedImageProtocolGuid,(VOID*)&LoadedImage);
/* 從 ImageHandle 裡拿 protocol:gEfiLoadedImageProtocolGuid 的 Handle,並存在 LoadedImage */ gBS->HandleProtocol(LoadedImage->DeviceHandle,&gEfiSimpleFileSystemProtocolGuid,(VOID*)&Vol);
/* 從 LoadedImage->DeviceHandle 裡拿 protocol:gEfiSimpleFileSystemProtocolGuid 的 Handle,並存在 Vol */ Vol->OpenVolume(Vol,&RootFs);
/* 用 Vol 來拿到 File System 的 handlef */
status = RootFs->Open(RootFs,&FileHandle,(CHAR16*)L"kernel.bin",EFI_FILE_MODE_READ,0);
if(EFI_ERROR(status))
{
Print(L"Open kernel.bin Failed.\n");
return status;
}
/* 在此用 RootFs 來拿到 "kernel.bin" 這個檔案的 handler */ EFI_FILE_INFO* FileInfo;
UINTN BufferSize = 0;
EFI_PHYSICAL_ADDRESS pages = 0x100000; BufferSize = sizeof(EFI_FILE_INFO) + sizeof(CHAR16) * 100;
gBS->AllocatePool(EfiRuntimeServicesData,BufferSize,(VOID**)&FileInfo);
/* AllocatePool 主要是用來 alloc 一段比較小的 buffer */
FileHandle->GetInfo(FileHandle,&gEfiFileInfoGuid,&BufferSize,FileInfo);
/* 使用 kernel.bin 的 filehandle 來取得檔案的資訊 */
Print(L"\tFileName:%s\t Size:%d\t FileSize:%d\t Physical Size:%d\n",FileInfo->FileName,FileInfo->Size,FileInfo->FileSize,FileInfo->PhysicalSize); gBS->AllocatePages(AllocateAddress,EfiConventionalMemory,(FileInfo->FileSize + 0x1000 - 1) / 0x1000,&pages);
/* AllocatePages 主要是用來 alloc 一段比較大的 buffer,對齊 4KB */
Print(L"Read Kernel File to Memory Address:%018lx\n",pages); BufferSize = FileInfo->FileSize;
FileHandle->Read(FileHandle,&BufferSize,(VOID*)pages);
/* 將 kernel.bin 載入到我們指定的物理位址 */ gBS->FreePool(FileInfo);
FileHandle->Close(FileHandle);
RootFs->Close(RootFs);
2. 獲取顯示器的資訊,並將 frame buffer 映射到 某一段記憶體
/* Read graphics information start */
EFI_GRAPHICS_OUTPUT_PROTOCOL* gGraphicsOutput = 0;
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION* Info = 0;
UINTN InfoSize = 0;pages = 0x60000;
kernel_boot_para_info = (struct xa00000 */KERNEL_BOOT_PARAMETER_INFORMATION *)0x60000;
gBS->AllocatePages(AllocateAddress,EfiConventionalMemory,1,&pages);
/* Allocate 一個 page 來放 kernel_boot_para_info,這個 structure 是用來傳遞資訊給 kernel 的 */ gBS->SetMem((void*)kernel_boot_para_info,0x1000,0);
/* 初始化 kernel_boot_para_info 為 0 */ int fdgkResolutionMode = gGraphicsOutput->Mode->Mode;
for(i = 0;i < gGraphicsOutput->Mode->MaxMode;i++)
{
gGraphicsOutput->QueryMode(gGraphicsOutput,i,&InfoSize,&Info);
// 因為我的電腦在最高畫質的時候,顯示起來怪怪的,所以我在這邊只要解析有到 1024 * 768 ,
我就停下來。
if((Info->PixelFormat == 1) && Info->HorizontalResolution == 1024 && Info->VerticalResolution == 768) {
fdgkResolutionMode = i;
break;
}
gBS->FreePool(Info);
}
/* 遍尋此顯示器支援的所有顯示模式,並選擇自己想要的顯示模式 */ gGraphicsOutput->SetMode(gGraphicsOutput, fdgkResolutionMode);
/* 設定顯示模式 */ gBS->LocateProtocol(&gEfiGraphicsOutputProtocolGuid,NULL,(VOID **)&gGraphicsOutput);
Print(L"Current Mode:%02d,Version:%x,Format:%d,Horizontal:%d,Vertical:%d,ScanLine:%d,FrameBufferBase:%018lx,FrameBufferSize:%018lx\n",gGraphicsOutput->Mode->Mode,gGraphicsOutput->Mode->Info->Version,gGraphicsOutput->Mode->Info->PixelFormat,gGraphicsOutput->Mode->Info->HorizontalResolution,gGraphicsOutput->Mode->Info->VerticalResolution,gGraphicsOutput->Mode->Info->PixelsPerScanLine,gGraphicsOutput->Mode->FrameBufferBase,gGraphicsOutput->Mode->FrameBufferSize);kernel_boot_para_info->Graphics_Info.HorizontalResolution = gGraphicsOutput->Mode->Info->HorizontalResolution;
kernel_boot_para_info->Graphics_Info.VerticalResolution = gGraphicsOutput->Mode->Info->VerticalResolution;
kernel_boot_para_info->Graphics_Info.PixelsPerScanLine = gGraphicsOutput->Mode->Info->PixelsPerScanLine;
kernel_boot_para_info->Graphics_Info.FrameBufferBase = gGraphicsOutput->Mode->FrameBufferBase;
kernel_boot_para_info->Graphics_Info.FrameBufferSize = gGraphicsOutput->Mode->FrameBufferSize;
/* 填寫一些往後將傳遞給 kernel 的資訊 */ // 這邊開始將 frame buffer 映射到 Virtual Address
Print(L"Map Graphics FrameBufferBase to Virtual Address 0xffff800003000000\n");
Print(L"gGraphicsOuput->Mode->FrameBufferSize : %d, gGraphicsOutput->Mode->FrameBufferBase : %ld\n", gGraphicsOutput->Mode->FrameBufferBase);
long * PageTableEntry = (long *)0x103000;
for(i = 0;i < (gGraphicsOutput->Mode->FrameBufferSize + 0x200000 - 1) >> 21;i++)
{
*(PageTableEntry + 24 + i) = gGraphicsOutput->Mode->FrameBufferBase | 0x200000 * i | 0x87;
Print(L"Page %02d,Address:%018lx,Value:%018lx\n",i,(long)(PageTableEntry + 24 + i),*(PageTableEntry + 24 + i));
/* map to virtual address 0xffff800003000000 */
}
/* 這一段 code 應該要搭配 head.S 會比較容易理解*/ /* 由下面可知道, PageTableEntry 的第 24 個 entry 會對應到 0x3000000 ,只是在這個位址我們填上了 FrameBufferBase | 0x200000 * i | 0x87 */
/* 可使用 python 算算看 hex(1024 * 1024 * 2 *24 ) */
/* 每個 page 為 2MB ,第 24 個 page 為 0x3000000 */
head.S, 設定 paging
align 8.org 0x1000
// 應該是指定現在開始的 instruction 是在 0x100000 ( base ) + 0x1000 ( offset ) 的地方__PML4E:.quad 0x102007
.fill 255,8,0
.quad 0x102007
.fill 255,8,0.org 0x2000__PDPTE:.quad 0x103007 /* 0x103003 */
.fill 511,8,0.org 0x3000__PDE:
.quad 0x000087
.quad 0x200087
.quad 0x400087
.quad 0x600087
.quad 0x800087
.quad 0xa00087 /* 0xa00000 */
.quad 0xc00087
.quad 0xe00087
.quad 0x1000087
.quad 0x1200087 /* 0x1000000*/
.quad 0x1400087
.quad 0x1600087
.quad 0x1800087
.fill 499,8,0
簡單畫成圖的話,差不多是這個感覺。
因為我們是使用 2MB 的物理頁 ( 每個 page 的大小為 2 MB ) ,所以 Offset 就佔了 21 個 bits 。
當我們的 Linear Address 為 0x3000000 時,代表我們使用第 24 個 PDT ,並且 Offset 為 0 。假如我們在這裡填入 graphic FrameBuffer 的物理位址 ( 在我們的例子, graphic FrameBuffer 的基底位址為 0xD0000000 ) 的話,當我們對 Linear Address : 0x3000000 進行存取時,就能對 graphic FrameBuffer == Physical Address : 0xD0000000 進行存取。
3. 取得記憶體配置
struct EFI_E820_MEMORY_DESCRIPTOR *E820p = kernel_boot_para_info->E820_Info.E820_Entry;
struct EFI_E820_MEMORY_DESCRIPTOR *LastE820 = NULL;
unsigned long LastEndAddr = 0;
int E820Count = 0; /* 紀錄總共有多少個 E820 */ UINTN MemMapSize = 0;
EFI_MEMORY_DESCRIPTOR* MemMap = 0;
UINTN MapKey = 0;
UINTN DescriptorSize = 0;
UINT32 DesVersion = 0;gBS->GetMemoryMap(&MemMapSize,MemMap,&MapKey,&DescriptorSize,&DesVersion);
/* 嘗試取得記憶體資訊 */
/* 關於為什麼這邊要使用兩次 GetMemoryMap,可以看看 Reference5 的說明 */
/* 第一次呼叫 EFI_GET_MEMORY_MAP, 如果給的 MemoryMap 空間太小, 會得到 EFI_BUFFER_TOO_SMALL 錯誤, 並會得知所需要的空間大小 (透過 MemoryMapSize 傳回), 所以可以用 AllocatePool() 重新配置一個大小, 再次呼叫 EFI_GET_MEMORY_MAP。 */ MemMapSize += DescriptorSize * 5; gBS->AllocatePool(EfiRuntimeServicesData,MemMapSize,(VOID**)&MemMap);
/* 分配一塊空間,用起來就很像 c 的 malloc */ Print(L"Get MemMapSize:%d,DescriptorSize:%d,count:%d\n",MemMapSize,DescriptorSize,MemMapSize/DescriptorSize); gBS->SetMem((void*)MemMap,MemMapSize,0);
/* 初始化 MemMap 成 0*/ status = gBS->GetMemoryMap(&MemMapSize,MemMap,&MapKey,&DescriptorSize,&DesVersion);
/* 真正獲取記憶體資訊的地方 */ Print(L"Get MemMapSize:%d,DescriptorSize:%d,count:%d\n",MemMapSize,DescriptorSize,MemMapSize/DescriptorSize);
if(EFI_ERROR(status)) {
Print(L"status:%018lx\n",status);
} Print(L"Get EFI_MEMORY_DESCRIPTOR Structure:%018lx\n",MemMap); /* 針對每一個記憶體區塊的訊息進行解析 */
for(i = 0;i < MemMapSize / DescriptorSize;i++)
{
int MemType = 0;
EFI_MEMORY_DESCRIPTOR* MMap = (EFI_MEMORY_DESCRIPTOR*) ((CHAR8*)MemMap + i * DescriptorSize);
if(MMap->NumberOfPages == 0)
continue;
switch(MMap->Type)
{
case EfiReservedMemoryType:
case EfiMemoryMappedIO:
case EfiMemoryMappedIOPortSpace:
case EfiPalCode:
MemType= 2; //2:ROM or Reserved
break; case EfiUnusableMemory:
MemType= 5; //5:Unusable
break; case EfiACPIReclaimMemory:
MemType= 3; //3:ACPI Reclaim Memory
break; case EfiLoaderCode:
case EfiLoaderData:
case EfiBootServicesCode:
case EfiBootServicesData:
case EfiRuntimeServicesCode:
case EfiRuntimeServicesData:
case EfiConventionalMemory:
case EfiPersistentMemory:
MemType= 1; //1:RAM
break; case EfiACPIMemoryNVS:
MemType= 4; //4:ACPI NVS Memory
break; default:
Print(L"Invalid UEFI Memory Type:%4d\n",MMap->Type);
continue;
} if((LastE820 != NULL) && (LastE820->type == MemType) && (MMap->PhysicalStart == LastEndAddr))
{
LastE820->length += MMap->NumberOfPages << 12;
LastEndAddr += MMap->NumberOfPages << 12;
}
else
{
E820p->address = MMap->PhysicalStart;
E820p->length = MMap->NumberOfPages << 12;
E820p->type = MemType;
LastEndAddr = MMap->PhysicalStart + (MMap->NumberOfPages << 12);
LastE820 = E820p;
E820p++;
E820Count++;
}
} kernel_boot_para_info->E820_Info.E820_Entry_count = E820Count;
LastE820 = kernel_boot_para_info->E820_Info.E820_Entry;
int j = 0;
for(i = 0; i< E820Count; i++)
{
struct EFI_E820_MEMORY_DESCRIPTOR* e820i = LastE820 + i;
struct EFI_E820_MEMORY_DESCRIPTOR MemMap;
for(j = i + 1; j< E820Count; j++)
{
struct EFI_E820_MEMORY_DESCRIPTOR* e820j = LastE820 + j;
if(e820i->address > e820j->address)
{
MemMap = *e820i;
*e820i = *e820j;
*e820j = MemMap;
}
}
} LastE820 = kernel_boot_para_info->E820_Info.E820_Entry;
for(i = 0;i < E820Count;i++)
{
//Print(L"MemoryMap (%10lx<->%10lx) %4d\n",LastE820->address,LastE820->address+LastE820->length,LastE820->type);
LastE820++;
}
Print(L"kernel_boot_para_info->E820_Info.E820_Entry : 0x%lx", kernel_boot_para_info->E820_Info.E820_Entry);
gBS->FreePool(MemMap);
4. 將 protocol 關閉並跳到 kernel 內,開始執行 kernel
/* close protocol and jump to kernel */
Print(L"Call ExitBootServices And Jmp to Kernel.\n");
gBS->GetMemoryMap(&MemMapSize,MemMap,&MapKey,&DescriptorSize,&DesVersion);
/* 我找不到資料,不知道為什麼這邊還要再呼叫一次 GetMemoryMap */ gBS->CloseProtocol(LoadedImage->DeviceHandle,&gEfiSimpleFileSystemProtocolGuid,ImageHandle,NULL);
gBS->CloseProtocol(ImageHandle,&gEfiLoadedImageProtocolGuid,ImageHandle,NULL);gBS->CloseProtocol(gGraphicsOutput,&gEfiGraphicsOutputProtocolGuid,ImageHandle,NULL);
/* 在離開 UEFI Boot&Loader 前,先把 protocol 關一關 */ status = gBS->ExitBootServices(ImageHandle,MapKey);
/* 在不想使用 UEFI 的服務後,可以 ExitBootService */ if(EFI_ERROR(status))
{
Print(L"ExitBootServices: Failed, Memory Map has Changed.\n");
return EFI_INVALID_PARAMETER;
} func = (void *)0x100000;
/* 用一個 function pointer 指向上面我們載入 kernel.bin 的位址 */
func();
/* 開始執行 kernel! */ return EFI_SUCCESS;
}
結語
到此為止,已經使用 uefi 來做一個簡易的 Boot&Loader,接下來的文章希望能來介紹怎麼編譯與執行一個簡易的系統核心,然後顯示簡單的畫面在螢幕上~感謝大家的收看。
Todo
1. 需要更理解 "取得記憶體配置" 這一步的意義與原理。
Reference
2. Step to Uefi 系列文章
https://www.lab-z.com/iof/
3. Step to Uefi (40)
http://www.lab-z.com/shellwin/
4. UEFI : memory allocation service
https://edk2-docs.gitbook.io/edk-ii-uefi-driver-writer-s-guide/5_uefi_services/51_services_that_uefi_drivers_commonly_use/511_memory_allocation_services
5. GetMemoryMap
https://descent-incoming.blogspot.com/2019/04/uefi-os-loader-3-get-memory-map.html?showComment=1619352479169#c8609921321902681328
6. GetMemoryMap
https://edk2-docs.gitbook.io/edk-ii-uefi-driver-writer-s-guide/5_uefi_services/readme.3/5311_getmemorymap
7. ExitBootServices
https://edk2-docs.gitbook.io/edk-ii-uefi-driver-writer-s-guide/5_uefi_services/readme.3/5312_exitbootservices