【问题标题】:Is it allowed to access memory that spans the zero boundary in x86?是否允许访问跨越 x86 中零边界的内存?
【发布时间】:2018-05-21 23:51:39
【问题描述】:

在 x861 中是否允许单个访问跨越 00xFFFFFF... 之间的边界?

例如,假设eax(64 位中的rax)为零,是否允许以下​​访问:

mov ebx, DWORD [eax - 2]

如果答案不同,我对 x86(32 位)和 x86-64 都感兴趣。


1当然考虑到该区域已映射到您的进程等中。

【问题讨论】:

  • 我在手册中找不到任何关于它的信息,但如果我真的尝试过,它就可以工作。
  • 我已经测试了真实模式和保护模式以及前者的故障,而后者没有。但是,32 位的情况是特定于实现的。我认为这在主流英特尔处理器中永远不会改变,但对于其他品牌或衍生架构来说,它可能会改变。
  • 我做了一些实验。在我的处理器上,如果我处于 32 位保护模式并且我使用的是普通的平面 4gb 内存模型(其中基数为 0)并且我在内存末尾写了一个字,则没有故障。作为一个不同的实验,如果我将描述符中的基数更改为 1(而不是正常的零),保持 4gb 的限制,并且我尝试使用偏移量为 0xffffffff 的描述符(即 ES)写入选择器,它将出错。如果计算的地址为 2^32 或更高,如果添加到内存操作数的有效地址,它似乎具有非零基数,它将出错(它不会回绕到 0)。
  • 在我上一条评论的第二个实验中(基数为 1 的情况),我使用偏移量为 0xffffffffe 的描述符(即 ES)使用选择器写字,它不会出错并且会换行。因此,在内存访问完成之前的基址+有效地址检查无法回行,但之后如果写入本身越过内存末尾,它将回行。
  • 那么,如果我们稍微破坏一下实模式会发生什么。启用保护模式并使用 16 位描述符设置 GDT,其基数为 0xffffffff(使用 0xffff 的限制)。将 ES 设置为该描述符,然后在不重新加载 ES 的情况下关闭保护模式(如果您不使用值重新加载 ES,处理器仍将使用缓存的基地址)。如果我们向 0x0000 写入一个字,则写入成功并且内存操作结束。实模式的行为似乎与保护模式相同。

标签: assembly x86 x86-64 intel cpu-architecture


【解决方案1】:

这并不是一个真正的新答案,但对于评论来说太大了。这是@prl 的代码转换后的,因此它应该与许多Linux 发行版上可用的基本gnu-efipackage 一起运行。档案wraptest.c

#include <efi.h>
#include <efiapi.h>
#include <efilib.h>
#include <inttypes.h>
#include <stdint.h>

uint8_t *p = (uint8_t *)0xfffffffffffffffcULL;

EFI_STATUS
EFIAPI
efi_main (EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
    uint64_t cr3;

    InitializeLib(ImageHandle, SystemTable);
    asm("mov %%cr3, %0" : "=r"(cr3));
    uint64_t *pml4 = (uint64_t *)(cr3 & ~0xfffULL);

    Print(L"cr3 %lx\n", cr3);
    Print(L"pml4[0] %lx\n", pml4[0]);
    uint64_t *pdpt = (uint64_t *)(pml4[0] & ~0xfffULL);
    Print(L"pdpt[0] %lx\n", pdpt[0]);
    if (!(pdpt[0] & 1)) {
        uefi_call_wrapper(BS->AllocatePages, 4, AllocateAnyPages, \
                          EfiBootServicesData, 1, &pdpt[0]);
        pdpt[0] |= 0x03;
        Print(L"pdpt[0] %lx\n", pdpt[0]);
    }
    uint64_t *pd = (uint64_t *)(pdpt[0] & ~0xfffULL);
    Print(L"pd[0] %lx\n", pd[0]);
    if (!(pd[0] & 1)) {
        uefi_call_wrapper(BS->AllocatePages, 4, AllocateAnyPages, \
                          EfiBootServicesData, 1, &pd[0]);
        pd[0] |= 0x03;
        Print(L"pd[0] %lx\n", pd[0]);
    }
    if (!(pd[0] & 0x80)) {
        uint64_t *pt = (uint64_t *)(pd[0] & ~0xfffULL);
        Print(L"pt[0] %lx\n", pt[0]);
        if (!(pt[0] & 1)) {
            uefi_call_wrapper(BS->AllocatePages, 4, AllocateAnyPages, \
                              EfiBootServicesData, 1, &pt[0]);
            pt[0] |= 0x03;
            Print(L"pt[0] %lx\n", pt[0]);
        }
    }

    Print(L"[0] = %08x\n", *(uint32_t *)(p+4));

    Print(L"pml4[0x1ff] %lx\n", pml4[0x1ff]);
    if (pml4[0x1ff] == 0) {
        uint64_t *pt;
        uefi_call_wrapper(BS->AllocatePages, 4, AllocateAnyPages, \
                          EfiBootServicesData, 4, &pt);
        uint64_t x = (uint64_t)pt;

        Print(L"pt = %lx\n", pt);

        pml4[0x1ff] = x | 0x3;
        pt[0x1ff] = (x + 0x1000) | 0x3;
        pt[0x3ff] = (x + 0x2000) | 0x3;
        pt[0x5ff] = (x + 0x3000) | 0x3;

        *(uint32_t *)p = 0xabcdabcd;
        *(uint32_t *)(p + 4) = 0x12341234;

        Print(L"[0] = %08x\n", *(uint32_t *)(p+4));
        Print(L"[fffffffffffc] = %08x\n", *(uint32_t *)(x + 0x3ffc));

        /* This write should place 0x5678 in the last 16-bit word of memory
         * and 0x5678 at the first 16-bit word in memory. If the wrapping
         * works as expected p[0] should be 0x5678ABCD and
         * p[1] should be 0x12345678 when displayed. */
        *(uint32_t *)(p + 2) = 0x56785678;

        Print(L"p[0] = %08x\n", ((uint32_t *)p)[0]);
        Print(L"p[1] = %08x\n", ((uint32_t *)p)[1]);
    }

    return 0;
}

应该在 64 位 Ubuntu 和 64 位 Debian 上运行的 Makefile 可能如下所示:

ARCH            ?= $(shell uname -m | sed s,i[3456789]86,ia32,)
ifneq ($(ARCH),x86_64)
LIBDIR          = /usr/lib32
else
LIBDIR          = /usr/lib
endif

OBJS            = wraptest.o
TARGET          = wraptest.efi

EFIINC          = /usr/include/efi
EFIINCS         = -I$(EFIINC) -I$(EFIINC)/$(ARCH) -I$(EFIINC)/protocol
LIB             = $(LIBDIR)
EFILIB          = $(LIBDIR)
EFI_CRT_OBJS    = $(EFILIB)/crt0-efi-$(ARCH).o
EFI_LDS         = $(EFILIB)/elf_$(ARCH)_efi.lds

CFLAGS          = $(EFIINCS) -fno-stack-protector -fpic \
                  -fshort-wchar -mno-red-zone -Wall -O3
ifeq ($(ARCH),x86_64)
  CFLAGS += -DEFI_FUNCTION_WRAPPER
endif

LDFLAGS         = -nostdlib -znocombreloc -T $(EFI_LDS) -shared \
                  -Bsymbolic -L $(EFILIB) -L $(LIB) $(EFI_CRT_OBJS)

all: $(TARGET)

wraptest.so: $(OBJS)
        ld $(LDFLAGS) $(OBJS) -o $@ -lefi -lgnuefi

%.efi: %.so
        objcopy -j .text -j .sdata -j .data -j .dynamic \
                -j .dynsym  -j .rel -j .rela -j .reloc \
                --target=efi-app-$(ARCH) $^ $@

编写的代码只有在为 x86-64 编译时才能正常工作。您可以使用以下命令制作此 EFI 应用程序:

make ARCH=x86_64

生成的文件应该是wraptest.efi,可以复制到您的 EFI 系统分区。 make 文件基于Roderick Smith's tutorial

【讨论】:

    【解决方案2】:

    我刚刚用这个 EFI 程序进行了测试。 (正如预期的那样,它起作用了。)如果你想重现这个结果,你需要一个 efi_printf 的实现,或者其他方式来查看结果。

    #include <stdint.h>
    #include "efi.h"
    
    uint8_t *p = (uint8_t *)0xfffffffffffffffcULL;
    
    int main()
    {
        uint64_t cr3;
        asm("mov %%cr3, %0" : "=r"(cr3));
        uint64_t *pml4 = (uint64_t *)(cr3 & ~0xfffULL);
    
        efi_printf("cr3 %lx\n", cr3);
        efi_printf("pml4[0] %lx\n", pml4[0]);
        uint64_t *pdpt = (uint64_t *)(pml4[0] & ~0xfffULL);
        efi_printf("pdpt[0] %lx\n", pdpt[0]);
        if (!(pdpt[0] & 1)) {
            pdpt[0] = (uint64_t)efi_alloc_pages(EFI_BOOT_SERVICES_DATA, 1) | 0x03;
            efi_printf("pdpt[0] %lx\n", pdpt[0]);
        }
        uint64_t *pd = (uint64_t *)(pdpt[0] & ~0xfffULL);
        efi_printf("pd[0] %lx\n", pd[0]);
        if (!(pd[0] & 1)) {
            pd[0] = (uint64_t)efi_alloc_pages(EFI_BOOT_SERVICES_DATA, 1) | 0x03;
            efi_printf("pd[0] %lx\n", pd[0]);
        }
        if (!(pd[0] & 0x80)) {
            uint64_t *pt = (uint64_t *)(pd[0] & ~0xfffULL);
            efi_printf("pt[0] %lx\n", pt[0]);
            if (!(pt[0] & 1)) {
                pt[0] = (uint64_t)efi_alloc_pages(EFI_BOOT_SERVICES_DATA, 1) | 0x03;
                efi_printf("pt[0] %lx\n", pt[0]);
            }
        }
    
        efi_printf("[0] = %08x\n", *(uint32_t *)(p+4));
    
        efi_printf("pml4[0x1ff] %lx\n", pml4[0x1ff]);
        if (pml4[0x1ff] == 0) {
    
            uint64_t *pt = (uint64_t *)efi_alloc_pages(EFI_BOOT_SERVICES_DATA, 4);
            uint64_t x = (uint64_t)pt;
    
            efi_printf("pt = %p\n", pt);
    
            pml4[0x1ff] = x | 0x3;
            pt[0x1ff] = x + 0x1000 | 0x3;
            pt[0x3ff] = x + 0x2000 | 0x3;
            pt[0x5ff] = x + 0x3000 | 0x3;
    
            *(uint32_t *)p = 0xabcdabcd;
            *(uint32_t *)(p + 4) = 0x12341234;
    
            efi_printf("[0] = %08x\n", *(uint32_t *)(p+4));
            efi_printf("[fffffffffffc] = %08x\n", *(uint32_t *)(x + 0x3ffc));
    
            *(uint32_t *)(p + 2) = 0x56785678;
    
            efi_printf("p[0] = %08x\n", ((uint32_t *)p)[0]);
            efi_printf("p[1] = %08x\n", ((uint32_t *)p)[1]);
        }
    
        return 0;
    }
    

    如果它按预期工作,最后 4 行应该是:

    [0] = 12341234
    [fffffffffffc] = ABCDABCD
    p[0] = 5678ABCD
    p[1] = 12345678
    

    从内存的最后一个 16 位字开始写入 0x56785678 值,并且应该回绕到内存的第一个 16 位字。


    注意:p 必须是全局变量,否则 GCC 将 *(p+4) 更改为 ud2

    【讨论】:

    • I found 仅使用 -Og 而不是 -O2 或更高版本进行编译的 gcc 可以生成我想要的 asm。但可以肯定的是,全局也可以阻止优化器看到编译时常量。
    猜你喜欢
    • 2021-10-06
    • 2016-11-19
    • 2013-08-12
    • 2020-09-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-06
    • 1970-01-01
    相关资源
    最近更新 更多