编写MIPS机器指令并从C执行它们

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

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

我已经尝试使用我的交叉编译器(sourcery codebench工具链)并且它不起作用(是的,在后面的视线中我认为它确实看起来很幼稚)。 我怎么能正确地做到这一点?

#include  #include  #include  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应该够了)

使用Codesourcery mips-linux-gnu-gcc编写的OP编写的代码没有错误。

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

内存保护是MIPS的一个选项,但大多数MIPS CPU并非选择此function,因此在大多数真正的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); } 

你不恰当地使用指针。 或者,为了更准确,您没有使用指针。

试试这个尺码:

 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左右),以便地址页面对齐。

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

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

编辑

例:

 #include  #include  #include  #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来分配整个页面,在这种情况下,您可以安全地使代码只读。

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

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

  • 如何返回值?

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

  • 当被调用函数完成时,调用者或被调用者是否负责弹出堆栈帧?

不同的呼叫约定对这些问题有不同的答案。 虽然我从来没有尝试过你正在做的事情,但我认为你必须编写你的机器代码来匹配约定,然后告诉编译器你的函数指针使用那个约定(不同的编译器有不同的做法)这个 – gcc用函数属性来做 。