【问题标题】:Setting a C array as a new call stack (ESP) from inline asm?从内联 asm 将 C 数组设置为新的调用堆栈(ESP)?
【发布时间】:2020-07-18 03:56:21
【问题描述】:

正如我的标题所暗示的,我很感兴趣是否可以分配一个 C 数组,并通过相应地将 ESP 寄存器指向它来使其表现得像一个堆栈。

一些代码示例...

void foo(){
   int x = 99;
   int y = 89;
   return;
}
char myStack[1024];
void main(){
     int main_num = 66;
     __asm volatile("movl %0, %%esp": : "rm" (&myStack+1)); //Move ESP to the end of the array
     foo();
     return 0;
}

这段代码背后的想法是创建一个单独的堆栈,专门用于 foo(),方法是首先将 ESP 指向 myStack 数组的末尾(因为堆栈会向低地址增长),然后调用 foo() ,现在将其返回地址和局部变量存储在这个新堆栈(我们的 C 数组)中。

我想知道这种方法是否可行?如果可以,如何实现?

在尝试实现上述代码时,我运行 GDB 只是为了查看有关我的堆栈的一些信息(例如:GDB 中的 info stack 命令),我不断收到“No Stack”,这可能意味着堆栈指针被发送到了深渊。

PS:我将其实现为内核级代码

【问题讨论】:

  • 也许是一个巨大的不安全黑客,只适用于玩具实验。如果要设置新堆栈,请在调用 C 函数之前在手写 asm 中执行此操作。这个hack可以调用foo(),但是return 0;呢?编译器生成的代码将尝试从当前的%esp 弹出一个返回地址。 (实际上它可能首先使用leave 并设置ESP = EBP,然后弹出一个保存的EBP,这将切换回实际堆栈。因此行为取决于优化级别!)使用GDB 单步执行您的代码并实际观看reg 值发生变化,例如layout reg.
  • 但是,是的,&myStack + 1 是数组末尾的地址,并且确实导致 movl $myStack+1024, %eax 作为 asm 语句的设置(其中 %0 扩展为 @ 987654331@)。 godbolt.org/z/Nz6DgA 表明它“工作”,然后当它通过优化到达 ret 时会立即崩溃。
  • 您是否希望通过myStack[i] 从其他C 代码访问堆栈内容?或者这只是静态保留空间的一种方式? GCC 仍然会假设没有任何东西可以给本地人起别名。没有办法告诉它堆栈是一个可观察到的副作用。例如它仍然可以内联函数并将本地变量保存在寄存器中。 你真正希望在这里完成什么? 除了临时使用这个静态数组作为调用堆栈之外还有什么?我不认为您的意思是将调用堆栈用作堆栈数据结构(通常需要手写asm)。
  • 嗨,是的,它主要用作静态保留空间。作为一个想法,使用 C 数组似乎更容易,但据我了解,您的 cmets 应该改为在汇编中实现?
  • 特别是如果main 必须实际上是return,那么是的,您需要设置一个堆栈之前 使用它来call 任何东西。例如,在 Linux 下,pthread 库通过使用mmap() 分配它来创建一个具有新堆栈的新线程,然后将该堆栈地址作为操作数传递给clone()。所以新线程从不使用主堆栈,只使用它自己的堆栈。我假设为新任务创建内核端线程堆栈是类似的。您分配一个新堆栈,然后将它用于新的线程上下文。

标签: gcc x86 kernel inline-assembly callstack


【解决方案1】:

也许这可以作为一个巨大的不安全黑客,只适用于玩具实验。如果要设置新堆栈,请在调用 C 函数之前以手写 asm 进行。

这个技巧可以调用foo(),但是return 0;呢?编译器生成的代码会尝试从当前的 %esp 中弹出一个返回地址。

(或者如果优化被禁用,将使用leave,在弹出保存的EBP之前设置ESP = EBP。这将切换回初始堆栈。因此行为取决于优化级别!你不想那样。)

使用 GDB 单步执行您的代码并实际观察 reg 值的变化,例如layout reg

但是是的,&myStack + 1 是数组末尾的地址,并且确实导致 movl $myStack+1024, %eax 作为 asm 语句的设置(其中 %0 在模板中扩展为 %eax , 因为编译器为 "rm" 操作数选择了该寄存器。您没有给它立即常量的选项,否则它会使用 movl $myStack+1024, %esp 来做到这一点。

https://godbolt.org/z/Nz6DgA 表明它“正常工作”,然后当它到达启用优化的 ret 时会立即崩溃,因为它会尝试 pop 且 ESP 指向 myStack 的过去一结束。

我目前正在尝试在内核级别实现线程,因此想法是为每个线程分配单独的堆栈,并在它们之间切换

特别是如果main 必须实际上是return,那么是的,您需要在使用call 之前设置一个堆栈。否则最终的返回地址将在错误的堆栈上!

例如,在 Linux 下,pthread 库通过使用mmap() 为其分配新堆栈来创建一个新线程,然后将该堆栈地址作为操作数传递给clone()。所以新线程永远不会使用父级的堆栈,只会使用它自己的堆栈。我假设为新任务创建内核端线程堆栈是类似的。您分配一个新堆栈,然后将它用于新的线程上下文。

您可以在顶部放置一个“返回地址”,这样在新线程中调用的第一个函数实际上将返回到一个线程退出/清理函数。可能有一些asm。或者使实际的线程入口点成为一个不返回的函数,而是清理线程上下文并切换到另一个线程或调用您的调度程序或其他东西。

这就是我想要的,让线程使用自己的堆栈而不是主堆栈。但是,我没有启用分页,现在希望尽可能简单地实现堆栈创建(因此 C 数组似乎是简单的解决方案)

不幸的是,这太简单了,实际上不起作用。

是的,您可以将 C 数组用于线程堆栈(如果您正好有一个额外的线程...),问题是您如何切换到它。

您将需要在某个时候编写一个上下文切换函数来保存一个寄存器上下文并加载另一个。 (例如 Google,您可以在 Stack Overflow 上找到一些,也可能在 https://www.osdev.org/ 上找到一些东西。)

在内存中创建一个新的线程上下文结构,其堆栈指针指向线程堆栈的顶部,其 EIP 指向线程入口点。调用你的上下文切换函数来切换到新的上下文。

从 C 编译器的 POV 来看,上下文切换函数看起来就像任何其他函数调用。它最终返回,并且可能已经修改了任何全局可访问的 C 对象。暂时让 ESP 指向其他地方并不重要。“像任何其他函数调用一样”包括破坏调用破坏寄存器,顺便说一句,因此您不需要保存/恢复 EAX/ECX/EDX。上下文切换函数的调用者已经假定它们已被销毁。

您通常应该在 asm 中手写,而不是 inline asm。从 inline asm 更改 ESP 充满了危险,is officially documented as not supported by GCC

这是因为编译器要求堆栈指针的值在 asm 语句之后与进入语句时的值相同

另见https://gcc.gnu.org/wiki/DontUseInlineAsm

【讨论】:

  • 在内存中创建一个新的线程上下文结构,其堆栈指针指向你的线程堆栈的顶部关于如何创建线程堆栈本身有什么建议?
  • @Anton9988:如果您有固定数量的线程,则静态数组很好。就像我说的,问题是你如何切换堆栈,而不是你如何分配它。只需将指向&myStack[1024] 的指针分配给void* 结构成员即可。否则,您必须编写内存管理器来分配线程堆栈。顺便说一句,1024 字节并不多;您可能需要至少 4k 堆栈。 Linux 目前使用 8k 或 16k 堆栈来为复杂文件系统和虚拟块设备代码导致堆栈溢出的最坏情况腾出空间。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-11
  • 1970-01-01
  • 1970-01-01
  • 2015-09-22
  • 1970-01-01
  • 2016-12-17
  • 1970-01-01
相关资源
最近更新 更多