如何调用存储在char数组中的机器码?

How to call machine code stored in char array?

我正在尝试调用本地机器语言代码。这是我目前所拥有的(出现总线错误):

char prog[] = {'\xc3'}; // x86 ret instruction

int main()
{
    typedef double (*dfunc)();

    dfunc d = (dfunc)(&prog[0]);
    (*d)();
    return 0;
}

它确实正确地调用了函数,并且到达了 ret 指令。但是当它尝试执行 ret 指令时,出现 SIGBUS 错误。是因为我在未清除执行权限的页面上执行代码还是类似原因?

那么我做错了什么?

一个明显的错误是 \xc3 没有返回您声称它返回的 double

第一个问题可能是存储 prog 数据的位置不可执行。

至少在 Linux 上,生成的二进制文件会将全局变量的内容放入 "data" segment or here, which is not executable in most normal cases

第二个问题可能是您调用的代码在某些方面无效。在 C 中有调用方法的特定过程,称为 calling convention(例如,您可能正在使用 "cdecl")。被调用函数仅 "ret" 可能还不够。它可能还需要做一些堆栈清理等,否则程序会出现意外行为。一旦你解决了第一个问题,这可能会证明是一个问题。

您需要调用memprotect 才能使prog 所在的页面可执行。以下代码确实进行了此调用,并且可以执行 prog.

中的文本
#include <unistd.h>
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/mman.h>

char prog[] = {
   0x55,             // push   %rbp
   0x48, 0x89, 0xe5, // mov    %rsp,%rbp
   0xf2, 0x0f, 0x10, 0x05, 0x00, 0x00, 0x00,
       //movsd  0x0(%rip),%xmm0        # c <x+0xc>
   0x00,
   0x5d,             // pop    %rbp
   0xc3,             // retq
};

int main()
{
    long pagesize = sysconf(_SC_PAGE_SIZE);
    long page_no = (long)prog/pagesize;
    int res = mprotect((void*)(page_no*pagesize), (long)page_no+sizeof(prog), PROT_EXEC|PROT_READ|PROT_WRITE);
    if(res)
    {
        fprintf(stderr, "mprotect error:%d\n", res);
        return 1;
    }
    typedef double (*dfunc)(void);

    dfunc d = (dfunc)(&prog[0]);
    double x = (*d)();
    printf("x=%f\n", x);
    fflush(stdout);
    return 0;
}

基本上这已被取缔,因为它是对病毒编写者的公开邀请。但是您可以在直接 C 中分配和缓冲并使用本机机器代码进行设置——这没问题。问题是调用它。虽然您可以尝试使用缓冲区的地址设置一个函数指针并调用它,但这不太可能起作用,并且如果您以某种方式设法哄骗它做您想做的事情,很可能会在下一个版本的编译器上中断.所以最好的办法是简单地求助于一些内联汇编,设置 return 并跳转到自动生成的代码。但是,如果系统对此进行保护,您将不得不找到规避保护的方法,正如 Rudi 在他的回答中所描述的那样(但非常特定于一个特定系统)。

正如大家已经说过的,您必须确保 prog[] 是可执行的,但是除非您正在编写 JIT 编译器,否则正确的方法是将符号放入可执行区域,或者通过使用链接器脚本或在编译器允许的情况下在 C 代码中指定部分,例如:

const char prog[] __attribute__((section(".text"))) = {...}

几乎所有 C 编译器都允许您通过在代码中嵌入常规汇编语言来执行此操作。当然,它是对 C 的非标准扩展,但编译器编写者认识到它通常是必需的。作为非标准扩展,您必须阅读编译器手册并检查如何操作,但 GCC "asm" extension 是一种相当标准的方法。

 void DoCheck(uint32_t dwSomeValue)
 {
    uint32_t dwRes;

    // Assumes dwSomeValue is not zero.
    asm ("bsfl %1,%0"
      : "=r" (dwRes)
      : "r" (dwSomeValue)
      : "cc");

    assert(dwRes > 3);
 }

由于在汇编程序中很容易破坏堆栈,因此编译器通常还允许您识别将用作汇编程序一部分的寄存器。然后,编译器可以确保该函数的其余部分避开这些寄存器。

如果您自己编写汇编代码,则没有充分的理由将该汇编程序设置为字节数组。这不仅仅是代码味道 - 我想说这是一个真正的错误,只有在不知道 "asm" 扩展时才会发生,这是将汇编程序嵌入 C 的正确方法。