1.进程地址空间
1.1程序的结构与进程的结构
[root@localhost demo]# size test
text data bss dec hex filename
1193 492 16 1701 6a5 test
一个可执行程序包含三个部分:
代码段:主要存放指令,操作以及只读的常量数据例如字符串常量。
数据段:全局或者静态的已经初始化的变量
BSS段:全局或者静态的未初始化的变量
执行一个程序要运行,需要将程序加载到内存(磁盘有专门的读写驱动,但是速度比较慢,内存可有cpu直接访问读写,比磁盘速度要快)
名称 | 速度 |
CPU寄存器 | 1ns |
cache | 1ns~10ns |
DRAM | 100ns |
磁盘 | 10ss |
其资源分为内核资源和用户资源。内核资源是PCB---进程控制块,也就是一个结构体,负责管理进程所有资源。
mm_struct指向该进程相关的内存资源:从低到高地址分为:代码段->数据段->BSS段->堆段->栈段
在执行程序时,系统首先在内核空间创建一个进程,为这个进程申请一个进程控制块PCB(task_struct),用于管理整个进城的所有资源。
代码段,数据段,BSS段,直接从磁盘拷贝到当前内存空间,大小相等。
动态的空间:堆和栈空间,以及mmap段(主要是映射其他库的相关信息)
命令:pmap 进程号或者cat /proc/进程号/maps
[root@localhost proc]# cat 31281/maps
00400000-004d4000 r-xp 00000000 08:02 390951 /bin/bash
006d3000-006dd000 rw-p 000d3000 08:02 390951 /bin/bash
006dd000-006e2000 rw-p 00000000 00:00 0
008dc000-008e5000 rw-p 000dc000 08:02 390951 /bin/bash
00b45000-00ba8000 rw-p 00000000 00:00 0 [heap]
32c0c00000-32c0c20000 r-xp 00000000 08:02 781828 /lib64/ld-2.12.so
32c0e1f000-32c0e20000 r--p 0001f000 08:02 781828 /lib64/ld-2.12.so
32c0e20000-32c0e21000 rw-p 00020000 08:02 781828 /lib64/ld-2.12.so
32c0e21000-32c0e22000 rw-p 00000000 00:00 0
32c1000000-32c1002000 r-xp 00000000 08:02 782740 /lib64/libdl-2.12.so
32c1002000-32c1202000 ---p 00002000 08:02 782740 /lib64/libdl-2.12.so
32c1202000-32c1203000 r--p 00002000 08:02 782740 /lib64/libdl-2.12.so
32c1203000-32c1204000 rw-p 00003000 08:02 782740 /lib64/libdl-2.12.so
32c1400000-32c158b000 r-xp 00000000 08:02 782725 /lib64/libc-2.12.so
32c158b000-32c178a000 ---p 0018b000 08:02 782725 /lib64/libc-2.12.so
32c178a000-32c178e000 r--p 0018a000 08:02 782725 /lib64/libc-2.12.so
32c178e000-32c178f000 rw-p 0018e000 08:02 782725 /lib64/libc-2.12.so
32c178f000-32c1794000 rw-p 00000000 00:00 0
3e6a000000-3e6a01d000 r-xp 00000000 08:02 781958 /lib64/libtinfo.so.5.7
3e6a01d000-3e6a21c000 ---p 0001d000 08:02 781958 /lib64/libtinfo.so.5.7
3e6a21c000-3e6a220000 rw-p 0001c000 08:02 781958 /lib64/libtinfo.so.5.7
3e6a220000-3e6a221000 rw-p 00000000 00:00 0
7f2a4f326000-7f2a551b7000 r--p 00000000 08:02 914111 /usr/lib/locale/locale-archive
7f2a551b7000-7f2a551c3000 r-xp 00000000 08:02 781857 /lib64/libnss_files-2.12.so
7f2a551c3000-7f2a553c3000 ---p 0000c000 08:02 781857 /lib64/libnss_files-2.12.so
7f2a553c3000-7f2a553c4000 r--p 0000c000 08:02 781857 /lib64/libnss_files-2.12.so
7f2a553c4000-7f2a553c5000 rw-p 0000d000 08:02 781857 /lib64/libnss_files-2.12.so
7f2a553c5000-7f2a553c8000 rw-p 00000000 00:00 0
7f2a553cc000-7f2a553ce000 rw-p 00000000 00:00 0
7f2a553ce000-7f2a553d5000 r--s 00000000 08:02 914369 /usr/lib64/gconv/gconv-modules.cache
7f2a553d5000-7f2a553d6000 rw-p 00000000 00:00 0
7fffc0a71000-7fffc0a86000 rw-p 00000000 00:00 0 [stack]
7fffc0b0a000-7fffc0b0b000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
所看到的地址不是物理地址,都是虚拟地址。出于对资源的保护,一个程序并不需要立即将所有的资源全部加载到内存,而是实际上可采用写时申请的方法。而且保护系统,很多用户非法访问不会造成内核的崩溃。
1.2虚拟地址空间的申请
32位平台下,一个进程拥有4G的虚拟地址,这个地址如何来分配和使用呢
- 代码段,数据段,BSS段,这三个部分直接从磁盘拷贝到内存,起始地址在当前的32位平台下的地址为0x08048000.
- 堆栈。是动态变化
- mmap映射的文件
- 栈段
- 高地址1G空间,仅供内核映射处理,用户空间不能直接处理
内核空间 1G gap间隙 为了安全 栈 高地址向低地址增长 map 映射区域 堆 gap间隙 为了安全 BSS 数据段 代码段
- 堆和栈的起始地址是随机产生的,其目的是为了避免安全漏洞。但是可以指定在堆中申请空间的起始地址(brk/sbrk函数);
#include<stdio.h>
#include<stdlib.h>
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char*argv[])
{
brk(0x09050000);
printf("brk=%p\n",sbrk(0));
char *ptr=malloc(1024);
printf("new=%p\n",ptr);
return 1;
}
结果: - [root@localhost demo]# ./t
brk=0x9050000
new=0x9050010
- 系统执行一个过程,到底怎么来加载这些空间的?可以用strace工具来查看。
- strace ./t
execve("./t", ["./t"], [/* 25 vars */]) = 0
brk(0) = 0x7e0000//起始地址
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb578bba000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=52315, ...}) = 0
mmap(NULL, 52315, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fb578bad000
close(3) = 0
open("/lib64/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000\356A\3012\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1926800, ...}) = 0
mmap(0x32c1400000, 3750152, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x32c1400000
mprotect(0x32c158b000, 2093056, PROT_NONE) = 0
mmap(0x32c178a000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18a000) = 0x32c178a000
mmap(0x32c178f000, 18696, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x32c178f000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb578bac000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb578bab000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb578baa000
arch_prctl(ARCH_SET_FS, 0x7fb578bab700) = 0
mprotect(0x32c178a000, 16384, PROT_READ) = 0
mprotect(0x32c0e1f000, 4096, PROT_READ) = 0
munmap(0x7fb578bad000, 52315) = 0
brk(0x9050000) = 0x9050000
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb578bb9000
write(1, "brk=0x9050000\n", 14brk=0x9050000
) = 14
brk(0x9071000) = 0x9071000
write(1, "new=0x9050010\n", 14new=0x9050010
) = 14
exit_group(1) = ?
- 堆空间的起始地址是随机的,可设置为不随机。大小也可以设置成固定大小
代码段,数据段,BSS段的地址在编译链接时固定
在BSS结束和堆起始地址有空隙,这个空隙是随机的
brk函数仅仅是调整在堆中申请空间的起始值。使用以下命令可以指定堆的起始地址固定
sudo sysctl -w kernel/randomize_va_space=0
- 系统默认为每个进程分配的堆空间大小是固定的。使用 sbrk(0)得到的是我们堆空间的结束
值。第一次使用 malloc 申请资源返回的地址接近于堆空间的起始值。全用 brk(addr)改变的是新申请数据
的堆空间起始值。 - 在真正编程中,很少全用 brk/sbrk,使用 malloc函数来新申请堆空间,效率更高。部分时间使
用 mmap来映射 mmap区,
栈:栈从高地址向低地址增长,栈的起始值也是随机的。栈中主要存放的是局部变量,新调用子函
数时函数的参数及返回值。由 OS自动管理。
1.3内存空间的申请和释放
- 代码段中:由只读数据和代码组成: const,字符串常量,这些内容的空间在编译链接时申请好
且指定存储地址。 - 数据段 BSS 段申请:定义的全局的或者是静态的变量。已经初始化的在数据段,未初始化的
在 BSS段中。因此也是在编译链接时已经申请且指定的地址。
以上空间的申请在运行时候就加载到内存中,直到程序的结束,不在发生变化,除了exec函数执行 - 堆:由程序员自己动态管理。
动态申请,堆的起始位置由brk函数指定,但是具体编程中一般不会自己使用它,而是使用malloc函数。内核对内存的管理是页式管理,因此在 malloc申请空间时,使用链式结构来管理已经
申请的堆空间。
- 栈:由os管理
- mmap的库以及相关文件
将一个文件的内容全部或者部分的映射到虚拟地址空间中,后面释放时操作这段内存空间可以被同
步到磁盘文件的操作,效率比较高。共享映射,其它进程如果也映射这个文件,可以立即看到这个更改,但并不是立即更新磁盘文件的内容,如果要更新到磁盘,必须使用 msync以及 munmap时。
且指定存储地址。
在 BSS段中。因此也是在编译链接时已经申请且指定的地址。
以上空间的申请在运行时候就加载到内存中,直到程序的结束,不在发生变化,除了exec函数执行
动态申请,堆的起始位置由brk函数指定,但是具体编程中一般不会自己使用它,而是使用malloc函数。内核对内存的管理是页式管理,因此在 malloc申请空间时,使用链式结构来管理已经
申请的堆空间。
将一个文件的内容全部或者部分的映射到虚拟地址空间中,后面释放时操作这段内存空间可以被同
步到磁盘文件的操作,效率比较高。共享映射,其它进程如果也映射这个文件,可以立即看到这个更改,但并不是立即更新磁盘文件的内容,如果要更新到磁盘,必须使用 msync以及 munmap时。
2.常见内存错误以及valgrind使用
代码段:只读数据。因此对这一部分的数据,试图写只读数据。这个在编译的时候基本可以检测。
数据段/BSS段:未初始化直接访问。即使没有显式初始化,仍然会初始化为 0。
栈空间数据:
(1)局部变量,未初始化这类变量会给随机的初值,出现异常情况更诡异。
(2) 栈溢出,在栈中申请过大的局部变量。
堆空间数据:
内存泄漏,( 1)申请未释放( 2)申请后,双重释放。
对于所有的地址空间:
( 1)野指针的问题。未初始化指针。去访问这个指针指向的空间。
( 2)越界访问,例如一个数组 a[10],试图访问 a[10]以及以后。
( 3)非法的越权访问。例如 mmap的空间只读,但试图写。
( 4)空间不在控制范围仍然去访问空间,例如返回局部变量地址,且后面访问这个空间。
使用工具,来检测常见的内存错误。 valgrind工具。
1041 gcc -o valgrind_example01 valgrind_example01.c -g
1042 valgrind --tool=memcheck --show-reachable=yes --read-var-info=yes --verbose --time-stamp=yes --
leak-check=full --log-file=mycode.log ./valgrind_example01
1043 less mycode.log
如果要使用图形化的工具,要安装 QT,这个工具名字叫 valkyrie。