在标准C中,是否可能从堆栈执行代码?

19
以下代码并不能如预期般工作,但希望可以说明我的尝试:
long foo (int a, int b) {
  return a + b;
}

void call_foo_from_stack (void) {
  /* reserve space on the stack to store foo's code */
  char code[sizeof(*foo)];

  /* have a pointer to the beginning of the code */
  long (*fooptr)(int, int) = (long (*)(int, int)) code;

  /* copy foo's code to the stack */
  memcpy(code, foo, sizeof(*foo));

  /* execute foo from the stack */
  fooptr(3, 5);
}

很明显,sizeof(*foo)并不返回foo()函数的代码大小。

我知道在一些CPU上执行堆栈是受限制的(或者至少在设置限制标志时)。除了GCC的嵌套函数可以最终存储在堆栈上之外,在标准C中是否有方法可以做到这一点?


11
这通常是邪恶的。 - Steven Sudit
3
这是一个糟糕的想法,但是一个有趣的问题。 - Brian
可以做到,但是很邪恶。在许多平台上,它会失败,这是一件非常好的事情。 - Steven Sudit
@Michael Dorgan:这可能在某些系统上有效,但在其他系统上绝对不可能。在某些系统上,从RAM运行代码是不可能的。 - supercat
1
@c.. C编译器和链接器通常是C程序,因此您可以从标准C语言中明确生成机器代码。 生成的机器代码以及将其加载到内存中并使其运行都取决于平台(在某些机器上根本不可能,如哈佛架构),而将其“放在堆栈上”则是进一步的复杂化(也可能是不必要的)。 - dmckee --- ex-moderator kitten
显示剩余8条评论
9个回答

13
这种情况下的一个有效用例是嵌入式系统通常运行在闪存内存不足的情况下,但需要能够在现场重新编程自己。为此,代码的一部分必须从其他存储设备运行(在我的情况下,闪存设备无法擦除和编程一页,同时允许从任何其他页读取,但有些设备可以这样做),并且系统中有足够的RAM来容纳flash writer以及要写入的新应用程序映像。
我们使用C编写了必要的FLASH编程函数,但使用#pragma指令使其放置在与其他代码不同的.text段中。在链接器控制文件中,我们让链接器为该段的开头和结尾定义全局符号,并将其定位在RAM中的基址处,同时将生成的代码和初始化数据以及纯只读.rodata段的数据放置在与FLASH相同的加载区域中;FLASH中的基地址也被计算并定义为全局符号。
在运行时,当应用程序更新功能被执行时,我们将新的应用程序映像读入其缓冲区(并进行所有应该完成的合理性检查,以确保它实际上是该设备的应用程序映像)。然后,我们将更新内核从其休眠位置复制到RAM中的链接位置(使用链接器定义的全局符号),然后像任何其他函数一样调用它。在调用站点,我们甚至不需要做任何特殊的事情(甚至不需要函数指针),因为对于链接器来说,它整个时间都位于RAM中。在正常操作期间,该特定的RAM部分具有非常不同的目的,这一点对链接器来说并不重要。
尽管如此,使这一切成为可能的所有机制都超出了标准的范围或是确定实现的行为。标准并不关心代码在执行之前如何加载到内存中。它只是说系统可以执行代码。

1
+1 对于将函数复制到另一个内存部分的典型用例示例,我做了类似的事情,但其中大部分代码都是汇编语言。 - Thomas Matthews

10

sizeof(*foo) 并不是函数 foo 的大小,而是指向 foo指针的大小(通常与平台上的其他指针大小相同)。

sizeof 无法测量函数的大小。原因在于 sizeof 是一个静态运算符,而函数的大小在编译时是未知的。

由于函数的大小在编译时是未知的,这也意味着您不能定义足够大以包含函数的静态大小数组。

您可能可以使用 alloca 和一些令人讨厌的技巧做一些可怕的事情,但简短的答案是不行,我认为您不能通过标准 C 来实现这一点。

还应该注意到,堆栈在现代安全操作系统上是不可执行的。在某些情况下,您可能能够使其可执行,但这是一个非常糟糕的想法,会使您的程序容易受到堆栈溢出攻击和可怕的错误影响。


由于编译器无法知道函数代码的大小,是否有一种技巧可以定义一个具有固定代码大小的“填充”函数?想象一下将foo()函数用nop指令填充到给定大小,或类似的方法。 - Blagovest Buyukliev
我不相信你可以用C标准的方式定义这个大小。你可以在函数结尾(甚至是接下来的函数)处放置一个类C语言的goto标签,然后使用自定义(汇编)代码计算函数头和最后一个标签之间的字节差来得到大小。这是否有效取决于编译器在目标文件中能够移动多少代码。GCC有一个开关可以防止函数在内存中重新排序;你可以使用它来达到良好的效果,但根本上你的解决方案将依赖于实现。 - Ira Baxter
@snemarch:实际上,我以前使用过一个虚拟函数的地址作为开始标志,另一个虚拟函数的地址作为结束标志,利用编译后的函数不会乱序执行的特性来判断程序计数器(PC)是否在相关活动的特定函数内。 我并没有复制函数主体;正如其他人所观察到的那样,它可能有一些不可重定位的位置。 - Ira Baxter
@Ira Baxter:dummy-before 是必要的吗? - snemarch
@snemarch:如果您不确定函数的地址是否为函数体的最低地址,则是这样。例如,如果编译器在函数体代码之前发出函数体需要的文字字符串,会怎么样? - Ira Baxter
显示剩余2条评论

4

除了其他问题之外,我认为还没有人提到一个普遍的问题:在内存中最终形成的代码通常无法重新定位。也许你的foo函数可以,但考虑以下情况:

int main(int argc, char **argv) {
    if (argc == 3) {
        return 1;
    } else {
        return 0;
    }
}

结果的一部分:

    if (argc == 3) {
  401149:       83 3b 03                cmpl   $0x3,(%ebx)
  40114c:       75 09                   jne    401157 <_main+0x27>
        return 1;
  40114e:       c7 45 f4 01 00 00 00    movl   $0x1,-0xc(%ebp)
  401155:       eb 07                   jmp    40115e <_main+0x2e>
    } else {
        return 0;
  401157:       c7 45 f4 00 00 00 00    movl   $0x0,-0xc(%ebp)
  40115e:       8b 45 f4                mov    -0xc(%ebp),%eax
    }

注意 jne 401157 <_main+0x27>。在这种情况下,我们有一个 x86 条件跳转指令 0x75 0x09,它向前跳 9 个字节。因此它是可重定位的:如果我们将代码复制到其他地方,那么我们仍然希望向前跳 9 个字节。但是如果它是相对跳转或调用,则跳转到您复制的函数之外的代码?您将跳转到堆栈上某个任意位置。
并非所有跳转和调用指令都是这样的(不是所有体系结构,甚至不是 x86 的所有指令)。一些通过将地址加载到寄存器中,然后进行远跳转/调用来引用绝对地址。当代码准备执行时,所谓的“加载器”将通过填充目标实际在内存中具有的任何地址来“修复”代码。复制这样的代码将(最好)导致跳转或调用与原始代码相同的地址。如果目标不在您要复制的代码中,则可能是您想要的。如果目标在您要复制的代码中,则会跳转到原始代码而不是复制品。
相对地址和绝对地址的相同问题也适用于代码以外的其他事物。例如,对数据部分(包含字符串文字,全局变量等)的引用如果被相对寻址并且不是复制的代码的一部分,则会出错。
另外,函数指针不一定包含函数中第一条指令的地址。例如,在 ARM 处理器上的 ARM/thumb 交互工作模式下,thumb 函数的地址比其第一条指令的地址大 1。实际上,值的最低有效位不是地址的一部分,它是一个标志,告诉 CPU 在跳转时切换到 thumb 模式。

如果代码在最终形式下无法被重新定位,那么操作系统如何将您的代码加载到不同的区域呢?嗯,我认为操作系统不会通过将程序从源位置复制到固定的“可执行”区域来交换任务。这样做会消耗太多时间。我使用的许多编译器都有一个用于生成位置无关代码(PIC)的标志。 - Thomas Matthews
@Thomas:我说代码在最终形式下通常不能被重新定位。有些代码可以,有些则不行。此外,仅因为整个程序(或dll)是位置无关的,并不意味着每个单独的函数都可以独立于可执行文件的其余部分进行重新定位,正如提问者所希望的那样。反汇编一些使用这些标志编译的代码:看看是否可以找到一个引用函数之外相对地址的函数。例如,尝试编写两个包含“相同”字符串文字的函数。 - Steve Jessop
@Thomas,可执行文件格式(特别是在*nix上广泛使用的ELF和在Windows上使用的PE)包括重定位修正部分。操作系统加载程序负责在代码首次加载到进程中时应用这些修正。因为这很昂贵,而虚拟内存允许所有进程具有相同的内存映射,所以这些重定位表通常几乎为空。位置无关代码还有助于减少重定位条目的使用。 - RBerteig
当然,有些操作系统要么没有受保护的内存,要么为共享库保留了一段虚拟地址空间,因此可执行文件可以在进程之间共享,而无需是可重定位的,因为它们在每个进程中都映射到相同的地址。并非所有东西都具有可执行重映射和ASLR。 - Steve Jessop

1

你的想法中保留和复制部分是没问题的。但要获取指向你的堆栈代码/数据的代码指针就比较困难了。将堆栈地址强制转换为代码指针应该可以解决问题。


{
   u8 code[256];

   int (*pt2Function)() = (int (*)())&code

   code();
}

在托管系统上,这段代码不应该被允许执行。但是,在共享代码和数据内存的嵌入式系统中,它应该可以正常工作。当然,这也存在缓存问题、安全问题、同事阅读代码时的工作保障问题等等。

1
如果您需要测量函数的大小,可以让编译器/链接器输出一个映射文件,然后根据该信息计算函数的大小。

不是一个非常好的解决方案 - 当函数大小发生很大变化时需要手动更新。由于整个操作是一个超级依赖平台的事情,你可以写不可移植的代码来获取函数长度。 - snemarch
@snemarch - 不必手动,程序可以读取和解析自己的地图文件。这需要保留地图文件,但解析纯文本文件通常比尝试分析可执行文件中的二进制数据更容易。您甚至可以在构建过程中解析地图文件数据并将其嵌入到二进制文件的一部分中。虽然这可能更类似于启用调试符号进行编译,然后从嵌入式调试信息中提取所需内容。 - bta
将信息提取作为构建过程的一部分可以帮助一些,但您仍需要针对每个环境编写特定的构建代码,因此您不会获得很大的收益 - 而且它对于其他注意事项也没有帮助。 - snemarch

1

你的操作系统不应该让你轻易地做到那一点。不应该有任何同时拥有读写和执行权限的内存,尤其是栈有许多不同的保护机制(参见ExecShield、OpenWall patches等)。我记得Selinux也包含堆栈执行约束。你必须找到一种方法来执行以下操作之一:

  • 在操作系统级别上禁用堆栈保护。
  • 允许在特定可执行文件上从堆栈执行。
  • 使用mprotect()为堆栈设置访问权限。
  • 或者其他一些方法...

除了其他可能需要的内容之外,您可能还需要一个依赖于CPU的信号,以表明您正在修改的内存中执行指令。有关英特尔CPU相关的更多细节,请参阅英特尔参考手册;对于其他CPU类型,您可能需要其他内容。 - Ira Baxter

1

有很多方法可以尝试这样做,但是它可能会出错,而且确实已经发生过。这是缓冲区溢出攻击的一种方式--编写一个小型恶意程序,针对目标计算机的架构以及可能导致处理器最终执行恶意代码的代码和/或数据。

也有一些不那么邪恶的用途,但通常受到操作系统和/或CPU的限制。由于代码和堆栈内存位于不同的地址空间中,因此某些CPU根本不允许这种情况发生。

如果您确实想要这样做,其中一件需要考虑的事情是,您将需要在堆栈空间中编写的代码必须是编译为位置无关代码(或者如果编写为汇编或机器代码,则编写为)或者您必须确保它最终位于某个特定地址(并且它被编写/编译为期望这种情况)。

我认为C标准没有关于此的规定。


1

你的问题与动态生成代码类似,只是你想从堆栈中执行,而不是从通用内存区域中执行。

你需要获取足够的堆栈空间来容纳函数的副本。你可以通过编译foo()函数并查看生成的汇编代码来确定它的大小。然后硬编码code[]数组的大小,以至少适应那么多的空间。同时确保code[]或者将foo()复制到code[]的方式,为处理器架构提供正确的指令对齐。

如果你的处理器有指令预取缓冲,则在复制之后并在从堆栈中执行函数之前,你需要清除它,否则它几乎肯定会预取错误的数据,导致执行垃圾数据。管理预取缓冲和相关缓存是我在实验动态生成代码时遇到的最大障碍。

正如其他人所提到的,如果你的堆栈不可执行,那么这是行不通的。


1
你可以将代码编写到堆分配的数据中,并更改其保护。请查看MS Windows的VAlloc;一个参数允许您指定分配的空间是否可以执行。 - Ira Baxter
@Ira Baxter:或者使用VirtualProtect()来保护你的堆栈 :) - snemarch

1

正如其他人所说,以标准方式做这件事是不可能的 - 最终得到的结果将是特定于平台的:CPU因为操作码的结构方式(相对引用与绝对引用),操作系统因为你可能需要设置页面保护以允许从堆栈执行。此外,它还依赖于编译器:没有标准和保证的方法来获取函数的大小。

如果您确实有一个好的用例,比如RBerteig提到的flash reprogramming,请准备好处理链接器脚本,验证反汇编,并知道您正在编写非标准和不可移植的代码 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接