【问题标题】:Inline 64bit Assembly in 32bit GCC C Program32 位 GCC C 程序中的内联 64 位汇编
【发布时间】:2016-03-31 17:49:37
【问题描述】:

我正在编译一个 32 位二进制文​​件,但想在其中嵌入一些 64 位程序集。

void method() {
   asm("...64 bit assembly...");
}

当然,当我编译时,由于寄存器是 64 位的,所以在引用坏寄存器时会出错。

evil.c:92: Error: bad register name `%rax'

是否可以添加一些注释,以便 gcc 将使用 64 位汇编程序来处理 asm 部分。我有一个单独编译的解决方法,使用 PROT_EXEC|PROT_WRITE 映射到页面中并复制到我的代码中,但这很尴尬。

【问题讨论】:

  • 嗯,下面的答案是正确的,但你可以解决这个问题。如果单独组装 64 位指令并从目标文件中提取字节,则可以将它们作为字节直接嵌入代码中。您只需要分配内存,将其标记为可执行并将字节放在那里,并使用指向它的函数指针。我不知道的是,如果您使用的是 x86_64 架构操作系统,您是否可以切实执行该代码。 :)
  • 相关:Is it possible to use both 64 bit and 32 bit instructions in the same executable in 64 bit Linux?。是的,你可以跳到一个新的cs,但它没有得到很好的支持,而且很难想象有什么用例。

标签: gcc assembly


【解决方案1】:

不,这是不可能的。您不能从 32 位二进制文​​件运行 64 位程序集,因为在运行您的程序时处理器将不在 long mode 中。

将 64 位代码复制到可执行页面会导致该代码被错误地解释为 32 位代码,这将产生不可预知的不良结果。

【讨论】:

  • 如果您的程序是 32 位二进制文​​件,则它不是在长模式下运行。仅仅在 64 位处理器或操作系统上是不够的。整个过程必须是 64 位才能使用 x86-64 指令。
  • @benmmurphy 您可以使用 far jmp 切换到 long 模式或调用 64 位代码段,但如果您正在这样做,那么使用内联汇编编写 64 是没有意义的位代码。您应该编写一个单独的汇编文件,您可以在 64 位模式下汇编该文件以生成 64 位机器代码,然后您可以(以某种方式)将其作为二进制 blob 包含在您的 32 位代码中。请注意,据我所知,任何 64 位操作系统都不支持这样做,因此如果在 32 位进程的上下文中执行 64 位代码时发生硬件中断、页面错误或其他异常,它可能会崩溃.
  • @PeterCordes Long 模式有两个子模式,兼容模式(为 16 位和 32 位保护模式程序提供兼容性)和 64 位模式。子模式由当前代码段描述符中的位 (L) 选择,就像另一个位 (D/B) 在 16/32 位保护(和兼容)模式中选择默认操作数和地址大小一样。由于 64 位操作系统在 GDT 中都有一个环形 3 64 位代码选择器供 64 位进程使用,因此在兼容模式下使用远跳转到该段以切换到 64 位模式,没有什么可以阻止进程。反之亦然。
  • @PeterCordes 我不确定这在 Linux 或 Windows 下是否有效。我不认为官方记录了哪些选择器是 64 位和 32 位 ring 3 代码选择器,所以它似乎没有得到官方支持。在 Windows 下,WOW64 被实现为在 32 位进程的上下文中运行的 64 位代码,但我不知道这是否意味着它适用于其他代码。在 ARM 平台上,Microsoft 仅通过强制任务切换以 Thumb 模式恢复来允许 Thumb 代码。
  • @RossRidge:谢谢,这就是我所希望的解释。我没有意识到已经存在您可以使用而无需编写它们的现有描述符(对于非特权进程来说是不可能的)。即使没有记录实际使用的正确数字或任何东西,这仍然使它在理论上成为可能,这就是我感兴趣的全部。它可能对 Linux / Unix / OSX / Windows 中的任何东西实际上都没有用,即使它是支持,因为如果你想运行任何 64 位代码,你不妨运行 all 64 位代码。
【解决方案2】:

不要尝试将 64 位机器代码放入编译器生成的函数中。它可能会起作用,因为函数 prologue/epilogue 的编码在 32 位和 64 位中是相同的,但只有一个单独的 64 位代码块会更简洁。

最简单的方法可能是将该块组装到一个单独的文件中,使用 GAS .code64 或 NASM BITS 64 在目标文件中获取 64 位代码,您可以链接到 32 位可执行文件。

您说in a comment 您正在考虑将其用于从 32 位用户空间进程中针对 64 位内核的内核利用,因此您只需要在进程内存的可执行部分中使用一些代码字节以及获取指向该块的指针的方法。这当然是合理的;如果您可以从 32 位进程中获得对内核 RIP 的控制,这就是您想要的,因为内核代码将始终以长模式运行。

如果您在以 32 位模式启动的进程中使用 64 位用户空间代码执行某些操作,您可能可以将 far jmp 转换为 64 位代码块 (as @RossRidge suggests),使用内核的已知值__USER_CS 64 位代码段描述符。来自 64 位代码的 syscall 应该以 64 位模式返回,但如果不是,请尝试 int 0x80 ABI。它总是返回到您所处的模式,保存/恢复csss 以及riprflags。 (What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code?)


.rodata 是可执行文件测试段的一部分,因此只需让编译器将字节放入const 数组中。有趣的事实:const int main = 195; 编译为一个没有段错误退出的程序,因为195 = 0xc3 = ret 的 x86 编码(并且 x86 是 little-endian)。对于任意长度的机器码序列,const char funcname[] = { 0x90, 0x90, ..., 0xc3 } 可以工作const 是必需的,否则它将进入 .data (read/write/noexec) 而不是 .rodata

您可以使用const char funcname[] __attribute__((section(".text"))) = { ... }; to control what section it 进入(例如.text 以及编译器生成的函数),甚至可以使用链接器脚本来获得更多控制。


如果您真的想在一个 .c 文件中完成所有操作,而不是使用单独组装的纯 asm 源的更简单解决方案:

要将一些 64 位代码与编译器生成的 32 位代码组合在一起,请在 asm 语句 *在任何函数之外中使用 the .code64 GAS directive。 IDK,如果 gcc 发出您的 asm 时哪个部分将处于活动状态有任何保证 gcc 将如何将该 asm 与其 asm 混合,但它不会将其放在函数的中间。

asm(".pushsection .text \n\t"   // AFAIK, there's no guarantee how this will mix with compiler asm output
    ".code64            \n\t"
    ".p2align 4         \n\t"
    ".globl my_codebytes  \n\t" // optional
    "my_codebytes:      \n\t"
    "inc %r10d          \n\t"
    "my_codebytes_end:  \n\t"
    //"my_codebytes_len: .long  . - my_codebytes\n\t"  // store the length in memory.  Optional
    ".popsection        \n\t"
#ifdef __i386
    ".code32"      // back to 32-bit interpretation for gcc's code
    // "\n\t inc %r10"  // uncomment to check that it *doesn't* assemble
#endif
    );

#ifdef __cplusplus
extern "C" {
#endif
   // put C names on the labels.
   // They are *not* pointers, their addresses are link-time constants
    extern char my_codebytes[], my_codebytes_end[];
    //extern const unsigned my_codebytes_len;
#ifdef __cplusplus
}
#endif
// This expression for the length isn't a compile-time constant, so this isn't legal C
//static const unsigned len = &my_codebytes_end - &my_codebytes;

#include <stddef.h>
#include <unistd.h>

int main(void) {
    size_t len = my_codebytes_end - my_codebytes;
    const char* bytes = my_codebytes;

    // do whatever you want.  Writing it to stdout is one option!
    write(1, bytes, len);
}

这使用 gcc 和 clang (compiler explorer) 进行编译和汇编。

我在我的桌面上尝试过仔细检查:

peter@volta$ gcc -m32 -Wall -O3 /tmp/foo.c
peter@volta$ ./a.out  | hd
00000000  41 ff c2                                          |A..|
00000003

这是inc %r10d 的正确编码:)

该程序在没有-m32 的情况下也可以运行,因为我使用#ifdef 来决定最后是否使用.code32。 (没有像 section 那样的 push/pop 模式指令。)

当然,反汇编二进制会告诉你:

00000580 <my_codebytes>:
 580:   41                      inc    ecx
 581:   ff c2                   inc    edx

因为反汇编程序不知道为该块切换到 64 位反汇编。 (我想知道 ELF 是否有相应的属性……我没有使用任何汇编指令或链接器脚本来生成这样的属性,如果存在的话。)

【讨论】:

    【解决方案3】:

    长模式和兼容模式之间的切换是通过更改 CS 来完成的。用户模式代码不能修改描述符表,但它可以对描述符表中已经存在的代码段执行远跳转或远调用。在 Linux 中,存在所需的描述符(根据我的经验;这可能不适用于所有安装)。

    这里是 64 位 Linux (Ubuntu) 的示例代码,它以 32 位模式启动,切换到 64 位模式,运行一个函数,然后切换回 32 位模式。使用 gcc -m32 构建。

    #include <stdlib.h>
    #include <stdio.h>
    #include <stdbool.h>
    
    extern bool switch_cs(int cs, bool (*f)());
    extern bool check_mode();
    
    int main(int argc, char **argv)
    {
        int cs = 0x33;
        if (argc > 1)
            cs = strtoull(argv[1], 0, 16);
        printf("switch to CS=%02x\n", cs);
    
        bool r = switch_cs(cs, check_mode);
    
        if (r)
            printf("cs=%02x: 64-bit mode\n", cs);
        else
            printf("cs=%02x: 32-bit mode\n", cs);
    
        return 0;
    }
    
    
            .intel_syntax noprefix
            .text
    
            .code32
            .globl  switch_cs
    switch_cs:
            mov     eax, [esp+4]
            mov     edx, [esp+8]
            push    0
            push    edx
            push    eax
            push    offset .L2
            lea     eax, [esp+8]
            lcall   [esp]
            add     esp, 16
            ret
    
    .L2:
            call    [eax]
            lret
    
    
            .code64
            .globl check_mode
    check_mode:
            xor     eax, eax
            // In 32-bit mode, this instruction is executed as
            // inc eax; test eax, eax
            test    rax, rax
            setz    al
            ret
    

    【讨论】:

    • 这个答案,stackoverflow.com/a/48855022/8422330,显示了从 64 位模式到 32 位模式的相反方向的切换。
    • Emm 你是怎么发现它是 0x33 的?有没有一种动态的方法来找出答案?
    • @Pyjong,如果我没记错的话,我以访客身份运行 Linux 并从管理程序中转储 GDT。另一种方法是查看 Linux 源代码,看看它是如何填充 GDT 的。
    • @Pyjong,你也许可以使用sgdt,然后阅读/dev/kmem
    猜你喜欢
    • 2011-01-31
    • 1970-01-01
    • 1970-01-01
    • 2015-06-14
    • 2011-09-04
    • 2010-11-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多