【问题标题】:C preprocessor and "_asm _emit" directiveC 预处理器和“_asm _emit”指令
【发布时间】:2016-10-22 03:51:48
【问题描述】:

我正在尝试在 Visual Studio 2015 中与 _asm _emit 一起实现布尔运算,想在 x86 程序中插入一些 x64 操作码,我必须对内存命令做很多 mov,所以我尝试制作某种宏,获取地址并以 little-endian 形式发出,因此:

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

但我收到一个错误inline assembler syntax error in 'first operand'; 这个想法是,将变量的地址传递给宏,因此它会直接发送到机器代码中,然后我可以这样做:

 #define EmitDword(x)\
 {\
        _asm _emit (x & 0x000000FF) \
        _asm _emit ((x >> 8) & 0x000000FF) \
        _asm _emit ((x >> 16) & 0x000000FF) \
        _asm _emit ((x >> 24) & 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(&someData,_rax);
}

有没有办法使用预处理器内联汇编实现非常量值的发射?

【问题讨论】:

  • 不,您发送到编译器的 asm 输出中的每个字节都必须是编译时常量。我怀疑您是否会通过这种尝试解决仅在 32 位模式下支持 _asm 的 MSVC 来创建有用的 x86-64 代码的运气。您可能应该只在 asm 中编写函数并从 C++ 中调用它们,因为没有办法告诉编译器您插入的任何指令的操作。
  • 另请注意,地址是链接时常量,而不是真正的编译时,因此用宏分割地址是行不通的:编译器和汇编器不能分别知道每个字节,并且只需要留出空间并在目标文件的符号表中放置一个重定位。该值将由链接器在链接时填写。
  • 如果您只需要 asm 指令作为数据,并且自己编码,为什么不使用 struct 和 memcpy?

标签: c visual-c++ assembly c-preprocessor inline-assembly


【解决方案1】:

您无需在 32 位进程中处理 64 位代码即可访问 64 位进程环境块。您可以使用 32 位代码获取其地址,并且它位于 32 位地址空间内。如果您需要访问在 32 位地址空间之外分配的内存,您只需要使用 64 位代码,而且我认为 Windows 永远不会在 32 位进程中这样做。

如果您确实需要在 32 位可执行文件中包含 64 位函数,那么有比使用_asm _emit 更好的方法。首先要做的是用普通汇编编写整个 64 位函数,然后用普通的外部汇编器进行汇编。例如,下面是一个从 MASM 语法中的 64 位指针读取的函数:

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

这个简单的函数将一个 64 位指针作为堆栈上的参数。位于指向地址的 64 位值被放入 EAX 和 EDX。此函数旨在使用 32 位远调用指令调用。

请注意,返回值占用两个 32 位堆栈槽,一个用于返回地址的 32 位偏移量,另一个用于选择器。尽管 RETF 指令是在 64 位模式下执行的,但它默认使用 32 位堆栈大小(与 64 位 near RET 指令不同),并且可以与保存在堆栈。

很遗憾,我们不能直接将此程序集文件与 Visual Studio 提供的工具一起使用。 64 位版本的 MASM 只创建 64 位目标文件,链接器不会让我们混合 32 位和 64 位目标文件。应该可以使用 NASM 将 64 位代码组装成 32 位对象并与 Microsoft 的链接器链接,但也可以仅使用 Microsoft 的工具间接使用代码。

为此,组装文件并将机器代码手动复制到位于 .text 部分的 C 数组中:

#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 的间接寻址是必要的,因为在 Microsoft 内联汇编中无法编写 call 33h:_read64ptr。另请注意,64 位代码选择器0x33 在此示例中是硬编码的,希望它不会改变。

下面是一个使用上述代码从 64 位 TEB 中读取 64 位 PEB 的地址的示例(即使两者都位于 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 %08lx\n",
           __readfsdword(offsetof(NT_TIB, Self)));
    printf("32-bit PEB address %08lx\n", __readfsdword(0x30));
    unsigned long long teb64 = readgsqword(offsetof(NT_TIB64, Self));
    printf("64-bit TEB address %016llx\n", teb64);
    printf("64-bit PEB address %016llx\n", readgsqword(0x60));
    printf("64-bit PEB address %016llx\n", 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 能够正确处理在 32 位进程中运行的 64 位代码。如果在执行 64 位代码时发生中断,则进程可能最终崩溃。

【讨论】:

  • 您好,感谢您的回答。我会接受这个答案,因为它是最好的,而且似乎唯一真正要做的事情就是编写 x64 masm 代码,组装它并直接复制粘贴。问题是我需要访问 x64 内存位置。我想通过 PEB_LDR_DATA 循环,在 Windows 10 上它的地址是 x64 位。还有一些带有 wchar 缓冲区的条目,加载的模块名称(UNICODE_STRING64)也有 x64 地址。我还有一个问题,你能解释一下可能会在 wow64 进程触发时中断执行的系统中断吗?
  • @Vlad: 由于内核认为它是一个 32 位进程,它可能会在 32 位兼容模式下返回到用户空间,即使中断发生在用户空间处于 64 位时也是如此长模式。
  • 您也许可以在 MSVC 内联汇编中使用 push whatever / db 9Ah / dd _read64ptr / dw 33h 为直接远程呼叫发出正确的编码。与 OP 尝试在编译时切分链接时间值不同,这应该让链接器负责填充 4 字节地址。
  • @PeterCordes 不幸的是,无论出于何种原因,DB、DD 等指令在 Microsoft 内联汇编中都不起作用。您需要改用_emit,但它仅适用于字节值。
【解决方案2】:

很抱歉,您尝试做的事情毫无意义。

  • “在 x86 程序中插入一些 x64 操作码” - 如果是 x86 程序,则不会运行 x64 操作码。
  • "_asm _emit (x & 0x000000FF)" - 你读过docs 的发射吗?您只能发出字节,不能发出 C 代码。
  • “emit emit emit emit” - 为什么您认为执行 4 次 1 字节写入(此代码无论如何都不会这样做)会比执行 1 次 4 字节写入更快?
  • “有什么方法可以实现非常量值的发射” - 如果您使用的是发射,那么您需要知道您尝试写入的值当前存储在哪个寄存器中(这几乎是不可能的来完成,因为每次代码更改时编译器可能会使用不同的寄存器,并且值甚至可能不在寄存器中)。

我试图回顾一下你是如何写这个来解决你原来的问题的。然而:

我意识到人们倾向于认为“asm 更快”。但请记住,C 代码会被翻译成汇编程序。编译器已经在生成 asm 代码,几乎可以肯定它比通过 emit 拼凑的代码要高效得多。

如果使用 64 位指令会产生更好的代码(这当然是可能的),您应该构建一个 64 位的可执行文件。

如果您确信您可以创建比 C 编译器更高效的 asm 代码,请考虑创建一个完整的 asm 例程,然后从您的 C 代码中调用它。请注意,您将无法将 x64 汇编器链接到您的 x86 程序中。

【讨论】:

  • 我认为他试图以 little-endian 顺序发出地址的 4 个字节,而不是发出实际计算 x & 0x000000FF 的指令。不过,我同意这似乎没有意义。我不完全确定他想要运行发出的机器代码,因为他说“将 x64 操作码发送到 x86 程序中”,这就是为什么我建议只使用结构或其他东西的原因。我想发布一个答案,但我认为这个问题没有足够的意义来回答。
  • 他对i must do a lot of mov to memory commands 的评论听起来像是为了提高性能。 “每个人都知道 asm 比 C 更快”的信念加上对 asm 工作原理的有限理解可能会产生这样的问题。尝试使用 x64 将是另一种让事情变得更快的尝试。
  • 哦,是的,我试图找到一种方法来解释这个问题,而不是答案是“当然不是,这不可能工作”。但我认为你可能是对的,基于问题中的那句话。
  • 我试图在运行时在 x86 进程的上下文中执行 x64 代码。可以通过远跳转到代表 x64 代码段的选择器 0x33
  • 这是不是因为你觉得从 x86 代码调用 x64 代码很酷,想尝试一下?还是您希望让事情变得更快?还是有其他目标?
猜你喜欢
  • 1970-01-01
  • 2013-11-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-05
  • 1970-01-01
相关资源
最近更新 更多