You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在运行中的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 12:49:30