进程—内存段机制
开始阅读之前可以先看看The Curse of Segments
1.x86的硬件段机制
1.1 段机制的引入
? 就在8086CPU出现之前,地址总线已经是16位(64KB)的了,在刚开始,段的引入是为了解决“地址总线的宽度大于寄存器的宽度”这个问题。例如8086的寄存器只有16位,但是地址总线却有20位(1MB),为了使程序能利用到1MB的物理内存空间却又不对寄存器长度作出改变(内存容量扩大,成本基本保存不变),Intel在8086中引入了段机制。
? 8086提供了四个段寄存器 CS, DS, SS, 和ES ,从缩写的含义来看,这四个寄存器分别表示当前运行进程的code segment (CS), data segment (DS),stack segment (SS), 和一个用户自定义的段 (ES),操作系统可以同时跟踪一个进程的四个段。
1.2 段机制汇编
? 从纯硬件的角度来讨论段机制对汇编程序员的影响:
x86寻址模式之register indirect addressing modes:
The 80x86 CPUs let you access memory indirectly through a register using the register indirect addressing modes. There are four forms of this addressing mode on the 8086, best demonstrated by the following instructions:
mov al, [bx] //将DS:bx地址中的字节拷贝到al中 mov al, [bp] //将SS:bp地址中的字节拷贝到al中 mov al, [si] //将DS:si地址中的字节拷贝到al中 mov al, [di] //将DS:di地址中的字节拷贝到al中
As with the x86 [bx] addressing mode, these four addressing modes reference the byte at the offset found in the bx, bp, si, or di register, respectively. The [bx], [si], and [di] modes use the ds segment by default. The [bp] addressing mode uses the stack segment (ss) by default.
You can use the segment override prefix symbols if you wish to access data in different segments. The following instructions demonstrate the use of these overrides:
mov al, cs:[bx] mov al, ds:[bp] mov al, ss:[si] mov al, es:[di]
Intel refers to [bx] and [bp] as base addressing modes and bx and bp as base registers (in fact, bp stands for base pointer). Intel refers to the [si] and [di] addressing modes as indexed addressing modes (si stands for source index, di stands for destination index). However, these addressing modes are functionally equivalent. This text will call these forms register indirect modes to be consistent.
? 更多寻址模式请参考Art of Assembly: Chapter Four- The 80x86 Addressing Modes。
? 很显然,没有操作系统对内存进行管理,硬件段机制主要是用来扩展内存,每一条访存指令执行之前不会检查内存访问是否合理,也就不存在保护机制,段的共享也无法实现。
1.3 实模式下硬件怎么计算地址的呢?
1.3.1 汇编代码
1.在编写汇编代码时,将程序组织成不同的段。每一个段内的数据的地址都是相对于段的可重定位的相对偏移地址。从而一个程序的地址空间是一个二维的(x, y),一个维度表示段(0~x),另一个维度表示段内偏移(0~y)。? 在x86硬件段机制下,使用段寄存器进行的地址引用(eg.
mov al, [bx]
)可以用< segment, offset >的二元对来表示,其中segment表示段,offset表示段内偏移。在实模式时(real mode),segment就是段寄存器的值,所以< segment:offset >就是物理地址。? segment为16bit,offset是16bit,通过地址运算segment:offset得到一个20位的地址< segment:offset >,这个地址运算的计算按下图方式进行:
? 可以看到,段基址是16(4位)的倍数,一共可以取216个不同的值,段内偏移可以取216个不同的值。很明显,段之间有重叠。
? 因为存在重叠,所以对于同一个物理内存地址值,可由多个不同的< segment, offset >二元对得到。故< segment, offset >到物理内存地址(address)的映射是一个多对一的映射。
1.3.2 A C/C++ compiler for DOS in those days
1.进程
感谢操作系统的出现,尤其感谢进程这一非常重要的概念以及相关的一些技术的出现,程序员可以不用再基于RAM编程了,不用去绞尽脑汁考虑怎么把程序布局到物理内存才能使之正常运行。
进程的重要性再怎么强调也不为过,如果说操作系统是应用软件与计算机硬件的接口,那么进程就是操作系统提供给编程人员的物理硬件的虚拟视图。编程人员不再过分考虑硬件,转而考虑进程视图。每一个进程都给编程人员提供了一个单用户单任务的虚拟计算机视图,在进程视图里永远只有一个程序在跑着。对基于进程编程的用户而言,他们只需认为自己的程序是唯一一个运行着的任务,不用考虑自己的任务是否会被其他任务覆盖之类的问题,从而只需重点专注于如何编写一个单用户单任务的程序。进程的内存视图:
2.DOS and Turboc compiler
1.DOS是一个基于x86计算机的单用户单进程个人PC操作系统,运行在实模式下,没有进入保护模式。基于DOS操作系统的C编译器:
3.far pointer in C:扩大程序可访问地址范围
1.far类型的指针在早期DOS和OS/2个人操作系统流行的时候被用来访问扩展区(或者其他段空间)的内存地址。2.In a segmented architecture computer, a far pointer is a pointer which includes a segment selector, making it possible to point to addresses outside of the default segment. — Far pointer
3.We programmers’ view of a c program address space(which is arranged by a C compiler) like this:
Understanding C by learning assembly
4.On C compilers targeting the 8086 processor family, far pointers were declared using a non-standard far qualifier.
Size of far pointer is 4 byte.
First 16 bit stores: Segment selector
Next 16 bit stores: Offset address( or effective address)
? With the far pointer’s help, we can tell the C compiler to use two registers to calculate the address, one for segment selector(16 bit) and one for offset address(16 bit) to this segment.
For more information about far pointer see Load Far Pointer and the references below. There I find a forum which has the topic near, far and huge pointers.
Example of far pointer(targeting the 8086 processor):
//What is segment number and offset address? #include<stdio.h> int main(){ int x=100; int far *ptr; ptr=&x; printf("%Fp",ptr); return 0; }
Output: 8FD8:FFF4 (Assume)
Here 8FD8 is segment address and FFF4 is offset address in hexadecimal number format.
Note: %Fp is used for print offset and segment address of pointer in printf function in hexadecimal number format.
In the header file dos.h there are three macro functions to get the offset address and segment address from far pointer and vice versa.
- FP_OFF(): To get offset address from far address.
- FP_SEG(): To get segment address from far address.
- MK_FP(): To make far address from segment and offset address.
//What will be output of following c program? #include <dos.h> #include<stdio.h> int main(){ int i=25; int far*ptr=&i; unsigned int s,o; s=FP_SEG(ptr); o=FP_OFF(ptr); printf("%Fp",MK_FP(s,o)); return 0; }
Output: 8FD9:FFF4 (Assume)
Note: We cannot guess what will be offset address, segment address and far address of any far pointer. These address are decided by the compiler. also note that the output(address of i) of printf function is an address from the process’s view, it’s not the actual physic memory of the RAM(because we are programming in a view provided by the process(not RAM)).The actual address of the contents in a process is arranged by the memory management system of OS(different OS may have different memory management system that use different mapping strategy).
1.4 保护模式下的段机制
? x86后续系列的cpu引入了保护模式,为操作系统实现分段和分页的虚拟内存管理机制提供了硬件支持。Protected mode
? 高级语言如C、C++等编写的程序在生成的过程中会编译成汇编语言,为了更好的理解系统,我们不妨假设自己就是一个汇编程序员,然后接着往下思考操作系统的段机制内存管理策略。
1.4.1 x86系列cpu中无法避开的硬件段机制
? 参考x86的寻址模式,你会发现所有寻址模式都使用了段寄存器,要么寻址指令显式地指定一个段寄存器,要么硬件根据寻址指令的形式使用默认的段寄存器,总之,x86的硬件段机制渗入到了它的指令集中,无法避开。
When operating in protected mode, some form of segmentation must be used. There is no mode bit to disable segmentation. The use of paging, however, is optional. — 64-IA-32-architectures-software-developer-system-programming-manual-chapter-3
1.4.2 从逻辑上取消段机制
在(基于80x86的)Window和Linux平台上编过程序的人知道,我们写的程序的虚拟地址空间(或者说进程地址空间)完全没有用到分段,也可以看作所有的内容(数据、栈、代码…)都在一个段中。把程序虚拟内存的这种布局模式叫做平坦内存模式,要绕过x86的硬件段机制,只需将相关的段选择器指向的段的基地址设为0,段的长度设为4G。
Memory layout of a process in linux:
1.4.2 使用段机制管理进程内存
虽然主流的PC操作系统如Linux,Window等都没有用到段机制,但是,x86系列CPU为实现分段内存管理的操作系统提供了硬件支持,不过要求该类型操作运行在保护模式下。
2. 操作系统分段机制
multics-vm
—multics 操作系统的内存管理使用了分段分页机制。
—OS/2 was the only operating system which made full use of segmentation features
Segmentation and the Design of Multiprogrammed Computer Systems
2.1 分段机制
分段,就是将一个程序的进程视图按逻辑方式组织成不同的段(segment),每一个段是一个独立的对象,也可以看作独立的视图,所以,利用分段机制进行编程的程序员可以将编程任务划分为不同的段,然后分给不同的人去编写(或许在编译器等软件工具的帮助下)不同的段,也可以在程序运行时创建新的段来扩展内存,例如用C在早期的OS/2系统下编程时可以:SEL selArray ; PCH pchArray ; USHORT i ; DosAllocSeg (512 , &selArray , 0) ;//创建一个512字节的段,并将段选择子赋值给selArray。 pchArray = MAKEP (selArray , 0) ; //返回一个far指针,指向selArray段的段首。 for (i = 0 ; i < 512 ; i + + ) pchArray[i] = 0 ;//通过far指针跨段访问新建段的内存空间。
虽然实现分段内存管理是操作系统(在硬件的支持下)的事,但是要想利用分段内存管理提高编程的工作效率以及分段内存管理所具有的其他优点(段共享,扩展内存…)还得程序员对程序编程任务进行合理地分段。
? 如果不考虑具体的实现,从原理上说,进程的逻辑地址是一个< segment_number, offset >的二元组,其中,segment_number给出了段号,用来选择进程的某个段进而得到与这个段相关的全部信息,offset给出了段内偏移量,逻辑地址会被转换成线性地址,若是没有开启分页机制,这个线性地址就作为物理地址,被送上地址总线。
`
划分段之后,程序员可以在自己负责的段的段视图下进行编程,不用过分考虑进程视图,更不用考虑RAM物理内存视图。
问题来了:
既然进程的逻辑地址是一个< segment_number, offset>的二元组,那么在进程执行过程中进行访存时,怎么确定内存的物理地址呢?
站在汇编程序员的角度来看这个问题,在执行一条寻址的汇编指令时,操作系统必须能利用硬件的特性将这条寻址指令转化为确切的线性地址,进而输出到地址总线上。下面就这个问题给出一种解决方法。
2.2 虚拟地址分段
将整个进程地址空间看作连续的一个地址空间(虚拟地址空间),然后利用虚拟地址的前几位将虚拟地址空间划分为不同的段,这种实现方式将虚拟地址分为两部分,一部分是段号位,一部分是段内偏移位。
将一个程序的进程视图划分为不同段之后,负责一个段的编程人员得到自己负责的段的段号,在对段内进程编程之前先设置段号,之后的编程就可以不用考虑段号了。例如先用指令将相关的段选择器设置为自己对应的段号,后面的所有访存指令都会在硬件的支持下自动相对于这个段选择器选择的段进行偏移。或者把段号、共享段等设置交给编译工具去完成,程序员只需在自己的段视图下进行编程。至于操作系统会将段选择器标定的段映射到物理内存的什么位置则完全是操作系统的事,与段编程人员无关。
下面用一个例子说明用直接方式实现的段机制的内存转换过程:
这是一个程序的逻辑地址空间:
这个进程的大小为16KB,有3个段,分别是代码段、堆段、栈段。
这是进程在物理内存中的布局:
在段机制下,操作系统以对用户透明的方式,将上图的进程加载到了物理内存中。
这是操作系统维持的段描述表
一个进程有一个本地的段表,在进程被创建的时候创建,由操作系统维护,表项是一个段描述符,记录了与进程的某个段相关的全部信息。操作系统在将上面的程序映射到主存时为执行该程序的进程维护着它的本地描述符表,这里给出了表中每个表项的4个主要的信息,它们分别是段基址、段大小、段偏移方向和保护信息。可以用一个寄存器指向当前进程的本地段表,发生进程切换的时候,更正这个寄存器的值。
下面就基于上面的图示进行实例说明:
- 假设一个指令引用了进程的逻辑地址100,当这个引用指令被执行时,硬件就会自动解析逻辑地址值100(00 0000 0110 0100),得到segment_number = 00 = 0,offset = 000001100100 = 100,首先,硬件拿offset与Size比较,检查offset是否越界,100<2K,没有越界。所得得到最终的物理地址不是100,而是Base of setment0 + offset = 32K + 100 = 32868。
- 假设一个指令引用了进程的逻辑地址4200,当这个引用指令被执行时,硬件就会自动解析逻辑地址值4200(01 0000 0110 1000),得到segment_number = 01 = 1,offset = 000001101000 = 104,首先,硬件拿offset与Size比较,检查offset是否越界,104<2K,没有越界。所得得到最终的物理地址不是4200,而是Base of setment1 + offset = 34K + 104 = 34920。
- 假设一个指令引用了进程的逻辑地址7KB(01 1100 0000 0000)时,得到segment_number = 01 = 1,offset = 110000000000 = 3KB,首先,硬件拿offset与Size比较,检查offset是否越界,3KB > 2KB,硬件检测到对堆段的访问越界。
- 假设一个指令引用了进程的逻辑地址15KB,当这个引用指令被执行时,硬件就会自动解析逻辑地址值15KB(11 1100 0000 0000),得到segment_number = 11 = 3,发现Grows Positive字段为0,表示这个段由高地址向低地址方向作偏移,所以offset = 1100 0000 0000 - 4KB = - 1KB,首先,硬件拿offset与Size比较,检查offset是否越界,1K<2K,没有越界。所得得到最终的物理地址不是15KB,而是Base of setment2 + offset = 28K + -1K = 27K。
这样看来,段机制就和页机制十分相似,它们之间最大的区别就是:
- 一个段的空间和一个页的空间在物理内存中的映射都是连续分配的,它们分别是两种机制下,进程在物理内存中进行连续内存分配的单位,不同的是,段的大小通常要比页的大小大很多,而且段的大小不固定,可以小到1B也可以大到offset所能表示的最大值。
- 页的大小由系统决定,是固定的,段的大小由用户决定,不固定。
- 段是逻辑上相关的数据的集合。例如代码段、数据段、堆栈段。页则只是进程虚拟内存空间中位于同一个虚拟页内的数据的简单集合,所以对段的保护比对页的保护更具有意义。
- 段可以更好的实现共享。
分段机制主要功能:
- 运行一个程序(可能比主存大)而不用考虑主存的实际大小,但必须保证主存能够容纳下这个进程最大的一个段。
- 权限控制。将每个段设置权限位,对程序的访问进行控制。
- 共享段。
- 通过段换入换出的方式,程序不必全部放入内存。
references:
Segmented Memory management (operating systems)
morgan_david/cs40/segmentation
How are the segment registers (fs, gs, cs, ss, ds, es) used in Linux?
Art of Assembly: Chapter Four- The 80x86 Addressing Modes
Removing the Mystery from SEGMENT : OFFSET Addressing
far pointer in c with examples
COMPILER, ASSEMBLER, LINKER AND LOADER:A BRIEF STORY