Препроцессор C и директива «_asm _emit»

#c #visual-c #assembly #c-preprocessor #inline-assembly

#c #visual-c #сборка #c-препроцессор #встроенная сборка

Вопрос:

Я пытаюсь достичь логической арифметики вместе с _asm _emit в Visual Studio 2015, хочу вставить некоторые коды операций x64 в программу x86, я должен выполнить много mov команд в памяти, поэтому я попытался создать какой-то макрос, который принимает адрес и выдает в строчном порядке, так что:

 #define EmitDword(x)
{
    _asm _emit (x amp; 0x000000FF) 
    _asm _emit ((x >> 8) amp; 0x000000FF) 
    _asm _emit ((x >> 16) amp; 0x000000FF) 
    _asm _emit ((x >> 24) amp; 0x000000FF) 
}
  

Но я получаю сообщение об ошибке inline assembler syntax error in 'first operand';
Идея в том, чтобы передать адрес переменной макросу, чтобы он передавался непосредственно в машинный код, тогда я мог бы сделать что-то вроде этого:

  #define EmitDword(x)
 {
        _asm _emit (x amp; 0x000000FF) 
        _asm _emit ((x >> 8) amp; 0x000000FF) 
        _asm _emit ((x >> 16) amp; 0x000000FF) 
        _asm _emit ((x >> 24) amp; 0x000000FF) 
 }

/* mov qword ptr [addr],reg  */
#define X64_MovToMem(addr,reg)
{
  _asm _emit 0x48
  _asm _emit 0x89 
  _asm _emit reg 
  _asm _emit 0x25
  EmitDword(addr) 
}

#define _rax 4
void test()
{
    DWORD64 someData;
    X64_MovToMem(amp;someData,_rax);
}
  

Есть ли какой-либо способ добиться выдачи непостоянных значений с использованием встроенной сборки препроцессора?

Комментарии:

1. Нет, каждый байт, который вы передаете в asm-вывод компилятора, должен быть константой времени компиляции. Я сомневаюсь, что вам повезет создать полезный код x86-64 с этой попыткой обойти MSVC, поддерживающий только _asm 32-разрядный режим. Вероятно, вам следует просто писать функции в asm и вызывать их из C , потому что нет способа сообщить компилятору о действиях любых инструкций, которые вы вставляете.

2. Также обратите внимание, что адреса являются константами времени соединения, а не во время компиляции, поэтому измельчение адреса с помощью макроса не сработает: компилятор и ассемблер не могут знать каждый байт отдельно, и им нужно просто оставить пробел и поместить перемещение в таблицу символов объектного файла. Значение будет заполнено компоновщиком во время компоновки.

3. Если вам просто нужны инструкции asm в виде данных, и вы кодируете их самостоятельно, почему бы не использовать struct и memcpy?

Ответ №1:

Вам не нужно возиться с 64-разрядным кодом в 32-разрядном процессе, чтобы получить доступ к 64-разрядному блоку Process Environment. Вы можете получить его адрес, используя 32-разрядный код, и он находится в пределах 32-разрядного адресного пространства. Вам нужно будет использовать 64-разрядный код, только если вам нужно получить доступ к памяти, выделенной за пределами 32-разрядного адресного пространства, и я не думаю, что Windows когда-либо сделает это в 32-разрядном процессе.

Если вам действительно нужно было иметь 64-разрядную функцию в 32-разрядном исполняемом файле, есть лучший способ сделать это, чем использовать _asm _emit . Первое, что нужно сделать, это написать всю вашу 64-разрядную функцию в простой сборке и собрать ее с помощью обычного внешнего ассемблера. Например, вот функция, которая считывает данные из 64-разрядного указателя в синтаксисе MASM:

 _TEXT   SEGMENT
__read64ptr:
    mov rax, [rsp   8]
    mov eax, [rax]
    mov edx, [rax   4]
    retf
_TEXT   ENDS
    END
  

Эта простая функция принимает 64-разрядный указатель в качестве аргумента в стеке. 64-разрядное значение, расположенное по указанному адресу, помещается в EAX и EDX. Эта функция предназначена для вызова с помощью 32-разрядной инструкции far call.

Обратите внимание, что возвращаемое значение занимает два 32-разрядных слота стека, один для 32-разрядного смещения адреса возврата, а другой для селектора. Несмотря на то, что команда RETF выполняется в 64-разрядном режиме, по умолчанию она использует 32-разрядный размер стека (в отличие от 64-разрядной команды near RET) и будет корректно работать с 32-разрядным адресом возврата far, сохраненным в стеке.

К сожалению, мы не можем использовать этот файл сборки напрямую с инструментами, поставляемыми с Visual Studio. 64-разрядная версия MASM создает только 64-разрядные объектные файлы, а компоновщик не позволяет нам смешивать 32-разрядные и 64-разрядные объектные файлы. Должна быть возможность собрать 64-разрядный код в 32-разрядный объект с помощью NASM и связать с компоновщиком Microsoft, но можно использовать код косвенно, используя только инструменты Microsoft.

Для этого соберите файл и скопируйте машинный код вручную в массив C, который находится в .text разделе:

 #pragma code_seg(push, ".text")
#pragma code_seg(pop)
char const __declspec(allocate(".text")) _read64ptr[] = {
    0x48, 0x8b, 0x44, 0x24, 0x08,   /* mov rax, [rsp   8] */
    0x8b, 0x00,                     /* mov eax. [rax] */
    0x8b, 0x50, 0x04,               /* mov edx, [rax   4] */
    0xcb                            /* retf */
};
  

Чтобы вызвать его, вам просто нужно использовать код, подобный этому:

 struct {
    void const *offset;
    unsigned short selector;
} const _read64ptr_ind = { _read64ptr, 0x33 };

unsigned long long
read64ptr(unsigned long long address) {
    unsigned long long value;
    _asm {
        push    DWORD PTR [address   4]
        push    DWORD PTR [address]
        call    FWORD PTR [_read64ptr_ind]
        add     esp, 8
        mov     DWORD PTR [value], eax
        mov     DWORD PTR [value   4], edx
    }
    return value;
}
  

Косвенное обращение с помощью _read64ptr_ind необходимо, потому что нет способа написать call 33h:_read64ptr во встроенной сборке Microsoft. Также обратите внимание, что 64-разрядный селектор кода 0x33 жестко запрограммирован в этом примере, надеюсь, он не изменится.

Вот пример, в котором приведенный выше код используется для считывания адреса 64-разрядного PEB-файла из 64-разрядного TEB-файла (хотя оба расположены в 32-разрядном адресном пространстве):

 unsigned long long
readgsqword(unsigned long off) {
    unsigned long long value;
    _asm {
        mov edx, [off]
        mov eax, gs:[edx]
        mov edx, gs:[edx   4]
        mov DWORD PTR [value], eax
        mov DWORD PTR [value   4], edx
    }
    return value;
}

int
main() {
    printf("32-bit TEB address lxn",
           __readfsdword(offsetof(NT_TIB, Self)));
    printf("32-bit PEB address lxn", __readfsdword(0x30));
    unsigned long long teb64 = readgsqword(offsetof(NT_TIB64, Self));
    printf("64-bit TEB address 6llxn", teb64);
    printf("64-bit PEB address 6llxn", readgsqword(0x60));
    printf("64-bit PEB address 6llxn", read64ptr(teb64   0x60));
}
  

Запуск его на моем компьютере генерирует следующий результат:

 32-bit TEB address 7efdd000
32-bit PEB address 7efde000
64-bit TEB address 000000007efdb000
64-bit PEB address 000000007efdf000
64-bit PEB address 000000007efdf000
  

Как вы можете видеть, доступ ко всем структурам возможен с использованием 32-разрядных указателей и без какого-либо 64-разрядного кода. В частности, в примере показано, как получить 32-разрядный указатель на 64-разрядный PEB, используя только 32-разрядный код.

И последнее замечание: нет гарантии, что Windows правильно обработает 64-разрядный код, выполняемый в 32-разрядном процессе. Если в любой момент во время выполнения 64-разрядного кода произойдет прерывание, процесс может закончиться сбоем.

Комментарии:

1. Здравствуйте, спасибо за ответ. Я приму этот ответ, поскольку он является лучшим, и, похоже, единственное, что действительно хорошо сделать, это написать код 64 masm, собрать его и скопировать напрямую. Дело в том, что мне нужно получить доступ к ячейкам памяти x64. Я хочу выполнить цикл через PEB_LDR_DATA, а в Windows 10 его адрес равен 64 битам. Также некоторые записи с буферами wchar с именами загруженных модулей (UNICODE_STRING64) также имеют адреса x64. У меня есть еще один вопрос, можете ли вы объяснить о возможном системном прерывании, которое может прервать выполнение процесса wow64 в случае его срабатывания?

2. @Vlad: Поскольку ядро считает, что это 32-разрядный процесс, он может вернуться в пространство пользователя в 32-разрядном режиме совместимости, даже если прерывание произошло, когда пространство пользователя находилось в 64-разрядном режиме long.

3. Возможно, вам удастся создать правильную кодировку для прямого удаленного вызова, используя push whatever / db 9Ah / dd _read64ptr / dw 33h в MSVC inline-asm. В отличие от попытки операционной системы сократить значение времени соединения во время компиляции, это должно позволить компоновщику позаботиться о заполнении 4-байтового адреса.

4. @PeterCordes К сожалению, по какой-либо причине директивы DB, DD и т.д. Не работают во встроенной сборке Microsoft. Вместо этого нужно использовать _emit , но это работает только с байтовыми значениями.

Ответ №2:

Извините, но то, что вы пытаетесь сделать, не имеет смысла.

  • «вставьте некоторые коды операций x64 в программу x86» — если это программа x86, она не будет запускать коды операций x64.
  • «_asm _emit (x amp; 0x000000FF)» — Вы читали документы по emit? Вы можете выдавать только байты, но не C-код.
  • «emit emit испускать» — Почему вы думаете, что выполнение 4 операций записи по 1 байту каждая (чего этот код все равно не делает) будет быстрее, чем выполнение 1 операции записи по 4 байта?
  • «Есть ли какой-либо способ добиться выдачи непостоянных значений» — Если вы используете emit, то вам нужно будет знать, в каком регистре в данный момент хранится значение, которое вы пытаетесь записать (что практически невозможно выполнить, поскольку компилятор может использовать разные регистры при каждом изменении кода, и значения может даже не быть в регистре).

Я пытаюсь не обращать внимания на то, как вы это пишете, чтобы решить вашу первоначальную проблему. Однако:

Я понимаю, что существует тенденция думать, что «asm быстрее». Но помните, что код C переводится на ассемблер. Компилятор уже генерирует asm-код, который почти наверняка намного эффективнее того, что вы можете скомпоновать с помощью emit.

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

Если вы убеждены, что можете создать asm-код, более эффективный, чем компилятор C, рассмотрите возможность создания целой asm-подпрограммы, затем вызовите ее из своего C-кода. Обратите внимание, что вы не сможете связать ассемблер x64 с вашей программой x86.

Комментарии:

1. Я думаю, что он пытается передать 4 байта адреса в порядке младших порядков, а не выдавать инструкцию, которая на самом деле вычисляет x amp; 0x000000FF . Я согласен, что это, похоже, не имеет смысла. Я не совсем уверен, что он хочет запускать выданный машинный код, учитывая, что он говорит «выдавать коды операций x64 в программу x86», поэтому я предложил просто использовать struct или что-то в этом роде. Я думал о публикации ответа, но я не думал, что вопрос имеет достаточно смысла, чтобы отвечать.

2. Его замечание о i must do a lot of mov to memory commands звучит так, будто целью является повышение производительности. Убеждение, что «все знают, что asm быстрее, чем C», в сочетании с ограниченным пониманием того, как работает asm, может привести к возникновению подобного вопроса. Попытка использовать x64 была бы еще одной попыткой ускорить работу.

3. О да, я пытался найти способ интерпретации вопроса, отличный от того, где ответ «конечно, нет, это никак не может сработать». Но я думаю, что вы, возможно, правы, основываясь на этом предложении в вопросе.

4. Я пытаюсь выполнить 64-разрядный код в контексте процесса x86 во время выполнения. Это возможно, выполнив дальний переход к селектору 0x33 , который обозначает сегмент кода x64.

5. Это потому, что вы считаете, что вызывать x64-код из x86-кода круто, и вы хотите попробовать это? Или вы надеетесь ускорить работу? Или есть какая-то другая цель?