Linux kernel 为什么x86上的Linux对用户进程和内核使用不同的段?

Linux kernel 为什么x86上的Linux对用户进程和内核使用不同的段?,linux-kernel,x86,memory-segmentation,Linux Kernel,X86,Memory Segmentation,因此,我知道Linux对x86处理器使用四个默认段(内核代码、内核数据、用户代码、用户数据),但它们都有相同的基数和限制(0x00000000和0xfffff),这意味着每个段映射到同一组线性地址 既然如此,为什么还要有用户/内核段呢?我理解为什么代码和数据应该有单独的段(这取决于x86处理器如何处理cs和ds寄存器),但为什么不有一个单独的代码段和一个单独的数据段呢?内存保护是通过分页完成的,用户和内核段无论如何都映射到相同的线性地址。内核内存不应该从用户空间中运行的程序中读取 程序数据通常不

因此,我知道Linux对x86处理器使用四个默认段(内核代码、内核数据、用户代码、用户数据),但它们都有相同的基数和限制(0x00000000和0xfffff),这意味着每个段映射到同一组线性地址


既然如此,为什么还要有用户/内核段呢?我理解为什么代码和数据应该有单独的段(这取决于x86处理器如何处理cs和ds寄存器),但为什么不有一个单独的代码段和一个单独的数据段呢?内存保护是通过分页完成的,用户和内核段无论如何都映射到相同的线性地址。

内核内存不应该从用户空间中运行的程序中读取

程序数据通常不可执行(DEP,一种处理器功能,有助于防止执行溢出的缓冲区和其他恶意攻击)


这都是关于访问控制-不同的部分有不同的权利。这就是为什么访问错误的段会导致“分段错误”。

x86内存管理体系结构同时使用分段和分页。粗略地说,段是进程地址空间的一个分区,它有自己的保护策略。因此,在x86体系结构中,可以将进程看到的内存地址范围拆分为多个连续段,并为每个段分配不同的保护模式。分页是一种将进程地址空间的小区域(通常为4KB)映射到实际物理内存块的技术。因此,分页控制段内的区域如何映射到物理RAM

所有流程都有两个部分:

  • 一个段(地址0x00000000到0xBFFFFF)用于用户级、进程特定数据,如程序代码、静态数据、堆和堆栈。每个进程都有自己独立的用户段

  • 一个段(地址0xC0000000到0xFFFFFF),包含内核特定的数据,如内核指令、数据、内核代码可以执行的一些堆栈,更有趣的是,该段中的一个区域直接映射到物理内存,这样内核就可以直接访问物理内存位置,而不必担心地址转换。相同的内核段映射到每个进程中,但只有在受保护的内核模式下执行时,进程才能访问它

  • 因此,在用户模式下,进程只能访问小于0xC0000000的地址;对高于此值的地址的任何访问都会导致故障。但是,当用户模式进程开始在内核中执行时(例如,在进行系统调用之后),CPU中的保护位更改为监控模式(并且一些分段寄存器更改),这意味着该进程因此能够访问0xC0000000以上的地址


    请参阅:

    x86体系结构将类型和权限级别与每个段描述符相关联。描述符的类型允许将段设置为只读、读/写、可执行等,但不同段具有相同基础和限制的主要原因是允许使用不同的描述符特权级别(DPL)

    DPL为两位,允许对值0到3进行编码。当特权级别为0时,则称为,这是最特权的。Linux内核的段描述符是环0,而用户空间的段描述符是环3(最低特权)。对于大多数分段操作系统来说都是如此;操作系统的核心是环0,其余的是环3

    正如您所提到的,Linux内核设置了四个部分:

    • __内核(内核代码段,基=0,限制=4GB,类型=10,DPL=0)
    • __内核数据段(内核数据段,基=0,限制=4GB,类型=2,DPL=0)
    • __用户代码段(用户代码段,基数=0,限制=4GB,类型=10,DPL=3)
    • __用户数据段(用户数据段,基数=0,限制=4GB,类型=2,DPL=3)
    所有四个的基础和限制都是相同的,但内核段是DPL 0,用户段是DPL 3,代码段是可执行和可读的(不可写),数据段是可读和可写的(不可执行)

    另见:


    在X86中,linux段寄存器用于缓冲区溢出检查[请参阅下面的代码段,它在堆栈中定义了一些字符数组]:

    static void
    printint(int xx, int base, int sgn)
    {
        char digits[] = "0123456789ABCDEF";
        char buf[16];
        int i, neg;
        uint x;
    
        neg = 0;
        if(sgn && xx < 0){
            neg = 1;
            x = -xx;
        } else {
            x = xx;
        }
    
        i = 0;
        do{
            buf[i++] = digits[x % base];
        }while((x /= base) != 0);
        if(neg)
            buf[i++] = '-';
    
        while(--i >= 0)
            my_putc(buf[i]);
    }
    
    静态无效
    printint(int-xx,int-base,int-sgn)
    {
    字符数字[]=“0123456789ABCDEF”;
    char-buf[16];
    int i,neg;
    uint x;
    负=0;
    如果(sgn&&xx<0){
    负=1;
    x=-xx;
    }否则{
    x=xx;
    }
    i=0;
    做{
    buf[i++]=位数[x%基数];
    }而((x/=base)!=0);
    如果(负)
    buf[i++]='-';
    而(--i>=0)
    我的putc(buf[i]);
    }
    
    现在,如果我们看到gcc生成的代码的分解

    函数printint的汇编程序代码转储:

     0x00000000004005a6 <+0>:   push   %rbp
       0x00000000004005a7 <+1>: mov    %rsp,%rbp
       0x00000000004005aa <+4>: sub    $0x50,%rsp
       0x00000000004005ae <+8>: mov    %edi,-0x44(%rbp)
    
    
      0x00000000004005b1 <+11>: mov    %esi,-0x48(%rbp)
       0x00000000004005b4 <+14>:    mov    %edx,-0x4c(%rbp)
       0x00000000004005b7 <+17>:    mov    %fs:0x28,%rax  ------> obtaining an 8 byte guard from based on a fixed offset from fs segment register [from the descriptor base in the corresponding gdt entry]
       0x00000000004005c0 <+26>:    mov    %rax,-0x8(%rbp) -----> pushing it as the first local variable on to stack
       0x00000000004005c4 <+30>:    xor    %eax,%eax
       0x00000000004005c6 <+32>:    movl   $0x33323130,-0x20(%rbp)
       0x00000000004005cd <+39>:    movl   $0x37363534,-0x1c(%rbp)
       0x00000000004005d4 <+46>:    movl   $0x42413938,-0x18(%rbp)
       0x00000000004005db <+53>:    movl   $0x46454443,-0x14(%rbp)
    
    ...
    ...
      // function end
    
       0x0000000000400686 <+224>:   jns    0x40066a <printint+196>
       0x0000000000400688 <+226>:   mov    -0x8(%rbp),%rax -------> verifying if the stack was smashed
       0x000000000040068c <+230>:   xor    %fs:0x28,%rax  --> checking the value on stack is matching the original one based on fs
       0x0000000000400695 <+239>:   je     0x40069c <printint+246>
       0x0000000000400697 <+241>:   callq  0x400460 <__stack_chk_fail@plt>
       0x000000000040069c <+246>:   leaveq 
       0x000000000040069d <+247>:   retq 
    
    0x00000000004005a6:推送%rbp
    0x00000000004005a7:mov%rsp,%rbp
    0x00000000004005aa:低于$0x50,%rsp
    0x00000000004005ae:mov%edi,-0x44(%rbp)
    0x00000000004005b1:mov%esi,-0x48(%rbp)
    0x00000000004005b4:mov%edx,-0x4c(%rbp)
    0x00000000004005b7:mov%fs:0x28,%rax----------->根据fs段寄存器的固定偏移量[从相应gdt项中的描述符基址]从获取8字节保护
    0x00000000004005c0:mov%rax,-0x8(%rbp)--->将其作为第一个局部变量推送到堆栈上
    0x00000000004005c4:异或%eax,%eax
    0x00000000004005c6:movl$0x33323130,-0x20(%rbp)
    0x00000000004005cd:movl$0x37363534,-0x1c(%rbp)
    0x00000000004005d4:movl$0x42413938,-0x18(%rbp)
    0x00000000004005db:movl$0x46454443,-0x14(%rbp)
    ...
    ...
    //功能端
    0x0000000000400686:jn
    config CC_STACKPROTECTOR
     699        bool "Enable -fstack-protector buffer overflow detection (EXPERIMENTAL)"
     700        depends on SUPERH32
     701        help
     702          This option turns on the -fstack-protector GCC feature. This
     703          feature puts, at the beginning of functions, a canary value on
     704          the stack just before the return address, and validates
     705          the value just before actually returning.  Stack based buffer
     706          overflows (that need to overwrite this return address) now also
     707          overwrite the canary, which gets detected and the attack is then
     708          neutralized via a kernel panic.
     709
     710          This feature requires gcc version 4.2 or above.