如何在C语言中将内存段标记为可执行?

3

我最近在学习JIT编译器。据我所知,JIT是一种技术,可以在执行之前将某些脚本语言代码实时编译成本地代码。当我想象这样的编译器内部时,我发现必须有一个动态分配缓冲区段,用于存放生成的本地代码。但是我们需要一种方法来从缓冲区内运行代码,因为缓冲区持有数据。我的意思是,您不能只是将一些代码放入char[]中,然后跳转到执行,因为这会带来安全问题,操作系统必须阻止您这样做。必须有一些方式将缓冲区标记为可执行。考虑以下幼稚的方法:

#include <stdlib.h>

void *jit_some_native_code(void) {
  void *code_segment = malloc(1024);
  /*
   * bla bla bla...
   * Generate code into this code_segment.
   */

  return code_segment;
}

int main(void) {
  void *code = jit_some_native_code();
  /*
   * How can I start executing instruction in code?
   */

  typedef void (*func_ptr_t)(void);

  /*
   * This won't work. OS bans you doing so.
   */
  ((func_ptr_t)code)();

}

在Ubuntu上,代码将会运行但会以状态码26退出。考虑到C的不安全类型特性,代码可以编译通过,但在C++中,编译器将直接停止您。这是否意味着JIT必须绕过编译器,并设置可执行标志?
编辑:除了mprotect之外,如果您使用mmap,您还可以指定要映射的页面权限。
   PROT_EXEC  Pages may be executed.
   PROT_READ  Pages may be read.
   PROT_WRITE Pages may be written.
   PROT_NONE  Pages may not be accessed.

因此,该页面将具有可执行权限。


1
((func_t) &code) - 你正在执行 code 指针的地址,而不是函数本身.... 应该是 ((func_t)code)()。对于C++,编译器会直接停止你。只需将 void *code = (void*)&some_jit_func 进行强制转换,然后就可以使用C++进行编译了。 - KamilCuk
@KamilCuk 感谢您指出代码中的错误。但我记得 C++ 禁止将 void* 强制转换为函数指针? - cgsdfc
神奇的是,static_cast 禁止这种转换,而 C 风格的转换允许它! - cgsdfc
@ryyker 这个问题是关于如何将一个内存区域标记为可执行的,而不是如何正确地调用函数指针。我认为我的评论并不是一个答案。 - KamilCuk
1
一个 void* 可能不足以容纳一个函数指针。将其转换为不同大小的指针是未定义的行为。因此,无论是在 C 还是 C++ 中,你都不能只将 void* 转换为函数指针。为什么不将 code = &some_jit_func 声明为它所代表的函数指针呢?jit-me-this-bit-of-script 函数应该返回一个函数指针而不是 void*。 - Goswin von Brederlow
2个回答

13

如果您想要使堆中的某一区域可执行,可以使用mprotect函数。

int main() {
  typedef void (*func_t)(void);
  void *code = &some_jit_func;
  int pagesize = getpagesize();
  mprotect(code, pagesize,PROT_EXEC);
  ((func_t)code)();
}

你还可以使用OR运算符将标志与PROT_READ/PROT_WRITE相结合


1
措辞有点混乱,但PROT_EXEC实际上意味着允许该区域被执行。如果您完全不想访问该内存区域,则可以使用PROT_NONE。如果您只想读取和写入,则可以使用PROT_READ | PROT_WRITE。默认情况下,堆不是可执行的内存区域。 - Irelia
2
@ryyker 不行。正如其manpage中明确提到的那样,mprotect会_更改_内存页面的访问权限。除此之外,大多数操作系统将拒绝执行既可执行又可写的页面。 - user10678532
@Nina和mosvy - 感谢您们的澄清! - ryyker
@Nina 从你的回答中我知道操作系统确实提供了这样一种方式。但是仍然有人可以将任意内容放入缓冲区,然后调用 mprotect 来获取执行权限。这不会开启一个安全漏洞吗? - cgsdfc
1
如果他们有调用mprotect的能力,那么他们已经有执行代码的能力了。所有mprotect所做的就是将其扩展到执行动态生成的代码。安全问题只有在有人找到一种方法(通常是利用程序中的其他缺陷)来同时填充缓冲区以及说服程序调用mprotect时才会出现。它实际上并没有提供任何新的特权;任何程序没有被允许做的事情,它也不能使用新代码做到;你还需要特权升级才能做到这一点。 - ShadowRanger
显示剩余2条评论

0

在您的代码中,您正在获取现有函数的地址。这将自然地指向已经可执行的内存区域。但是,在任何现代系统上,同一区域将不可写。

另一方面,如果您malloc()一些内存,它将是可写的,但不可执行。因此,在JIT编译器中构造的任何代码都将无法执行,并且尝试调用在那里构造的函数将失败。您必须首先使用mprotect使内存可执行。

出于安全原因,您应遵循W^X原则。这意味着任何页面只能是可写或可执行的,但不能同时具备两者。当您使用mprotect使代码可执行时,也要使其不可写,反之亦然。永远不要组合可写和可执行。


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