在Linux上以64位进程运行32位代码-内存访问

6
我正在尝试在64位Linux进程中运行32位代码。这个32位代码是完全自包含的,可以直接在IA32系统上调用。如果我将此代码加载到32位进程中,则可以正常运行。
起初,我认为我只需要为32位代码分配一个堆栈,切换到它,一切都会正常工作,但情况并非如此。主要是因为与堆栈相关的指令(POP/PUSH/...)进行了8字节移位而不是4字节。
通过谷歌搜索,我了解到可以通过切换到段选择器0x23来转换为32位模式。不幸的是,我对段知之甚少。
我可以使用类似以下的方法(内联AT&T汇编)转换为32位模式:
movl $0x23, 4(%%rsp) // segment selector 0x23
movq %0, %%rax
movl %%eax, (%%rsp) // target 32-bit address to jump to
lret

%0 包含代码映射的32位地址。代码开始运行,我可以看到PUSH/POP现在按照预期工作,但是它甚至比在64位模式下运行代码时更早崩溃,即使是一个看似无害的指令也会导致崩溃:

0x8fe48201      mov    0xa483c(%rbx),%ecx

在这段代码中,%rbx(或者更像是%ebx,因为代码已经是32位的,但GDB不知道)包含0x8fe48200。它试图从地址0x8feeca3c读取数据,该地址有效且可读(根据/proc/XXX/maps),当我在GDB中读取它时,它包含期望值。

然而,Linux会在此指令上发送一个SIGSEGV给进程,故障地址为0(如stracep $_siginfo._sifields._sigfault.si_addrgdb内部所报告)。不知何故,看起来0x8feeca3c不是32位实际可用的地址。

有什么建议吗?

更新:我已经编写了一个最小化示例,在读取地址为0的情况下会发生段错误,尽管实际上并没有引用地址0。似乎无法读取内存中的任何地址(即使是刚执行过的指令的地址!),但堆栈操作正常。

#include <sys/mman.h>
#include <string.h>
#include <stdio.h>
#include <stdint.h>

// 32-bit code we're executing
const unsigned char instructions[] = {
        0x6a, 0, // push 0
        0x58, // popl %eax
        0xe8, 0, 0, 0, 0, // call the next line to get our location in memory
        0x5b, // pop %ebx
        // THE FOLLOWING mov SEGFAULTS, but it is well within the mapped area (which has size 0x3000)
        // A simpler "mov (%ebx), %eax" (0x8b, 0x03) would fail as well
        0x8b, 0x83, 0, 0x20, 0, 0, // mov 0x2000(%ebx), %eax
        0xf4 // hlt, not reached
};

int main()
{
        void* area;
        void* stack;

        area = mmap(NULL, 3*4096, PROT_WRITE|PROT_READ|PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE | MAP_32BIT, -1, 0);
        memcpy(area, instructions, sizeof(instructions));

        stack = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE | MAP_32BIT, -1, 0);
        stack = (void*) (((uint64_t) stack) + 4096 - 4);

        memset(((char*)area) + 2*4096, 0xab, 100); // Place 0xAB in the area we mov from in 32-bit instructions

        // Switch to 32-bit mode and jump into the code
        __asm__ volatile("movq %1, %%rsp;" \
                         "subq $8, %%rsp;" \
                         "movl $0x23, 4(%%rsp);" \
                         "movq %0, %%rax;" \
                         "movl %%eax, (%%rsp);" \
                         "lret" :: "m"(area), "r"(stack) :);
}

尝试缩小到一个 [mcve]。验证指令的内存是否可执行。此外,查看系统日志,有时会提供信息。 - Jester
是的,该指令是可执行的。我添加了一个最小化示例以重现此问题。 - LubosD
1个回答

4

很好的问题 :)

问题在于ds仍然被设置为零,在64位模式下它不被使用。所以,你需要重新加载它,这样它就能工作了。将您最初的测试push/pop更改为push $0x2b; pop %ds即可解决问题:

const unsigned char instructions[] = {
        0x6a, 0x2b, // push $0x2b
        0x1f, // pop %ds

我从一个32位程序中提取了0x2b的值,一直在想为什么push有效。经过仔细查看,在64位模式下也设置了ss,因此将其复制到dses可能更安全。


哈,我刚刚发现在你写答案的同时DS也扮演着重要的角色:-D虽然我做了一些不同的事情:mov $0x23,%ax; mov %ax,%ds。这似乎也可以工作,但我不确定0x23和0x2B之间的区别是什么。有什么想法吗? - LubosD
1
0x23是一个代码选择器,理论上它可能是不可写的,但在实践中似乎是可写的。 - Jester
你说得对,我不能直接用 0x23 写入堆栈,但是 0x2b 可以。谢谢! - LubosD

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