A Za, A Za, Fighting...

坚信:勤能补拙

     摘要: 程序分析是以某种语言书写的程序为对象,对其内部的运作流程进行分析。程序分析的目的主要有三点:一是通过程序内部各个模块之间的调用关系,整体上把握程序的运行流程,从而更好地理解程序,从中汲取有价值的内容。二是以系统优化为目的,通过对程序中关键函数的跟踪或者运行时信息的统计,找到系统性能的瓶颈,从而采取进一步行动对程序进行优化。最后一点,程序分析也有可能用于系统测试和程序调试中。当系统跟踪起来比较复杂,...  阅读全文
posted @ 2011-08-12 10:33 simplyzhao 阅读(420) | 评论 (0)编辑 收藏
转载: http://www.ibm.com/developerworks/cn/linux/l-cn-valgrind/

应用 Valgrind 发现 Linux 程序的内存问题


Valgrind 概述

体系结构

Valgrind是一套Linux下,开放源代码(GPL V2)的仿真调试工具的集合。Valgrind由内核(core)以及基于内核的其他调试工具组成。内核类似于一个框架(framework),它模拟了一个CPU环境,并提供服务给其他工具;而其他工具则类似于插件 (plug-in),利用内核提供的服务完成各种特定的内存调试任务。Valgrind的体系结构如下图所示:


图 1 Valgrind 体系结构
Valgrind 体系结构 

Valgrind包括如下一些工具:

  1. Memcheck。这是valgrind应用最广泛的工具,一个重量级的内存检查器,能够发现开发中绝大多数内存错误使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。这也是本文将重点介绍的部分。
  2. Callgrind。它主要用来检查程序中函数调用过程中出现的问题。
  3. Cachegrind。它主要用来检查程序中缓存使用出现的问题。
  4. Helgrind。它主要用来检查多线程程序中出现的竞争问题。
  5. Massif。它主要用来检查程序中堆栈使用中出现的问题。
  6. Extension。可以利用core提供的功能,自己编写特定的内存调试工具。

Linux 程序内存空间布局

要发现Linux下的内存问题,首先一定要知道在Linux下,内存是如何被分配的?下图展示了一个典型的Linux C程序内存空间布局:


图 2: 典型内存空间布局
典型内存空间布局 

一个典型的Linux C程序内存空间由如下几部分组成:

  • 代码段(.text)。这里存放的是CPU要执行的指令。代码段是可共享的,相同的代码在内存中只会有一个拷贝,同时这个段是只读的,防止程序由于错误而修改自身的指令。
  • 初始化数据段(.data)。这里存放的是程序中需要明确赋初始值的变量,例如位于所有函数之外的全局变量:int val=100。需要强调的是,以上两段都是位于程序的可执行文件中,内核在调用exec函数启动该程序时从源程序文件中读入。
  • 未初始化数据段(.bss)。位于这一段中的数据,内核在执行该程序前,将其初始化为0或者null。例如出现在任何函数之外的全局变量:int sum;
  • 堆(Heap)。这个段用于在程序中进行动态内存申请,例如经常用到的malloc,new系列函数就是从这个段中申请内存。
  • 栈(Stack)。函数中的局部变量以及在函数调用过程中产生的临时变量都保存在此段中。

内存检查原理

Memcheck检测内存问题的原理如下图所示:


图 3 内存检查原理
内存检查原理 

Memcheck 能够检测出内存问题,关键在于其建立了两个全局表。

  1. Valid-Value 表:

对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits;对于 CPU 的每个寄存器,也有一个与之对应的 bit 向量。这些 bits 负责记录该字节或者寄存器值是否具有有效的、已初始化的值。

  1. Valid-Address 

对于进程整个地址空间中的每一个字节(byte),还有与之对应的 1 个 bit,负责记录该地址是否能够被读写。

检测原理:

  • 当要读写内存中某个字节时,首先检查这个字节对应的 A bit。如果该A bit显示该位置是无效位置,memcheck 则报告读写错误。
  • 内核(core)类似于一个虚拟的 CPU 环境,这样当内存中的某个字节被加载到真实的 CPU 中时,该字节对应的 V bit 也被加载到虚拟的 CPU 环境中。一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则 memcheck 会检查对应的V bits,如果该值尚未初始化,则会报告使用未初始化内存错误。

回页首

Valgrind 使用

第一步:准备好程序

为了使valgrind发现的错误更精确,如能够定位到源代码行,建议在编译时加上-g参数,编译优化选项请选择O0,虽然这会降低程序的执行效率。

这里用到的示例程序文件名为:sample.c(如下所示),选用的编译器为gcc。

生成可执行程序 gcc –g –O0 sample.c –o sample


清单 1
清单 1 

第二步:在valgrind下,运行可执行程序。

利用valgrind调试内存问题,不需要重新编译源程序,它的输入就是二进制的可执行程序。调用Valgrind的通用格式是:valgrind [valgrind-options] your-prog [your-prog-options]

Valgrind 的参数分为两类,一类是 core 的参数,它对所有的工具都适用;另外一类就是具体某个工具如 memcheck 的参数。Valgrind 默认的工具就是 memcheck,也可以通过“--tool=tool name”指定其他的工具。Valgrind 提供了大量的参数满足你特定的调试需求,具体可参考其用户手册。

这个例子将使用 memcheck,于是可以输入命令入下:valgrind <Path>/sample.

第三步:分析 valgrind 的输出信息。

以下是运行上述命令后的输出。


清单 2
清单 2 
  • 左边显示类似行号的数字(32372)表示的是 Process ID。
  • 最上面的红色方框表示的是 valgrind 的版本信息。
  • 中间的红色方框表示 valgrind 通过运行被测试程序,发现的内存问题。通过阅读这些信息,可以发现:
    1. 这是一个对内存的非法写操作,非法写操作的内存是4 bytes。
    2. 发生错误时的函数堆栈,以及具体的源代码行号。
    3. 非法写操作的具体地址空间。
  • 最下面的红色方框是对发现的内存问题和内存泄露问题的总结。内存泄露的大小(40 bytes)也能够被检测出来。

示例程序显然有两个问题,一是fun函数中动态申请的堆内存没有释放;二是对堆内存的访问越界。这两个问题均被valgrind发现。

回页首

利用Memcheck发现常见的内存问题

在Linux平台开发应用程序时,最常遇见的问题就是错误的使用内存,我们总结了常见了内存错误使用情况,并说明了如何用valgrind将其检测出来。

使用未初始化的内存

问题分析:

对于位于程序中不同段的变量,其初始值是不同的,全局变量和静态变量初始值为0,而局部变量和动态申请的变量,其初始值为随机值。如果程序使用了为随机值的变量,那么程序的行为就变得不可预期。

下面的程序就是一种常见的,使用了未初始化的变量的情况。数组a是局部变量,其初始值为随机值,而在初始化时并没有给其所有数组成员初始化,如此在接下来使用这个数组时就潜在有内存问题。


清单 3
清单 3 

结果分析:

假设这个文件名为:badloop.c,生成的可执行程序为badloop。用memcheck对其进行测试,输出如下。


清单 4
清单 4 

输出结果显示,在该程序第11行中,程序的跳转依赖于一个未初始化的变量。准确的发现了上述程序中存在的问题。

内存读写越界

问题分析:

这种情况是指:访问了你不应该/没有权限访问的内存地址空间,比如访问数组时越界;对动态内存访问时超出了申请的内存大小范围。下面的程序就是一个典型的数组越界问题。pt是一个局部数组变量,其大小为4,p初始指向pt数组的起始地址,但在对p循环叠加后,p超出了pt数组的范围,如果此时再对p进行写操作,那么后果将不可预期。


清单 5
清单 5 

结果分析:

假设这个文件名为badacc.cpp,生成的可执行程序为badacc,用memcheck对其进行测试,输出如下。


清单 6
清单 6 

输出结果显示,在该程序的第15行,进行了非法的写操作;在第16行,进行了非法读操作。准确地发现了上述问题。

内存覆盖

问题分析:

C 语言的强大和可怕之处在于其可以直接操作内存,C 标准库中提供了大量这样的函数,比如 strcpy, strncpy, memcpy, strcat 等,这些函数有一个共同的特点就是需要设置源地址 (src),和目标地址(dst),src 和 dst 指向的地址不能发生重叠,否则结果将不可预期。

下面就是一个 src 和 dst 发生重叠的例子。在 15 与 17 行中,src 和 dst 所指向的地址相差 20,但指定的拷贝长度却是 21,这样就会把之前的拷贝值覆盖。第 24 行程序类似,src(x+20) 与 dst(x) 所指向的地址相差 20,但 dst 的长度却为 21,这样也会发生内存覆盖。


清单 7
清单 7 

结果分析:

假设这个文件名为 badlap.cpp,生成的可执行程序为 badlap,用 memcheck 对其进行测试,输出如下。


清单 8
清单 8 

输出结果显示上述程序中第15,17,24行,源地址和目标地址设置出现重叠。准确的发现了上述问题。

动态内存管理错误

问题分析:

常见的内存分配方式分三种:静态存储,栈上分配,堆上分配。全局变量属于静态存储,它们是在编译时就被分配了存储空间,函数内的局部变量属于栈上分配,而最灵活的内存使用方式当属堆上分配,也叫做内存动态分配了。常用的内存动态分配函数包括:malloc, alloc, realloc, new等,动态释放函数包括free, delete。

一旦成功申请了动态内存,我们就需要自己对其进行内存管理,而这又是最容易犯错误的。下面的一段程序,就包括了内存动态管理中常见的错误。


清单 9
清单 9 

常见的内存动态管理错误包括:

    • 申请和释放不一致

由于 C++ 兼容 C,而 C 与 C++ 的内存申请和释放函数是不同的,因此在 C++ 程序中,就有两套动态内存管理函数。一条不变的规则就是采用 C 方式申请的内存就用 C 方式释放;用 C++ 方式申请的内存,用 C++ 方式释放。也就是用 malloc/alloc/realloc 方式申请的内存,用 free 释放;用 new 方式申请的内存用 delete 释放。在上述程序中,用 malloc 方式申请了内存却用 delete 来释放,虽然这在很多情况下不会有问题,但这绝对是潜在的问题。

    • 申请和释放不匹配

申请了多少内存,在使用完成后就要释放多少。如果没有释放,或者少释放了就是内存泄露;多释放了也会产生问题。上述程序中,指针p和pt指向的是同一块内存,却被先后释放两次。

    • 释放后仍然读写

本质上说,系统会在堆上维护一个动态内存链表,如果被释放,就意味着该块内存可以继续被分配给其他部分,如果内存被释放后再访问,就可能覆盖其他部分的信息,这是一种严重的错误,上述程序第16行中就在释放后仍然写这块内存。

结果分析:

假设这个文件名为badmac.cpp,生成的可执行程序为badmac,用memcheck对其进行测试,输出如下。


清单 10
清单 10 

输出结果显示,第14行分配和释放函数不一致;第16行发生非法写操作,也就是往释放后的内存地址写值;第17行释放内存函数无效。准确地发现了上述三个问题。

内存泄露

问题描述:

内存泄露(Memory leak)指的是,在程序中动态申请的内存,在使用完后既没有释放,又无法被程序的其他部分访问。内存泄露是在开发大型程序中最令人头疼的问题,以至于有人说,内存泄露是无法避免的。其实不然,防止内存泄露要从良好的编程习惯做起,另外重要的一点就是要加强单元测试(Unit Test),而memcheck就是这样一款优秀的工具。

下面是一个比较典型的内存泄露案例。main函数调用了mk函数生成树结点,可是在调用完成之后,却没有相应的函数:nodefr释放内存,这样内存中的这个树结构就无法被其他部分访问,造成了内存泄露。

在一个单独的函数中,每个人的内存泄露意识都是比较强的。但很多情况下,我们都会对malloc/free 或new/delete做一些包装,以符合我们特定的需要,无法做到在一个函数中既使用又释放。这个例子也说明了内存泄露最容易发生的地方:即两个部分的接口部分,一个函数申请内存,一个函数释放内存。并且这些函数由不同的人开发、使用,这样造成内存泄露的可能性就比较大了。这需要养成良好的单元测试习惯,将内存泄露消灭在初始阶段。


清单 11
清单 1 

清单 11.2
清单 11.2 

清单 11.3
清单 11.3 

结果分析:

假设上述文件名位tree.h, tree.cpp, badleak.cpp,生成的可执行程序为badleak,用memcheck对其进行测试,输出如下。


清单 12
清单 12 

该示例程序是生成一棵树的过程,每个树节点的大小为12(考虑内存对齐),共8个节点。从上述输出可以看出,所有的内存泄露都被发现。Memcheck将内存泄露分为两种,一种是可能的内存泄露(Possibly lost),另外一种是确定的内存泄露(Definitely lost)。Possibly lost 是指仍然存在某个指针能够访问某块内存,但该指针指向的已经不是该内存首地址。Definitely lost 是指已经不能够访问这块内存。而Definitely lost又分为两种:直接的(direct)和间接的(indirect)。直接和间接的区别就是,直接是没有任何指针指向该内存,间接是指指向该内存的指针都位于内存泄露处。在上述的例子中,根节点是directly lost,而其他节点是indirectly lost。

回页首

总结

本文介绍了valgrind的体系结构,并重点介绍了其应用最广泛的工具:memcheck。阐述了memcheck发现内存问题的基本原理,基本使用方法,以及利用memcheck如何发现目前开发中最广泛的五大类内存问题。在项目中尽早的发现内存问题,能够极大地提高开发效率,valgrind就是能够帮助你实现这一目标的出色工具。

posted @ 2011-08-11 17:22 simplyzhao 阅读(301) | 评论 (0)编辑 收藏
一、什么是对齐,以及为什么要对齐:

1. 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

2. 对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况, 但是最常见的是如果不按照适合其平台的要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为 32位)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低 字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。

二、对齐的实现

通常,我们写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。
但是,正因为我们一般不需要关心这个问题,所以因为编辑器对数据存放做了对齐,而我们不了解的话,常常会对一些问题感到迷惑。最常见的就是struct数据结构的sizeof结果,出乎意料。为此,我们需要对对齐算法所了解。
对齐的算法:
由于各个平台和编译器的不同,现以本人使用的gcc version 3.2.2编译器(32位x86平台)为例子,来讨论编译器对struct数据结构中的各成员如何进行对齐的。
设结构体如下定义:
struct A {
    int a;
    char b;
    short c;
};
结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个。所以A用到的空间应该是7字节。但是因为编译器要对数据成员在空间上进行对齐。
所以使用sizeof(strcut A)值为8。
现在把该结构体调整成员变量的顺序。
struct B {
    char b;
    int a;
    short c;
};
这时候同样是总共7个字节的变量,但是sizeof(struct B)的值却是12。
下面我们使用预编译指令#pragma pack (value)来告诉编译器,使用我们指定的对齐值来取代缺省的。
#progma pack (2) /*指定按2字节对齐*/
struct C {
    char b;
    int a;
    short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct C)值是8。

修改对齐值为1:
#progma pack (1) /*指定按1字节对齐*/
struct D {
    char b;
    int a;
    short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct D)值为7。

对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
这里面有四个概念值:
1)数据类型自身的对齐值:就是上面交代的基本数据类型的自身对齐值。

2)指定对齐值:#pragma pack (value)时的指定对齐值value。

3)结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。

4)数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小的那个值。

       有了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0".而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是 数据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整 数倍,结合下面例子理解)。这样就不难理解上面的几个例子的值了。
例子分析:
分析例子B;
struct B {
    char b;
    int a;
    short c;
};
假设B从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4。第一个成员变量b的自身对齐值是1,比指定或者默认指 定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000符合0x0000%1=0.第二个成员变量a,其自身对齐值为4,所以有效对齐值也为 4,所以只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,复核0x0004%4=0,且紧靠第一个变量。第三个变量c,自身对齐 值为2,所以有效对齐值也是2,可以存放在0x0008到0x0009这两个字节空间中,符合0x0008%2=0。所以从0x0000到0x0009存 放的都是B内容。再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求, 0x0009到0x0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到0x000B 共有12个字节,sizeof(struct B)=12;

同理,分析上面例子C:
#pragma pack (2) /*指定按2字节对齐*/
struct C {
    char b;
    int a;
    short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1= 0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续 字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放
在0x0006、0x0007中,符合0x0006%2=0。所以从0x0000到0x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以 C的有效对齐值为2。又8%2=0,C只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8.

有 了以上的解释,相信你对C语言的字节对齐概念应该有了清楚的认识了吧。在网络程序中,掌握这个概念可是很重要的喔,在不同平台之间(比如在Windows 和Linux之间)传递2进制流(比如结构体),那么在这两个平台间必须要定义相同的对齐方式,不然莫名其妙的出了一些错,可是很难排查的哦^_^。
posted @ 2011-08-10 19:18 simplyzhao 阅读(120) | 评论 (0)编辑 收藏

原文标题:Anatomy of a Program in Memory

原文地址:http://duartes.org/gustavo/blog/

 

[注:本人水平有限,只好挑一些国外高手的精彩文章翻译一下。一来自己复习,二来与大家分享。]

 

    内存管理模块是操作系统的心脏;它对应用程序和系统管理非常重要。今后的几篇文章中,我将着眼于实际的内存问题,但也不避讳其中的技术内幕。由于不少概念是通用的,所以文中大部分例子取自32x86平台的LinuxWindows系统。本系列第一篇文章讲述应用程序的内存布局。

 

    在多任务操作系统中的每一个进程都运行在一个属于它自己的内存沙盘中。这个沙盘就是虚拟地址空间virtual address space),在32位模式下它总是一个4GB的内存地址块。这些虚拟地址通过页表page table)映射到物理内存,页表由操作系统维护并被处理器引用。每一个进程拥有一套属于它自己的页表,但是还有一个隐情。只要虚拟地址被使能,那么它就会作用于这台机器上运行的所有软件,包括内核本身。因此一部分虚拟地址必须保留给内核使用:

 

 

 

 

 

    这并不意味着内核使用了那么多的物理内存,仅表示它可支配这么大的地址空间,可根据内核需要,将其映射到物理内存。内核空间在页表中拥有较高的特权级ring 2或以下),因此只要用户态的程序试图访问这些页,就会导致一个页错误(page fault)。在Linux中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址的,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化:

 

 

 

 

    蓝色区域表示映射到物理内存的虚拟地址,而白色区域表示未映射的部分。在上面的例子中,Firefox使用了相当多的虚拟地址空间,因为它是传说中的吃内存大户。地址空间中的各个条带对应于不同的内存段(memory segment),如:堆、栈之类的。记住,这些段只是简单的内存地址范围,与Intel处理器的段没有关系。不管怎样,下面是一个Linux进程的标准的内存段布局:

 

 

 

    当计算机开心、安全、可爱、正常的运转时,几乎每一个进程的各个段的起始虚拟地址都与上图完全一致,这也给远程发掘程序安全漏洞打开了方便之门。一个发掘过程往往需要引用绝对内存地址:栈地址,库函数地址等。远程攻击者必须依赖地址空间布局的一致性,摸索着选择这些地址。如果让他们猜个正着,有人就会被整了。因此,地址空间的随机排布方式逐渐流行起来。Linux通过对内存映射的起始地址加上随机的偏移量来打乱布局。不幸的是,32位地址空间相当紧凑,给随机化所留下的空当不大,削弱了这种技巧的效果

 

    进程地址空间中最顶部的段是栈,大多数编程语言将之用于存储局部变量和函数参数。调用一个方法或函数会将一个新的栈桢stack frame)压入栈中。栈桢在函数返回时被清理。也许是因为数据严格的遵从LIFO的顺序,这个简单的设计意味着不必使用复杂的数据结构来追踪栈的内容,只需要一个简单的指针指向栈的顶端即可。因此压栈(pushing)和退栈(popping)过程非常迅速、准确。另外,持续的重用栈空间有助于使活跃的栈内存保持在CPU缓存中,从而加速访问。进程中的每一个线程都有属于自己的栈。

 

    通过不断向栈中压入的数据,超出其容量就有会耗尽栈所对应的内存区域。这将触发一个页故障(page fault),并被Linuxexpand_stack()处理,它会调用acct_stack_growth()来检查是否还有合适的地方用于栈的增长。如果栈的大小低于RLIMIT_STACK(通常是8MB),那么一般情况下栈会被加长,程序继续愉快的运行,感觉不到发生了什么事情。这是一种将栈扩展至所需大小的常规机制。然而,如果达到了最大的栈空间大小,就会栈溢出(stack overflow),程序收到一个段错误(Segmentation Fault)。当映射了的栈区域扩展到所需的大小后,它就不会再收缩回去,即使栈不那么满了。这就好比联邦预算,它总是在增长的。

 

    动态栈增长是唯一一种访问未映射内存区域(图中白色区域)而被允许的情形。其它任何对未映射内存区域的访问都会触发页故障,从而导致段错误。一些被映射的区域是只读的,因此企图写这些区域也会导致段错误。

 

    在栈的下方,是我们的内存映射段。此处,内核将文件的内容直接映射到内存。任何应用程序都可以通过Linuxmmap()系统调用(实现)或WindowsCreateFileMapping() MapViewOfFile()请求这种映射。内存映射是一种方便高效的文件I/O方式,所以它被用于加载动态库。创建一个不对应于任何文件的匿名内存映射也是可能的,此方法用于存放程序的数据。在Linux中,如果你通过malloc()请求一大块内存,C运行库将会创建这样一个匿名映射而不是使用堆内存。‘大块’意味着比MMAP_THRESHOLD还大,缺省是128KB,可以通过mallopt()调整。

 

    说到堆,它是接下来的一块地址空间。与栈一样,堆用于运行时内存分配;但不同点是,堆用于存储那些生存期与函数调用无关的数据。大部分语言都提供了堆管理功能。因此,满足内存请求就成了语言运行时库及内核共同的任务。在C语言中,堆分配的接口是malloc()系列函数,而在具有垃圾收集功能的语言(如C#)中,此接口是new关键字。

 

    如果堆中有足够的空间来满足内存请求,它就可以被语言运行时库处理而不需要内核参与。否则,堆会被扩大,通过brk()系统调用(实现)来分配请求所需的内存块。堆管理是很复杂的,需要精细的算法,应付我们程序中杂乱的分配模式,优化速度和内存使用效率。处理一个堆请求所需的时间会大幅度的变动。实时系统通过特殊目的分配器来解决这个问题。堆也可能会变得零零碎碎,如下图所示:

 

 

 

    最后,我们来看看最底部的内存段:BSS,数据段,代码段。在C语言中,BSS和数据段保存的都是静态(全局)变量的内容。区别在于BSS保存的是未被初始化的静态变量内容,它们的值不是直接在程序的源代码中设定的。BSS内存区域是匿名的:它不映射到任何文件。如果你写static int cntActiveUsers,则cntActiveUsers的内容就会保存在BSS中。

 

    另一方面,数据段保存在源代码中已经初始化了的静态变量内容。这个内存区域不是匿名的。它映射了一部分的程序二进制镜像,也就是源代码中指定了初始值的静态变量。所以,如果你写static int cntWorkerBees = 10,则cntWorkerBees的内容就保存在数据段中了,而且初始值为10。尽管数据段映射了一个文件,但它是一个私有内存映射,这意味着更改此处的内存不会影响到被映射的文件。也必须如此,否则给全局变量赋值将会改动你硬盘上的二进制镜像,这是不可想象的。

 

    下图中数据段的例子更加复杂,因为它用了一个指针。在此情况下,指针gonzo4字节内存地址)本身的值保存在数据段中。而它所指向的实际字符串则不在这里。这个字符串保存在代码段中,代码段是只读的,保存了你全部的代码外加零零碎碎的东西,比如字符串字面值。代码段将你的二进制文件也映射到了内存中,但对此区域的写操作都会使你的程序收到段错误。这有助于防范指针错误,虽然不像在C语言编程时就注意防范来得那么有效。下图展示了这些段以及我们例子中的变量:

 

 

 

    你可以通过阅读文件/proc/pid_of_process/maps来检验一个Linux进程中的内存区域。记住一个段可能包含许多区域。比如,每个内存映射文件在mmap段中都有属于自己的区域,动态库拥有类似BSS和数据段的额外区域。下一篇文章讲说明这些“区域”(area)的真正含义。有时人们提到“数据段”,指的就是全部的数据段 + BSS + 堆。

 

    你可以通过nmobjdump命令来察看二进制镜像,打印其中的符号,它们的地址,段等信息。最后需要指出的是,前文描述的虚拟地址布局在Linux中是一种“灵活布局”(flexible layout),而且以此作为默认方式已经有些年头了。它假设我们有值RLIMIT_STACK。当情况不是这样时,Linux退回使用“经典布局”(classic layout),如下图所示:

 

 

 

    对虚拟地址空间的布局就讲这些吧。下一篇文章将讨论内核是如何跟踪这些内存区域的。我们会分析内存映射,看看文件的读写操作是如何与之关联的,以及内存使用概况的含义

posted @ 2011-08-10 17:11 simplyzhao 阅读(451) | 评论 (0)编辑 收藏
数组与指针,究竟有什么关联?什么情况下等同,什么时候不等同? 老问题,新收获
前两天看《C专家编程》,结果硬是卡在数组与指针这块好久,原本以为自己已经掌握的东西,原来还是没有掌握精髓

抛出两个问题,然后我们在解决这两个问题的过程中review指针与数组:
问题一:
char arr[] = "abcdefg";
char *ptr = "abcdefg";
我们知道,arr[i]与ptr[i]都是正确的,但两者又有什么根本的差异呢?如果是char (*pa)[10]与char **pp,那么pa[1][2]与pp[1][2]有什么区别呢?

问题二:
qsort在针对指针数组与二维数组排序时的区别?

----------------------------------------------------------------------------------------------------------

问题一:
首先,要明确一点,编译器为每个标识符分配一个地址,这个地址在编译阶段可知,相反,存储在标识符中的值只有在运行时才可知

对于数组,有 &arr = arr = &(arr[0])
对于指针,有 &ptr != ptr = &(ptr[0])
对于编译器,它所知道的是&arr与&ptr,这两个标识符的地址
基于上述三条(前两条,写个简单的程序即可测试),即可得出结论: 虽然arr[i]与ptr[i]都能正确访问某个字符(事实上,都会被编译器改写成*(arr+i)或者*(ptr+i)的形式),但是两者生成的汇编代码是不同的

验证:
C语言代码:
char arr[] = "abcdefg";
char *ptr = "abcdefg";

void
difference()
{
    
char a = arr[3];
    
char b = ptr[3];
}

汇编代码(gcc -S test.c)
difference:
    pushl    
%ebp
    movl    
%esp, %ebp
    subl    $
16%esp

    /* char a = arr[3] */
    movzbl    arr
+3%eax /* 取地址(arr+3)处的值,放入寄存器 */
    movb    
%al, -1(%ebp)

    /* char b = ptr[3] */
    movl    ptr, 
%eax /* 取ptr的值(Mem[ptr]),放入寄存器 */
    addl    $
3%eax
    movzbl    (
%eax), %eax /* 将寄存器%eax的值作为地址,取该地址的值放入寄存器 */
    movb    
%al, -2(%ebp)

    leave
    ret

略微对上述汇编代码进行了注释,可以发现两者的差异,指针跟数组相比,前者多了“一层间接引用”
(Note: 汇编寻址,例如,$Imm是立即数,Imm是绝对寻址(=Mem[Imm]),Ea是寄存器寻址,(Ea)则是间接寻址(=Mem[Register[Ea]]);可参考《深入理解计算机系统》第3章)

如果是char (*pa)[10]与char **pp,pa是指向数组的指针,而pp是指向指针的指针,这里有点类似于之前的讨论
需要明白的就是: pa是指向数组的指针,保存的就是数组的地址;数组的地址等于数组首元素的地址;pa[i]等同于一元数组名称,就是指向数组首元素的指针,也保存的是数组首元素的地址,因此有: pa+i(指向数组的指针) = pa[i] = &(pa[i][0])
表达式pa[1][2]与pp[1][2]所产生的汇编代码也是不同的
(Note: 指向数组的指针p与数组名a(指向数组首元素的指针),两者的值相同,但含义不一样,例如p+1与a+1就完全不同)
验证:
C语言代码:
void
array_print(
char (*pa)[10])
{
    
char c = pa[1][2];
}

void
pointer_print(
char **pp)
{
    
char c = pp[1][2];
}

汇编代码:
array_print:
    pushl    
%ebp
    movl    
%esp, %ebp
    subl    $
16%esp

    movl    
8(%ebp), %eax /* 将pa的值(Mem[pa])放入寄存器 */
    addl    $
10%eax 
    movzbl    
2(%eax), %eax
    movb    
%al, -1(%ebp)

    leave
    ret

pointer_print:
    pushl    
%ebp
    movl    
%esp, %ebp
    subl    $
16%esp

    movl    
8(%ebp), %eax /* 将pp的值放入寄存器 */    addl    $4%eax
    movl    (
%eax), %eax
    addl    $
2%eax
    movzbl    (
%eax), %eax
    movb    
%al, -1(%ebp)

    leave
    ret

问题二:
有了前面的讲解,这里直接上代码
#include "basic.h"

char *strings[] = { "zhao"
    
"pan",
    
"anan",
    NULL };

char arr[][5= { "zhao",
    
"pan",
    
"anan" };

void
test_a2_addr()
{
    printf(
"Addr Testing\n");
    printf(
"addr(arr+1): %p\n", arr+1);
    printf(
"addr(arr[1]): %p\n", arr[1]);
    printf(
"addr(arr[1][0]): %p\n"&arr[1][0]);
}

int
compare_pp(
const void *arg1, const void *arg2)
{
    
return strcmp(*(char **)arg1, *(char **)arg2);
}

int
compare_a2(
const void *arg1, const void *arg2)
{
    
return strcmp(*(char (*)[5])arg1, *(char (*)[5])arg2);
    
//return strcmp((char *)arg1, (char *)arg2); //ok,too
}

void
test_pp()
{    
    qsort(strings, 
3sizeof(char *), compare_pp);

    printf(
"\nResult of qsort for POINTER-ARRAY\n");
    
char **= strings;
    
while(*!= NULL) {
        printf(
"%s\n"*s);
        
++s;
    }
}

void
test_a2()
{
    qsort(arr, 
3sizeof(char)*5, compare_a2);
    
    printf(
"\nResult of qsort for TWO-ARRAY\n");
    
int i;
    
for(i=0; i<3++i)
        printf(
"%s\n", arr[i]);
}

int
main(
int argc, char **argv)
{
    test_a2_addr();
    test_pp();
    test_a2();
}

posted @ 2011-08-10 16:38 simplyzhao 阅读(109) | 评论 (0)编辑 收藏
比较排序算法的下界: O(nlogn)

非比较排序: 位排序,计数排序,基数排序

位排序适用于: 1. 输入数据限制在相对较小的范围内; 2. 数据没有重复; 3. 对于每条记录而言,除了单一整数外,没有任何其他关联数据

计数排序
前提: n个输入数据的每一个都是介于0到k之间的整数
时间复杂度: O(n),如果k=O(n)
基本思想: 对于每个输入元素x,统计出小于等于x的元素的个数,有了这个信息,就可以把x直接放到最终输出数组的正确位置上
特点: 计数排序是稳定的。计数排序的稳定性,对于基数排序是至关重要的
代码:
/*
 * counting-sort: stable, O(n)
 * precondition: the value of n input integers must between 0 and k, k = O(n)
 
*/
void
counting_sort(
int *array, int *ret, int n, int k)
{
    
int i, j, *count = (int *)malloc((k+1* sizeof(int));
    assert(count 
!= NULL);
    memset(count, 
0sizeof(int)*(k+1));

    
for(i=0; i<n; ++i) {
        assert(array[i]
>=0 && array[i]<=k);
        
++count[array[i]];
    }

    
for(i=1; i<=k; ++i)
        count[i] 
+= count[i-1];

    
for(j=n-1; j>=0--j) {
        ret[count[array[j]]
-1= array[j];
        
--count[array[j]];
    }

    free(count);
}

基数排序
基本思想: 按照最低有效位到最高有效位的顺序(这里的位,可以看作是关键值),采用一种稳定性排序算法对该位进行排序
伪代码:
     RADIX-SORT(A, d)
            for i = 1..d /* 每一位,按低到高的顺序 */
                  do use a stable sort to sort array A on digit i
应用举例:
假设我们想根据三个关键字年,月,日来对日期进行排序
对于这个问题,可以用带有比较函数的排序算法来做,而另一方面,我们可以采用另一种方法,即用一种稳定的排序方法对所给的信息进行三次排序:先以日,其次对月,再对年
posted @ 2011-08-10 15:15 simplyzhao 阅读(124) | 评论 (0)编辑 收藏
堆排序: 时间复杂度 O(nlogn),非稳定排序

比如:3 27 36 27
如果堆顶3先输出,则,第三层的27(最后一个27)跑到堆顶,然后堆稳定,继续输出堆顶,是刚才那个27,这样说明后面的27先于第二个位置的27输出,不稳定。

#define LEFT(i) (((i)<<1)+1)
#define RIGHT(i) (((i)+1)<<1)
#define PARENT(i) (((i)-1)>>1)

void
min_heapify(
int index, int *heap, int heap_size) //precondition: LEFT(index) and RIGHT(index) are both already min-heap
{
    
int min = index;
    
if(LEFT(index)<heap_size && heap[LEFT(index)]<heap[min])
        min 
= LEFT(index);
    
if(RIGHT(index)<heap_size && heap[RIGHT(index)]<heap[min])
        min 
= RIGHT(index);

    
if(min != index) {
        swap(heap
+min, heap+index);
        min_heapify(min, heap, heap_size);
    }
}

void
build_heap(
int *heap, int heap_size)
{
    
int i = (heap_size>>1);
    
for( ; i>=0--i)
        min_heapify(i, heap, heap_size);
}

void
heap_sort(
int *heap, int heap_size)
{
    build_heap(heap, heap_size);

    
while(heap_size > 1) {
        swap(heap, heap
+heap_size-1);
        
--heap_size;
        min_heapify(
0, heap, heap_size);
    }
}
posted @ 2011-07-29 20:25 simplyzhao 阅读(93) | 评论 (0)编辑 收藏
归并排序: 平均时间复杂度与最坏时间复杂度都是O(nlogn),稳定排序

归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。


void
merge(
int *array, int *aux, int begin, int mid, int end)
{
    
int len = end - begin + 1;
    memcpy(aux
+begin, array+begin, sizeof(int)*len);

    
int *first, *second, *ptr = array+begin;
    first 
= aux+begin;
    second 
= aux+mid+1;
    
while(first<=aux+mid && second<=aux+end) {
        
if(*first <= *second)
            
*ptr++ = *first++;
        
else
            
*ptr++ = *second++;
    }
    
if(first <= aux+mid)
        
while(first <= aux+mid)
            
*ptr++ = *first++;
    
if(second <= aux+end)
        
while(second <= aux+end)
            
*ptr++ = *second++;
}

void
merge_sort_dc(
int *array, int *aux, int begin, int end)
{
    
if(begin >= end)
        
return;
    
int mid = begin + ((end-begin)>>1);
    merge_sort_dc(array, aux, begin, mid);
    merge_sort_dc(array, aux, mid
+1, end);
    merge(array, aux, begin, mid, end);
}

void 
merge_sort(
int *array, int len)
{
    
int *aux = (int *)malloc(sizeof(int* len);
    merge_sort_dc(array, aux, 
0, len-1);
    free(aux);
}

struct Node {
    
int val;
    
struct Node *next;
};

struct Node *
list_merge(
struct Node *first, struct Node *second)
{
    
if(first == NULL)
        
return second;
    
if(second == NULL)
        
return first;

    
struct Node *node = NULL;
    
if(first->val <= second->val) {
        node 
= first;
        first 
= first->next;
    } 
else {
        node 
= second;
        second 
= second->next;
    }
    node
->next = list_merge(first, second);
    
return node;
}

struct Node *
list_merge_sort(
struct Node *list)
{
    
if(list==NULL || list->next==NULL)
        
return list;
    
struct Node *once = list;
    
struct Node *twice = list;
    
while(twice->next && twice->next->next) {
        once 
= once->next;
        twice 
= twice->next->next;
    }
    twice 
= once->next;
    once
->next = NULL;
    once 
= list;
    list_merge(list_merge_sort(once), list_merge_sort(twice));
}
posted @ 2011-07-29 19:39 simplyzhao 阅读(322) | 评论 (0)编辑 收藏
快速排序: 平均时间复杂度O(nlogn),最坏时间复杂度O(n^2),非稳定排序

快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j]交换的时刻(这里所展示的是partition的第二种方案, partition2)。

partition1与partition2的优劣具体参考《编程珠玑》 第11章

int
partition1(
int *array, int begin, int end)
{
    
int pivot = array[begin];
    
int i, j = begin;
    
for(i=begin+1; i<=end; ++i) {
        
if(array[i] < pivot) {
            
++j;
            
if(i != j)
                swap(array
+i, array+j);
        }
    }
    
if(begin != j)
        swap(array
+begin, array+j);
    
return j;
}

int
partition2(
int *array, int begin, int end)
{
    
int i, j, pivot = array[begin];
    i 
= begin+1;
    j 
= end;
    
while(1) {
        
while(i<=end && array[i]<pivot)
            
++i;
        
while(j>begin && array[j]>pivot)
            
--j;
        
if(i >= j)
            
break;
        swap(array
+i, array+j);
        
++i;
        
--j;
    }
    
if(begin != j)
        swap(array
+begin, array+j);
    
return j;
}

void
quick_sort(
int *array, int begin, int end)
{
    
if(begin >= end)
        
return;
    
int mid = partition2(array, begin, end);
    quick_sort(array, begin, mid
-1);
    quick_sort(array, mid
+1, end);
}
posted @ 2011-07-29 12:14 simplyzhao 阅读(128) | 评论 (0)编辑 收藏
希尔排序: 针对插入排序的改进,缩小增量的思想,非稳定排序

希尔排序(Shell sort)也称“缩小增量排序”。它的做法不是每次一个元素挨一个元素的比较。而是先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行一次直接插入排序。这样大大减少了记录移动次数,提高了排序效率。 
算法思路:先取一个正整数d1(d1<n),把全部记录分成d1个组,所有距离为dl的倍数的记录看成是一组,然后在各组内进行插入排序;接着取d2(d2<d1),重复上述分组和排序操作;直到di=1 (i>=1),即所有记录成为一个组为止。希尔排序对增量序列的选择没有严格规定,一般选d1约为n/2,d2为d1/2,d3为d2/2,…,di=1。

在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。因此,希尔排序在效率上较直接插人排序有较大的改进。

希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

void
shell_sort(
int *array, int len)
{
    
int i, j, step, backup;
    
for(step=len>>1; step>0; step>>=1) {
        
for(i=step; i<len; ++i) {
            backup 
= array[i];
            
for(j=i-step; j>=0 && array[j]>backup; j-=step) 
                array[j
+step] = array[j];
            array[j
+step] = backup;
        }
    }
}
posted @ 2011-07-28 18:10 simplyzhao 阅读(129) | 评论 (0)编辑 收藏
仅列出标题
共21页: 1 2 3 4 5 6 7 8 9 Last 

导航

<2024年8月>
28293031123
45678910
11121314151617
18192021222324
25262728293031
1234567

统计

常用链接

留言簿(1)

随笔分类

随笔档案

搜索

最新评论

阅读排行榜

评论排行榜