首先明确三个概念(以80x86为例):
逻辑地址:包含在机器语言指令中用来指定一个操作数或一条指令的地址。我们程序中用到的地址,都是逻辑地址。
线性地址:是一个32位无符号整数,可以用来表示高达4G的地址。
物理地址:用于内存芯片级内存单元寻址,即在物理内存上的地址。
一个进程拥有4G的虚拟空间,其中0-3G属于用户空间,这3G的空间划分为多个段,如下图所示:
从下往上,5个部分依次是:
代码区:存放可执行的指令,只能读,不能写
数据区:存放初始化的全局变量和静态变量
BSS区:存放未初始化的全局变量和静态变量
heap:存放malloc/new申请的变量
stack:存放临时变量,函数参数等
下面是一段测试代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 0;
char *b = "hello";
char *p = (char*)malloc(4);
printf("[stack]a=0x%08x\n[data] b=0x%08x\n[heap] p=0x%08x\n",
&a, b, p);
return 0;
}
变量a栈上,变量b在数据区,变量p在堆上,执行结果:
[stack]a=0xa288094c
[data] b=0x004005d8
[heap] p=0x0071b010
一、段选择符
linux是通过分段机制,将逻辑地址转化为线性地址。上面的变量b就属于数据段。通过数据段寄存器ds,可以找到此进程数据段所在的段描述符,再通过段描述符找到相应的线性地址。
寄存器ds中保存了16位的段选择符,段选择符格式如下:
TI:指明段描述符是在全局描述符表(GDT, TI=0)中或局部描述符表(LDT, TI=1)中
索引号 index:指定该段在GDT或LDT中的位置。
二、段描述符
每个段都有不同的属性,如首字节的线性地址,段的访问级别(DPL)等,段描述符就保存了段的这些属性。段描述符长度为8字节,格式如下:
BASE:包含段的首字节的线性地址
DPL:用于限制对这个段的存取。为0时,只有内核态可以访问;为1时,都可以访问
三、逻辑地址到线性地址
做以下假设:
- 寄存器ds的值为0x13
- 全局描述符表中的内容为0x00020000
获取变量b的线性地址过程如下:
- 段选择符为0x13,则TI为0,该段信息保存在GDT中;index值为2(0x13的高13位)
- GDT中的内容为0x00020000,每个段描述符占8字节,则变量b所在数据段描述符地址为0x00020000+(2 * 8),即0x00020010。
段描述符中BASE字段(16-39位和56-63位),保存的就是变量b所在数据段的线性地址基址,基址+变量b在段内的偏移量可得到变量b的线性地址(0x004005d8)。