您的位置:首页 > 运维架构 > Linux

Linux X86_64位虚拟地址空间布局与试验

2017-08-15 23:35 786 查看

Linux虚拟地址布局

x64 layout

在x86_64下面,其实虚拟地址只使用了48位。所以C程序里,打印的地址都是只有12位16进制。48位地址长度也就是对应了256TB的地址空间。

而在Linux下有效的地址区间是从
0x00000000 00000000 ~ 0x00007FFF FFFFFFFF
还有
0xFFFF8000 00000000 ~ 0xFFFFFFFF FFFFFFFF
两个地址区间。而每个地址区间都有128TB的地址空间可以使用,所以总共是256TB的可用空间。地址空间的划分就如下所示

ffffffff`ffffffff     _____________
|            |
|   内核空间  |
ffff8000`00000000     |____________|
|            |
|   未使用    |
|   的空间    |
|            |
00007fff`ffffffff     |____________|
|            |
|   用户空间  |
00000000`00000000     |____________|


下面分析用户空间的地址布局。一个程序的内存布局,可以通过

cat /proc/<pid>/maps
pmap <pid> -X




这是一个简单的helloworld程序的示例布局

前三行分别是
Text Segment
Data Segment
BSS Segment
Text Segment
其实就是存放二进制可执行代码的位置,所以它的权限是读与可执行,
Data Segment
存放的是静态常量,所以该地址段权限是只读,
BSS Segment
存放未初始化的静态变量,所以也就是可以随意读写。

接下来是
Heap
地址段,heap地址是往高地址增长的,是用来动态分配内存的区域。它跟栈相反,是往高地址增长的,对应的内存申请系统调用是brk()

接下来的区域是
Memory Mapping Segment
。这块地址也是用来分配内存区域的,一般是用来把文件映射进内存用的,但是你也可以直接在这里申请内存空间来使用,对应的内存申请系统调用是mmap()

再下面就是
Stack
地址段了。这个栈已经用了136KB了。栈的最大范围,我们可以通过
prlimit
命令看到,默认的情况下是8MB,和Linux-x86一样。

再下面就是
vvar
vdso
vsyscall
了。这三个东西都为了加速访问内核数据,比如读取时间
gettimeofday
,肯定不能频繁地进行系统调用陷入内核,所以就映射到用户空间了。所有程序都有这3个映射地址段。

关于vvar,vdso和vsyscall。先说vsyscall,这东西出现最早,比如读取时间
gettimeofday
,内核会把时间数据和
gettimeofday
的实现映射到这块区域,用户空间可以直接调用。但是vsyscall区域太小了,而且映射区域固定,有安全问题。后来又造出了vdso,之所以保留是为了兼容用户空间程序。vdso相当于加载一个linux-vd.so库文件一样(名字也由此而来),也就是把一些函数实现映射到这个区域,而vvar也就是存放数据的地方了,那么用户可以通过调用vdso里的函数,使用vvar里的数据,来获得自己想要的信息。而且地址是随机的,更安全。

具体的x64的内存布局如下图所示:



可以发现里面有不少Random xx offset,这是Linux里的ASLR策略。ASLR的话就是Address space layout randomization,是一种安全机制,主要防止缓冲区溢出攻击。

brk尝试

brk系统调用可以通过调整heap区域的brk指针,从而调整heap对的虚拟内存空间大小。实验代码如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
void *curr_brk, *tmp_brk = NULL;
printf("Welcome to sbrk example:%d\n", getpid());
tmp_brk = curr_brk = sbrk(0);   //获得当然brk的地址
printf("Program Break Location1:%p\n", curr_brk);
getchar();
brk(curr_brk+4096); //增加Heap段4KB空间
curr_brk = sbrk(0);
printf("Program break Location2:%p\n", curr_brk);
int* a = (int *)tmp_brk+32; //尝试使用刚刚扩展的内存空间
*a = 32;
printf("%d\n", *a); //没有问题
getchar();
brk(tmp_brk);   //恢复到原来的brk大小
curr_brk = sbrk(0);
printf("Program Break Location3:%p\n", curr_brk);
getchar();
printf("%d\n", *a); //产生SIGSEGV,Segmentation Fault
return 0;
}


初始状态



按回车,调用brk(curr_brk+4096)之后



看到heap大小从132KB变成了136KB,而且地址空间可以正常使用。再次回车,调用brk(tmp_brk)



发现heap堆大小变回去了。再回车发现会产生段错误,因为内存已经被回收,进程无法使用该内存地址。

mmap尝试

mmap系统调用是把一个文件映射到一段内存地址空间,如上面截图里的/lib/libc.so所做的一样。也可以匿名直接申请一段内存空间使用。实验代码如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <errno.h>

int main()
{
void * addr;
printf("Welcome to sbrk example:%d\n", getpid());
getchar();
//申请4096*2=8KB的空间,地址空间可读写,私有且匿名。
addr = mmap(NULL, 4096 * 2, PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
if(addr < 0) {
perror("mmap");
}
printf("Program Allocate :%p\n", addr);
getchar();
if(munmap(addr, 1024) != 0){
perror("munmap");
}
return 0;
}


运行结果:在mmap之前,有这么一条地址空间,大小是12KB



在mmap之后,发现大小变成了20KB,刚好是我们申请了8KB



注意mmap申请内存的时候,如果申请地址长度小于一个PAGE_SIZE=2KB=4096字节=0x1000字节,那么会直接申请2KB。而PAGE_SIZE的话可以通过
getconf PAGE_SIZE
命令来查看。

其实mmap不一定要在
Memory Mapping Segment
进行内存申请,你可以指定任意的内存地址,当然只要不跟已有的冲突就好。这个地址也一定要是000结尾,才使得能页对齐。比如你可以申请0x100000的地址段,他的地址比
Text Segment
的地址更低。这没有任何问题。而mmap可申请的最低的地址由一个mmap_min_addr内核参数来控制,可以通过下面的命令读取。

sysctl vm.mmap_min_addr


在debian下,它的默认值是4096。也就是你申请的首地址必须比0x1000大。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  linux 操作系统