内存可以随意访问?

软件和网站开发以及相关技术探讨
blue-fish
帖子: 8
注册时间: 2009-09-27 11:26

内存可以随意访问?

#1

帖子 blue-fish » 2014-04-30 11:34

无意中写了如下代码:

char *h = (char*)malloc( 1 );
*h = 128;

int i = 0;
for( i=0;i<135200;i++)
printf("\n h:%d I- %d\n", *(h+i), i);

本以为很快会出现内存访问错误的问题,没想到运行到 i=135151 都没有问题.
后又改写为如下形式:

char h = 128;
int i = 0;
for( i=0;i<135200;i++)
printf("\n h:%d I- %d\n", *(&h+i), i);

也运行到 i=7520 才出现 “segmentation fault” 错误......

运行环境是 虚拟机上的 ubuntu-14.04 amd64 server 版本。
头像
huangbster
帖子: 187
注册时间: 2012-10-29 11:35
系统: UBUNTU

Re: 内存可以随意访问?

#2

帖子 huangbster » 2014-04-30 13:43

一个是读取堆上的内存,一个是读取栈上的内存。读取系统为进程分配的内存是合法的。
头像
dryland718
帖子: 287
注册时间: 2011-08-17 12:54

Re: 内存可以随意访问?

#3

帖子 dryland718 » 2014-04-30 17:51

这能说明什么呢??
头像
qgymib
帖子: 539
注册时间: 2010-04-02 16:44
系统: openSUSE 13.2 x64

Re: 内存可以随意访问?

#4

帖子 qgymib » 2014-04-30 20:07

内存访问越界并不等于内存可以随意访问。
系统自动维护内存管理链表,申请的内存并不一定相邻与另一个程序的内存。系统仅保护被程序注册的内存(已在内存管理链表中登记),对空闲内存不作保护。
所以对于一块不属于任何程序的内存是可以访问的,但是不保证这块内存区域数据正确性。这就是lz第一个例子的运行情况。
在第二个例子中,程序内存先访问越界,随后尝试访问其他程序的内存区域。因此一开始没有错误,后来才有。

要指出的是,上面说的“空闲内存”并不是真正的空闲内存,而指的是操作系统根据内存策略,为每个程序段分配的“页空间”减去“程序实际占用空间”的剩余部分。
上次由 qgymib 在 2014-04-30 20:19,总共编辑 1 次。
正在建设中的个人博客
头像
qgymib
帖子: 539
注册时间: 2010-04-02 16:44
系统: openSUSE 13.2 x64

Re: 内存可以随意访问?

#5

帖子 qgymib » 2014-04-30 20:09

还有就是内存管理策略与操作系统有关。同一个程序在windows平台和linux平台可能有不同表现。
正在建设中的个人博客
头像
royclark
帖子: 301
注册时间: 2011-05-15 1:01
系统: Debian GNU/Linux sid

Re: 内存可以随意访问?

#7

帖子 royclark » 2014-05-01 2:01

qgymib 写了:内存访问越界并不等于内存可以随意访问。
系统自动维护内存管理链表,申请的内存并不一定相邻与另一个程序的内存。系统仅保护被程序注册的内存(已在内存管理链表中登记),对空闲内存不作保护。
所以对于一块不属于任何程序的内存是可以访问的,但是不保证这块内存区域数据正确性。这就是lz第一个例子的运行情况。
在第二个例子中,程序内存先访问越界,随后尝试访问其他程序的内存区域。因此一开始没有错误,后来才有。

要指出的是,上面说的“空闲内存”并不是真正的空闲内存,而指的是操作系统根据内存策略,为每个程序段分配的“页空间”减去“程序实际占用空间”的剩余部分。
我觉得这个说法不太对。现代的操作系统都用了虚拟内存机制(此处虚拟内存指的不是 swap),每一个进程都有自己的内存空间,一般情况下一个进程无法看到其他进程的内存空间,自己的内存空间也无法被其他进程所看到。每个进程都相当于自己独占整个物理内存。进程实际运行时,由操作系统完成从虚拟内存到物理内存的对应,(用户空间)进程无法访问到物理内存。

参考这里这里,尽管每个进程都有自己独立的虚拟内存空间,但不是所有内存都被允许访问。有以下三种情况不允许,当这些情况发生时就会发生段错误:
1. 访问了操作系统的部分。以 32 位 Linux 为例,一个进程有 4G 的虚拟内存空间,3~4 G 是操作系统的,这部分不允许进程访问,剩下 0~3 G 才是给进程的。
2. 尽管 0~3 G 的部分是给进程用的,但并不是一开始就全部分配好的,未分配就访问是不允许的。不过这一点不一定完全严格。
3. 对只读的内存进行写操作。
头像
royclark
帖子: 301
注册时间: 2011-05-15 1:01
系统: Debian GNU/Linux sid

Re: 内存可以随意访问?

#8

帖子 royclark » 2014-05-01 2:03

开始试验。试验 1,下面代码相当于楼主第 2 个例子。

代码: 全选

#include <stdio.h>
#include <stdlib.h>

int main() {
	char h = 128;
	int i = 0;

	for( i = 0; i < 135200; i++ ) {
		fprintf( stderr, "h[%d]: %d\n", i, *( &h + i ) );
	}

	return 0;
}
用 gdb 运行。

代码: 全选

$ gcc -g -Wall -o test test.c 
$ gdb ./test 
GNU gdb (GDB) 7.4.1-debian
...# 省略。
(gdb) r 
Starting program: /tmp/test 
h[0]: -128
...# 省略。
h[3156]: 0

Program received signal SIGSEGV, Segmentation fault.
0x08048466 in main () at test.c:9
9			fprintf( stderr, "h[%d]: %d\n", i, *( &h + i ) );
(gdb) p &h 
$1 = 0xbffff3ab "\200U\f"
(gdb) p &h + i 
$2 = 0xc0000000 <Address 0xc0000000 out of bounds>
(gdb) 
重复试验的结果相同,h 的地址总是 0xbffff3ab,当 i = 3157 时发生段错误。因为此时 &h + i = 0xc0000000,已经对应于操作系统的部分,所以会段错误,属于前述 1.。
不用 gdb 直接运行时,&h、发生段错误时的 i 和 &h + i 随不同试验而不同,原因我还不清楚。但估计段错误的原因也是 1.。

试验 1.1,同样是 test.c,不过把 &h + i 改成了 &h - i。
这时无段错误发生。改 stack 大小,减少到 64K。

代码: 全选

$ ulimit -a 
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 12050
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 12050
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
$ ulimit -c unlimited 
$ ulimit -S -s 64 
这时有段错误发生。

代码: 全选

h[62443]: 0

Program received signal SIGSEGV, Segmentation fault.
0x0804846a in main () at test.c:9
9                       fprintf( stderr, "h[%d]: %d\n", i, *( &h - i ) );
(gdb) p &h 
$1 = 0xbffff3eb "\200\354", <incomplete sequence \363>
(gdb) p &h - i 
$2 = 0xbffeffff <Address 0xbffeffff out of bounds>
(gdb) p &h - i + 1 
$3 = 0xbfff0000 ""
从 0xbfff0000 到 0xc0000000 恰好是 64K。按这里,开始只给 h 分配了一个 char 的内存,当访问 h 附近的未分配内存时,内核会自动扩展 stack 而不发生段错误,当 stack 达到最大可用大小后才会生段错误。这属于前述 2. 的情况。多次重复结果相同。
不用 gdb 直接运行时,&h、发生段错误时的 i 和 &h - i 同样随不同试验而不同,原因我也不清楚。
头像
royclark
帖子: 301
注册时间: 2011-05-15 1:01
系统: Debian GNU/Linux sid

Re: 内存可以随意访问?

#9

帖子 royclark » 2014-05-01 2:04

试验 2,下面代码相当于楼主第 1 个例子。

代码: 全选

#include <stdio.h>
#include <stdlib.h>

int main() {
	char *h;
	int i;

	h = ( char * ) malloc( 1 );
	*h = 128;

	for( i = 0; i < 135200; i++ ) {
		printf( "h[%d]: %d\n", i, *( h + i ) );
	}

	return 0;
}
无论是直椄运行,还是用 gdb 运行,总是在 i = 135160 时发生段错误。
具体原因不太清楚。man ld 里 --heap reserve,commit 里有“The default is 1Mb reserved, 4K committed.”。1Mb + 4KB 是 135168,接近 135160。据猜测,可能是 heap 也有与 stack 类似的特性。可能是尽管 h 只分配 1 字节的内存,当访问 h 附近的未分配内存时,内核会自动将其扩展为已分配内存,而不发生段错误。当达到最大保留大小后才会生段错误。

试验 2.1,同样是 test2.c,不过把 h + i 改成了 h - i。

代码: 全选

h[8200]: 127

Program received signal SIGSEGV, Segmentation fault.
0x08048484 in main () at test2.c:12
12                      printf( "h[%d]: %d\n", i, *( h - i ) );
(gdb) p h 
$1 = 0x804a008 "\200"
(gdb) p h - i 
$2 = 0x8047fff <Address 0x8047fff out of bounds>
(gdb) p h - i + 1 
$3 = 0x8048000 "\177ELF\001\001\001"
在 gdb 中运行时,总是在 i = 8201 时产生段错误。h - i + 1 = 0x8048000 对应内存中代码段开始地址,从 h - i = 0x8047fff 到 0x00000000 属于未分配的区域(参考这里),访问则会发生段错误,这属于前述 2. 情况。
不用 gdb,直接运行时总是在 i = 9 时产生段错误,原因我不清楚。

代码: 全选

$ ./test2 
h[0]: -128
h[1]: 0
h[2]: 0
h[3]: 0
h[4]: 17
h[5]: 0
h[6]: 0
h[7]: 0
h[8]: 0
Segmentation fault (core dumped)
上次由 royclark 在 2014-05-01 2:10,总共编辑 1 次。
头像
royclark
帖子: 301
注册时间: 2011-05-15 1:01
系统: Debian GNU/Linux sid

Re: 内存可以随意访问?

#10

帖子 royclark » 2014-05-01 2:05

初步结论,有限的部分未分配内存可以访问,绝大部分未分配不允许访问,如果访问会发生段错误。还有一些细节不太清楚,快来人继续分析呀。我睡觉去了。 :em20

系统 Debian GNU/Linux wheezy i386

代码: 全选

$ uname -a 
Linux kralcyor-Debian-ThinkPad-T42 3.2.0-4-486 #1 Debian 3.2.54-2 i686 GNU/Linux
$ gcc --version 
gcc (Debian 4.7.2-5) 4.7.2
$ ld --version 
GNU ld (GNU Binutils for Debian) 2.22
$ gdb --version 
GNU gdb (GDB) 7.4.1-debian
$ bash --version 
GNU bash, version 4.2.37(1)-release (i486-pc-linux-gnu)
头像
qgymib
帖子: 539
注册时间: 2010-04-02 16:44
系统: openSUSE 13.2 x64

Re: 内存可以随意访问?

#11

帖子 qgymib » 2014-05-01 10:10

royclark, 你给出的结论与我的并不冲突,因为我的结论比较笼统,属于教科书的说教式结论,可能比较难以理解,以下是具体解释:

1. 程序在运行前,操作系统给予程序的运行内存地址为0至操作系统机器字长的内存地址,在32位机器中为0-4GB,64位中,呃,自己算吧。程序需要访问指定地址时,操作系统利用你所说的“虚拟内存机制”,通过 “内存管理单元”(MMU) 实现 “逻辑地址空间” 到 “物理地址空间” 的转换。

2. 程序在运行时,其所占用的内存空间是呈现阶梯形增长的。其原因在于操作系统使用了“分页”的策略。程序所有使用的数据(包括堆栈)均位于分页中。分页内存管理方案允许进程的物理地址空间可以是非连续的,同时实现在用户视角的内存和物理内存的分离。分页技术的具体细节不做讨论,有兴趣的可以自己百度,但是这里需要清楚两个概念:外部碎片内部碎片。外部碎片指的是没有被操作系统划分为“帧”的内存块;内部碎片指的是在“分页”中没有被程序利用的部分。

3. 同一个程序访问属于自己的分页是合法的!而对于一个诸如

代码: 全选

char *h = (char*)malloc( 1 );
的语句,其所申请的内存极有可能不能占满整个分页(所谓的内部碎片),所以可以通过内存访问越界继续访问其余内存。注意,此处的内存分页属于这个程序,但是从逻辑上来讲,由于程序并没有申请这么多内存,所以这部分程序理应不能访问。这个机制使得lz开始疑惑为何内存可以随意访问?

4. 关于segmentation fault。相信lz已经从上面看出来了,程序只能访问属于自己的分页。对于一个诸如

代码: 全选

    for( i = 0; i < 135200; i++ ) {
      fprintf( stderr, "h[%d]: %d\n", i, *( &h + i ) );
   }
这样尝试连续访问内存区域的语句,经由“页表”进行转换,一旦访问至程序最后一个“分页”而尝试继续访问下去时,由于其地址的下一位所对应的物理内存帧不属于这个程序,因此会抛出内存访问错误信号。(这一点真的需要具体讨论。若取的是实际的物理地址,那么当指针按照程序逻辑,顺序访问物理地址,当尝试访问不属于自己的物理内存帧的时候就会引发内存访问错误信号;若取的是用户视角的连续虚拟内存的地址,那么只有指针尝试访问最后一个分页之后的内存区域时才报错。)

以上所有内容均可以从《操作系统概念》中内存管理章节找到。以上,over。
上次由 qgymib 在 2014-05-01 11:06,总共编辑 8 次。
正在建设中的个人博客
头像
qgymib
帖子: 539
注册时间: 2010-04-02 16:44
系统: openSUSE 13.2 x64

Re: 内存可以随意访问?

#12

帖子 qgymib » 2014-05-01 10:18

程序在运行时,其所占用的内存空间是呈现阶梯形增长的。
关于这个观点还有一点补充说明,因为这个容易造成误会,还是解释清楚比较好

当一个进程需要执行时,操作系统会检查该进程的大小(按页计算),进程的每一页都需要一帧。而此处的进程大小应该并不包括动态申请的内存区域(可能与编译器以及操作系统的实现有关)。阶梯形增长的说法在于,程序申请了动态内存。若程序没有申请动态内存,那么程序所占用的空间大小基本是不变的。
正在建设中的个人博客
头像
qgymib
帖子: 539
注册时间: 2010-04-02 16:44
系统: openSUSE 13.2 x64

Re: 内存可以随意访问?

#13

帖子 qgymib » 2014-05-01 11:14

对于 royclark所说程序访问了kernel部分我并不认同。你也看出来了,在gdb中程序地址极有可能不是真实的物理地址,在这种情况下就说程序访问了kernel空间是不是有点过了?
正在建设中的个人博客
头像
royclark
帖子: 301
注册时间: 2011-05-15 1:01
系统: Debian GNU/Linux sid

Re: 内存可以随意访问?

#14

帖子 royclark » 2014-05-01 17:20

qgymib 写了:对于 royclark所说程序访问了kernel部分我并不认同。你也看出来了,在gdb中程序地址极有可能不是真实的物理地址,在这种情况下就说程序访问了kernel空间是不是有点过了?
gdb 打印的地址,C 里指针的地址应该都是虚拟内存空间的地址,不是实际的物理地址。
我理解是进程运行时栈的地址范围是由操作系统决定的。在 gdb 中运行时栈的地址总是从某处到 0xbfffffff,所以试验 1 段错误发生时确实是试图访问内核在进程虚拟内存空间的部分。而不用 gdb,直接运行时,栈的地址并不到 0xbfffffff,而是比 0xbfffffff 小的某一位置,且每次运行都不太一样,参见下面的试验。这时候的段错误则不是因为访问了内核的部分。

所以我觉得我在 8 楼所说“但估计段错误的原因也是 1.。”是不对的,但前一句“因为此时 &h + i = 0xc0000000,已经对应于操作系统的部分,所以会段错误,属于前述 1.。”仍是对的。

仍然用试验 1 里的程序,设置 core file size 无限制,让程序吐核。吐出的核是进程的虚拟内存,其中的地址都不是实际的物理地址。

代码: 全选

$ ulimit -c unlimited 
$ ./test                              
h[0]: -128
...
h[6532]: 0
Segmentation fault (core dumped)
$ objdump -h core 

core:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 note0         0000022c  00000000  00000000  00000214  2**0
                  CONTENTS, READONLY
  1 .reg/27487    00000044  00000000  00000000  00000270  2**2
                  CONTENTS
...
 17 load12        00001000  b771c000  00000000  0000f000  2**12
                  CONTENTS, ALLOC, LOAD, READONLY
 18 load13        00001000  b771d000  00000000  00010000  2**12
                  CONTENTS, ALLOC, LOAD
 19 load14        00022000  bf935000  00000000  00011000  2**12
                  CONTENTS, ALLOC, LOAD
其中 load14 对应的是栈,地址从 0xbf935000 到 0xbf56fff,大小为 136K。用 gdb 检查吐出的核,段错误发生在试图访问 &h + i = 0xbf957000 时,在 0xbf56fff 和 0xc0000000 之间。

代码: 全选

$ gdb ./test core 
Core was generated by `./test'.
Program terminated with signal 11, Segmentation fault.
#0  0x08048466 in main () at test.c:9

warning: Source file is more recent than executable.
9             fprintf( stderr, "h[%d]: %d\n", i, *( &h + i ) );
(gdb) p &h 
$1 = 0xbf95567b "\200\205\031"
(gdb) p &h + i 
$2 = 0xbf957000 <Address 0xbf957000 out of bounds>
(gdb) p &h + i - 1 
$3 = 0xbf956fff ""
头像
royclark
帖子: 301
注册时间: 2011-05-15 1:01
系统: Debian GNU/Linux sid

Re: 内存可以随意访问?

#15

帖子 royclark » 2014-05-01 17:22

另外我还是怀疑一个进程可以试图访问分配给另一个进程的物理内存。

之所以发贴的动因就是,我所了解的是不同进程的内存是互不可见的,而 4 楼的表述让我觉得很容易就看到其他进程的内存(不管看到是否意味着可以访问)。
我的理解是虚拟内存和物理内存都是分页的,虚拟地址到物理地址的转换也就是虚拟页到物理页的映射。内核将地址的转换交给硬件,也就是 CPU 处理。当 CPU 进行地址转换,没有找到某一虚拟页的映射时,会产生缺页异常。内核捕捉到缺页异常,会检查虚拟地址是否合法。对于不合法的比如地址大于 0xc0000000、对只读内存进行写操作、访问未分配的虚拟页等,会产生段错误;对于合法的地址,则分配物理页,并建立映射。
所以对于所有虚拟页,要么没有映射到物理页,要么映射到了分配给该进程的物理页。另一个角度看,所有从虚拟页到物理页的映射都是内核分配的,正常情况下内核没有道理会把一个进程的某一虚拟页,映射到已经分配给了其他进程的物理页。除了内核部分、动态库和共享内存等外,一般情况下一个进程无论是虚拟内存还是物理内存都不会与其他进程重叠。
关于某些内存没有申请也能访问,也就像 qgymib 所说的,申请时内存是按页分配的,由内核建立从虚拟页到物理页的映射。当访问没有申请的内存时,CPU 进行地址转换,由于已经有了映射,不会缺页异常,内核也就不会检查这些内存是否已经分配或者虚拟地址合不合法了。
回复