唐凌 发表于 2020-6-14 15:09:59

【UEFI】【PE文件结构】【位图文件结构】使用UEFI图形输出协议播放位图

自从上次我发表了一篇UEFI的Hello World的制作教程后,有人问能不能实现图形输出。于是我随手实现了一个播放图片的demo,但我随后发现了一个问题:
我当时使用了UEFI提供的“简易文件系统协议”接口,可以轻松的读写一个文件。但这个接口并非随时可用,如果在固件中选用Fast Boot,那么这个接口可能就没了。经测试,我的两台华硕笔记本在选用Fast Boot后,简易文件系统协议接口就没了。这个问题需要解决,于是这就是本文的核心了。
由于EFI文件是PE文件结构,那自然就可以把任何玩意编译为PE文件里的资源,然后在运行时提取出来玩。之前有人跟我说把位图的数据设置为一个全局变量然后丢进一个头文件里。我倒是不认同这么干。
上次我的demo是720p画质(1280x720)的,这次干脆就搞个1080p的。上次截的图其实也是1080p的,但是被我缩放到720p并且忘记保存为副本了,于是1080p的原本就丢了。
那么就去新科娘的录播里再截一张图然后用画图程序保存到BMP。作为demo,这里截取了一张神砂岚。
保存好了之后,创建一个.rc文件,然后用Visual Studio打开,创建一个位图资源,导入刚才的神砂岚BMP即可。
原本我打算直接用LLVM来编译这个资源,可是我发现llvm-rc.exe这个玩意似乎没有写完,是个残次品,无法使用,于是只好用vs的rc.exe来编译了。如果不会用命令行,那就搭个其他VC工程的顺风车把这个位图给编译了,然后提出.res文件来使用。
最后直接把它放进链接器参数即可。

我在这个工程中手动实现了基础设施,本来不想造这个轮,因为TianoCore已经在EDK II中写好了,但我编译出来后并不能正常运行,可能是操作不对,但是无心追查了,那还是干脆麻烦点写一些基础设施吧。这个基础设施的最终指向是实现printf,即便我只需要几个关键字,代码还是相当繁琐。这个基础设施的代码就不贴了,实在是太占篇幅。
图形输出需要EFI_GRAPHICS_OUTPUT_PROTOCOL协议,而且不在EFI_SYSTEM_TABLE结构体中,需要自己定位。定位协议接口使用BootServices->LocateProtocol函数,作为初始化,代码如下:
EFI_STATUS EfiInit(IN EFI_SYSTEM_TABLE *SystemTable)
{
        EFI_STATUS st=EFI_SUCCESS;
        EfiBoot=SystemTable->BootServices;
        StdIn=SystemTable->ConIn;
        StdOut=SystemTable->ConOut;
        StdErr=SystemTable->StdErr;
        st=EfiBoot->LocateProtocol(&EfiGraphicsOutputGuid,NULL,(VOID**)&GraphicsOutput);
        if(st!=EFI_SUCCESS)return st;
        st=EfiBoot->LocateProtocol(&EfiUnicodeCollationGuid,NULL,(VOID**)&UnicodeCollation);
        return st;
}
其中我还顺势定位了UnicodeCollation协议接口。定位它是为了在Ansi转码Unicode的过程中省事,其实这还是给printf用的。

完成初始化后,我们需要定位并取出资源。我上次在https://www.0xaa55.com/thread-25934-1-1.html帖子中讲解了PE文件结构的版本资源,这里就不再重复讲资源表的结构了。只说提取位图资源的过程。
由于是在EFI环境中,没有什么接口供你加载资源,所以我们需要自己提取资源。
位图资源的类型ID是2;名称ID取决于resource.h中定义的值,具体是几请自己去看;语言ID和Windows中用的一样,比方说美式英语是1033,简体中文是2052。
位图资源的图片结构不符合BMP文件结构,但相差不多,只是缺了个BMP文件头,最重要的DIB头还在。EDK II的头文件中有定义PE文件结构和BMP文件结构,但是却把BMP文件头和DIB文件头定义到一块去了,于是EDK里定义BMP的头文件就算作废,我们可以另行定义。以下定义是直接从MSDN抄的:
typedef struct tagBITMAPINFOHEADER
{
        UINT32 HeaderSize;
        UINT32 PixelWidth;
        UINT32 PixelHeight;
        UINT16 Planes;          ///< Must be 1
        UINT16 BitPerPixel;   ///< 1, 4, 8, or 24
        UINT32 CompressionType;
        UINT32 ImageSize;       ///< Compressed image size in bytes
        UINT32 XPixelsPerMeter;
        UINT32 YPixelsPerMeter;
        UINT32 NumberOfColors;
        UINT32 ImportantColors;
}BITMAPINFOHEADER,*PBITMAPINFOHEADER;
我们提前编译了位图资源,那就有必要定义一下我们需要什么ID。
#define RT_BITMAP        2
#define IDB_BITMAP1        102
#define LANGID_CHINESE_SIMPLIFIED        2052
常量IDB_BITMAP1的值需要自行前往resource.h文件中确定,本文的例子中是102。如果是一个大工程可以直接include这个resource.h然后直接使用这个常量就行了。
在EDK II中的MdePkg/Include/IndustryStandard/PeImage.h中定义了PE文件结构。不需要复制一个ntimage.h过来用。
解析PE映像需要一个具体的映像地址,但是加载EFI时没有直接提供这个映像地址,所以需要取得映像地址。映像地址位于EFI_LOADED_IMAGE_PROTOCOL结构体中,通过HandleProtocol函数可以取得结构体地址。
综上所述,代码如下:
EFI_STATUS FindBitmapFromImage(IN EFI_HANDLE ImageHandle,IN UINT32 LangID,IN UINT32 BitmapID,OUT PBITMAPINFOHEADER *BitmapData)
{
        EFI_LOADED_IMAGE_PROTOCOL *LoadedImage=NULL;
        EFI_STATUS st=EfiBoot->HandleProtocol(ImageHandle,&EfiLoadedImageGuid,(VOID**)&LoadedImage);
        if(st==EFI_SUCCESS)
        {
                EFI_IMAGE_DOS_HEADER *DosHead=(EFI_IMAGE_DOS_HEADER*)LoadedImage->ImageBase;
                st=EFI_INVALID_PARAMETER;
                if(DosHead->e_magic==EFI_IMAGE_DOS_SIGNATURE)
                {
#if defined(_EFI32)
                        EFI_IMAGE_NT_HEADERS32 *NtHead=(EFI_IMAGE_NT_HEADERS32*)((UINTN)DosHead+DosHead->e_lfanew);
#else
                        EFI_IMAGE_NT_HEADERS64 *NtHead=(EFI_IMAGE_NT_HEADERS64*)((UINTN)DosHead+DosHead->e_lfanew);
#endif
                        if(NtHead->Signature==EFI_IMAGE_NT_SIGNATURE)
                        {
                                UINT32 ResDirOffset=NtHead->OptionalHeader.DataDirectory.VirtualAddress;
                                EFI_IMAGE_RESOURCE_DIRECTORY *ResDir=(EFI_IMAGE_RESOURCE_DIRECTORY*)((UINTN)DosHead+ResDirOffset);
                                EFI_IMAGE_RESOURCE_DIRECTORY_ENTRY *ResEntry=(EFI_IMAGE_RESOURCE_DIRECTORY_ENTRY*)((UINTN)ResDir+sizeof(EFI_IMAGE_RESOURCE_DIRECTORY));
                                st=EFI_NOT_FOUND;
                                for(UINT16 i=0;i<ResDir->NumberOfNamedEntries+ResDir->NumberOfIdEntries;i++)
                                {
                                        if(ResEntry.u1.Id==RT_BITMAP && ResEntry.u2.s.DataIsDirectory)
                                        {
                                                EFI_IMAGE_RESOURCE_DIRECTORY *ImgDir=(EFI_IMAGE_RESOURCE_DIRECTORY*)((UINTN)ResDir+ResEntry.u2.s.OffsetToDirectory);
                                                EFI_IMAGE_RESOURCE_DIRECTORY_ENTRY *ImgList=(EFI_IMAGE_RESOURCE_DIRECTORY_ENTRY*)((UINTN)ImgDir+sizeof(EFI_IMAGE_RESOURCE_DIRECTORY));
                                                for(UINT16 j=0;j<ImgDir->NumberOfNamedEntries+ImgDir->NumberOfIdEntries;j++)
                                                {
                                                        if(ImgList.u1.Id==BitmapID && ImgList.u2.s.DataIsDirectory)
                                                        {
                                                                EFI_IMAGE_RESOURCE_DIRECTORY *LangDir=(EFI_IMAGE_RESOURCE_DIRECTORY*)((UINTN)ResDir+ImgList.u2.s.OffsetToDirectory);
                                                                EFI_IMAGE_RESOURCE_DIRECTORY_ENTRY *LangList=(EFI_IMAGE_RESOURCE_DIRECTORY_ENTRY*)((UINTN)LangDir+sizeof(EFI_IMAGE_RESOURCE_DIRECTORY));
                                                                for(UINT16 k=0;k<LangDir->NumberOfNamedEntries+LangDir->NumberOfIdEntries;k++)
                                                                {
                                                                        if(LangList.u1.Id==LangID)
                                                                        {
                                                                                EFI_IMAGE_RESOURCE_DATA_ENTRY *ImageEntry=(EFI_IMAGE_RESOURCE_DATA_ENTRY*)((UINTN)ResDir+LangList.u2.OffsetToData);
                                                                                *BitmapData=(PBITMAPINFOHEADER)((UINTN)DosHead+ImageEntry->OffsetToData);
                                                                                return EFI_SUCCESS;
                                                                        }
                                                                }
                                                                if(LangID)break;
                                                        }
                                                }
                                        }
                                }
                        }
                }
        }
        return st;
}
我自认为不需要加注释了,就变量名而言,我的定义足够一目了然了。

提取出位图资源后,就应该把它输出到屏幕上了。使用UEFI的图形输出协议接口时,可以用Blt方法,当然也可以直接对着屏幕帧的地址去写玩意。(我还没去具体了解该怎么写)
由于懒的问题,我就直接限定屏幕分辨率设置为1920x1080,如果不支持那我就不玩了。
首先设置屏幕分辨率,我们需要查询所有图形输出接口的模式,然后选择我们要的再设置之。代码如下:
UINT32 ChooseGraphicMode(IN UINT32 Horizontal,IN UINT32 Vertical)
{
        UINT32 Index=0xFFFFFFFF;
        for(UINT32 i=0;i<GraphicsOutput->Mode->MaxMode;i++)
        {
                UINTN InfoSize=0;
                EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *ModeInfo=NULL;
                EFI_STATUS st=GraphicsOutput->QueryMode(GraphicsOutput,i,&InfoSize,&ModeInfo);
                if(st==EFI_SUCCESS)
                {
                        ConsolePrintfW(L"Mode=%d Resolution: %dx%d\r\n",i,ModeInfo->HorizontalResolution,ModeInfo->VerticalResolution);
                        if(Horizontal==ModeInfo->HorizontalResolution && Vertical==ModeInfo->VerticalResolution)Index=i;
                }
        }
        return Index;
}
这个函数会顺便打印出图形输出接口支持的所有分辨率模式。不过这里仅仅选择了模式,而非应用了设置。
处理位图时要注意它的行像素是颠倒的,输出时需要反向输出行。此外还需要注意BMP的每行都要对齐在4字节边界上。(虽然1080p每行有1920个像素,不论每个像素有几位,必然是已经对齐的)
说了这么多,贴代码吧:
EFI_STATUS EFIAPI EfiMain(IN EFI_HANDLE ImageHandle,IN EFI_SYSTEM_TABLE *SystemTable)
{
        // Initialize Protocols and Variables we need.
        EFI_STATUS st=EfiInit(SystemTable);
        if(st==EFI_SUCCESS)
        {
                PBITMAPINFOHEADER BmpData=NULL;
                // Locate the Bitmap from PE Resource Table.
                st=FindBitmapFromImage(ImageHandle,LANGID_CHINESE_SIMPLIFIED,IDB_BITMAP1,&BmpData);
                if(st==EFI_SUCCESS)
                {
                        // Choose the Graphics Resolution Mode we need.
                        UINT32 Mode=ChooseGraphicMode(BmpData->PixelWidth,BmpData->PixelHeight);
                        ConsolePrintfW(L"Located Bitmap! Image Size: %dx%d\r\n",BmpData->PixelWidth,BmpData->PixelHeight);
                        // We don't support BMP Compression.
                        if(BmpData->CompressionType)
                        {
                                ConsoleOutput(L"Bitmap Compression Algorithm is unsupported!\r\n");
                                goto Error;
                        }
                        // We support 24-bit True-Color Bitmap only.
                        if(BmpData->BitPerPixel!=24)
                        {
                                ConsoleOutput(L"Only 24-bit True-Color Format Bitmap is supported!\r\n");
                                goto Error;
                        }
                        // Check if our mode is supported.
                        if(Mode!=0xFFFFFFFF)
                                ConsolePrintfW(L"We will use Graphics Output Mode %d!\r\n",Mode);
                        else
                        {
                                ConsoleOutput(L"Your Graphics Adapter does not support the required resolution!\r\n");
Error:
                                ConsoleOutput(L"Press Enter key to continue...\r\n");
                                BlockUntilKeyStroke(L'\r');
                                return EFI_UNSUPPORTED;
                        }
                        // Block the console here because we, as users, might want to check what have right now.
                        ConsoleOutput(L"Press Enter key to continue...\r\n");
                        BlockUntilKeyStroke(L'\r');
                        // Set the resolution mode. Screen will be cleared simultaneously.
                        st=GraphicsOutput->SetMode(GraphicsOutput,Mode);
                        if(st==EFI_SUCCESS)
                        {
                                // Allocate the BLT Buffer we need.
                                EFI_GRAPHICS_OUTPUT_BLT_PIXEL *BltBuffer=NULL;
                                st=EfiBoot->AllocatePool(EfiLoaderData,BmpData->PixelWidth*BmpData->PixelHeight*sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL),(VOID**)&BltBuffer);
                                if(st==EFI_SUCCESS)
                                {
                                        // Get pointer of the Pixel Array.
                                        UINT8 *BmpPixels=(UINT8*)((UINTN)BmpData+BmpData->HeaderSize);
                                        // Traverse the Pixel Rows.
                                        for(UINT32 i=BmpData->PixelHeight-1;i>0;i--)
                                        {
                                                UINT32 j=BmpData->PixelHeight-i-1;
                                                // Calculate the pointer of Row Pixel Array.
                                                // Note that Row Pixel Array is aligned at 4-byte boundary.
                                                // The summation guarantees the alignment. Don't know why? Learn Mathematics!
                                                UINT8 *HoriPixels=(UINT8*)((UINTN)BmpPixels+j*3*BmpData->PixelWidth+(BmpData->PixelWidth&3));
                                                // Traverse the Pixels in a Row.
                                                for(UINT32 m=0;m<BmpData->PixelWidth;m++)
                                                {
                                                        // Locate the Pixel.
                                                        UINT32 n=i*BmpData->PixelWidth+m;
                                                        UINT32 k=m*3;
                                                        // Copy the Pixel onto BLT Buffer.
                                                        BltBuffer.Blue=HoriPixels;
                                                        BltBuffer.Green=HoriPixels;
                                                        BltBuffer.Red=HoriPixels;
                                                        BltBuffer.Reserved=0;
                                                }
                                        }
                                        // Preparation of BLT Buffer is completed. Perform BLT.
                                        st=GraphicsOutput->Blt(GraphicsOutput,BltBuffer,EfiBltBufferToVideo,0,0,0,0,BmpData->PixelWidth,BmpData->PixelHeight,0);
                                        // Free BLT Buffer.
                                        EfiBoot->FreePool(BltBuffer);
                                }
                        }
                }
        }
        // Block the system so you can see what's going on.
        BlockUntilKeyStroke(L'\r');
        return st;
}
还是在代码里加点注释好。

以下是VMware虚拟机的运行结果:

注意,这里需要把虚拟机的操作系统设置为其他(64位),否则不支持1080p分辨率。
有些人可能不认同虚拟机的运行结果。那么就在实机上测试吧。不过鉴于笔记本接视频信号采集卡根本采集不到视频,而我又不喜欢手机拍屏,手头上还没有台式机,就只好找个中间办法:找个能输出视频信号的嵌入式UEFI平台。最后,我选择了钓鱼派。
选择钓鱼派的原因很简单:钓鱼派使用UEFI固件,搭载Intel Atom E3845四核心处理器,有2GiB的DDR3内存,还支持输出1080p分辨率的视频信号。钓鱼派上有一个mHDMI接口,通过mHDMI转HDMI的线接到视频信号采集卡上,然后再通过USB3.0连接到电脑上,在电脑上运行OBS来录制视频采集设备,最后用你自己的方法把视频剪辑一下,删除不需要给别人看的东西,输出为GIF。
http://tangptr.com/wp-content/uploads/2020/06/efi_xinke2.gif
由于编译完的文件实在是太大,只能删掉编译好的二进制文件再上传了。编译环境和上次写的UEFI Hello World配置的环境一样。

0xAA55 发表于 2020-6-14 19:23:20

使用UEFI进行简单的图形输出,然后支持一下网络,NTFS,多线程,GPU,基本就是一个简单完整的操作系统了。

Ayala 发表于 2020-6-16 02:39:14

做个uefi的远控带个 做成u盘 就轻松使用可以远程装机了 类似hp的 ilo
页: [1]
查看完整版本: 【UEFI】【PE文件结构】【位图文件结构】使用UEFI图形输出协议播放位图