Написание машинных инструкций MIPS и их выполнение из C

Я пытаюсь написать самоизменяющийся код на C и MIPS.

Поскольку я хочу изменить код позже, я пытаюсь написать настоящие машинные инструкции (в отличие от встроенной сборки) и пытаюсь выполнить эти инструкции. Кто-то сказал мне, что можно просто выделить часть памяти, записать туда инструкции, указать на нее указатель функции C и затем перейти к ней. (я привожу пример ниже)

Я пробовал это с помощью своего кросс-компилятора (инструментарий исходного кода), и он не работает (да, в ретроспективе я полагаю, что это кажется довольно наивным). Как я мог правильно это сделать?

#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 должно хватить)


person C.E.Sally    schedule 31.10.2012    source источник
comment
Когда вы говорите, что это не работает, что вместо этого происходит?   -  person Shahbaz    schedule 31.10.2012
comment
Вы сохраняете указатель, возвращенный malloc, в int. Достаточно ли для этого int? Почему бы не использовать правильный тип указателя? Кроме того, вы уверены, что ваш int имеет правильный размер для инструкции?   -  person amaurea    schedule 31.10.2012
comment
Я недостаточно разбираюсь в MIPS, но когда я проделал такую ​​работу (1) вам нужно убедиться, что память имеет правильные разрешения с помощью mprotect и (2) вам может потребоваться очистить кеши инструкций и данных, если архитектура имеет отдельные кэши инструкций/данных, которые не являются согласованными.   -  person Dietrich Epp    schedule 31.10.2012
comment
Помните, что jr выполняет инструкцию из своего слота задержки перед передачей управления на целевой адрес (в данном случае $ra). Вам нужна безопасная инструкция (например, nop) в *(addone + 2).   -  person Alexey Frunze    schedule 12.10.2013
comment
Или поменяйте местами две инструкции, чтобы addi находилась в слоте задержки jr.   -  person pat    schedule 13.04.2016


Ответы (4)


Написанный код OP компилируется без ошибок с помощью Codesourcery mips-linux-gnu-gcc.

Как упоминалось выше, самомодифицирующийся код на MIPS требует, чтобы кеш инструкций был синхронизирован с кешем данных после записи кода. Версия MIPS32R2 архитектуры MIPS добавил SYNCI инструкция, которая является инструкцией пользовательского режима, которая делает то, что вам здесь нужно. Все современные процессоры MIPS реализуют MIPS32R2, включая SYNCI.

Защита памяти является опцией для MIPS, но большинство процессоров MIPS не построены с выбранной функцией, поэтому использование системного вызова mprotect, вероятно, не требуется на большинстве реальных аппаратных средств MIPS.

Обратите внимание, что если вы используете любую оптимизацию, кроме -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);
}
person markgz    schedule 01.11.2012
comment
Спасибо, но я не совсем понимаю, что происходит. Прочитал в ссылке про кеш строчку. но тогда почему мы используем malloc(... *4) вместо 8? Разве указатель не имеет длину 4 байта? Кроме того, когда я компилирую ваш код (единственное изменение, которое я делаю, это импортировать заголовки в начале), я получаю следующую ошибку ассемблера: Недопустимые операнды 'synci $2' - person C.E.Sally; 01.11.2012
comment
Я исправил пару опечаток, так что теперь код должен иметь больше смысла. - person markgz; 01.11.2012
comment
Обратите внимание, что synci доступен только на более новых процессорах MIPS — он есть в MIPS64, и процессоры, которые поддерживают как MIPS64, так и MIPS32, вероятно, имеют его, но процессоры только для MIPS32 могут не быть. У старых процессоров нет непривилегированного способа выполнить необходимую очистку кэша. - person Chris Dodd; 01.11.2012
comment
Как я упоминал в своем ответе, все процессоры MIPS32R2 имеют SYNCI. MIPS32R2 был представлен в 2003 году (как и MIPS64R2), поэтому любой процессор MIPS младше 10 лет имеет SYNCI. (этой инструкции не было в MIPS64R1). - person markgz; 01.11.2012
comment
Вам нужна синхронизация для каждой строки кэша, которую необходимо синхронизировать. В этом примере две инструкции могут оказаться в разных строках кэша (в зависимости от минимальной гарантии выравнивания malloc). - person pat; 13.04.2016

Вы неправильно используете указатели. Или, если быть более точным, вы не используете указатели там, где должны быть.

Попробуйте это для размера:

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);

Чтобы это работало, вам может дополнительно потребоваться выделить значительно больший блок памяти (4 КБ или около того), чтобы адрес был выровнен по страницам.

person Community    schedule 31.10.2012

Вы также должны убедиться, что рассматриваемая память является исполняемой, и убедиться, что она правильно сбрасывается из 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 мог продолжать использовать остальные страницы для других целей. Вместо этого вы можете использовать valloc или memalign для выделения целых страниц, и в этом случае вы можете безопасно сделать исполняемый код доступным только для чтения.

person Chris Dodd    schedule 31.10.2012
comment
Спасибо! я проверю это - person C.E.Sally; 02.11.2012

Вызов функции намного сложнее, чем просто переход к инструкции.

  • Как передаются аргументы? Сохраняются ли они в регистрах или помещаются в стек вызовов?

  • Как возвращается значение?

  • Где размещается обратный адрес для обратного перехода? Если у вас есть рекурсивная функция, $ra ее не обрезает.

  • Ответственен ли вызывающий или вызываемый объект за извлечение фрейма стека после завершения вызываемой функции?

Различные соглашения о вызовах дают разные ответы на эти вопросы. Хотя я никогда не пробовал ничего подобного тому, что вы делаете, я предполагаю, что вам придется написать свой машинный код в соответствии с соглашением, а затем сообщить компилятору, что ваш указатель на функцию использует это соглашение (у разных компиляторов есть разные способы выполнения это - gcc делает это с атрибутами функции).

person Matt Kline    schedule 31.10.2012
comment
Привет slavik262. Я отредактировал, и да, я совершенно уверен, что мошенник должен работать. Tbh, я не знаю, будет ли $ra установлено то, что я хочу, так как я еще не смог проверить. Я прочитал о вызовах, моя главная проблема сейчас в том, что я понятия не имею (или, что еще хуже, ошибка и плохая идея), как вызывать эти машинные инструкции. - person C.E.Sally; 31.10.2012
comment
Одна вещь, которую вы можете сделать, это посмотреть на сборку, которую генерирует gcc, и посмотреть, что пытается сделать компилятор. - person Matt Kline; 31.10.2012
comment
@DietrichEpp Не могли бы вы уточнить? Очевидно, что что-то не так, если все работает не так, как хотелось бы. - person Matt Kline; 31.10.2012
comment
Я не знаю, как уточнить, что сборка правильная, но вы можете посмотреть Соглашения о вызовах MIPS сами и проверьте страницу 4 - на ней есть пример, который почти идентичен тому, что указан в вопросе. Если сборка правильная, что-то еще не так. - person Dietrich Epp; 31.10.2012
comment
@slavik262: гм, я не могу скомпилировать... (и, следовательно, не вижу, что происходит). - person C.E.Sally; 31.10.2012