一个奇怪的死循环:帮助你理解地址空间和栈
2009-11-28 12:56
197 查看
有这样一个控制台程序,你知道他运行的结果吗?
int main()
{
int i, array[10];
for( i = 0; i <= 10; i++ )
{
array[i]=0;
}
}
试着运行一下吧,你会发现,死循环了,为什么呢?
跟踪一下,你会发现,array[10] = 0; 这个操作改变了i的值,使得i值重新变成了0,这样就导致了死循环。也就是说array[10]就是i。为什么呢,是巧合吗?
首先,来学习点操作系统的知识,来了解一下地址空间的概念吧。对于32位 Windows下的每一个进程而言,都有一个大小为4G的地址空间,这个地址空间不是物理上的内存的地址,而是逻辑上的地址,也就是说,地址空间内的数据的实际的物理存储地址是由操作系统实现的,你并不知道。
这4G的地址空间并不是都能被进程使用的,因为这4G的地址空间被分成了不同功能的几个区域。来看一下Windows 2000的地址空间长什么样子吧:
4G的地址空间对应的地址范围 0x00000000 ~ 0xFFFFFFFF。
· 0x00000000 ~ 0x0000FFFF 大小 64K
这是一个NULL指针分区,对这个分区的任何操作将导致一个访问违规,设立这个分区的目的是帮助程序员发现NULL指针相关的错误。比如我们new一块内存的时候,如果空间不够而导致返回的为NULL指针,如果不加判断就使用这个指针,往往会产生致命错误。
----------------------------
int * pInt = new int;
*pInt = 5;
----------------------------
这个分区的设立,使得对NULL地址的访问会导致一个访问违规,程序员会收到明显的提示,进而避免相关的错误发生。
· 0x0001000 ~ 0x7FFEFFF 大小 约2GB
这个是用户方式分区,维护了进程的,包括代码和数据的大部分内容的地方。而且,这个分区时进程私有的,其他的进程访问不到,使得系统更加健壮。
·0x8000000 ~ 0xFFFFFFFF 大小2G
这个是内核方式分区,也就是说,这个分区存放的都是内核的代码和数据,包括:内核代码、设备驱动程序代码、设备 I / O高速缓存、非页面内存池的分配和进程页面表等等。也就是说,比如线程对用Windows API的时候,访问并执行的就是这个分区内的代码。其实,每个进程的地址空间的内核方式分区都是相同的,这样,Windows的系统DLL(比如Kernel32.dll、User32.dll、GDI32.dll)可以被所有的进程调用就不奇怪了。
·细心的人是不是发现还少了64KB。这个就是 0x7FFEFFFF ~ 0x7FFFFFFF 的禁止进入分区。
这个分区处在用户方式分区和内核方式分区中间,目的是防止因为对用户方式分区的误操作而改变了内核方式分区的数据,进而导致的操作系统崩溃。
比如下面的代码:
BYTE bBuf[70000];
DWORD dwNumBytesWritten;
WriteProcessMemory( GetCurrentProcess(), (PVOID)0x7FFEFF00, bBuf, sizeof(bBuf), &dwNumBytesWritten );
WriteProcessMemory是Windows API,是内核的代码,所以是有权限改写内核方式分区的内容的,执行后,会将bBuf数组的数据写到 0x7FFEFF00 ~ 0x80001070 这个地址范围上,于是就改写了内核分区的数据,将导致不可预知的错误。
而这个禁止进入的分区同NULL分区一样,都是禁止进入的,任何时候任何访问都会导致违规,进而会将上面的代码中止,避免对内核方式分区的修改,保证系统稳定运行。
看了以上的内容,我们知道了进城所能使用的地址空间大约只有2G(除非在特别需要的情况下,Windows可以将内核方式分区压缩到1G,使得进程拥有3G的地址空间)。
我们编写的程序的所有内容都在这个分区范围内。比如线程的栈。
什么是栈呢?
栈是线程使用内存的一种方式,是分布在用户方式地址空间中的一块连续的区域,用于保存程序运行时的临时数据,包括局部变量和调用函数时使用的参数。
为什么说是线程的栈?
因为进程是没有栈的,因为进程不是代码的执行者,线程才是代码的执行者,进程只是线程执行的容器而已。
栈有什么特别之处?
首先,栈是大小固定的,是在程序编译时就设定好的, Windows下的线程的栈的默认大小为1MB,所以,如果你的局部变量太大,或者函数的递归调用太厉害,都有可能导致栈被用完,从而导致程序运行失败。
其次,每个线程都有一个自己的栈,线程在被创建时就会分配一个栈。所以,一般情况下,一个进程只能创建2000个左右的线程(栈大小为1M时),因为2G的地址空间都被线程的栈占领了,没有空间可以分配给新的线程了,线程的创建就失败了。
还有一点比较特别的就是栈的排列方式,栈和地址空间内地址的增长是反向排列的。也就是说,栈底位于高地址,栈顶位于低地址位置。这点是和上面的死循环问题相关的。
啊,讲了半天,终于回到问题上面来了。
来看一下上面那个程序运行循环时的栈的使用情况:
![](http://b23.photo.store.qq.com/http_imgload.cgi?/rurl4_b=587fd965909361fea77ecdba05c822648285a5131c49d8600fb9261abdddd55023dcac8f67e787c91e54f793b8561192aa7bf1a18b926f37add61c34f041213a419967fd7b093169444068d954fe635e33329a8e)
定义了局部变量i和数组array,于是在栈上为i和array分配了空间,应为i先定义,array后定义,所以i在栈底(之前没有其他的任何局部变量),array紧随其后,如图所示。
数组元素的增加方向同地址递增方向相同,所以array[10]就是i的所在,array[10] = 0; 导致i始终无法超过10而使得循环不断的进行下去。
小心避免这种莫名其妙的错误吧,如果不明白其中的道理,改对了也不明白原因,那就错过提高自己的机会了。
其实,解释这个死循环的原因,就上面一段的解释就够了,不过如果不把Windows的内存结构和地址空间解释清楚的话,总觉得还有不理解的地方,多学点东西总是好的,这也算程序员的内功吧。关于Windows内存结构和地址空间的知识,可以看看《Windows核心编程》,上面讲的很详细,很经典的一本书。
int main()
{
int i, array[10];
for( i = 0; i <= 10; i++ )
{
array[i]=0;
}
}
试着运行一下吧,你会发现,死循环了,为什么呢?
跟踪一下,你会发现,array[10] = 0; 这个操作改变了i的值,使得i值重新变成了0,这样就导致了死循环。也就是说array[10]就是i。为什么呢,是巧合吗?
首先,来学习点操作系统的知识,来了解一下地址空间的概念吧。对于32位 Windows下的每一个进程而言,都有一个大小为4G的地址空间,这个地址空间不是物理上的内存的地址,而是逻辑上的地址,也就是说,地址空间内的数据的实际的物理存储地址是由操作系统实现的,你并不知道。
这4G的地址空间并不是都能被进程使用的,因为这4G的地址空间被分成了不同功能的几个区域。来看一下Windows 2000的地址空间长什么样子吧:
4G的地址空间对应的地址范围 0x00000000 ~ 0xFFFFFFFF。
· 0x00000000 ~ 0x0000FFFF 大小 64K
这是一个NULL指针分区,对这个分区的任何操作将导致一个访问违规,设立这个分区的目的是帮助程序员发现NULL指针相关的错误。比如我们new一块内存的时候,如果空间不够而导致返回的为NULL指针,如果不加判断就使用这个指针,往往会产生致命错误。
----------------------------
int * pInt = new int;
*pInt = 5;
----------------------------
这个分区的设立,使得对NULL地址的访问会导致一个访问违规,程序员会收到明显的提示,进而避免相关的错误发生。
· 0x0001000 ~ 0x7FFEFFF 大小 约2GB
这个是用户方式分区,维护了进程的,包括代码和数据的大部分内容的地方。而且,这个分区时进程私有的,其他的进程访问不到,使得系统更加健壮。
·0x8000000 ~ 0xFFFFFFFF 大小2G
这个是内核方式分区,也就是说,这个分区存放的都是内核的代码和数据,包括:内核代码、设备驱动程序代码、设备 I / O高速缓存、非页面内存池的分配和进程页面表等等。也就是说,比如线程对用Windows API的时候,访问并执行的就是这个分区内的代码。其实,每个进程的地址空间的内核方式分区都是相同的,这样,Windows的系统DLL(比如Kernel32.dll、User32.dll、GDI32.dll)可以被所有的进程调用就不奇怪了。
·细心的人是不是发现还少了64KB。这个就是 0x7FFEFFFF ~ 0x7FFFFFFF 的禁止进入分区。
这个分区处在用户方式分区和内核方式分区中间,目的是防止因为对用户方式分区的误操作而改变了内核方式分区的数据,进而导致的操作系统崩溃。
比如下面的代码:
BYTE bBuf[70000];
DWORD dwNumBytesWritten;
WriteProcessMemory( GetCurrentProcess(), (PVOID)0x7FFEFF00, bBuf, sizeof(bBuf), &dwNumBytesWritten );
WriteProcessMemory是Windows API,是内核的代码,所以是有权限改写内核方式分区的内容的,执行后,会将bBuf数组的数据写到 0x7FFEFF00 ~ 0x80001070 这个地址范围上,于是就改写了内核分区的数据,将导致不可预知的错误。
而这个禁止进入的分区同NULL分区一样,都是禁止进入的,任何时候任何访问都会导致违规,进而会将上面的代码中止,避免对内核方式分区的修改,保证系统稳定运行。
看了以上的内容,我们知道了进城所能使用的地址空间大约只有2G(除非在特别需要的情况下,Windows可以将内核方式分区压缩到1G,使得进程拥有3G的地址空间)。
我们编写的程序的所有内容都在这个分区范围内。比如线程的栈。
什么是栈呢?
栈是线程使用内存的一种方式,是分布在用户方式地址空间中的一块连续的区域,用于保存程序运行时的临时数据,包括局部变量和调用函数时使用的参数。
为什么说是线程的栈?
因为进程是没有栈的,因为进程不是代码的执行者,线程才是代码的执行者,进程只是线程执行的容器而已。
栈有什么特别之处?
首先,栈是大小固定的,是在程序编译时就设定好的, Windows下的线程的栈的默认大小为1MB,所以,如果你的局部变量太大,或者函数的递归调用太厉害,都有可能导致栈被用完,从而导致程序运行失败。
其次,每个线程都有一个自己的栈,线程在被创建时就会分配一个栈。所以,一般情况下,一个进程只能创建2000个左右的线程(栈大小为1M时),因为2G的地址空间都被线程的栈占领了,没有空间可以分配给新的线程了,线程的创建就失败了。
还有一点比较特别的就是栈的排列方式,栈和地址空间内地址的增长是反向排列的。也就是说,栈底位于高地址,栈顶位于低地址位置。这点是和上面的死循环问题相关的。
啊,讲了半天,终于回到问题上面来了。
来看一下上面那个程序运行循环时的栈的使用情况:
定义了局部变量i和数组array,于是在栈上为i和array分配了空间,应为i先定义,array后定义,所以i在栈底(之前没有其他的任何局部变量),array紧随其后,如图所示。
数组元素的增加方向同地址递增方向相同,所以array[10]就是i的所在,array[10] = 0; 导致i始终无法超过10而使得循环不断的进行下去。
小心避免这种莫名其妙的错误吧,如果不明白其中的道理,改对了也不明白原因,那就错过提高自己的机会了。
其实,解释这个死循环的原因,就上面一段的解释就够了,不过如果不把Windows的内存结构和地址空间解释清楚的话,总觉得还有不理解的地方,多学点东西总是好的,这也算程序员的内功吧。关于Windows内存结构和地址空间的知识,可以看看《Windows核心编程》,上面讲的很详细,很经典的一本书。
相关文章推荐
- 一个帮助我们理解for循环的好例子
- 我是一个线程(对理解多线程很有帮助)-转载
- for循环产生的Cortex-M3汇编代码的一个奇怪现象
- PHP递归算法的一个实例 帮助理解
- 一个奇怪的死循环
- 局部QEventLoop帮助QWidget不消失(也就是有一个局部事件循环始终在运行,导致程序被卡住那里,但仍可以接受事件。说白了就是有一个while语句死活不肯退出,直到收到退出信号)
- 私有继承的一个例子,帮助加强理解
- 帮助理解c#中委托+事件的一个例子
- 帮助大家学习对接口和反射理解,另外帮我解惑下其中的一个问题
- 循环一个节点列表(NodeList)或者数组,并且绑定事件处理函数引发对闭包的理解
- 很棒一个差分约束系统的理解,希望对你有帮助,果断转了
- 我是一个线程(对理解多线程很有帮助)
- 一个帮助你处理延迟,重复,循环操作的jQuery插件 - timing
- 一个帮助你处理延迟,重复,循环操作的jQuery插件 - timing
- 一个Java小程序,帮助理解Java继承中的初始化过程
- 深入理解闭包系列第四篇——常见的一个循环和闭包的错误详解
- 一个帮助你理解prototype中Class.create()方法的例子
- tensorflow学习二:概念知识和一个帮助理解的demo
- 一个帮助你处理延迟,重复,循环操作的jQuery插件 - timing