几个重要的段寄存器Linux2.6中的线性地址范围一个“完整”的8086汇编器在前两篇文章中,我们了解了CPU和内存在8086处理器中的基本使用,重点介绍了段寄存器和内存寻址方式.可能有些朋友会对此不屑一顾:现在都是多核现代处理器,操作系统也变得很强大了。为什么还要学习这些古老的知识?前几天看到下面这段话,可以回答这个问题:“我们都想学最新用的东西,但是学习的过程是客观的。”“任何合理的学习过程(排除走弯路、盲目探索、尽可能不系统化)都是一个循序渐进的过程。”“我们首先要通过一个容易全面掌握的事物来学习和探索一般规律和方法。”把学习Linux操作系统作为一个长期的学习计划,不可能一上代码就把最新的Linux5.13版本看一遍,更有可能先学习0.11版本,了解一些原理和思想,然后一步步学习探索到更高版本。所以对于这个系列文章《Linux 从头学》,希望能把学习路线拉长一点,从底层的硬件机制和驱动原理开始,从由简单到复杂,循序渐进,终于把Linux操作系统这块硬骨头啃下来了。所以今天我们继续8086下的学习,看看一个比较“完整”的程序的基本结构。几个重要的段registers在x86系统中,段寻址机制和相关的寄存器非常重要,这里不能不总结几个段寄存器代码段:用来存放代码,段的基地址放在寄存器CS中,指令指针寄存器IP用于指示下一条指令在段中的偏移地址;数据段:用来存放程序处理过的数据,段的基址地址存放在寄存器DS中。对数据段中的某个数据进行操作时,直接在汇编代码中通过立即数或寄存器指定偏移地址;堆栈段:本质上也是用来存放数据的,只是其操作方式比较特殊:通过PUSH和POP指令来操作。段的基地址存放在寄存器SS中,栈顶单元的偏移地址存放在寄存器IP中。这里的段本质上就是我们利用内存上的某一块连续的存储空间来存储某一类数据。我们之所以能做到这一点,是因为CPU利用了上面的寄存器,让我们的“安排”成为可能。一句话概括:CPU把内存中某段内容当成代码,因为CS:IP指向那里;CPU将某个段视为堆栈,因为CS:SP指向那里。在之前的一篇文章中,我演示了ELF格式的可执行文件中包含了哪些段:虽然这张图描述的段结构比较复杂,但本质上和8086中描述的段结构是一样的!Linux2.6中的线性地址范围在现代操作系统中,进程使用的地址空间一般称为虚拟地址(也称为逻辑地址)。虚拟地址先经过段转换得到线性地址;然后将线性地址进行页转换,得到最终的物理地址。让我在这里更详细一点。很多书里对内存地址的称呼很多,都是按照作者的习惯来称呼的。我按照上图理解:编译器生成的地址称为虚拟地址,也称为逻辑地址,然后经过两级转换得到最终的物理地址。在Linux2.6代码中,由于Linux把整个4GB地址空间当做一个“平坦”的结果(段基地址为0x0000_0000,最大偏移地址为4GB),所以虚拟地址(逻辑地址)在数值上等于线性地址。结合上次给的图来理解一下:这张图的意思是:在Linux2.6中,用户代码段的起始地址为0,最大范围为4GB;用户数据段起始地址为0,最大extent也是4GB;内核的数据和代码段也是如此。为什么:虚拟地址(逻辑地址)在数值上等于线性地址?线性地址=段基地址+虚拟地址(偏移量),因为段基地址为0,所以线性地址在数值上等于虚拟地址。Linux之所以这样安排,是不想过多利用x86提供的段机制进行内存地址管理,而是想充分利用分页机制,进行更灵活的地址管理。还要提醒一点:在上面的描述中,我会指出一种机制或策略,无论是x86平台提供的还是Linux操作系统提供的。分页机制也是如此。x86硬件提供了分页机制,但是Linux在x86提供的分页机制的基础上进行了扩展,实现了更灵活的内存地址管理。所以,大家在看一些书的时候,脑子里肯定有一个频谱:当前描述的上下文是什么。当我们创建一个进程时,该进程拥有的所有线性地址范围都会被记录在内核中。一个进程拥有的所有线性地址范围都是一个动态的进程,可以根据程序的需要随时扩大或缩小。例如:将文件映射到内存,动态加载/卸载动态库等。我们知道,内核在操作物理内存时,是通过“页框”为单位进行管理的。一个页框可以包含1-n页,每页的大小一般为4KB,这是对物理内存的管理。线性地址范围可以包含多个物理页面。每个线性地址通过多级页表转换最终得到一个物理地址。注意:上图中,线性地址范围1映射到物理地址空间中的N个Page,这些Page可能连续也可能不连续。虽然在物理内存中是不连续的,但是被分页转换机制屏蔽了,所以我们在应用中把它当作连续空间来使用。“完整”的8086汇编器让我们回到8086系统。这里描述的地址,经过段地址转换后,是一个物理地址,没有经过复杂的页表转换。这也是我们使用8086系统作为学习平台的目的:直接探索底层的东西,不用复杂的操作系统。在这个最简单的汇编程序中,使用了三个段:代码、数据和堆栈。前面说过:所谓段就是一个地址空间。既然是地址空间,就必须包含2个元素:从哪里开始,长度是多少。或者直接放代码:assumes:addr1,ss:addr2,cs:addr3addr1segment;在此位置安排数据段db32dup(0);这32个字节是数据段addr1endaddr2segment的大小;在此位置安排堆栈段db32dup(0);这32个字节是堆栈段addr2endaddr3segment的大小;在这个位置安排代码段startmovax,addr1movds,ax;设置数据段寄存器movax、addr2movss、ax;设置堆栈段寄存器movsp,20h;设置栈顶指针寄存器...;othercodeaddr3endsendstart以上是一段汇编代码的基本程序结构,我们为它安排了3段。3个标签:addr1、addr2和addr3,代表每个段的起始地址。在代码段的开头,将数据段标号addr1所代表的地址赋值给DS寄存器;将堆栈段标签addr2表示的地址分配给SS寄存器。这里的标签是不是类似于C语言中的goto标签?两者都代表一个地址。注意这里赋给栈顶指针SP寄存器的值是20H。因为栈段的使用是从高地址向低地址方向进行的,所以需要将栈顶指针设置为最大地址单元的下一个地址空间。假设当第一个数据入栈时(eg:先执行movax,1234h,再执行pushax),CPU需要做的是:先执行SP=SP-2,此时SS:SP指向1000:001E,然后把1234h存入这个地址空间:另外,代码中最后一句endstart是用来告诉编译器:代码段中start标号所代表的地址就是这个程序的入口地址,而这个入口地址信息在编译写入可执行程序后也会被删除。当可执行文件加载到内存中时,加载器会找到这个入口地址,然后设置CS:IP指向这个入口地址,从而开始执行第一条指令。我们来对比一下《Linux系统中编译、链接的基石-ELF文件:扒开它的层层外衣,从字节码的粒度来探索》中列出的ELF可执行文件中的入口地址。本质上和上面8086下的开始标签代表的入口地址是一样的:转载本文请联系物联小镇公众号。【小编推荐】教你用Python轻松打造淘宝主图视频生成神器为什么NanoID会取代UUID加密货币世界的黑客防御和缓解近日,腾讯一名35岁员工的薪水被曝光。这辈子还能赶上吗?
