x86 - GDT - GCC 可以处理不同的段(代码和数据)吗?

0 c x86 assembly gcc operating-system

我有一个关于GDTGCC 的一般性问题。出于学习目的,开始编写操作系统内核。目前我使用GCC来编译代码。在设置GDT的几个教程中,相同的基址 (0) 和限制 (0xFFFFF) 用于代码和数据段。作为 x86 新手,我的第一个想法是,如果我使用不同的基地址和限制,这可能是一种额外的保护。

这是我尝试过的(缩短):

链接器脚本:

ENTRY(_start)

SECTIONS {
    . = 1M;

    _kern_start = .;

    .text  ALIGN(4096) :  {
        _kern_text_start = .;
        *(.multiboot)
        *(.text)
        _kern_text_end = .;

    }

    .rodata ALIGN(4096) : {
        _kern_rodata_start = .;
        *(.rodata)
        _kern_rodata_end = .;
    }




    .data  ALIGN(4096):  {
        _kern_data_start = .;
        *(.data)
        _kern_data_end = .;
    }



    .bss ALIGN(4096) :  {
        _kern_bss_start = .;
        *(.bss)
        _kern_bss_end = .;
    }




    .stack ALIGN(4096) :  {
        _kern_stack_start = .;
        *(.stack)
        _kern_stack_end = .;
    }



    .heap  ALIGN(4096) :  {
        _kern_heap_start = .;
        *(.heap)
        _kern_heap_end = .;
    }

    _kern_end = .;
}
Run Code Online (Sandbox Code Playgroud)

我为每个部分添加了符号,然后编写了简单的汇编器函数来获取我在C中调用的每个部分的起始地址和大小:

汇编器函数(作为示例):

FUNCTION(_kern_text_get_addr)
    pushl %ebp
    movl %esp, %ebp
    movl $_kern_text_start, %eax
    leave
    ret

FUNCTION(_kern_text_get_size)
    pushl %ebp
    movl %esp, %ebp
    movl $_kern_text_start, %ebx
    movl $_kern_text_end, %eax
    sub %ebx, %eax
    leave
    ret
Run Code Online (Sandbox Code Playgroud)

我使用不同的部分在GDT中设置代码和数据(以下代码片段中未显示)段:

uint32_t base;
uint32_t limit;

base = _kern_text_get_addr();
limit = _kern_text_get_size() / 4096;

/* Kernel Code */
gdt_set_entry(&gdt[GDT_KERN_CODE], base, limit, GDT_ACCESS_EXEC | 
                                                GDT_ACCESS_SEGMENT | 
                                                GDT_ACCESS_RING0 | 
                                                GDT_ACCESS_PRESENT, 
                                                GDT_FLAG_SIZE | 
                                                GDT_FLAG_GRAN);
Run Code Online (Sandbox Code Playgroud)

使用汇编指令加载lgdt是可行的。但是当我用长跳转刷新段寄存器时,我遇到了一般保护(#GP)错误。所以我检查了生成的机器代码。问题是当我使用GCC使用默认选项编译它时,长跳转指令的跳转地址不正确。它需要某种地址转换才能跳转到正确的位置。数据段也使用错误的地址。好吧,我可以使用分页来代替,但即使这个问题听起来很愚蠢:

是否有可能使用GCC 的代码和数据使用不同的段,或者换句话说,GCC可以处理不同的段吗?我知道GCCPIC中有这个论点,但还没有尝试过。

Ros*_*dge 5

除了一个小例外,GCC 可以毫无问题地处理单独的代码和数据段。我唯一知道的中断是在获取嵌套函数的地址时创建的蹦床。这些蹦床是在堆栈上创建的,但作为代码执行,因此如果代码需要位于与数据不同的段中,则这些蹦床将不起作用。由于嵌套函数是很少使用的 GCC 扩展,因此在实践中不会造成问题。您仍然可以通过提供一种在运行时动态分配和初始化代码段中的内存的方法来使其工作。

然而这样做并没有什么好处。您必须将 4G 32 位线性地址空间划分为两个独立的非重叠代码和数据段才能获得任何类型的安全优势。但是,通过使用不执行页保护位,您可以获得相同的安全优势,而无需对线性地址空间进行分段。这就是为什么当前所有操作系统都使用平面分段模型,代码和数据段都以 0 为基数,32 位代码的限制为 4G。出于同样的原因,64 位 x86 CPU 让您别无选择,只能在 64 位模式下使用平面模型。

与位置无关的代码不受分段的影响,并且无论您在使用长跳转(远跳转)指令时遇到什么问题,都无法通过使用单独的代码和数据段来解决。

  • @krjdev:在 x86 通过分页对 NX 进行普遍支持之前,曾有人尝试对至少不可执行的堆栈使用 x86 分段(以防止简单的代码注入缓冲区溢出)。Linux 有针对此问题的补丁(https://en.wikipedia.org/wiki/Executable_space_protection#Exec_Shield 和 PaX),OpenBSD 也是如此。现在一切都已经过时了,因为所有内容都支持 32 位内核的 PAE 格式页表,而 64 位内核(您应该始终使用)始终支持 NX。其他非 x86 ISA 根本没有分段,但可能有内存区域而不是分页。 (3认同)