如何在运行中的UEFI应用内确定栈大小限制/栈终点,并实现栈高水位调试?
如何在运行中的UEFI应用内确定栈大小限制/栈终点,并实现栈高水位调试?
嘿,我来给你捋捋怎么在UEFI应用里搞定栈高水位调试,还有怎么确定栈的大小和边界——毕竟你那2.1MB的压缩应用,栈里还堆着几KB的结构,确实得盯着点栈的使用情况,别一不小心爆栈了。
一、先搞清楚UEFI应用的栈是啥情况
首先得明白,UEFI加载器会给你的应用单独分配一块内存当栈,x86/x64架构下栈都是从高地址往低地址生长的——也就是说,刚启动时栈指针在最高的那个地址,调用函数、压栈数据时,指针会往低地址挪。我们要找的就是这块栈区域的两头:最高地址(初始栈指针)和最低地址(栈底,再往下就不属于你的栈了)。
二、怎么确定栈的边界和总大小
有两个靠谱的方法,你可以选适合自己的:
方法1:结合内存映射找栈区域
UEFI提供了获取系统内存映射的接口,我们可以用它定位到当前应用的栈:
- 第一步:调用
gBS->GetMemoryMap拿到系统的内存映射。这里要注意,得先调用一次获取需要的缓冲区大小,再分配缓冲区,再正式调用一次拿真实的映射(因为内存映射可能动态变化)。 - 第二步:拿到当前的栈指针——x64可以用
__readrsp()内建函数,x86用__readesp(),嫌麻烦也可以写两行内联汇编直接读rsp/esp寄存器。 - 第三步:遍历内存映射里的每一条,找到包含当前栈指针且类型为
EfiStack的条目。这条里的PhysicalStart就是栈的最低地址(栈底),NumberOfPages * EFI_PAGE_SIZE是栈的总大小,那栈的最高地址就是PhysicalStart + 总大小。
方法2:启动时记录初始栈指针
在你的应用入口函数UefiMain刚执行的时候,栈几乎没被用,这时候记录下当前的栈指针(rsp/esp),这就是栈的最高地址。然后再结合方法1的内存映射找到栈底,用最高地址 - 栈底就能算出栈的总大小,简单直接。
三、实现栈高水位调试
核心思路就是:先给整个空闲栈区域填个独有的标记值,之后检查的时候,找到第一个不是标记值的位置,那就是栈曾经用到的最低地址——也就是栈的“高水位”(用得最深的地方)。具体步骤:
1. 初始化时填充栈标记
在UefiMain刚启动,还没执行太多逻辑的时候:
- 先确定栈的边界(用上面的方法拿到栈底和初始栈顶)。
- 注意别覆盖已经在使用的栈空间(比如函数的返回地址、参数都在栈上),可以通过rbp寄存器定位当前已用栈的底部,然后只填充从栈底到rbp之间的空闲区域,用一个显眼的标记值,比如
0xDEADBEEFDEADBEEF(x64用8字节,x86用4字节0xDEADBEEF)。
2. 编写高水位检查函数
在你觉得关键的节点(比如递归函数调用后、大内存结构分配后)调用这个函数:
- 从栈底开始往上遍历,找到第一个不是标记值的地址,这就是栈用到过的最低位置。
- 然后算一算:已用栈大小=初始栈顶 - 这个最低地址;剩余栈大小=这个最低地址 - 栈底。
- 把这些信息打印出来,就能清楚看到栈的使用情况了。
给你整个简单的代码示例(x64架构)
先定义全局变量存栈的边界:
UINT64 gInitialStackTop; UINT64 gStackBottom;
然后在UefiMain里做初始化:
EFI_STATUS UefiMain ( IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable ) { // 记录初始栈顶 gInitialStackTop = __readrsp(); // 获取内存映射找栈区域 EFI_STATUS Status; UINTN MapSize = 0; UINTN MapKey; UINTN DescriptorSize; UINT32 DescriptorVersion; EFI_MEMORY_DESCRIPTOR *MemoryMap = NULL; // 先拿缓冲区大小 Status = gBS->GetMemoryMap(&MapSize, MemoryMap, &MapKey, &DescriptorSize, &DescriptorVersion); if (Status == EFI_BUFFER_TOO_SMALL) { // 分配缓冲区 Status = gBS->AllocatePool(EfiLoaderData, MapSize, (VOID**)&MemoryMap); if (!EFI_ERROR(Status)) { // 正式获取内存映射 Status = gBS->GetMemoryMap(&MapSize, MemoryMap, &MapKey, &DescriptorSize, &DescriptorVersion); if (!EFI_ERROR(Status)) { UINTN i; UINT64 CurrentRsp = __readrsp(); // 遍历找当前栈对应的条目 for (i = 0; i < MapSize / DescriptorSize; i++) { EFI_MEMORY_DESCRIPTOR *Desc = (EFI_MEMORY_DESCRIPTOR*)((UINT8*)MemoryMap + i * DescriptorSize); if (Desc->Type == EfiStack && CurrentRsp >= Desc->PhysicalStart && CurrentRsp < Desc->PhysicalStart + Desc->NumberOfPages * EFI_PAGE_SIZE) { gStackBottom = Desc->PhysicalStart; break; } } } // 用完释放缓冲区 gBS->FreePool(MemoryMap); } } // 填充空闲栈区域为标记值 if (gStackBottom != 0) { UINT64 CurrentRbp; // 读rbp寄存器,拿到当前已用栈的底部 __asm__ volatile ("mov %0, rbp" : "=r"(CurrentRbp)); UINT64 *Ptr = (UINT64*)gStackBottom; while (Ptr < (UINT64*)CurrentRbp) { *Ptr = 0xDEADBEEFDEADBEEF; Ptr++; } } // 这里写你的应用逻辑... // 比如在某个关键点检查高水位 PrintStackHighWatermark(); return EFI_SUCCESS; }
再实现检查函数:
VOID PrintStackHighWatermark(VOID) { if (gInitialStackTop == 0 || gStackBottom == 0) { Print(L"Stack bounds not initialized!\n"); return; } UINT64 *Ptr = (UINT64*)gStackBottom; // 找第一个不是标记值的位置 while (Ptr < (UINT64*)gInitialStackTop) { if (*Ptr != 0xDEADBEEFDEADBEEF) { break; } Ptr++; } UINT64 LowestStackAddr = (UINT64)Ptr; UINT64 UsedStackSize = gInitialStackTop - LowestStackAddr; UINT64 FreeStackSize = LowestStackAddr - gStackBottom; UINT64 TotalStackSize = gInitialStackTop - gStackBottom; Print(L"Stack High Watermark Info:\n"); Print(L" Total Stack Size: %llu bytes\n", TotalStackSize); Print(L" Used Stack Size: %llu bytes (%.1f%%)\n", UsedStackSize, (UsedStackSize * 100.0) / TotalStackSize); Print(L" Free Stack Size: %llu bytes (%.1f%%)\n", FreeStackSize, (FreeStackSize * 100.0) / TotalStackSize); }
四、一些要注意的坑
- 有些固件可能把栈标记为
EfiLoaderData而不是EfiStack,这时候遍历内存映射时,就找包含当前栈指针的可写内存区域就行。 - 不同架构的寄存器不一样,x86要用ebp而不是rbp,标记值也要改成4字节的。
- 别在多线程场景下用(UEFI一般是单线程,但如果用了多处理器服务要注意),避免填充或检查时栈数据被改。
- 标记值别选正常栈操作会用到的数值,
0xDEADBEEF这种一眼就能认出的就挺好。
内容来源于stack exchange
相关产品推荐
相关产品推荐

