【问题标题】:Can you enter x64 32-bit "long compatibility sub-mode" outside of kernel mode?可以在内核模式之外进入 x64 32 位“长兼容性子模式”吗?
【发布时间】:2012-09-24 20:22:25
【问题描述】:

这可能与 Is it possible to execute 32-bit code in 64-bit process by doing mode-switching?,但这个问题是一年前的问题,只有一个没有给出任何源代码的答案。我希望得到更详细的答案。

我正在运行 64 位 Linux(Ubuntu 12.04,如果重要的话)。这是一些分配页面的代码,将一些 64 位代码写入其中,然后执行该代码。

#include <assert.h>
#include <malloc.h>
#include <stdio.h>
#include <sys/mman.h>  // mprotect
#include <unistd.h>  // sysconf

unsigned char test_function[] = { 0xC3 };  // RET
int main()
{
    int pagesize = sysconf(_SC_PAGE_SIZE);
    unsigned char *buffer = memalign(pagesize, pagesize);
    void (*func)() = (void (*)())buffer;

    memcpy(buffer, test_function, sizeof test_function);

    // func();  // will segfault 
    mprotect(buffer, pagesize, PROT_EXEC);
    func();  // works fine
}

现在,纯粹出于娱乐价值,我想做同样的事情,但 buffer 包含任意 32 位 (ia32) 代码,而不是 64 位代码。 This page 意味着您可以通过将 CS 段描述符的位设置为 LMA=1, L=0, D=1 进入“长兼容性子模式”,在 64 位处理器上执行 32 位代码。我愿意将我的 32 位代码包装在执行此设置的序言/尾声中。

但是可以我在 Linux 中的用户模式下进行此设置吗? (BSD/Darwin 的答案也将被接受。)这是我开始对这些概念产生真正模糊的地方。我认为解决方案包括向 GDT(或者它是 LDT?)添加一个新的段描述符,然后通过 lcall 指令切换到该段。但是这一切都可以在用户模式下完成吗?

这是一个示例函数,当在兼容性子模式下成功运行时应返回 4,在长模式下运行时应返回 8。我的目标是获取指令指针以获取此代码路径并以%rax=4 从另一端出来,而无需进入内核模式(或仅通过记录的系统调用这样做)。

unsigned char behave_differently_depending_on_processor_mode[] = {
    0x89, 0xE0,  // movl %esp, %eax
    0x56,        // push %{e,r}si
    0x29, 0xE0,  // subl %esp, %eax
    0x5E,        // pop %{e,r}si
    0xC3         // ret
};

【问题讨论】:

  • 这不是和 x32 en.wikipedia.org/wiki/X32_ABI 有关吗?还是与stackoverflow.com/a/12712639/841108 中回答的问题相同?
  • 我使用mprotect 解决了stackoverflow.com/a/12712639/841108 中回答的问题(如何获取新的可执行页面);我的主要问题是关于兼容性子模式。我相信 x32 ABI 完全无关紧要——x32 只是古怪系统在常规 64 位长模式下使用的古怪 ABI,而我想要做的实际上是将解码器切换到 32 位兼容性子模式。 (换句话说,我的问题根本与 ABI 无关;它与处理器模式有关。)
  • 另一个问题中没有提到的一件事是,为了使其工作,您的缓冲区必须位于 4GB 的低虚拟内存中,因为其余部分在 32 位模式下不可用.除非你能保证,否则你的代码充其量是不可靠的。设置 GDT 和 LDT 的指令仅适用于内核,因此除非内核在已知位置已有 32 位代码段,或提供对 LDT 的访问,否则这是不可能的。我不知道linux是否提供了这些东西,所以我不能给你一个直接的答案。
  • @ughoavgfhw 关于低 4GB(嗯,实际上是最低 2GB 或最高 2GB),这是一个很好的观点。引用静态数据会很快变得丑陋。但是大多数 x86 代码 最终都与位置无关,甚至没有尝试,所以让我们假装我可以确保我的代码是 PIC,不知何故。重新“访问 LDT”,达尔文有 i386_set_ldt,Linux 有 modify_ldt,但我不明白他们在做什么。
  • PIC 还不够,你的代码和数据,包括堆栈,必须在那个区域,因为包括指令指针在内的所有内容都将被截断为 32位。我指的是这样做的身体能力。例如,我知道 OS X 在 64 位进程中保留该区域,所以这是不可能的。至于 LDT,您需要为代码和数据设置描述符。有关描述符格式的信息,请参阅here。设置好数据并将数据放在正确的位置后,您只需 lcall 并设置数据描述符 (ds,ss)

标签: linux x86-64 kernel compatibility-mode


【解决方案1】:

是的,你可以。使用完全支持的接口甚至是可行的。使用 modify_ldt 将 32 位代码段安装到 LDT 中,然后设置一个指向 32 位代码的远指针,然后使用 AT&T 表示法中的ljumpl *(%eax) 间接跳转到它。

不过,你会遇到各种各样的麻烦。堆栈指针的高位可能会被破坏。如果你真的想运行真正的代码,你可能需要一个数据段。而且您需要再进行一次远跳才能回到 64 位模式。

一个完整的例子在我的linux-clock-teststest_vsyscall.cc。 (在任何已发布的内核上都有点损坏:int cc 会崩溃。您应该将其更改为更聪明的东西,例如“nop”。查看intcc32

【讨论】:

  • In theory 您可以使用操作系统现有的 GDT 条目在 64 位和 32 位模式之间切换。同样在该评论线程的后面,Ross Ridge 指出modify_ldt 不支持设置 L 位,因此它可能不起作用。 (至少,不适用于想要从兼容模式切换到长模式的 32 位代码。)我没有测试这个答案中的代码。
  • 如果您从 64->32 开始,您会使用 ljmpq *(%rax),或者可能是 ljmpl,但没有理由使用 32 位寻址模式来加载 @987654330 @ 或m16:32 远跳目标。
猜你喜欢
  • 2015-03-08
  • 2011-08-22
  • 1970-01-01
  • 2013-08-09
  • 2016-03-29
  • 1970-01-01
  • 2015-08-03
  • 2013-10-01
  • 2014-07-29
相关资源
最近更新 更多