解释一下这个关于局部变量与matlab 全局变量量的程序,解释结果怎么得到

1694人阅读
局部变量:在一个函数内部定义的变量是内部变量,它只在本函数范围内有效,也就是说只有在本函数内才能使用它们,在此函数以外时不能使用这些变量的,它们称为局部变量;说明:1.主函数main中定义的变量也只在主函数中有效,而不因为在主函数中定义而在整个文件或程序中有效2.不同函数中可以使用名字相同的变量,它们代表不同的对象,互不干扰3.形式参数也使局部变量4.在一个函数内部,可以在复合语句中定义变量,这些变量只在本符合语句中有效全局变量:在函数外定义的变量是外部变量,外部变量是全局变量,全局变量可以为本文件中其它函数所共用,它的有效范围从定义变量的位置开始到本源文件结束;说明:1.设全局变量的作用:增加了函数间数据联系的渠道2.建议不再必要的时候不要使用全局变量,因为a.全局变量在程序的全部执行过程中都占用存储单元;
b.它使函数的通用性降低了c.使用全局变量过多,会降低程序的清晰性3.如果外部变量在文件开头定义,则在整个文件范围内都可以使用该外部变量,如果不再文件开头定义,按上面规定作用范围只限于定义点到文件终了。如果在定义点之前的函数想引用该外部变量,则应该在该函数中用关键字extern作外部变量说明4.如果在同一个源文件中,外部变量与局部变量同名,则在局部变量的作用范围内,外部变量不起作用;静态变量:在程序运行期间分配固定的存储空间的变量,叫做静态变量&
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:48025次
排名:千里之外
原创:20篇
转载:30篇
评论:12条
(5)(12)(1)(2)(3)(12)(7)(3)(5)第4节 全局变量、局部变量和作用域
更新于 01:44:57
&&&&& 我们把函数中定义的变量称为局部变量(Local Variable),由于形参相当于函数中定义的变量,所以形参也是一种局部变量。在这里&局部&有两层含义:
&&&&& 1、一个函数中定义的变量不能被另一个函数使用。例如print_time中的hour和minute在main函数中没有定义,不能使用,同样main函数中的局部变量也不能被print_time函数使用。如果这样定义:
&&&&& main函数中定义了局部变量hour,print_time函数中也有参数hour,虽然它们名称相同,但仍然是两个不同的变量,代表不同的存储单元。main函数的局部变量minute和print_time函数的参数minute也是如此。
&&&&& 2、每次调用函数时局部变量都表示不同的存储空间。局部变量在每次函数调用时分配存储空间,在每次函数返回时释放存储空间,例如调用print_time(23, 59)时分配hour和minute两个变量的存储空间,在里面分别存上23和59,函数返回时释放它们的存储空间,下次再调用print_time(12, 20)时又分配hour和minute的存储空间,在里面分别存上12和20。
&&&&& 与局部变量的概念相对的是全局变量(Global Variable),全局变量定义在所有的函数体之外,它们在程序开始运行时分配存储空间,在程序结束时释放存储空间,在任何函数中都可以访问全局变量,例如:
&&&&& 例 3.5. 全局变量
&&&&& 正因为全局变量在任何函数中都可以访问,所以在程序运行过程中全局变量被读写的顺序从源代码中是看不出来的,源代码的书写顺序并不能反映函数的调用顺序。程序出现了Bug往往就是因为在某个不起眼的地方对全局变量的读写顺序不正确,如果代码规模很大,这种错误是很难找到的。而对局部变量的访问不仅局限在一个函数内部,而且局限在一次函数调用之中,从函数的源代码很容易看出访问的先后顺序是怎样的,所以比较容易找到Bug。因此,虽然全局变量用起来很方便,但一定要慎用,能用函数传参代替的就不要用全局变量。
&&&&& 如果全局变量和局部变量重名了会怎么样呢?如果上面的例子改为:
&&&&& 例 3.6. 作用域
&&&&& 则第一次调用print_time打印的是全局变量的值,第二次直接调用printf打印的则是main函数局部变量的值。在C语言中每个标识符都有特定的作用域,全局变量是定义在所有函数体之外的标识符,它的作用域从定义的位置开始直到源文件结束,而main函数局部变量的作用域仅限于main函数之中。如上图所示,设想整个源文件是一张大纸,也就是全局变量的作用域,而main函数是盖在这张大纸上的一张小纸,也就是main函数局部变量的作用域。在小纸上用到标识符hour和minute时应该参考小纸上的定义,因为大纸(全局变量的作用域)被盖住了,如果在小纸上用到某个标识符却没有找到它的定义,那么再去翻看下面的大纸上有没有定义,例如上图中的变量x。
&&&&& 到目前为止我们在初始化一个变量时都是用常量做Initializer,其实也可以用表达式做Initializer,但要注意一点:局部变量可以用类型相符的任意表达式来初始化,而全局变量只能用常量表达式(Constant Expression)初始化。例如,全局变量pi这样初始化是合法的:
&&&&& 但这样初始化是不合法的:
&&&&& 然而局部变量这样初始化却是可以的。程序开始运行时要用适当的值来初始化全局变量,所以初始值必须保存在编译生成的可执行文件中,因此初始值在编译时就要计算出来,然而上面第二种Initializer的值必须在程序运行时调用acos函数才能得到,所以不能用来初始化全局变量。请注意区分编译时和运行时这两个概念。为了简化编译器的实现,C语言从语法上规定全局变量只能用常量表达式来初始化,因此下面这种全局变量初始化是不合法的:
&&&&& 虽然在编译时计算出hour的初始值是可能的,但是minute / 60不是常量表达式,不符合语法规定,所以编译器不必想办法去算这个初始值。
&&&&& 如果全局变量在定义时不初始化则初始值是0,如果局部变量在定义时不初始化则初始值是不确定的。所以,局部变量在使用之前一定要先赋值,如果基于一个不确定的值做后续计算肯定会引入Bug。
&&&&& 如何证明&局部变量的存储空间在每次函数调用时分配,在函数返回时释放&?当我们想要确认某些语法规则时,可以查教材,也可以查C99,但最快捷的办法就是编个小程序验证一下:
&&&&& 例 3.7. 验证局部变量存储空间的分配和释放
&&&&& 第一次调用foo函数,分配变量i的存储空间,然后打印i的值,由于i未初始化,打印的应该是一个不确定的值,然后把i赋值为777,函数返回,释放i的存储空间。第二次调用foo函数,分配变量i的存储空间,然后打印i的值,由于i未初始化,如果打印的又是一个不确定的值,就证明了&局部变量的存储空间在每次函数调用时分配,在函数返回时释放&。分析完了,我们运行程序看看是不是像我们分析的这样:
&&&&& 结果出乎意料,第二次调用打印的i值正是第一次调用末尾赋给i的值777。有一种初学者是这样,原本就没有把这条语法规则记牢,或者对自己的记忆力没信心,看到这个结果就会想:哦那肯定是我记错了,改过来记吧,应该是&函数中的局部变量具有一直存在的固定的存储空间,每次函数调用时使用它,返回时也不释放,再次调用函数时它应该还能保持上次的值&。还有一种初学者是怀疑论者或不可知论者,看到这个结果就会想:教材上明明说&局部变量的存储空间在每次函数调用时分配,在函数返回时释放&,那一定是教材写错了,教材也是人写的,是人写的就难免出错,哦,连C99也这么写的啊,C99也是人写的,也难免出错,或者C99也许没错,但是反正运行结果就是错了,计算机这东西真靠不住,太容易受电磁干扰和宇宙射线影响了,我的程序写得再正确也有可能被干扰得不能正确运行。
&&&&& 这是初学者最常见的两种心态。不从客观事实和逻辑推理出发分析问题的真正原因,而仅凭主观臆断胡乱给问题定性,&说你有罪你就有罪&。先不要胡乱怀疑,我们再做一次实验,在两次foo函数调用之间插一个别的函数调用,结果就大不相同了:
&&&&& 结果是:
&&&&& 这一回,第二次调用foo打印的i值又不是777了而是0,&局部变量的存储空间在每次函数调用时分配,在函数返回时释放&这个结论似乎对了,但另一个结论又不对了:全局变量不初始化才是0啊,不是说&局部变量不初始化则初值不确定&吗?
&&&&& 关键的一点是,我说&初值不确定&,有没有说这个不确定值不能是0?有没有说这个不确定值不能是上次调用赋的值?在这里&不确定&的准确含义是:每次调用这个函数时局部变量的初值可能不一样,运行环境不同,函数的调用次序不同,都会影响到局部变量的初值。在运用逻辑推理时一定要注意,不要把必要条件(Necessary Condition)当充分条件(Sufficient Condition),这一点在Debug时尤其重要,看到错误现象不要轻易断定原因是什么,一定要考虑再三,找出它的真正原因。例如,不要看到第二次调用打印777就下结论&函数中的局部变量具有一直存在的固定的存储空间,每次函数调用时使用它,返回时也不释放,再次调用函数时它应该还能保持上次的值&,这个结论倒是能推出777这个结果,但反过来由777这个结果却不能推出这样的结论。所以说777这个结果是该结论的必要条件,但不是充分条件。也不要看到第二次调用打印0就断定&局部变量未初始化则初值为0&,0这个结果是该结论的必要条件,但也不是充分条件。至于为什么会有这些现象,为什么这个不确定的值刚好是777,或者刚好是0,等学到例 19.1 &研究函数的调用过程&就能解释这些现象了。
&&&&& 从第 2 节 &自定义函数&介绍的语法规则可以看出,非定义的函数声明也可以写在局部作用域中,例如:
&&&&& 这样声明的标识符print_time具有局部作域,只在main函数中是有效的函数名,出了main函数就不存在print_time这个标识符了。
&&&&& 写非定义的函数声明时参数可以只写类型而不起名,例如上面代码中的void print_time(int, int);,只要告诉编译器参数类型是什么,编译器就能为print_time(23, 59)函数调用生成正确的指令。另外注意,虽然在一个函数体中可以声明另一个函数,但不能定义另一个函数,C语言不允许嵌套定义函数[5]。
[5] 但gcc的扩展特性允许嵌套定义函数,本书不做详细讨论。
&&& 下一页&&
| &|&&|&&|&
旗下网站: |
与非门科技(北京)有限公司 All Rights Reserved.
京ICP证:070212号 北京市公安局备案编号: 京ICP备:号请问,在易语言中。哪种情况下,需要添加:局部变量、程序集变量、全局变量。 (请详细说明) 或_百度知道
请问,在易语言中。哪种情况下,需要添加:局部变量、程序集变量、全局变量。 (请详细说明) 或
请问,在易语言中。哪种情况下,需要添加:局部变量、程序集变量、全局变量。 (请详细说明)
提问者采纳
1、局部变量:指在程序中,只在特定过程或函数中可以访问的变量,是相对与全局变量而言的。全局变量也称外部变量,是在函数外部定义的,作用域为从变量定义处开始,到本程序文件的末尾。全局变量全部存放在静态存储区,在程序开始执行时给全局变量分配存储区,程序完毕就释放。由于Java是面向对象语言,所有变量都是类成员,所以Java只有局部变量和成员变量,没有全局变量。同样地,在C++、C#、Ruby这些面向对象语言中,一般也只使用局部变量。面向对象编程是现在普遍采用的软件开发方法,因此现在已无需考虑是局部还是全局变量,说到变量,往往都是局部变量。2、程序集变量:就是扩大范围的局部变量,针对整个窗口的。3、全局变量:也称为外部变量,它是在函数外部定义的变量。 它不属于哪一个函数,它属于一个源程序文件。其作用域是从定义该变量的位置开始至源文件结束。解释 在函数中使用全局变量,一般应作全局变量声明。 只有在函数内经过声明的全局变量才能使用。 但在一个函数之前定义的全局变量,在该函数内使用可不再加以声明。
提问者评价
太给力了,你的回答完美解决了我的问题!
其他类似问题
等待您来回答
下载知道APP
随时随地咨询
出门在外也不愁范例:全局变量、局部变量和静态局部变量的应用示例。 【要求】(1) 分析并写出下列程序的执行结果,然后输_百度知道
范例:全局变量、局部变量和静态局部变量的应用示例。 【要求】(1) 分析并写出下列程序的执行结果,然后输
【要求】(1) 分析并写出下列程序的执行结果,然后输入计算机执行,比较分析结果与执行结果。如果两结果不相同请分析原因。#include&iostream&int a=300,b=400,c=500;void funa(int c){ static int a=5; a+=c; cout&&a&&' '&&c&&'\n';}void funb(int a){ a=b; cout&&a&&'\n';}void func(){ int c=0; cout&&a&&' '&&b&&' '&&c&&'\n';
::c-=100;}int main(){ funa(a);funb(b);funa(b);func();
cout&&a&&' '&&b&&' '&&c&&'\n';
return 0;}
我有更好的答案
305 300400705 400300 400 0300 400 400
那个 705 和400 是怎么输出的 并且 4个cout 为什么有五个输出值
其他类似问题
全局变量的相关知识
等待您来回答
下载知道APP
随时随地咨询
出门在外也不愁堆和栈的区别,全局变量、局部变量、静态变量(转)
堆和栈的区别
一、预备知识—程序的内存分配
一个由c/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)—&由编译器自动分配释放&,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap)&—&一般由程序员分配释放,&若程序员不释放,程序结束时可能由OS回收&。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,&未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。&-&程序结束后有系统释放&
4、文字常量区—常量字符串就是放在这里的。&程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
二、例子程序&
这是一个前辈写的,非常详细&
//main.cpp&
int&a&=&0;&全局初始化区&
char&*p1;&全局未初始化区&
int&b;&栈&
char&s[]&=&"abc";&栈&
char&*p2;&栈&
char&*p3&=&"123456";&在常量区,p3在栈上。&
static&int&c&=0;&全局(静态)初始化区&
p1&=&(char&*)malloc(10);&
p2&=&(char&*)malloc(20);&
分配得来得10和20字节的区域就在堆区。&
strcpy(p1,&"123456");&放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。&
二、堆和栈的理论知识&
2.1申请方式&
由系统自动分配。&例如,声明在函数中一个局部变量&int&b;&系统自动在栈中为b开辟空间&
需要程序员自己申请,并指明大小,在c中malloc函数&
如p1&=&(char&*)malloc(10);&
在C++中用new运算符&
如p2&=&(char&*)malloc(10);&
但是注意p1、p2本身是在栈中的。&
申请后系统的响应&
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。&
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,&
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。&
2.3申请大小的限制&
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。&
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。&
2.4申请效率的比较:&
栈由系统自动分配,速度较快。但程序员是无法控制的。&
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.&
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。&
2.5堆和栈中的存储内容&
栈:&在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。&
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。&
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。&
2.6存取效率的比较&
char&s1[]&=&"aaaaaaaaaaaaaaa";&
char&*s2&=&"bbbbbbbbbbbbbbbbb";&
aaaaaaaaaaa是在运行时刻赋值的;&
而bbbbbbbbbbb是在编译时就确定的;&
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。&
void&main()&
char&a&=&1;&
char&c[]&=&"";&
char&*p&="";&
a&=&c[1];&
a&=&p[1];&
对应的汇编代码&
10:&a&=&c[1];&
&8A&4D&F1&mov&cl,byte&ptr&[ebp-0Fh]&
0040106A&88&4D&FC&mov&byte&ptr&[ebp-4],cl&
11:&a&=&p[1];&
0040106D&8B&55&EC&mov&edx,dword&ptr&[ebp-14h]&
&8A&42&01&mov&al,byte&ptr&[edx+1]&
&88&45&FC&mov&byte&ptr&[ebp-4],al&
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。&
2.7小结:&
堆和栈的区别可以用如下的比喻来看出:&
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。&
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。&
windows进程中的内存结构
在阅读本文之前,如果你连堆栈是什么多不知道的话,请先阅读文章后面的基础知识。&
接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据。那么这些变量在内存中是如何存放的呢?程序又是如何使用这些变量的呢?下面就会对此进行深入的讨论。下文中的C语言代码如没有特别声明,默认都使用VC编译的release版。&
首先,来了解一下&C&语言的变量是如何在内存分部的。C&语言有全局变量(Global)、本地变量(Local),静态变量(Static)、寄存器变量(Regeister)。每种变量都有不同的分配方式。先来看下面这段代码:&
#include&&stdio.h&&
int&g1=0,&g2=0,&g3=0;&
int&main()&
static&int&s1=0,&s2=0,&s3=0;&
int&v1=0,&v2=0,&v3=0;&
//打印出各个变量的内存地址&
printf("0xx\n",&v1);&//打印各本地变量的内存地址&
printf("0xx\n",&v2);&
printf("0xx\n\n",&v3);&
printf("0xx\n",&g1);&//打印各全局变量的内存地址&
printf("0xx\n",&g2);&
printf("0xx\n\n",&g3);&
printf("0xx\n",&s1);&//打印各静态变量的内存地址&
printf("0xx\n",&s2);&
printf("0xx\n\n",&s3);&
return&0;&
编译后的执行结果是:&
0x0012ff78&
0x0012ff7c&
0x0012ff80&
0x004068dc&
输出的结果就是变量的内存地址。其中v1,v2,v3是本地变量,g1,g2,g3是全局变量,s1,s2,s3是静态变量。你可以看到这些变量在内存是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。&
├———————┤低端内存区域&
│&……&│&
├———————┤&
│&动态数据区&│&
├———————┤&
│&……&│&
├———————┤&
│&代码区&│&
├———————┤&
│&静态数据区&│&
├———————┤&
│&……&│&
├———————┤高端内存区域&
堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。我们可以先了解一下函数调用的过程,以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函数调用规定,这些因素有参数的压入规则和堆栈的平衡。windows&API的调用规则和ANSI&C的函数调用规则是不一样的,前者由被调函数调整堆栈,后者由调用者调整堆栈。两者通过“__stdcall”和“__cdecl”前缀区分。先看下面这段代码:&
#include&&stdio.h&&
void&__stdcall&func(int&param1,int&param2,int&param3)&
int&var1=param1;&
int&var2=param2;&
int&var3=param3;&
printf("0xx\n",&m1);&//打印出各个变量的内存地址&
printf("0xx\n",&m2);&
printf("0xx\n\n",&m3);&
printf("0xx\n",&var1);&
printf("0xx\n",&var2);&
printf("0xx\n\n",&var3);&
int&main()&
func(1,2,3);&
return&0;&
编译后的执行结果是:&
0x0012ff78&
0x0012ff7c&
0x0012ff80&
0x0012ff68&
0x0012ff6c&
0x0012ff70&
├———————┤&—函数执行时的栈顶(ESP)、低端内存区域&
│&……&│&
├———————┤&
│&var&1&│&
├———————┤&
│&var&2&│&
├———————┤&
│&var&3&│&
├———————┤&
│&RET&│&
├———————┤&—“__cdecl”函数返回后的栈顶(ESP)&
│&parameter&1&│&
├———————┤&
│&parameter&2&│&
├———————┤&
│&parameter&3&│&
├———————┤&—“__stdcall”函数返回后的栈顶(ESP)&
│&……&│&
├———————┤&—栈底(基地址&EBP)、高端内存区域&
上图就是函数调用过程中堆栈的样子了。首先,三个参数以从又到左的次序压入堆栈,先压“param3”,再压“param2”,最后压入“param1”;然后压入函数的返回地址(RET),接着跳转到函数地址接着执行(这里要补充一点,介绍UNIX下的缓冲溢出原理的文章中都提到在压入RET后,继续压入当前EBP,然后用当前ESP代替EBP。然而,有一篇介绍windows下函数调用的文章中说,在windows下的函数调用也有这一步骤,但根据我的实际调试,并未发现这一步,这还可以从param3和var1之间只有4字节的间隙这点看出来);第三步,将栈顶(ESP)减去一个数,为本地变量分配内存空间,上例中是减去12字节(ESP=ESP-3*4,每个int变量占用4个字节);接着就初始化本地变量的内存空间。由于“__stdcall”调用由被调函数调整堆栈,所以在函数返回前要恢复堆栈,先回收本地变量占用的内存(ESP=ESP+3*4),然后取出返回地址,填入EIP寄存器,回收先前压入参数占用的内存(ESP=ESP+3*4),继续执行调用者的代码。参见下列汇编代码:&
;--------------func&函数的汇编代码-------------------&
:&83EC0C&sub&esp,&0000000C&//创建本地变量的内存空间&
:&8B442410&mov&eax,&dword&ptr&[esp+10]&
:&8B4C2414&mov&ecx,&dword&ptr&[esp+14]&
:0040100B&8B542418&mov&edx,&dword&ptr&[esp+18]&
:0040100F&&mov&dword&ptr&[esp],&eax&
:&8D442410&lea&eax,&dword&ptr&[esp+10]&
:&894C2404&mov&dword&ptr&[esp+04],&ecx&
……………………(省略若干代码)&
:&83C43C&add&esp,&0000003C&;恢复堆栈,回收本地变量的内存空间&
:&C3&ret&000C&;函数返回,恢复参数占用的内存空间&
;如果是“__cdecl”的话,这里是“ret”,堆栈将由调用者恢复&
;-------------------函数结束-------------------------&
;--------------主程序调用func函数的代码--------------&
:&6A03&push&&//压入参数param3&
:&6A02&push&&//压入参数param2&
:&6A01&push&&//压入参数param1&
:&E875FFFFFF&call&&//调用func函数&
;如果是“__cdecl”的话,将在这里恢复堆栈,“add&esp,&0000000C”&
聪明的读者看到这里,差不多就明白缓冲溢出的原理了。先来看下面的代码:&
#include&&stdio.h&&
#include&&string.h&&
void&__stdcall&func()&
char&lpBuff[8]="\0";&
strcat(lpBuff,"AAAAAAAAAAA");&
int&main()&
return&0;&
编译后执行一下回怎么样?哈,“"0x"指令引用的"0x"内存。该内存不能为"read"。”,“非法操作”喽!"41"就是"A"的16进制的ASCII码了,那明显就是strcat这句出的问题了。"lpBuff"的大小只有8字节,算进结尾的\0,那strcat最多只能写入7个"A",但程序实际写入了11个"A"外加1个\0。再来看看上面那幅图,多出来的4个字节正好覆盖了RET的所在的内存空间,导致函数返回到一个错误的内存地址,执行了错误的指令。如果能精心构造这个字符串,使它分成三部分,前一部份仅仅是填充的无意义数据以达到溢出的目的,接着是一个覆盖RET的数据,紧接着是一段shellcode,那只要着个RET地址能指向这段shellcode的第一个指令,那函数返回时就能执行shellcode了。但是软件的不同版本和不同的运行环境都可能影响这段shellcode在内存中的位置,那么要构造这个RET是十分困难的。一般都在RET和shellcode之间填充大量的NOP指令,使得exploit有更强的通用性。&
├———————┤&—低端内存区域&
│&……&│&
├———————┤&—由exploit填入数据的开始&
│&buffer&│&—填入无用的数据&
├———————┤&
│&RET&│&—指向shellcode,或NOP指令的范围&
├———————┤&
│&NOP&│&
│&……&│&—填入的NOP指令,是RET可指向的范围&
│&NOP&│&
├———————┤&
│&shellcode&│&
├———————┤&—由exploit填入数据的结束&
│&……&│&
├———————┤&—高端内存区域&
windows下的动态数据除了可存放在栈中,还可以存放在堆中。了解C++的朋友都知道,C++可以使用new关键字来动态分配内存。来看下面的C++代码:&
#include&&stdio.h&&
#include&&iostream.h&&
#include&&windows.h&&
void&func()&
char&*buffer=new&char[128];&
char&bufflocal[128];&
static&char&buffstatic[128];&
printf("0xx\n",buffer);&//打印堆中变量的内存地址&
printf("0xx\n",bufflocal);&//打印本地变量的内存地址&
printf("0xx\n",buffstatic);&//打印静态变量的内存地址&
void&main()&
程序执行结果为:&
0x0012ff04&
可以发现用new关键字分配的内存即不在栈中,也不在静态数据区。VC编译器是通过windows下的“堆(heap)”来实现new关键字的内存动态分配。在讲“堆”之前,先来了解一下和“堆”有关的几个API函数:&
HeapAlloc&在堆中申请内存空间&
HeapCreate&创建一个新的堆对象&
HeapDestroy&销毁一个堆对象&
HeapFree&释放申请的内存&
HeapWalk&枚举堆对象的所有内存块&
GetProcessHeap&取得进程的默认堆对象&
GetProcessHeaps&取得进程所有的堆对象&
LocalAlloc&
GlobalAlloc&
当进程初始化时,系统会自动为进程创建一个默认堆,这个堆默认所占内存的大小为1M。堆对象由系统进行管理,它在内存中以链式结构存在。通过下面的代码可以通过堆动态申请内存空间:&
HANDLE&hHeap=GetProcessHeap();&
char&*buff=HeapAlloc(hHeap,0,8);&
其中hHeap是堆对象的句柄,buff是指向申请的内存空间的地址。那这个hHeap究竟是什么呢?它的值有什么意义吗?看看下面这段代码吧:&
#pragma&comment(linker,"/entry:main")&//定义程序的入口&
#include&&windows.h&&
_CRTIMP&int&(__cdecl&*printf)(const&char&*,&...);&//定义STL函数printf&
void&main()&
HANDLE&hHeap=GetProcessHeap();&
char&*buff=HeapAlloc(hHeap,0,0x10);&
char&*buff2=HeapAlloc(hHeap,0,0x10);&
HMODULE&hMsvcrt=LoadLibrary("msvcrt.dll");&
printf=(void&*)GetProcAddress(hMsvcrt,"printf");&
printf("0xx\n",hHeap);&
printf("0xx\n",buff);&
printf("0xx\n\n",buff2);&
执行结果为:&
hHeap的值怎么和那个buff的值那么接近呢?其实hHeap这个句柄就是指向HEAP首部的地址。在进程的用户区存着一个叫PEB(进程环境块)的结构,这个结构中存放着一些有关进程的重要信息,其中在PEB首地址偏移0x18处存放的ProcessHeap就是进程默认堆的地址,而偏移0x90处存放了指向进程所有堆的地址列表的指针。windows有很多API都使用进程的默认堆来存放动态数据,如windows&2000下的所有ANSI版本的函数都是在默认堆中申请内存来转换ANSI字符串到Unicode字符串的。对一个堆的访问是顺序进行的,同一时刻只能有一个线程访问堆中的数据,当多个线程同时有访问要求时,只能排队等待,这样便造成程序执行效率下降。&
最后来说说内存中的数据对齐。所位数据对齐,是指数据所在的内存地址必须是该数据长度的整数倍,DWORD数据的内存起始地址能被4除尽,WORD数据的内存起始地址能被2除尽,x86&CPU能直接访问对齐的数据,当他试图访问一个未对齐的数据时,会在内部进行一系列的调整,这些调整对于程序来说是透明的,但是会降低运行速度,所以编译器在编译程序时会尽量保证数据对齐。同样一段代码,我们来看看用VC、Dev-C++和lcc三个不同编译器编译出来的程序的执行结果:&
#include&&stdio.h&&
int&main()&
printf("0xx\n",&a);&
printf("0xx\n",&b);&
printf("0xx\n",&c);&
return&0;&
这是用VC编译后的执行结果:&
0x0012ff7c&
0x0012ff7b&
0x0012ff80&
变量在内存中的顺序:b(1字节)-a(4字节)-c(4字节)。&
这是用Dev-C++编译后的执行结果:&
0x0022ff7c&
0x0022ff7b&
0x0022ff74&
变量在内存中的顺序:c(4字节)-中间相隔3字节-b(占1字节)-a(4字节)。&
这是用lcc编译后的执行结果:&
0x0012ff6c&
0x0012ff6b&
0x0012ff64&
变量在内存中的顺序:同上。&
三个编译器都做到了数据对齐,但是后两个编译器显然没VC“聪明”,让一个char占了4字节,浪费内存哦。&
基础知识:&
堆栈是一种简单的数据结构,是一种只允许在其一端进行插入或删除的线性表。允许插入或删除操作的一端称为栈顶,另一端称为栈底,对堆栈的插入和删除操作被称为入栈和出栈。有一组CPU指令可以实现对进程的内存实现堆栈访问。其中,POP指令实现出栈操作,PUSH指令实现入栈操作。CPU的ESP寄存器存放当前线程的栈顶指针,EBP寄存器中保存当前线程的栈底指针。CPU的EIP寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。&
参考:《Windows下的HEAP溢出及其利用》by:&isno&
《windows核心编程》by:&Jeffrey&Richter&
摘要:&讨论常见的堆性能问题以及如何防范它们。(共&9&页)
您是否是动态分配的&C/C++&对象忠实且幸运的用户?您是否在模块间的往返通信中频繁地使用了“自动化”?您的程序是否因堆分配而运行起来很慢?不仅仅您遇到这样的问题。几乎所有项目迟早都会遇到堆问题。大家都想说,“我的代码真正好,只是堆太慢”。那只是部分正确。更深入理解堆及其用法、以及会发生什么问题,是很有用的。
什么是堆?
(如果您已经知道什么是堆,可以跳到“什么是常见的堆性能问题?”部分)
在程序中,使用堆来动态分配和释放对象。在下列情况下,调用堆操作:&
事先不知道程序所需对象的数量和大小。
对象太大而不适合堆栈分配程序。
堆使用了在运行时分配给代码和堆栈的内存之外的部分内存。下图给出了堆分配程序的不同层。
GlobalAlloc/GlobalFree:Microsoft&Win32&堆调用,这些调用直接与每个进程的默认堆进行对话。
LocalAlloc/LocalFree:Win32&堆调用(为了与&Microsoft&Windows&NT&兼容),这些调用直接与每个进程的默认堆进行对话。
COM&的&IMalloc&分配程序(或&CoTaskMemAlloc&/&CoTaskMemFree):函数使用每个进程的默认堆。自动化程序使用“组件对象模型&(COM)”的分配程序,而申请的程序使用每个进程堆。
C/C++&运行时&(CRT)&分配程序:提供了&malloc()&和&free()&以及&new&和&delete&操作符。如&Microsoft&Visual&Basic&和&Java&等语言也提供了新的操作符并使用垃圾收集来代替堆。CRT&创建自己的私有堆,驻留在&Win32&堆的顶部。
Windows&NT&中,Win32&堆是&Windows&NT&运行时分配程序周围的薄层。所有&API&转发它们的请求给&NTDLL。
Windows&NT&运行时分配程序提供&Windows&NT&内的核心堆分配程序。它由具有&128&个大小从&8&到&1,024&字节的空闲列表的前端分配程序组成。后端分配程序使用虚拟内存来保留和提交页。
在图表的底部是“虚拟内存分配程序”,操作系统使用它来保留和提交页。所有分配程序使用虚拟内存进行数据的存取。
分配和释放块不就那么简单吗?为何花费这么长时间?
堆实现的注意事项
传统上,操作系统和运行时库是与堆的实现共存的。在一个进程的开始,操作系统创建一个默认堆,叫做“进程堆”。如果没有其他堆可使用,则块的分配使用“进程堆”。语言运行时也能在进程内创建单独的堆。(例如,C&运行时创建它自己的堆。)除这些专用的堆外,应用程序或许多已载入的动态链接库&(DLL)&之一可以创建和使用单独的堆。Win32&提供一整套&API&来创建和使用私有堆。有关堆函数(英文)的详尽指导,请参见&MSDN。
当应用程序或&DLL&创建私有堆时,这些堆存在于进程空间,并且在进程内是可访问的。从给定堆分配的数据将在同一个堆上释放。(不能从一个堆分配而在另一个堆释放。)
在所有虚拟内存系统中,堆驻留在操作系统的“虚拟内存管理器”的顶部。语言运行时堆也驻留在虚拟内存顶部。某些情况下,这些堆是操作系统堆中的层,而语言运行时堆则通过大块的分配来执行自己的内存管理。不使用操作系统堆,而使用虚拟内存函数更利于堆的分配和块的使用。
典型的堆实现由前、后端分配程序组成。前端分配程序维持固定大小块的空闲列表。对于一次分配调用,堆尝试从前端列表找到一个自由块。如果失败,堆被迫从后端(保留和提交虚拟内存)分配一个大块来满足请求。通用的实现有每块分配的开销,这将耗费执行周期,也减少了可使用的存储空间。
Knowledge&Base&文章&Q10758,“用&calloc()&和&malloc()&管理内存”&(搜索文章编号),&包含了有关这些主题的更多背景知识。另外,有关堆实现和设计的详细讨论也可在下列著作中找到:“Dynamic&Storage&Allocation:&A&Survey&and&Critical&Review”,作者&Paul&R.&Wilson、Mark&S.&Johnstone、Michael&Neely&和&David&Boles;“International&Workshop&on&Memory&Management”,&作者&Kinross,&Scotland,&UK,&1995&年&9&月()(英文)。
Windows&NT&的实现(Windows&NT&版本&4.0&和更新版本)&使用了&127&个大小从&8&到&1,024&字节的&8&字节对齐块空闲列表和一个“大块”列表。“大块”列表(空闲列表[0])&保存大于&1,024&字节的块。空闲列表容纳了用双向链表链接在一起的对象。默认情况下,“进程堆”执行收集操作。(收集是将相邻空闲块合并成一个大块的操作。)收集耗费了额外的周期,但减少了堆块的内部碎片。
单一全局锁保护堆,防止多线程式的使用。(请参见“Server&Performance&and&Scalability&Killers”中的第一个注意事项,&George&Reilly&所著,在&“MSDN&Online&Web&Workshop”上(站点:(英文)。)单一全局锁本质上是用来保护堆数据结构,防止跨多线程的随机存取。若堆操作太频繁,单一全局锁会对性能有不利的影响。
什么是常见的堆性能问题?
以下是您使用堆时会遇到的最常见问题:&
分配操作造成的速度减慢。光分配就耗费很长时间。最可能导致运行速度减慢原因是空闲列表没有块,所以运行时分配程序代码会耗费周期寻找较大的空闲块,或从后端分配程序分配新块。
释放操作造成的速度减慢。释放操作耗费较多周期,主要是启用了收集操作。收集期间,每个释放操作“查找”它的相邻块,取出它们并构造成较大块,然后再把此较大块插入空闲列表。在查找期间,内存可能会随机碰到,从而导致高速缓存不能命中,性能降低。
堆竞争造成的速度减慢。当两个或多个线程同时访问数据,而且一个线程继续进行之前必须等待另一个线程完成时就发生竞争。竞争总是导致麻烦;这也是目前多处理器系统遇到的最大问题。当大量使用内存块的应用程序或&DLL&以多线程方式运行(或运行于多处理器系统上)时将导致速度减慢。单一锁定的使用—常用的解决方案—意味着使用堆的所有操作是序列化的。当等待锁定时序列化会引起线程切换上下文。可以想象交叉路口闪烁的红灯处走走停停导致的速度减慢。&
竞争通常会导致线程和进程的上下文切换。上下文切换的开销是很大的,但开销更大的是数据从处理器高速缓存中丢失,以及后来线程复活时的数据重建。
堆破坏造成的速度减慢。造成堆破坏的原因是应用程序对堆块的不正确使用。通常情形包括释放已释放的堆块或使用已释放的堆块,以及块的越界重写等明显问题。(破坏不在本文讨论范围之内。有关内存重写和泄漏等其他细节,请参见&Microsoft&Visual&C++(R)&调试文档&。)
频繁的分配和重分配造成的速度减慢。这是使用脚本语言时非常普遍的现象。如字符串被反复分配,随重分配增长和释放。不要这样做,如果可能,尽量分配大字符串和使用缓冲区。另一种方法就是尽量少用连接操作。
竞争是在分配和释放操作中导致速度减慢的问题。理想情况下,希望使用没有竞争和快速分配/释放的堆。可惜,现在还没有这样的通用堆,也许将来会有。
在所有的服务器系统中(如&IIS、MSProxy、DatabaseStacks、网络服务器、&Exchange&和其他),&堆锁定实在是个大瓶颈。处理器数越多,竞争就越会恶化。
尽量减少堆的使用
现在您明白使用堆时存在的问题了,难道您不想拥有能解决这些问题的超级魔棒吗?我可希望有。但没有魔法能使堆运行加快—因此不要期望在产品出货之前的最后一星期能够大为改观。如果提前规划堆策略,情况将会大大好转。调整使用堆的方法,减少对堆的操作是提高性能的良方。
如何减少使用堆操作?通过利用数据结构内的位置可减少堆操作的次数。请考虑下列实例:
struct&ObjectA&{
&&&//&objectA&的数据&
struct&ObjectB&{
&&&//&objectB&的数据&
//&同时使用&objectA&和&objectB
//&使用指针&
struct&ObjectB&{
&&&struct&ObjectA&*&pObjA;
&&&//&objectB&的数据&
//&使用嵌入
struct&ObjectB&{
&&&struct&ObjectA&pObjA;
&&&//&objectB&的数据&
//&集合&&&在另一对象内使用&objectA&和&objectB
struct&ObjectX&{
&&&struct&ObjectA&&objA;
&&&struct&ObjectB&&objB;
避免使用指针关联两个数据结构。如果使用指针关联两个数据结构,前面实例中的对象&A&和&B&将被分别分配和释放。这会增加额外开销—我们要避免这种做法。
把带指针的子对象嵌入父对象。当对象中有指针时,则意味着对象中有动态元素(百分之八十)和没有引用的新位置。嵌入增加了位置从而减少了进一步分配/释放的需求。这将提高应用程序的性能。
合并小对象形成大对象(聚合)。聚合减少分配和释放的块的数量。如果有几个开发者,各自开发设计的不同部分,则最终会有许多小对象需要合并。集成的挑战就是要找到正确的聚合边界。
内联缓冲区能够满足百分之八十的需要(aka&80-20&规则)。个别情况下,需要内存缓冲区来保存字符串/二进制数据,但事先不知道总字节数。估计并内联一个大小能满足百分之八十需要的缓冲区。对剩余的百分之二十,可以分配一个新的缓冲区和指向这个缓冲区的指针。这样,就减少分配和释放调用并增加数据的位置空间,从根本上提高代码的性能。
在块中分配对象(块化)。块化是以组的方式一次分配多个对象的方法。如果对列表的项连续跟踪,例如对一个&{名称,值}&对的列表,有两种选择:选择一是为每一个“名称-值”对分配一个节点;选择二是分配一个能容纳(如五个)“名称-值”对的结构。例如,一般情况下,如果存储四对,就可减少节点的数量,如果需要额外的空间数量,则使用附加的链表指针。&
块化是友好的处理器高速缓存,特别是对于&L1-高速缓存,因为它提供了增加的位置&—不用说对于块分配,很多数据块会在同一个虚拟页中。
正确使用&_amblksiz。C&运行时&(CRT)&有它的自定义前端分配程序,该分配程序从后端(Win32&堆)分配大小为&_amblksiz&的块。将&_amblksiz&设置为较高的值能潜在地减少对后端的调用次数。这只对广泛使用&CRT&的程序适用。
使用上述技术将获得的好处会因对象类型、大小及工作量而有所不同。但总能在性能和可升缩性方面有所收获。另一方面,代码会有点特殊,但如果经过深思熟虑,代码还是很容易管理的。
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

我要回帖

更多关于 全局变量 的文章

 

随机推荐