返回

UEFI栈指针异常分析与解决方案

Linux

UEFI 环境下栈指针行为差异分析与解决

在不同平台运行相同的测试程序时,栈指针的行为可能存在差异。一个简单的栈操作示例程序,在 Linux 和 UEFI POSIX 环境下运行结果不同,值得探讨。本文旨在分析这种差异产生的原因,并提供相应的解决思路。

问题的现象

测试程序中,通过 getsp 宏和 rsp 寄存器分别获取当前栈指针的值。在 Linux 系统上,程序运行表现符合预期:setstack 函数内部,栈指针发生了改变;函数返回后,栈指针恢复到了返回前的栈位置(稍微偏移)。但是,在 UEFI POSIX 环境下,getsp 宏获取的栈指针在 setstack 函数调用前后并没有改变;而寄存器 rsp 的值显示在函数内部发生了改变,且函数返回后值保持不变。该问题集中反映在 UEFI 环境中,使用 getsp 宏获取栈指针出现错误,同时寄存器变量和宏变量返回不一致。

问题分析

  1. 宏定义中的赋值问题
    首先,问题根源指向 getsp 宏的实现:
#define getsp(sp) __asm__("mov %%rsp, %0\n" \
: "=r" (sp)\
: )

getsp 宏尝试直接将 rsp 寄存器的值读取到指定的变量 sp 中。在C语言中,变量赋值时实际上是通过将寄存器的值加载到栈上的对应位置进行的。 当定义 char* sp 类型的变量并尝试通过 getsp 宏直接赋值时,可能发生编译器优化,使 sp 变量并未存储函数栈地址的值,而是编译器假设其他值或者将其存储在其他位置,导致 sp 返回预期之外的值。尤其是在 -O2 优化等级开启时,更容易出现该情况。

  1. UEFI 运行环境差异
    UEFI 作为一个独立于操作系统的运行环境,其内存管理、栈布局等与 Linux 系统有区别。在 UEFI 环境下,程序被编译为 EFI 应用程序,它运行在 BIOS 或者 EFI 系统固件加载的环境中。这不同于Linux下的进程环境,会对栈管理产生影响, 这种差异会导致预期行为不同, 这体现在rsp 寄存器变量能够正确的返回栈指针,但使用getsp 宏则不能正常返回栈地址。

  2. 寄存器访问的直接性

虽然宏在 UEFI 环境下可能出现一些问题,寄存器访问确能直接的读取当前的栈指针,这种区别更加反应出上述getsp 宏定义出现的问题。

解决方案

以下方法可以解决 getsp 宏获取栈指针错误的问题。

1. 避免直接操作 rsp

避免使用 getsp 宏直接获取栈指针值,避免与寄存器赋值操作直接发生关系。而是考虑用更安全且标准的方式去实现对栈指针的管理。具体可以通过在栈上开辟空间来定位函数栈的位置,从而减少因为编译器优化以及寄存器寻址带来的误差。

解决方法代码示例:

#ifdef UEFI
#include "uefi/uefi.h"
#else
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#endif

register void* rsp __asm__("rsp");

#define setsp(sp) __asm__("mov %0, %%rsp\n" \
: \
: "r" (sp))

char* sp;
char* sp2;
char* sp3;
char* sp4;
char stack_loc_before[1];
char stack_loc_after[1];
char *stack;

int setstack(int a)
{
  char* oldsp = &stack_loc_before[0];

  stack = malloc(2000);
    memcpy(stack + 1000, oldsp, 64);

    sp2 = stack + 1000;
    setsp(sp2);
    char *temp = &stack_loc_after[0];
    
    printf("rsp in function %p\n", temp);
     printf("register rsp in function %p\n", rsp);
    return 0;
} 

int main(int argc, char** argv)
{
    char* temp1 = &stack_loc_before[0];
    printf("rsp before function %p\n", temp1);
      printf("register rsp before function %p\n", rsp);
    setstack(5);
      char* temp2 = &stack_loc_after[0];
     printf("rsp after function %p\n", temp2);
     printf("register rsp after function %p\n", rsp);


    while(1);
    return 0;
}

该示例通过在栈上分配局部变量来定位函数栈指针的地址,而不是使用 getsp 宏或者直接从 rsp 寄存器读取。

操作步骤:

  1. 将示例代码编译为可执行程序,可以使用 clang 或者 gcc,注意UEFI环境需要按文中提供的命令编译。
  2. 使用 QEMU 运行编译的程序,注意BIOS以及必要的文件路径正确。

执行上述步骤后,即可得到修正后代码输出的栈指针信息。修正后的程序应该在UEFI环境以及其他环境都获得预期正确的栈指针。

2. 使用更通用的栈检测方法

除了上述直接栈地址寻址外,还可以使用平台无关性更高的方案。
可以在程序启动时记录一个栈起始地址。每次函数调用时都基于该起始地址来确定当前函数栈地址。

解决方法代码示例:

#ifdef UEFI
#include "uefi/uefi.h"
#else
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#endif
register void* rsp __asm__("rsp");

#define setsp(sp) __asm__("mov %0, %%rsp\n" \
: \
: "r" (sp))
char* sp;
char* sp2;
char* sp3;
char* sp4;

char *global_base_stack;

int setstack(int a)
{

    char *old_sp;
    old_sp=global_base_stack;
    char *stack = malloc(2000);
    memcpy(stack + 1000, old_sp, 64);
    sp2 = stack + 1000;
    setsp(sp2);
    sp3 = (char *) rsp;

    printf("rsp in function %p\n", sp3);
       printf("register rsp in function %p\n", rsp);

    return 0;
} 


int main(int argc, char** argv)
{
    
    sp= (char*)rsp;
    global_base_stack= sp;

      printf("rsp before function %p\n", sp);
    printf("register rsp before function %p\n", rsp);
    setstack(5);

     sp4 = (char*)rsp;
      printf("rsp after function %p\n", sp4);
      printf("register rsp after function %p\n", rsp);

    while(1);
    return 0;
}

此方案在 main 函数中获取初始栈地址并将其存储在 global_base_stack 中。通过rsp 寄存器获得更准确的栈地址, 函数栈地址通过global_base_stack来获取,避免 getsp宏的不确定行为。

操作步骤:

  1. 使用clang或其他编译器编译。注意在UEFI编译环境下,请使用上述的特定编译命令。
  2. 在 QEMU 环境下执行。

上述解决方案能够提供更为精准的栈指针地址。同时,也更易于理解和维护。

安全建议

  1. 避免栈溢出: 在手动分配和操作栈时,注意不要超过预定的栈大小,防止栈溢出导致程序崩溃或安全问题。
  2. 编译选项 : 编译时务必启用合适的编译选项,确保程序安全性以及获得期望的调试信息,例如启用 -g 生成调试信息,开启-fstack-protector-all 检测栈溢出。
  3. 充分测试 :在不同架构和平台进行充分测试,避免出现运行时错误或者产生无法预知的行为。
  4. 了解平台特性 : 需要充分理解目标平台的内存模型以及栈管理策略。UEFI 不同于 Linux,应该特别注意。

以上,对于在 UEFI POSIX 环境下栈指针的修改和 getsp 宏行为进行了探讨,希望可以帮助解决相关的问题。使用上述更安全的栈指针检测方式能够减少可能引入的问题,同时提高代码的可维护性和可靠性。