C + + Authoring operating system (1): EFI-based Bootloader

Source: Internet
Author: User

A long time ago on the operating system is very curious, with so many years of windows, the mechanism of his operation is not very clear, so always want to write one of their own, study how the operating system is how to achieve. Later on the internet also found some tutorials (such as: " self-write operating system "), most of the first to use sinks to write the first sector of the active partition (MBR). Around April 13 I have also tried to follow the tutorial, with assembly call BIOS interrupt read sector, load bootstrap. I have to say that the assembly is easy to make mistakes, readability is not good, so this time I would like to be able to write the operating system without the sink.

UEFI

After a search, I found something called Uefi, and here's a brief introduction:

The Unified Extensible Firmware Interface (Unified extensible Firmware Interface, UEFI) is a personal computer system specification used to define the operating system Software interface to the system firmware as an alternative to the BIOS. The Extensible Firmware Interface is responsible for the power-on self-test (POST), the connection operating system , and the interface that connects the operating system to the hardware.

--Excerpt from Wikipedia

In short, (U) EFI is an alternative to the traditional BIOS specification, OS startup phase we can no longer deal with the troublesome BIOS. And because Uefi completely uses the C-style programming interface, it means that we can only use C, C + + to guide our OS. Development Uefi can use EDK, however, in comparison, Intel's EFI Toolkit is no longer updated, but simple to use, for our development of bootloader is enough, so I chose to use EFI Toolkit to develop the EFI program.

1. Compiling EFI Toolkit

After downloading the EFI toolkit, we extracted him into a directory that was easy to find:

Since I am developing an EFI program running in the Intel 64 architecture, go to the BUILD\EM64T directory, open sdk.env and modify the configuration:

Change the selected section to the directory of the VC AMD64 compiler ("Xxx\microsoft Visual Studio 14.0\vc\bin\amd64", remember to add double quotes).

Then open vs 2015 (other versions can also) x64 local Tools command prompt, switch to the EFI Toolkit directory, execute build EM64T, and then execute NMAKE.

After a while, the EFI Toolkit is compiled.

2. Write a bootloader

Bootloader is the first software code to power up the system, recalling the architecture of the PC we can know that the boot loader in the PC is composed of the BIOS (which is essentially a piece of firmware) and the boot program located in the MBR of the hard disk . After completing hardware detection and resource allocation, the BIOS reads the bootloader in the hard disk MBR into the system's RAM, and then gives control to the boot program. The main running task of the bootloader is to read the kernel image from the hard disk into RAM and then jump to the kernel entry point to run, starting the operating system.

--excerpt from the Interactive Encyclopedia

2.1 Configuration Items

There are many types of EFI programs, and the bootloader we write belongs to the OSLoader in EFI application. EFI will look for the Bootx64.efi file in the Startup disk Efi\boot directory when it launches the OS, and this file is actually a pe32+ format application, and the VC also provides support for compiling such a program, so we can write bootloader directly using vs. After creating the project for easy administration we can set the output directory and output file name.

This way we only need to copy the entire output directory to the boot partition when we deploy the OS.

Also set the link option to set the subsystem to EFI application(IMPORTANT):

Also set the following compilation options:

    • Turn off C + + exceptions
    • Set basic run-time check to Default
    • Close security Check (/gs-)

Set the following link options:

    • Ignore default library (/NODEFAULTLIB)
    • Add additional libraries: Libefi.lib
    • Turn off UAC support (/manifestuac:no)
    • Close random base addresses (/dynamicbase:no)
    • Turn off DEP support (/nxcompat:no)
    • Set entry point (e.g. Efi_main)

Also set up VC + + directory, add the following directory to the Include directory:

    • Efi_toolkit_2.0\include\efi
    • efi_toolkit_2.0\include\efi\em64t

Add the directory to the Lib directory:

    • Efi_toolkit_2.0\build\em64t\output\lib\libefi

A whole bunch of stuff. Finally, you can write our code after it's done.

2.2 Writing code

The entry for the EFI program is defined as follows:

Efi_status __cdecl Efi_main (efi_handle imagehandle, efi_system_table *systemtable)

Where Efi_main's name is random, remember to set the entry point in the link options. There are also two parameters:

    • Imagehandle is the handle that we bootloader be loaded with LoadImage, from which we can get some information, which we will use later.
    • Systemtable contains all of the services that EFI provides to us, and we rely on him to deal with hardware, which contains a variety of APIs that can be used.

2.2.1 Loading kernel files

One of the most important things to do with bootloader is to load the kernel, and the first step is to read the kernel files from the hard drive or other storage devices to the memory, and EFI gives us a handy way to do this.

We assume that the kernel files and bootloader are in the same partition, and we can get the handle of this partition.

Loading efilib on the ingress function

Initializelib (Imagehandle, systemtable);

Gets the handle to the volume where the bootloader is located:

void Kernelfile::loadkernelfile () {efi_loaded_image* loadedimage; efi_file_io_interface* volume;//Get Loader volume Bs->handleprotocol (imagehandle, &loadedimageprotocol, (void**) &loadedimage); Bs->handleprotocol (Loadedimage->devicehandle, &filesystemprotocol, (void**) &volume);

The so-called protocol is similar to the concept of an interface, a handle is equivalent to an instance of a class, we use the Handleprotocol function provided by bootservices to get an interface of this class-the first argument is a handle, The second parameter is the GUID of protocol, and the third parameter is a pointer to protocol. See this usage do not know if anyone remembered com←_←.

Next is the remainder of the Loadkernelfile method:

Efi_file_handle RootFS, Filehandle;volume->openvolume (volume, &rootfs);//Read file exit_if_not_success (rootFS- >open (RootFS, &filehandle, (char16*) Kernelfilepath, Efi_file_mode_read, 0), Imagehandle, L "Cannot Open Tomato Kernel file.\r\n "); uint8* Kernelbuffer; Exit_if_not_success (Bs->allocatepool (Efiloaderdata, Kernelpoolsize, (void**) &kernelbuffer), ImageHandle, L " Cannot Allocate Tomato Kernel buffer.\r\n "); Exit_if_not_success (Filehandle->read (FileHandle, &kernelpoolsize, Kernelbuffer), ImageHandle, L "Cannot Read Tomato Kernel file.\r\n "); Filehandle->close (filehandle); kernelfilebuffer = Kernelbuffer;}

In this code we get a root directory interface from the partition interface obtained from above, and we use this root interface to get the interface of our kernel file, where the third parameter is the path of the file:

static const wchar_t kernelfilepath[] = LR "(tomato\system\oskernel.exe)";

We then used the memory management capabilities provided by Bootservices to allocate a kernelpoolsize-sized memory and then read the file contents into memory using the kernel file interface just acquired.

2.2.2 Parsing kernel files

The kernel file has been loaded into memory, because the kernel file is actually a PE-formatted application, we need to parse him like windows and read the required content to the memory where it should be put.

The head of the PE file is defined in the pe.h.

First we verify the validity of the PE file:

BOOL Kernelfile::validatekernel () {image_dos_header* Dosheader = (image_dos_header*) kernelfilebuffer;if (dosHeader- >e_magic! = image_dos_signature) return false;image_nt_headers* ntheaders = (image_nt_headers*) (KernelFileBuffer + Dosheader->e_lfanew); if (ntheaders->signature! = image_nt_signature) return False;return TRUE;}

The specific algorithm is to verify the DOS head of the MZ logo and PE head of the PE00 logo.

If the file is a valid PE image, we next need to parse each of the included sections and copy them into another piece of memory:

void Bootloader::P reparekernel (kernelfile& file) {if (file. Validatekernel ()) {Auto Kernelimagebase = file. Getkernelfilebuffer (); image_dos_header* Dosheader = (image_dos_header*) kernelimagebase;image_nt_headers* ntHeaders = (image_nt_headers*) (kernelimagebase + dosheader->e_lfanew); SectionStart = (image_section_header*) (((UINT8*) ntheaders) + sizeof (image_nt_headers)-sizeof (Image_optional_header) + ntheaders-> Fileheader.sizeofoptionalheader); sectioncount = ntheaders->fileheader.numberofsections; Uintn sectionalign = ntheaders->optionalheader.sectionalignment; Uintn filealign = ntheaders->optionalheader.filealignment; Uintn memallocpages = getallsectionsmemorypages (SectionStart, Sectioncount); Exit_if_not_success (Bs->allocatepages (Allocateanypages, Kernelpooltype, Memallocpages,&kernelmembuffer), Imagehandle, L "Cannot allocate Kernel memory.\r\n"); 

In the above function, we calculate the total number of pages of all sections in the PE file by Getallsectionsmemorypages function, and then use Bootservices's allocatepages function to allocate memory pages, as to why the page size is aligned. is because we do the memory paging requirements later.

          image_section_header* section = SectionStart; uint8* Membuffer = (uint8*) kernelmembuffer;for (Uintn i = 0; i < sectioncount; i++, section++) {BOOLEAN Datainfile = sec Tion->pointertorawdata! = 0; uint8* sectiondata = kernelimagebase + section->pointertorawdata; Uintn memallocsize = alignsize (Section->sizeofrawdata, Efi_page_size), if (memallocsize) {if (dataInFile) CopyMem ( Membuffer, Sectiondata, section->sizeofrawdata); Membuffer + = Memallocsize;}}

Next we'll copy the contents of each section to the memory we just allocated.

2.2.3 Sub-page

We have been using the physical address of the memory so far, though it is simple but there is a problem: if the base address of the kernel is large and beyond the physical memory range then we will not be able to execute the kernel. In order to solve this problem we need to introduce virtual address. There's a detailed description of the page in Intel's manual, where I use ia32e paging mode, which works in 64-bit mode, and we can manage 256TB of memory (physical or virtual) using 48-bit virtual addresses.

However, if the entire address space is paged, memory will be greatly wasted, even can not fit, so ia32e paging mode can use the Level 4 page table. This allows us to keep the page table in physical memory for one of the address spaces, and we can set the present bit of its page table to 0 to indicate that it is not in physical memory, greatly reducing the memory footprint.

My kernel base address is 0x140000000, which is the location of 5GB.

void mappingkerneladdress (Efi_handle imagehandle, EFI_PHYSICAL_ADDRESS kernelmembuffer,image_section_header* section, Uintn sectioncount, pdptable& pdptable) {enum:uint64_t { Kernelpml4eindex = Kernelimagebase/pml4entrymanagesize,kernelpml4erest = kernelimagebase% PML4EntryManageSize, Kernelpdpeindex = Kernelpml4erest/pdpentrymanagesize,kernelpdperest = kernelpml4erest% PDPEntryManageSize, Kernelpdeindex = Kernelpdperest/pdentrymanagesize,kernelpderest = kernelpdperest% Pdentrymanagesize,kernelpteindex = Kernelpderest/ptentrymanagesize,kernelpterest = kernelpderest% ptentrymanagesize};auto& kernelPageDir = * Allocatepagedirectory (imagehandle);auto& kernelpagedirref = Pdptable[kernelpdpeindex]; Kernelpagedirref.present = True;kernelpagedirref.readwrite = True;kernelpagedirref.setptentryaddress (kernelPageDir );

We assign one page directory (page catalog, map 1 GB), set its present to true, represent it in physical memory, and hang it on top of page directory Pointer Table (mapped to GB). Then fill in the corresponding Page table and page table entries according to the virtual address of each section of the kernel, and map to the physical address:

uint8_t* physicaladdr = (uint8_t*) kernelmembuffer;for (size_t i = 0; i < Sectioncount; i++) {auto& curSection = sect Ion[i];if (cursection.sizeofrawdata) {Auto datasize = alignsize (Cursection.sizeofrawdata, efi_page_size);//Start PAGE Table Indexauto Curptindex = Cursection.virtualaddress/pdentrymanagesize;auto Resttomap = dataSize;uint8_t* Startvirtualaddress = (uint8_t*) (Kernelpdpeindex * pdpentrymanagesize +curptindex * pdentrymanagesize); for (; RestToMap ; curptindex++) {auto& pagetableref = kernelpagedir[curptindex];//If not assigned, assign page table if (!pagetableref.present) { Pagetableref.setpagetableaddress (*allocatepagetable (imagehandle));p agetableref.present = TRUE; Pagetableref.readwrite = TRUE;} pagetable& pagetable = pagetableref.getpagetableaddress (); auto Curpeindex = (cursection.virtualaddress% pdentrymanagesize)/Ptentrymanagesize;auto curvirtualaddress = startvirtualaddress + CurPEIndex * PTEntryManageSize; for (size_t j = curpeindex; J < __crt_countof (pagetable); j + +) {auto& pTentry = Pagetable[j];p tentry.setphysicaladdress (physicaladdr);p tentry.present = True;ptentry.readwrite = TRUE; Physicaladdr + = efi_page_size;curvirtualaddress + Ptentrymanagesize;resttomap-= ptentrymanagesize;if (!RESTTOMAP) Break;}}}}

Next, use a similar method to map the first 1 GB of memory (used by EFI's runtime Services), and then enable paging:

Enable paging void bootloader::enablepaging () {//assign pml4tableauto& pml4table = *allocatepml4table (imagehandle);//Assignment pdptableauto& pdptable = *allocatepdptable (imagehandle);//Map the first 1 gbmappinglow1gb (Imagehandle, pdpTable);//The mapping kernel is located 1 gbmappingkerneladdress (Imagehandle, Kernelmembuffer, SectionStart, Sectioncount, pdptable);//Map before GBauto& Pdptableref = pml4table[0];PDP tableref.setpdptableaddress (pdptable);PDP tableref.present = TRUE; Pdptableref.readwrite = TRUE; Enableia32epaging (pml4table);}

Enabling ia32e paging requires setting up a series of registers:

inline void enableia32epaging (const pml4table& pml4table) {Const pml4entry* addr = pml4table;uint64_t CR3 = __READCR3 ( ); CR3 &= ~CR3_PML4_MASK;CR3 |= ((uint64_t) addr) & cr3_pml4_mask;//Save the page table CR3__WRITECR3 (CR3);//enable paging tagCR0 CR0 = __ Readcr0 (); cr0. PG = 1;__writecr0 (cr0.value);//enable PAEtagCR4 CR4 = __READCR4 (); CR4. PAE = 1;__WRITECR4 (cr4.value);//enable ia32e paging Tagmsr_ia32_efer Ia32efer = __READMSR (msr_ia32_efer); Ia32efer.lme = 1;__ WRITEMSR (Msr_ia32_efer, ia32efer.value);}

This completes the paging.

2.2.4 Configuring EFI Runtime Services

As we enter the paging mode and use the virtual address, we need to notify EFI to change his internal pointers to accommodate this change. But since I did the first 1GB paging is 1:1 paging, virtual address = Physical address, so only need simple assignment:

void Bootloader::P reparevirtualmemorymapping () {UINTN entries, Mapkey, Descriptorsize; UINT32 descriptorversion; efi_memory_descriptor* descriptor = Libmemorymap (&entries, &mapkey, &descriptorsize, & Descriptorversion); Bs->exitbootservices (Imagehandle, Mapkey);  efi_memory_descriptor* memorymapentry = descriptor;for (Uintn i = 0; i < entries; i++) {if (Memorymapentry->attribute & efi_memory_runtime) {memorymapentry->virtualstart = Memorymapentry->physicalstart;} Memorymapentry = Nextmemorydescriptor (Memorymapentry, descriptorsize);} Efi_status STATUS = rt->setvirtualaddressmap (Entries * descriptorsize, descriptorsize,efi_memory_descriptor_ VERSION, descriptor), if (status) Rt->resetsystem (Efiresetwarm, Efi_load_error, Efi_error, (char16*) L "Setting Memory mapping failed. "); Params. Memorydescriptor = Descriptor;params. Memorydescriptorsize = Descriptorsize;params. Memorydescriptorentrycount = entries;} 

Use Libmemorymap to get the current memory map and set his Virtualstart (in this case = Physical address) for each of the attributes with Efi_memory_runtime, and finally call runtime The Setvirtualaddressmap function of the services notifies the EFI change pointer.

2.2.5 booting the kernel

The kernel is loaded, the paging is done, EFI is configured, and finally we are going into the New World (←_←

Read the entry point from the kernel file, call, over~

void Bootloader::runkernel (Kernelentrypoint entrypoint) {entrypoint (params);}
Postscript

The first time you write a blog, maybe the Code heap a little more, will work hard to improve in the future. In addition, because the EFI development of very little information, I am also the first contact with this, there must be a lot of wrong understanding of the place, but also please the Garden friends to enlighten.

Recently on the development of operating systems are very interested in the process of learning also hope to communicate with you in depth, thank you:)

C + + Authoring operating system (1): EFI-based Bootloader

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.