关于入栈、出栈,栈顶栈底之类的分析见
下面继续分析C/C++的内存分布。
虽然0x10比一个变量需要的4个地址大了一些,但是0x10应该是规定的最小单位了。假如你要用的空间刚好是它的整数倍,其实是不浪费一分钱栈空间的,下边做一个数组,证明栈空间大小刚好是所有非静态变量占用空间的大小。
这里可以先无视hello的声明,它不在栈空间,后面会分析。
x6 #include<stdio.h> x
x7 int main(){ x
x9 static int hello; x
B+>x11 int array[100] = {7,4,1}; x
x16 return 0; x
x17 }
下边是汇编代码:比普通变量情况下复杂些,多用了一些寄存器,多了一些push、pop操作,还有rep指令。
x0x8048394 <main> push %ebp x
x0x8048395 <main+1> mov %esp,%ebp x
x0x8048397 <main+3> push %edi x
x0x8048398 <main+4> push %ebx x
x0x8048399 <main+5> sub $0x190,%esp x
B+>x0x804839f <main+11> lea -0x198(%ebp),%ebx x
x0x80483a5 <main+17> mov $0x0,%eax x
x0x80483aa <main+22> mov $0x64,%edx x100个变量(应该还有个标志类型大小是4的东西吧)
x0x80483af <main+27> mov %ebx,%edi x
x0x80483b1 <main+29> mov %edx,%ecx x
x0x80483b3 <main+31> rep stos %eax,%es:(%edi) x
x0x80483b5 <main+33> movl $0x7,-0x198(%ebp) x初始化了三个数。
x0x80483bf <main+43> movl $0x4,-0x194(%ebp) x
x0x80483c9 <main+53> movl $0x1,-0x190(%ebp) x
x0x80483d3 <main+63> mov $0x0,%eax x
x0x80483d8 <main+68> add $0x190,%esp
x0x80483de <main+74> pop %ebx x
x0x80483df <main+75> pop %edi x
x0x80483e0 <main+76> pop %ebp x
x0x80483e1 <main+77> ret x
x
下边是栈空间大小。两者相减,0x190,等于400bytes,刚好是100个int。
(gdb) print $esp
$1 = (void *) 0xbffff480
(gdb) print $ebp
$2 = (void *) 0xbffff618
我们都知道,”加了static就延长了变量的生命周期到程序结束“,或者还知道”加了static的变量地址不太一样“!
确实,地址变了,静态变量不在栈里。
int main(){
static int hello;
int top;
return 0;
}
地址,top在栈中,而hello不在
(gdb) print &top
$3 = (int *) 0xbffff614
(gdb) print &hello
$4 = (int *) 0x8049614
栈大小是0x10
(gdb) print $esp
$3 = (void *) 0xbffff608
(gdb) print $ebp
$4 = (void *) 0xbffff618
去掉int型的声明,只留一个静态变量的声明,会发现一个有意思的现象
也就是说,连语句都被”优化“走了,不在main()函数里边执行了,不在main()函数里执行意味着什么?生命周期不光被向后延长到程序结束,也被向前延长了,它会在main之外,更早的地方被初始化。
那么如果我在静态变量声明语句之前,main()函数之内执行,究竟允不允许呢?可能语法上会做屏蔽,但是按运行顺序应该是可以的。
这句话换算成代码的话,是这样的:
#include<stdio.h>
int main(){
hello = 4;
static int hello;
return 0;
}
连初学者都认为这样不可能,但是,如果我换一种写法呢?
#include<stdio.h>
static int hello;
int main(){
hello = 4;
return 0;
}
这样大家就都认可了吧!
但是我觉得这两个本质上是等价的,
只不过编译器在码农层面把这个操作给屏蔽了,可能太奇葩不便于理解。个人觉得是这样的!不用去试了,确实编译不过!!!!
换成可编译的版本运行一次:
x0x8048394 <main> push %ebp x
x0x8048395 <main+1> mov %esp,%ebp x
B+>x0x8048397 <main+3> movl $0x4,0x8049614 x
x0x80483a1 <main+13> mov $0x0,%eax x
x0x80483a6 <main+18> pop %ebp x
x0x80483a7 <main+19> ret x
main内声明肯定是看不到的,直接用了一下而已,如果没有那个赋值语句,main内部什么命理你个都看不到,直接就结束了。
我又想到另一个法子,因为编译后我可以看地址, 有时候(可能吧)不做”变量列表“(也就是所有变量类型和数量的综合)级的改动,其实地址都是差不多的,所以可以尝试先编译运行,看到地址后,再强行提取变量,C语言的强大!(C语言不是这样玩的好么-_-)
这是我的设想:
#include<stdio.h>
int main(){
int *ptr = 0x8049614;
printf("%d\n",*ptr);
static int hello = 100;
return 0;
}
print *ptr = 0(期望100)
没成功,但是还有希望,因为这次编译地址变了:
再来:
#include<stdio.h>
//static int hello = 100;
int main(){
int *ptr = 0x8049578;
printf("%d\n",*ptr);
static int hello = 100;
return 0;
}
不过讨厌的是又变了。。。。。。。这个(全局变量区?怎么叫来着?)地址分配很动态,不如栈的地址那么稳固。
一定要抓住它,这下试试664。
终于被我抓住了。
静态变量hello的声明和定义都在13行,但我在12行就提前并打印了出来。
来来来,再来个普通运行。
然并卵,现实中哪有人运行完了再去看结果,硬把地址写进去的,我也不知道这么玩能应用在哪。
这个例子只为证明:
1.静态变量的存储区域不在栈,并且也不像栈的地址规则那么稳定,但是还是能在一定程度上稳定住的,所以能抓住。
2.遇到那个“static”,这句声明加定义就和放在main外边没区别,这种代码的执行等于在程序正式执行之前就做完了,算初始化吧。
3.所以,无论静态变量声明在哪,它的生命周期都贯穿程序始终。
4.明明很早就声明定义了,之所以你不能直接这样提取静态变量的值,应该只是编译器设的规则,来避免一些不必要的人为错误吧。
5.
static叫静态,静态静在哪?动态又动在哪?动态当然是动在你运行它才有结果,不运行就没有。栈虽然也会提前(运行函数体前)分配够空间,但是最起码变量的定义赋值是不会有的,而static,是一早就定义赋值完了,所以叫静态。(所以有充分理由怀疑编译器就是根据变量类型和数量,假设叫”变量列表“来分配栈空间的,谁叫我学渣没怎么学编译原理呢,只能先猜测一下)
6.不要模仿
扩展训练:
多加两层函数呢?
请容许我复习一下C++:
C++类静态成员是否会有所不同?是很早就初始化了,还是从某一个对象运行该函数开始?假如说也是带定义的形式,比如,static int i = 1;而不是static int i;
首先,第一种形式,ISO C++又不允许了(错误:ISO C++ 不允许在类内初始化非常量静态成员)。
其次,又不允许使用A::i这种形式访问A的静态成员变量i。
“奇怪”的发现:
当用一个类连续声明多个类对象的时候,后边的类对象被某种优化机制放在不一样的地方了(堆?全局静态区?)
具体流程如下:
第一次编译:
F f,f2;//对象在栈
第二次编译:
F f,f2,f3,f4,f5,f6,f7,f8,f9;//只有f和f2在栈
第二次编译:
F f,f2,f3,f4,f5,f6,f7,f8,f9;//只有f和f2在栈
//注:此时esp和ebp没变,栈没有扩容,是出于某种“懒惰”,还是“不屑”?
第三次编译:
F f,f2,f3,f4;
F f5,f6,f7,f8,f9;//对象都在栈
第四次编译:
F f,f2,f3,f4,f5,f6,f7,f8,f9;//所有对象都在栈。
第三次编译:
F f,f2,f3,f4;
F f5,f6,f7,f8,f9;//对象都在栈
第四次编译:
F f,f2,f3,f4,f5,f6,f7,f8,f9;//所有对象都在栈。
首先,排除人为错误,虽然分配堆的时候F *pF = F();//print &pF指针地址在栈,print pF指针指向的对象在堆。但是此处并不是指针,是实体。。。
编译器做了某种优化,这个优化的区分绝对不是代码使用不使用,f2就没使用,为什么和f一样放到栈了。但是
我的总结就是,这个对象变量上次有地址,这次就还有,因为之前只有
F f,f2;//导致了f2在栈,其他的f3到f8都是后来追加了,
编译器因为习惯,把f2保留在了栈,其他的扔一边去了。这次也是,因为拆分成两句进行声明时f4,f5们也有了栈地址,所以再合并回去的时候,
编译器出于习惯,继续把他们放在栈中。
所以说,编译器是带记忆(缓存)的 。
不过话说回来,一次声明太多对象变量,他们为什么被扔到其他位置了?这是“把未声明的变量放在静态变量区”了?
而如果把E e,e2;//突然变成
E e,e2,e3,e33,e333,e3333,e33333,e333333,e3333333,e33333333;//再去编译
结果所有对象也在栈中,随机性过强,所以不再纠结这种编译器的个例特性。浪费时间。
=======================================================================================================================
20160324:补充一个关于函数体与代码块的
关于入栈、出栈与栈帧,此文和前边的一篇关于函数调用的博文已经写得很清楚(也可能不清楚,两篇有重合,归类乱了点~~~~)
总之,关于函数的局部变量,因为是在存在当前栈帧的,所以外部变量在此时是完全屏蔽的,局部变量也活不到外部。
但是代码块是个特例(只是从表象知识点来说):
代码块可以声明“局部”变量,而对这个“局部”变量的修改不会影响到上一层的“此”变量,说起来乱,上代码:
#include<stdio.h>
int main()
{
int i = 0;
{
int j = 1;
i = 1;
printf("i:%d\n",i);
int i = 2;
printf("inside of block:i:%d\n",i);
printf("j:%d\n",j);
}
printf("outside of block:i:%d\n",i);
}
i的打印结果是1,2,1.
这里i是可以“重复声明”的,并且声明的局部的i的修改不会影响外部的。有些人根据表面概念,可能对此有些迷惑。这看似也是局部,代码块的局部和函数体的局部有什么区别呢?
首先确定的是,如果不重复声明,在代码块内的修改,对外部是全部生效的,这点通过for循环之类的代码块也会有所体会。
首先确定的是,如果不重复声明,在代码块内的修改,对外部是全部生效的,这点通过for循环之类的代码块也会有所体会。
那么如果重复声明,区别在哪呢?其实这个“重复声明”,不过是新声明的一个变量罢了,你把它假设为k,这样对k的任何修改,当然都不会影响i。
其他的机制啦,协调工作啦,都是编译器自己映射好了,不用管了。
不过这样做也有缺点,都要占用栈空间嘛,又不是你真的去短暂覆盖原变量过后再去恢复,没有个内存用来记忆怎么可能覆盖了再恢复?
这样的好处可能也就是你常用
for(int i = 0;condition;modify i);
这种东西的话,避免互相干扰吧。
所以,如果有需求,局部干脆还是换个名字(k)吧,本质一样免得降低可读性。
======================================================================================================================
关于main函数之前的那些汇编,我还不太懂,有空再摸索,想知道静态变量的初始化,各种类,还有虚函数表的初始化过程。