你能在内核模式之外进入x64 32位“长兼容子模式”吗?

13
可能是这个问题的确切重复,但那个问题是一年前的,只有一个答案没有给出任何源代码。我希望得到更详细的答案。 我正在运行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
}

现在,纯粹出于娱乐价值,我想用包含任意32位(ia32)代码的buffer来做同样的事情,而不是64位代码。这个页面暗示您可以通过设置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
};

1
这与x32 ABI有关吗?参见http://en.wikipedia.org/wiki/X32_ABI 或者同样的问题已在http://stackoverflow.com/a/12712639/841108中得到回答了吗? - Basile Starynkevitch
我的mprotect的使用解决了http://stackoverflow.com/a/12712639/841108中回答的问题(如何获取新的可执行页面);我的主要问题是关于兼容子模式的兼容性。我相信x32 ABI完全不相关-x32只是由常规64位长模式中使用的奇怪系统使用的奇怪ABI,而我想要做的实际上是将解码器切换到32位兼容子模式。(换句话说,我的问题与ABI无关;它与处理器模式有关。) - Quuxplusone
1
另一个问题中没有提到的一件事是,为了使这个工作正常运行,你的缓冲区必须在虚拟内存的低4GB内,因为其余部分在32位模式下不可用。除非你能保证这一点,否则你的代码最多只能是不可靠的。设置GDT和LDT的指令仅适用于内核,因此除非内核已经在已知位置具有32位代码段,或者提供对LDT的访问,否则这是不可能的。我不知道Linux是否提供这些东西,所以我不能给你一个直接的答案。 - ughoavgfhw
@ughoavgfhw 你说得很对,关于低4GB(实际上是最低的2GB或最高的2GB)的问题。涉及静态数据会很快变得丑陋。但大多数x86 代码最终都变成了位置无关的,甚至不需要尝试,所以让我们假装我可以确保我的代码是PIC。关于“访问LDT”,Darwin有i386_set_ldt,Linux有modify_ldt,但我不明白它们的作用。 - Quuxplusone
PIC不够用,你的代码和数据(包括堆栈)必须在那个区域内,因为所有东西,包括指令指针,都将被截断为32位。我指的是物理上能够做到这一点。例如,我知道OS X在64位进程中保留了该区域,所以这是不可能的。至于LDT,你需要为代码和数据设置描述符。有关描述符格式的信息,请参见此处。一旦你设置好了描述符并将数据放在正确的位置,你只需要使用lcall并设置你的数据描述符(ds,ss)即可。 - ughoavgfhw
在Linux上,您可以使用标志MAP_ANONYMOUS|MAP_32BITmmap来获取低(高)内存中的页面。关于LDT,我仍然希望有人能给我代码。我认为我有点理解实际的lgdtlldt指令是做什么的,但我认为modify_ldt是一些更高级别的东西,它不会清除整个表;您实际上可以以某种方式向现有LDT添加新条目。也许我应该研究这个示例代码 - Quuxplusone
1个回答

11

可以。甚至可以使用完全支持的接口来实现。使用modify_ldt将一个32位代码段安装到LDT中,然后设置一个远指针到你的32位代码,最后使用AT&T符号中的 ljumpl *(%eax) 进行间接跳转。

但你可能会遇到各种问题。您的堆栈指针的高位可能会被破坏。如果您确实想运行真正的代码,则可能需要一个数据段。并且您需要另一个远跳转回到64位模式。

我在我的linux-clock-tests中的test_vsyscall.cc中提供了一个完整的示例。(在任何发布的内核上都有一点破损:int cc会崩溃。你应该将其更改为其他更聪明的东西,如“nop”。请参考intcc32。)


理论上,您可以使用操作系统现有的GDT条目在64位和32位模式之间切换。此外,在该评论线程中,Ross Ridge指出modify_ldt不支持设置L位,因此可能无法正常工作。(至少对于希望从兼容模式切换到长模式的32位代码而言)我没有测试过这个答案中的代码。 - Peter Cordes
如果你从64位转到32位,你可以使用ljmpq *(%rax)或者ljmpl,但是没有理由使用32位寻址模式来加载m16:64m16:32的远跳转目标。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接