毫无疑问,不管是32位,还是64位处理器,所有进程(执行的程序)都必须占用一定数量的内存,它或是用来存放从磁盘载入的程序代码,或是

存放取自用户输入的数据等等。不过进程对这些内存的管理方式因内存用途不一而不尽相同,有些内存是事先静态分配和统一回收的,而有些却是按需要动态分配和回收的。

对任何一个普通进程来讲,它都会涉及到5种不同的数据段。稍有编程知识的朋友都该能想到这几个数据段种包含有“程序代码段”、“程序数据段”、“程序堆栈段”等。不错,这几种数据段都在其中,但除了以上几种数据段之外,进程还另外包含两种数据段。下面我们来简单归纳一下进程对应的内存空间中所包含的5种不同的数据区。

代码段:代码段是用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存种的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。

数据段:数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。

BSS段:BSS段包含了程序中未初始化全局变量,在内存中 bss段全部置零。

堆(heap):堆是用于存放进程运行中被动态分配的内存段,它大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。

栈:栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味这在数据段中存放变量)。除此以外在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也回被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上将我们可以把堆栈看成一个临时数据寄存、交换的内存区。

静态分配内存就是编译器在编译程序的时候根据源程序来分配内存. 动态分配内存就是在程序编译之后, 运行时调用运行时刻库函数来分配内存的. 静态分配由于是在程序运行之前,所以速度快, 效率高, 但是局限性大. 动态分配在程序运行时执行, 所以速度慢, 但灵活性高。

术语"BSS"已经有些年头了,它是block started by symbol的缩写。因为未初始化的变量没有对应的值,所以并不需要存储在可执行对象中。但是因为C标准强制规定未初始化的全局变量要被赋予特殊的默认值(基本上是0值),所以内核要从可执行代码装入变量(未赋值的)到内存中,然后将零页映射到该片内存上,于是这些未初始化变量就被赋予了0值。这样做避免了在目标文件中进行显式地初始化,减少空间浪费(来自《Linux内核开发》)

我们在x86_64环境上运行以下经典程序:

#include<stdio.h>
#include<malloc.h>
#include<unistd.h>

int bss_var;
int data_var0=1;

int main(int argc,char **argv)
{
        printf("below are addresses of types of process's mem/n");

printf("Text location:/n");
        printf("/tAddress of main(Code Segment):%p/n",main);

printf("____________________________/n");

int stack_var0=2;

printf("Stack Location:/n");
        printf("/tInitial end of stack:%p/n",&stack_var0);

int stack_var1=3;

printf("/tnew end of stack:%p/n",&stack_var1);

printf("____________________________/n");

printf("Data Location:/n");
        printf("/tAddress of data_var(Data Segment):%p/n",&data_var0);

static int data_var1=4;

printf("/tNew end of data_var(Data Segment):%p/n",&data_var1);

printf("____________________________/n");

printf("BSS Location:/n");
        printf("/tAddress of bss_var:%p/n",&bss_var);

printf("____________________________/n");

char *b = sbrk((ptrdiff_t)0);

printf("Heap Location:/n");
        printf("/tInitial end of heap:%p/n",b);
        brk(b+4);
        b=sbrk((ptrdiff_t)0);

printf("/tNew end of heap:%p/n",b);

return 0;

}

运行结果:
[root@kollera updilogs]# ./memory
below are addresses of types of process's mem
Text location:
        Address of main(Code Segment):0x400568
____________________________
Stack Location:
        Initial end of stack:0x7fff0e0dc544
        new end of stack:0x7fff0e0dc540
____________________________
Data Location:
        Address of data_var(Data Segment):0x600bfc
        New end of data_var(Data Segment):0x600c00
____________________________
BSS Location:
        Address of bss_var:0x600c14
____________________________
Heap Location:
        Initial end of heap:0xb059000
        New end of heap:0xb059004

2 x86_64体系新变化

AMD x86_64的出现,给全新的64位的x86带来了很多结构上的变化:   

1)64位整型数   

在x86-64中,所有通用寄存器(GPRs)都从32位扩充到了64位,名字也发生了变化。8个通用寄存器(eax, ebx, ecx, edx,

ebp, esp, esi, edi)在新的结构中被命名为rax, rbx, rcx, rdx, rbp, rsp, rsi, rdi,它们都是64位的。呵呵,想当年,从16位扩充到32位时,同样也有一次名字的变化。所有算术逻辑操作、寄存器到内存的数据传输现在都能以64位的整形类型进行操作。堆栈的压栈和弹出操作都以8字节的单位进行,而且指针类型也拥有了64位。

2)新增寄存器   

在新的架构中,另外新增了8个通用寄存器:64位的r8, r9, r10, r11, r12, r13, r14, r15。这样就有利与编译器将函数参数、返回值等放在这些新增的GPR里面进行传递,从而提高了程序的运行速度。同时,128位的MMX寄存器也从原来的8个增加到了16个。

3)增大的逻辑地址空间   

目前在新的架构中,应用程序可以拥有的逻辑地址空间从4GB增加到了256TB(2^48),而且这一逻辑地址空间在未来可能增加到16EB

(2^64,1EB=1024PB,1PB=1024TB,1TB=1024GB)。

4)增大的物理地址空间   

目前的x86-64架构,可以支持的物理内存扩展到了1TB(2^40),当然,在未来该数字可以扩展到4PB(2^52)。相比于经过PAE技术扩展的i386的64GB物理内存,新的架构带来了不小的飞跃。

5)无缝使用SSE指令   

新的架构借鉴和吸收了Intel的SSE、SSE2的核心指令,并在2005年加入了SSE3。在这一新的架构下,可以不再需要x87浮点协处理器来完成浮点运算了。

6)NX位   

跟PAE技术一样,新的x86-64架构也在页表项中增加了NX位,来帮助CPU判断该页包含的内容是否是可以执行的,从而避免借助“buffer overrun”导致的病毒攻击。

7)去除旧的机制   

在新架构的“长模式(long mode)”下,很多在IA32中被提出,但确不经常被操作系统用到的一些机制不再被支持。这些机制包括段式地址变化机制(FS和GS仍然被保留),任务转移门(TSS)机制,以及虚拟86模式。当然,出于向下兼容的考虑,x86-64在“传统模式”(Legacy mode)下,仍然对这些机制进行了保留。

3 x86_64段式管理

x86的两种工作模式:实地址模式和虚地址模式(保护模式)。Linux主要工作在保护模式下。

在保护模式下,64位x86体系架构的虚地址空间可达2^48Byte,即256TB,这可比只能到达区区4GB的32位x86体系大多了。逻辑地址到线性地址的转换由x86分段机制管理。段寄存器CS、DS、ES、SS、FS或GS各标识一个段。这些段寄存器作为段选择器,用来选择该段的描述符。

Linux中关于段描述符的宏定义集中在文件/arch/x86/include/asm/Segment.h中,我们先贴出部分代码:

32位的:

#define GDT_ENTRY_KERNEL_BASE 12                             /* 0x0000000c c=>1100*/
#define GDT_ENTRY_KERNEL_CS (GDT_ENTRY_KERNEL_BASE + 0)      /* 0x0000000c c=>1100*/
#define GDT_ENTRY_KERNEL_DS (GDT_ENTRY_KERNEL_BASE + 1)      /* 0x0000000d c=>1101*/

64位的:

#define GDT_ENTRY_KERNEL32_CS 1         /* 0x00000001 */
#define GDT_ENTRY_KERNEL_CS 2           /* 0x00000002 */
#define GDT_ENTRY_KERNEL_DS 3           /* 0x00000003 */

#define __KERNEL32_CS   (GDT_ENTRY_KERNEL32_CS * 8)          /* 0x00000100 */

#define GDT_ENTRY_DEFAULT_USER32_CS 4   /* 0x00000004 */
#define GDT_ENTRY_DEFAULT_USER_DS 5     /* 0x00000005 */
#define GDT_ENTRY_DEFAULT_USER_CS 6     /* 0x00000006 */

#define __USER32_CS   (GDT_ENTRY_DEFAULT_USER32_CS * 8 + 3)  /* 0x00000403 */
#define __USER32_DS __USER_DS

不管32位还是64位的:(我们只关心64位)

#define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8)            /* 0x00000200 */
#define __KERNEL_DS (GDT_ENTRY_KERNEL_DS * 8)            /* 0x00000300 */
#define __USER_DS     (GDT_ENTRY_DEFAULT_USER_DS* 8 + 3)     /* 0x00000503 */
#define __USER_CS     (GDT_ENTRY_DEFAULT_USER_CS* 8 + 3)     /* 0x00000603 */

看见没有,我们熟悉的__USER_CS,__USER_DS,__KERNEL_CS,和__KERNEL_DS,就是传说中的段选择子。

我们看到,内核代码段的描述子存放在以0x200为基地址的内存单元中,占8个字节。同样,内核数据段、用户代码段、用户数据段分别存放在

以0x300、0x500、0x600为基地址的内存单元中。我们注意到,__USER_DS和__USER_CS的最低三位为3,也就是011,这正说明

其CPL位为11,代表用户模式,TI为0,代表GDT。

对于x86_64来说,虚拟地址由16位选择子和64位偏移量组成,段寄存器仅仅存放选择子。CPU的分段单元(SU)执行以下操作:
[1] 先检查选择子的TI字段,以决定描述子对应的描述子保存在哪一个描述符表中。TI字段指明描述子是在GDT中(在这种情况下,分段单元从gdtr寄存器中得到GDT的线性基地址)还是在激活的LDT中(在这种情况下,分段单元从ldtr寄存器中得到LDT的线性基地址)。
[2] 从选择子的13位index字段计算描述子的地址,index字段的值乘以8(一个描述子的大小,其实就是屏蔽掉末尾那三位指示特权级的CPL和指示TI的字段),这个结果与gdtr或ldtr寄存器中的内容相加。
[3] 将对应的段描述子从内存拷贝到CPU的影子Cache中,这样,只有在选择子改变的情况下才会修改影子Cache中的内容。
[4] 把虚拟地址的偏移量与隐Cache中描述子Base字段的值相加就得到了线性地址。

例如,为了对内核代码段寻址,内核只需要把__KERNEL_CS宏产生的选择子的值装进cs段寄存器即可。注意,与段相关的线性地址还是从

0开始,达到264 -1的寻址限长。这就意味着在用户态或内核态下的所有进程任然使用相同的虚拟地址,这就是传说中的“基本平坦模式”。
按照这个模式,虚拟地址跟线性地址数字一样,唯一的不同就是CS和DS装的内容不同,可能是KERNEL级别的选择子,也可能是USER级别
的选择子。

4 x86_64分页管理

虽然逻辑地址扩展到了64位,但是,现有的设计并没有完全用到这64位的空间(2^64=16EB),因为使用到如此大的空间,势必造成很大的

系统开销。AMD64在设计的时候就决定在x86_64的第一阶段,只用这64位中的低48位来做页式地址转换,高16位(48-64位)将填充第47位相同的内容(这种方式类似于符号扩展)。如果逻辑地址不符合此规定,系统将产生异常。符合此规定的地址称为canonical form,地址的范围分为两段:0 到 00007FFF-FFFFFFFF,以及FFFF8 000-0000 0000到FFFFFFFF-FFFFFFFF,总共为256TB。这种虚拟地址的分层结构,也为操作系统的设计带来了一定便利:可以取地址的上半段保留做为操作系统的逻辑地址空间,而低地址部分做为装载应用程序的空间,而canonical form不允许的地址空间则做为操作系统的标志、以及特权级的标识等。当然,这样的设计在未来地址进一步扩展的时候将成为一个新的问题。

采用64位地址空间的x86-86被称为是运行在“长模式”(long mode)下,该模式可以看成是对PAE模式的一个扩充。长模式允许使用三个不同的物理页面大小:4KB、2MB和1GB。在使用64位中的48位用来存放地址时,与PAE模式下的三级页面映射机制不同的是,长模式下线性地址到物理地址的映射需要经过四级地址映射。在这四级地址映射机制中,原来PAE模式下仅拥有4个表项的页目录指针表被扩展到512个表项。同时,在最末一级加入一级新的页面映射结构,该结构被称为第四级页表(Page-Map Level 4 Table,PML4),它跟PAE模式下的页目录及页表(在长模式中,成为了页目录)一样,拥有512个表项。如果地址进一步扩充,如把64位寻址全部用上,该页表就能够扩充到33,554,432个表项,或者干脆再加一层地址映射(PML5),当然,按照目前只用了48位的情况下,用到512个表项的PML4就已经够用了。   

可以想象,用到48位的x86-64虚拟地址的分配机制为:   
- 0-11(12)位:页内偏移;   
- 12-20(9)位:由PML4来映射;   
- 21-29(9)位:高一级页目录来映射(如果PS=1,则该页表项指向一个2MB的页);   
- 30-38(9)位:再高一级的页目录来映射(如果PS=2,则该页表项指向一个1GB的页);   
- 39-47(9)位:页目录指针表来映射。   

x86-64的长模式下,对16位以及32位代码进行了兼容,即使CPU上跑的是64位的操作系统,历史遗留的16位以及32位代码将都能够在该操作系统上运行。由于x86-64兼容IA32的指令,所以,这些代码在这种情况下运行,基本上没有性能损耗。   

在传统模式(Legacy mode)下,x86-64的CPU的工作模式跟传统的IA32没有什么两样。

Linux x86_64与i386区别之 —— 内存寻址的更多相关文章

  1. Linux内核源码分析 day01——内存寻址

    前言 Linux内核源码分析 Antz系统编写已经开始了内核部分了,在编写时同时也参考学习一点Linux内核知识. 自制Antz操作系统 一个自制的操作系统,Antz .半图形化半命令式系统,同时嵌入 ...

  2. Linux内存寻址之分段机制及分页机制【转】

    前言 本文涉及的硬件平台是X86,如果是其他平台的话,如ARM,是会使用到MMU,但是没有使用到分段机制: 最近在学习Linux内核,读到<深入理解Linux内核>的内存寻址一章.原本以为 ...

  3. linux 版本中 i386/i686/x86-64/pcc 等的区别

    在查看dpdk官方文档的时候,发现有 这样(kernel - devel.x86_64; kernel - devel.ppc64:glibc.i686)这样的安装包信息,收集了点资料来分析这三者的关 ...

  4. Linux内存寻址

    我会尽力以最简洁清晰的思路来写这篇文章. 所谓内存寻址也就是从写在指令里的地址,转化为实际物理地址的过程.因为操作系统要兼顾许多东西,所以也就变得复杂. 逻辑地址 → 线性地址 → 物理地址 逻辑地址 ...

  5. Linux内存寻址之分页机制

    在上一篇文章Linux内存寻址之分段机制中,我们了解逻辑地址通过分段机制转换为线性地址的过程.下面,我们就来看看更加重要和复杂的分页机制. 分页机制在段机制之后进行,以完成线性—物理地址的转换过程.段 ...

  6. Linux内存寻址之分段机制

    前言 最近在学习Linux内核,读到<深入理解Linux内核>的内存寻址一章.原本以为自己对分段分页机制已经理解了,结果发现其实是一知半解.于是,查找了很多资料,最终理顺了内存寻址的知识. ...

  7. 如何确定一台linux主机是Linux (i386/i686)还是Linux (x86_64)

    在下软件包的时候,往往会遇到一个选择: 假设自己的主机是Linux,那么Linux (i386/i686)和Linux (x86_64)究竟应该选哪一个呢? 针对当今的硬件而言,如果你主机的CPU是6 ...

  8. 【读书笔记::深入理解linux内核】内存寻址【转】

    转自:http://www.cnblogs.com/likeyiyy/p/3837272.html 我对linux高端内存的错误理解都是从这篇文章得来的,这篇文章里讲的 物理地址 = 逻辑地址 – 0 ...

  9. 【读书笔记::深入理解linux内核】内存寻址

    我对linux高端内存的错误理解都是从这篇文章得来的,这篇文章里讲的 物理地址 = 逻辑地址 – 0xC0000000:这是内核地址空间的地址转换关系. 这句话瞬间让我惊呆了,根据我的CPU的知识,开 ...

随机推荐

  1. HDU-6862 Hexagon (2020HDU 多校 D8 H)

    1008 题意:半径为n的六边形(由半径为1的小六边形组成),从某一个小六边形出发有六个方向,找到一条转向次数最多的路径(用方向表示)遍历所有的六边形(一个六边形只访问一次). 题解:先画出n=3/4 ...

  2. Java并发编程之基础理论

    内存模型   主内存.工作内存与Java堆.栈.方法区并不是同一个层次的内存划分 勉强对应起来 从定义来看,主内存对应Java堆中对象实例数据部分,工作内存对应虚拟机栈中部分区域 从更低层次来说,主内 ...

  3. P1162_填涂颜色(JAVA语言)(速看!全洛谷最暴力解法!QAQ)

    思路:看了看数据n<=30,于是我们可以暴力求解(主要是BFS学的不咋地~2333).枚举每个0的位置,看上下左右四个方向上是否都有1.都有1的话说明被1包围,即在闭合圈的内部,开个数组标记一下 ...

  4. 攻防世界 reverse Mysterious

    Mysterious  BUUCTF-2019 int __stdcall sub_401090(HWND hWnd, int a2, int a3, int a4) { char v5; // [e ...

  5. Caused by: java.lang.RuntimeException: JxBrowser license check failed: No valid license found

    使用jxbrower报错,原因时证书检验失败, 解决方案: 1.首先创建证书,下面是我在IDEA maven项目中创建的位置,Java项目中在src/目录下创建, META-INF/teamdev.l ...

  6. RepVGG

    RepVGG: Making VGG-style ConvNets Great Again 作者:elfin   资料来源:RepVGG论文解析 目录 1.摘要 2.背景介绍 3.相关工作 3.1 单 ...

  7. Jmeter(四十二) - 从入门到精通进阶篇 - Jmeter配置文件的刨根问底 -番外篇(详解教程)

    1.简介 为什么宏哥要对Jmeter的配置文件进行一下讲解了,因为有的童鞋或者小伙伴在测试中遇到一些需要修改配置文件的问题不是很清楚也不是很懂,就算修改了也是模模糊糊的.更有甚者觉得那是禁地神圣不可轻 ...

  8. maven setting.xml 阿里云镜像 没有一句废话

    <?xml version="1.0" encoding="UTF-8"?> <!-- Licensed to the Apache Soft ...

  9. 万字长文,带你彻底理解EF Core5的运行机制,让你成为团队中的EF Core专家

    在EF Core 5中,有很多方式可以窥察工作流程中发生的事情,并与该信息进行交互.这些功能点包括日志记录,拦截,事件处理程序和一些超酷的最新出现的调试功能.EF团队甚至从Entity Framewo ...

  10. 一般实现分布式锁都有哪些方式?使用redis如何设计分布式锁?使用zk来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?

    #(1)redis分布式锁 官方叫做RedLock算法,是redis官方支持的分布式锁算法. 这个分布式锁有3个重要的考量点,互斥(只能有一个客户端获取锁),不能死锁,容错(大部分redis节点创建了 ...