[2020]EFI驱动编写教程 简单粗暴的开始到结束 huoji 驱动,UEFI 2020-08-19 1703 次浏览 0 次点赞 19年全力研究hypervisor后就一直没空研究新的项目,而国外的论坛已经兴起了EFI驱动了.本着与时俱进的态度学习了一下EFI驱动.大概的给大家做了一下代码整理 具体的你可以查看 http://rodsbooks.com/ 与 https://edk2-docs.gitbook.io/ 这里面基本看一眼就会了 好了废话不多说,注释都写道代码里面了. ```cpp // EFI 入口函数 ImageHandle是控制器的Handle SystemTable是EFI接口(如果你要使用更多的EFI功能就必须要用这个接口) EFI_STATUS efi_main(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) { // 设置GNU-EFI里面的一堆全局变量 InitializeLib(ImageHandle, SystemTable); // Get handle to this image EFI_LOADED_IMAGE *LoadedImage = NULL; /* OpenProtocol 用于查询指定的Handle中是否支持指定的Protocol,如果支持,则打开该Protocol,否则返回错误代码。 Handle:查询次Handle提供的Protocol *Protocol:要打开的Protocol(指向次Protocol GUID的指针) **Interface:返回打开的Protocol的对象 AgentHandle:打开此Protocol的Image ControllerHandle:打开此Protocol的控制器 Attributes:打开此Protocol的方式 Handle是Protocol的提供者,如果Handle的Protocols链表中有该Protocol,则Protocol对象的指针写到*Interface,并返回EFI_SUCCESS,否则返回EFI_UNSUPPORTED。 如果在驱动中调用OpenProtocol,则ControllerHandle是拥有该驱动的控制器,也就是请求使用这个Protocol的控制器;AgentHandle是拥有该EFI_DRIVER_BINDING_PROTOCOL对象的Handle。EFI_DRIVER_BINDING_PROTOCOL是UEFI驱动开发一定会用到的一个Protocol,它负责驱动的安装与卸载。 如果调用OpenProtocol的是应用程序,那么AgentHandle是该用用程序的Handle,也就是UefiMain的第一个参数,ControllerHandle此时可以忽略。 */ EFI_STATUS status = BS->OpenProtocol(ImageHandle, &LoadedImageProtocol, (void **)&LoadedImage, ImageHandle, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL); // 错了就不加载了 if (EFI_ERROR(status)) { //注意这里的print是GNU-EFI提供的,这个实际上是调用了uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, L"Hello World!\n"); //为什么要用uefi_call_wrapper 因为EFI是用Microsoft ABI 而这个代码编写框架GNU-EFIS是用了ysV ABI uefi_call_wrapper函数就是用于转换不同的指针并且调用 Print(L"Can't open protocol: %d\n", status); //你想清理屏幕,使用 //uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut); return status; } //在控制器上安装protocol,这里面的ProtocolGuid其实是一组id DummyProtocalData dummy = {0}; status = LibInstallProtocolInterfaces( &ImageHandle, &ProtocolGuid, &dummy, NULL); // 失败就不继续走了 if (EFI_ERROR(status)) { Print(L"Can't register interface: %d\n", status); return status; } // 设置卸载的时候的操作 LoadedImage->Unload = (EFI_IMAGE_UNLOAD)efi_unload; // Create global event for VirtualAddressMap /* UEFI EVENT 的作用就是创建一个事件A,然后设定一个条件B,使得条件B满足的时候就执行这个事件A。条件B可以直接是 创建事件有两个函数CreateEvent()和CreateEventEx()。CreateEvent()只创建一个事件。后CreateEventEx()创建一个事件列表,最常用的就是创建ReadyToBootEvent 在启动OS之前处理某些事情。 这里创建了一个名字叫做NotifyEvent事件用一个叫做VirtualGuid的GUID去调用SetVirtualAddressMapEvent 系统在准备修正EFI物理空间与系统虚拟空间的时候会调用这个SetVirtualAddressMapEvent */ status = BS->CreateEventEx(EVT_NOTIFY_SIGNAL, TPL_NOTIFY, SetVirtualAddressMapEvent, NULL, VirtualGuid, &NotifyEvent); // 创建事件失败就不搞了 if (EFI_ERROR(status)) { Print(L"Can't create event (SetVirtualAddressMapEvent): %d\n", status); return status; } // 这边与上面一样,这个是当系统即将调用boot程序启动的时候调用ExitBootServicesEvent status = BS->CreateEventEx(EVT_NOTIFY_SIGNAL, TPL_NOTIFY, ExitBootServicesEvent, NULL, ExitGuid, &ExitEvent); // 创建事件失败就不搞了 if (EFI_ERROR(status)) { Print(L"Can't create event (ExitBootServicesEvent): %d\n", status); return status; } /* 这里就开始挂钩EFI的函数了 通过替换页表的方式 为什么要挂钩函数,因为这些函数是系统NT API CALLED的时候调用的 外挂想实现xxoo的功能,就可以在这里实现(做通讯) GetTime <-> NtQuerySystemTime SetTime <-> NtSetSystemTime GetVariable <-> NtQuerySystemEnvironmentValueEx GetNextVariableName <-> NtEnumerateSystemEnvironmentValuesEx SetVariable <-> NtSetSystemEnvironmentValueEx ResetSystem <-> NtShutdownSystem */ oSetVariable = (EFI_SET_VARIABLE)SetServicePointer(&RT->Hdr, (VOID **)&RT->SetVariable, (VOID **)&HookedSetVariable); // Hook all the other runtime services functions oGetTime = (EFI_GET_TIME)SetServicePointer(&RT->Hdr, (VOID **)&RT->GetTime, (VOID **)&HookedGetTime); oSetTime = (EFI_SET_TIME)SetServicePointer(&RT->Hdr, (VOID **)&RT->SetTime, (VOID **)&HookedSetTime); oGetWakeupTime = (EFI_SET_TIME)SetServicePointer(&RT->Hdr, (VOID **)&RT->GetWakeupTime, (VOID **)&HookedGetWakeupTime); oSetWakeupTime = (EFI_SET_WAKEUP_TIME)SetServicePointer(&RT->Hdr, (VOID **)&RT->SetWakeupTime, (VOID **)&HookedSetWakeupTime); oSetVirtualAddressMap = (EFI_SET_VIRTUAL_ADDRESS_MAP)SetServicePointer(&RT->Hdr, (VOID **)&RT->SetVirtualAddressMap, (VOID **)&HookedSetVirtualAddressMap); oConvertPointer = (EFI_CONVERT_POINTER)SetServicePointer(&RT->Hdr, (VOID **)&RT->ConvertPointer, (VOID **)&HookedConvertPointer); oGetVariable = (EFI_GET_VARIABLE)SetServicePointer(&RT->Hdr, (VOID **)&RT->GetVariable, (VOID **)&HookedGetVariable); oGetNextVariableName = (EFI_GET_NEXT_VARIABLE_NAME)SetServicePointer(&RT->Hdr, (VOID **)&RT->GetNextVariableName, (VOID **)&HookedGetNextVariableName); //oSetVariable = (EFI_SET_VARIABLE)SetServicePointer(&RT->Hdr, (VOID**)&RT->SetVariable, (VOID**)&HookedSetVariable); oGetNextHighMonotonicCount = (EFI_GET_NEXT_HIGH_MONO_COUNT)SetServicePointer(&RT->Hdr, (VOID **)&RT->GetNextHighMonotonicCount, (VOID **)&HookedGetNextHighMonotonicCount); oResetSystem = (EFI_RESET_SYSTEM)SetServicePointer(&RT->Hdr, (VOID **)&RT->ResetSystem, (VOID **)&HookedResetSystem); oUpdateCapsule = (EFI_UPDATE_CAPSULE)SetServicePointer(&RT->Hdr, (VOID **)&RT->UpdateCapsule, (VOID **)&HookedUpdateCapsule); oQueryCapsuleCapabilities = (EFI_QUERY_CAPSULE_CAPABILITIES)SetServicePointer(&RT->Hdr, (VOID **)&RT->QueryCapsuleCapabilities, (VOID **)&HookedQueryCapsuleCapabilities); oQueryVariableInfo = (EFI_QUERY_VARIABLE_INFO)SetServicePointer(&RT->Hdr, (VOID **)&RT->QueryVariableInfo, (VOID **)&HookedQueryVariableInfo); // Print confirmation text Print(L"efi-memory (build on: %a in: %a)\n", __DATE__, __TIME__); Print(L"https://github.com/SamuelTulach/efi-memory\n"); return EFI_SUCCESS; } // 这里HOOK住了 SetVariable() // 这个函数会被NtSetSystemEnvironmentValueEx调用 EFI_STATUS EFIAPI HookedSetVariable( IN CHAR16 *VariableName, IN EFI_GUID *VendorGuid, IN UINT32 Attributes, IN UINTN DataSize, IN VOID *Data) { // 下面代码必须是完全系统初始化了才可以运行 if (Virtual && Runtime) { // 空指针经检查 if (VariableName != NULL && VariableName[0] != CHAR_NULL && VendorGuid != NULL) { // VariableName 是R3传进来的.只有一个同样的VariableName才继续 if (StrnCmp(VariableName, VARIABLE_NAME, (sizeof(VARIABLE_NAME) / sizeof(CHAR16)) - 1) == 0) { if (DataSize == 0 && Data == NULL) { // Skip no data return EFI_SUCCESS; } // Check if the data size is correct if (DataSize == sizeof(MemoryCommand)) { //R3/R0调用了这个函数,xxoo return RunCommand((MemoryCommand *)Data); } } } } // Call the original SetVariable() function return oSetVariable(VariableName, VendorGuid, Attributes, DataSize, Data); } // 这个事件随后会被系统调用 VOID EFIAPI SetVirtualAddressMapEvent( IN EFI_EVENT Event, IN VOID *Context) { /* 这个函数干嘛的?说来话长,都知道普通驱动里面要做虚拟地址、物理地址互相转换对吧? 摘抄: https://zhuanlan.zhihu.com/p/26035864 UEFI的地址转换的解释: UEFI并不是一个操作系统,不需要先进的进程管理,不会把已装载入内存的程序进一步移动 UEFI采取了最简单的地址映射机制:虚拟地址==物理地址。 这样一来,各个UEFI模块(驱动程序或者应用程序)被逐一装入内存的不同地址,共存于内存中。在每个模块装载过程中,根据装载的首地址, 装载程序把这个模块内部各处对函数和数据的访问所使用的绝对地址重新改写一遍,这个过程叫做重定位(relocation)。 这样,模块在自己的装载地址上就能够正确运行了。请注意,装载程序对模块中绝对地址的改写是有依据的,它的依据是模块中的一张重定位表(relocation table), 这张表是模块生成时连接器自动生成的,这张表指出了这个模块中哪些地方编译器生成了对绝对地址的引用。 这个系统运行得很好,直到它装载了操作系统,这时操作系统就会接管机器,建立自己一整套全新的虚拟地址系统 ( 这时虚拟地址在绝大部分情况下不再等于物理地址 ), 即OS会换上一套自己的页表 。这看上没有瑕疵。但是我们要考虑到,有一部分UEFI代码在操作系统运行时仍然发挥作用, 这就是UEFI Runtime Services所用到的代码(及数据)。前面已经说过,这些代码已经在UEFI环境下被重定位过, 那么这些代码在操作系统环境下能否被直接调用呢?答案是否定的,因为UEFI环境下的虚拟地址很可能和操作系统环境下的虚拟地址冲突, 比如,RuntimeServiceA()的UEFI虚拟地址可能是0xCDEF0123,而操作系统很可能已经把0xCDEF0123映射在了另一端代码或数据上了。 如何解决这个问题呢?聪明的你一定想到了,那就是让操作系统的引导程序为UEFI Runtime Services专门指定一段虚拟地址空间, 然后请UEFI把UEFI Runtime Services所用到的代码根据新的地址空间再次重定位到指定的虚拟地址上 !!!!关键!!!!: 为操作系统做重定位时,这个模块已经运行了一段时间,它的状态已经发生了一定变化,具体来说,就是各个全局变量(包括局部静态变量)的内容可能已经包含了一些新的绝对地址, 比如指向了UEFI其他数据结或或新分配的内存。这些变化的内容,都是编译器和链接器无法预测的,当然也就不可能反映在重定位表中。 同时,有些绝对地址在UEFI中是一个常数(如Memory Mapped IO地址,也就是设备的物理地址),对这个常数地址的访问也不会出现在重定位表中, 而在操作系统环境下这个地址仍然要被访问。如果存在这些情况,为了让这些绝对地址在新的虚拟地中空间中也能指向正确的目标,UEFI Runtime Services驱动程序需要额外写一些代码, 把这些指针根据操作系统的要求加以修正。 */ // 这里就开始使用ConvertPointer来把以下的变量从EFI状态的 0 转化为有操作系统状态的 0 RT->ConvertPointer(0, &oSetVariable); // Convert all other addresses RT->ConvertPointer(0, &oGetTime); RT->ConvertPointer(0, &oSetTime); RT->ConvertPointer(0, &oGetWakeupTime); RT->ConvertPointer(0, &oSetWakeupTime); RT->ConvertPointer(0, &oSetVirtualAddressMap); RT->ConvertPointer(0, &oConvertPointer); RT->ConvertPointer(0, &oGetVariable); RT->ConvertPointer(0, &oGetNextVariableName); //RT->ConvertPointer(0, &oSetVariable); RT->ConvertPointer(0, &oGetNextHighMonotonicCount); RT->ConvertPointer(0, &oResetSystem); RT->ConvertPointer(0, &oUpdateCapsule); RT->ConvertPointer(0, &oQueryCapsuleCapabilities); RT->ConvertPointer(0, &oQueryVariableInfo); // 这个是把内部库指针转换为虚拟内存指针 RtLibEnableVirtualMappings(); // 搞完了,可以清理事件了 NotifyEvent = NULL; // 现在在有操作系统状态下的虚拟地址空间了.这个是作者的标记 Virtual = TRUE; } ``` 本文由 huoji 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。 点赞 0
还不快抢沙发