【问题标题】:Writing MIPS machine instructions and executing them from C编写 MIPS 机器指令并从 C 中执行它们
【发布时间】:2012-10-31 16:24:16
【问题描述】:

我正在尝试用 C 和 MIPS 编写一些自我修改的代码。

因为我想稍后修改代码,所以我正在尝试编写实际的机器指令(而不是内联汇编)并尝试执行这些指令。有人告诉我,可以只需 malloc 一些内存,在那里编写指令,将 C 函数指针指向它,然后跳转到它。(我包括下面的示例)

我已经用我的交叉编译器(sourcery codebench 工具链)尝试了这个,但它不起作用(是的,事后看来,我认为它确实看起来相当幼稚)。我怎样才能正确地做到这一点?

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>


void inc(){
    int i = 41;
    uint32_t *addone = malloc(sizeof(*addone) * 2); //we malloc space for our asm function
    *(addone) = 0x20820001; // this is addi $v0 $a0 1, which adds one to our arg (gcc calling con)
    *(addone + 1) = 0x23e00000; //this is jr $ra

    int (*f)(int x) = addone; //our function pointer
    i = (*f)(i);
    printf("%d",i);    
}

int main(){
    inc();
exit(0);}

我在这里遵循 gcc 调用约定,其中参数传递给 $a0,函数的结果预计在 $v0 中。我实际上不知道返回地址是否会被放入 $ra (但我还不能测试它,因为我无法编译。 我在指令中使用 int,因为我正在编译 MIPS32(因此 32 位 int 就足够了)

【问题讨论】:

  • 当你说“它不起作用”时,会发生什么?
  • 您将 malloc 返回的指针存储在一个 int 中。 int 是否足够大?为什么不使用正确的指针类型?另外,您确定您的 int 是指令的正确大小吗?
  • 我对 MIPS 的了解还不够,但是当我完成这种工作时 (1) 您需要确保内存具有正确的权限 mprotect 和 (2) 您如果架构具有不连贯的单独指令/数据缓存,则可能需要刷新指令和数据缓存。
  • 请记住,jr 在将控制权转移到目标地址(本例中为$ra)之前,会从其延迟槽执行一条指令。您需要*(addone + 2) 处的“安全”指令(例如nop)。
  • 或者把两条指令的顺序调换一下,让addi在jr的延迟槽中。

标签: c assembly mips inline-assembly mips32


【解决方案1】:

编写的 OP 代码使用 Codesourcery mips-linux-gnu-gcc 编译时不会出错。

正如上面其他人所提到的,MIPS 上的自修改代码需要在编写代码后将指令缓存与数据缓存同步。 MIPS 架构的 MIPS32R2 版本添加了 SYNCI instruction,这是一个用户模式指令,可以满足您的需要。所有现代 MIPS CPU 都实现了 MIPS32R2,包括 SYNCI

内存保护是 MIPS 上的一个选项,但大多数 MIPS CPU 在构建时并未选择此功能,因此在大多数真正的 MIPS 硬件上可能不需要使用 mprotect 系统调用。

请注意,如果您使用除 -O0 之外的任何优化,编译器可以并且确实优化了对 *addone 的存储和函数调用,这会破坏您的代码。使用 volatile 关键字可防止编译器执行此操作。

以下代码生成正确的 MIPS 程序集,但我没有方便的 MIPS 硬件来测试它:

int inc() {
    volatile int i = 41;
    // malloc 8 x sizeof(int) to allocate 32 bytes ie one cache line,
    // also ensuring that the address of function addone is aligned to
    // a cache line.
    volatile int *addone = malloc(sizeof(*addone) * 8);
    *(addone)     = 0x20820001; // this is addi $v0 $a0 1
    *(addone + 1) = 0x23e00000; //this is jr $ra
    // use a SYNCI instruction to flush the data written above from
    // the D cache and to flush any stale data from the I cache
    asm volatile("synci 0(%0)": : "r" (addone));
    volatile int (*f)(int x) = addone; //our function pointer
    int j = (*f)(i);
    return j;
}

int main(){
    int k = 0;
    k = inc();
    printf("%d",k);    
    exit(0);
}

【讨论】:

  • 谢谢,但我不太明白发生了什么。我在链接中阅读了有关缓存行的内容。但是为什么我们用 malloc(... *4) 而不是 8 呢?指针不是也有 4 个字节长吗?此外,当我编译您的代码时(我所做的唯一更改是在开头导入标题)我收到以下汇编错误:非法操作数'synci $2'
  • 我已经修正了几个错别字,所以现在代码应该更有意义了。
  • 请注意,synci 仅适用于较新的 MIPS 处理器——MIPS64 有它,支持 MIPS64 和 MIPS32 的处理器可能有它,但仅 MIPS32 的处理器可能没有。较旧的处理器没有进行必要的缓存刷新的非特权方式。
  • 正如我在回答中提到的,所有 MIPS32R2 处理器都有SYNCI. MIPS32R2 是在 2003 年推出的(MIPS64R2 也是如此),所以任何不到 10 年的 MIPS 处理器都有SYNCI.(这条指令是不在 MIPS64R1 中。)
  • 您需要为每个需要同步的缓存行同步。在本例中,两条指令可能最终位于不同的缓存行上(取决于 malloc 的最小对齐保证)。
【解决方案2】:

您不恰当地使用了指针。或者,更准确地说,你没有在你应该在的地方使用指针。

试穿一下尺寸:

uint32_t *addone = malloc(sizeof(*addone) * 2);
addone[0] = 0x20820001; // addi $v0, $a0, 1
addone[1] = 0x23e00000; // jr $ra

int (*f)(int x) = addone; //our function pointer
i = (*f)(i);
printf("%d\n",i);

您可能还需要在写入内存后,但在调用它之前将其设置为可执行:

mprotect(addone, sizeof(int) * 2, PROT_READ | PROT_EXEC);

要完成这项工作,您可能还需要分配一个相当大的内存块(4k 左右),以便地址与页面对齐。

【讨论】:

    【解决方案3】:

    您还需要确保有问题的内存是可执行的,并确保它在写入后从 dcache 中正确刷新并在执行前加载到 icache 中。如何做到这一点取决于您的 mips 机器上运行的操作系统。

    在 Linux 上,您可以使用 mprotect 系统调用来使内存可执行,并使用 cacheflush 系统调用来执行缓存刷新。

    编辑

    例子:

    #include <unistd.h>
    #include <sys/mman.h>
    #include <asm/cachecontrol.h>
    
    #define PALIGN(P)  ((char *)((uintptr_t)(P) & (pagesize-1)))
    uintptr_t  pagesize;
    
    void inc(){
        int i = 41;
        uint32_t *addone = malloc(sizeof(*addone) * 2); //we malloc space for our asm function
        *(addone) = 0x20820001; // this is addi $v0 $a0 1, which adds one to our arg (gcc calling con)
        *(addone + 1) = 0x23e00000; //this is jr $ra
    
        pagesize = sysconf(_SC_PAGESIZE);  // only needs to be done once
        mprotect(PALIGN(addone), PALIGN(addone+1)-PALIGN(addone)+pagesize,
                 PROT_READ | PROT_WRITE | PROT_EXEC);
        cacheflush(addone, 2*sizeof(*addone), ICACHE|DCACHE);
    
        int (*f)(int x) = addone; //our function pointer
        i = (*f)(i);
        printf("%d",i);    
    }
    

    请注意,我们使包含代码的整个页面既可写又可执行。这是因为内存保护在每个页面都有效,我们希望 malloc 能够继续将页面的其余部分用于其他事情。您可以改为使用vallocmemalign 来分配整个页面,在这种情况下,您可以使代码安全地只读可执行。

    【讨论】:

    • 谢谢!我去看看
    【解决方案4】:

    调用函数比仅仅跳转到一条指令要复杂得多。

    • 如何传递参数?它们是存储在寄存器中,还是推送到调用堆栈?

    • 如何返回值?

    • 返回跳转的返回地址放在哪里?如果你有一个递归函数,$ra 不会削减它。

    • 是调用者还是被调用者负责在被调用函数完成时弹出栈帧?

    不同的calling conventions对这些问题有不同的答案。尽管我从未尝试过像您正在做的事情,但我假设您必须编写机器代码以匹配约定,然后告诉编译器您的函数指针使用该约定(不同的编译器有不同的执行方式这个 - gcc does it with function attributes)。

    【讨论】:

    • 嗨 slavik262。我已经编辑过,是的,我相当确定调用 con 应该可以工作。 Tbh,我不知道 $ra 是否会设置为我想要的,因为我还不能测试。我已经阅读了调用 con,我现在的主要问题是我不知道(或者更糟糕的是,一个错误和坏主意)如何调用这些机器指令。
    • 您可以做的一件事是查看 gcc 生成的程序集,看看编译器试图做什么。
    • @DietrichEpp 你能详细说明一下吗?显然,如果事情没有按预期工作,那么事情就不对了。
    • 我不知道如何详细说明“程序集是正确的”,但您可以自己查看MIPS Calling Conventions 并查看第 4 页——它有一个示例,几乎与这个问题。如果程序集正确,则其他错误。
    • @slavik262:嗯,我无法编译...(因此看不到发生了什么)。
    猜你喜欢
    • 2013-03-27
    • 1970-01-01
    • 2012-09-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-22
    • 1970-01-01
    相关资源
    最近更新 更多