C/C++: 函数调用, 栈的大小是如何被确定的?

时间:2023-01-29 03:38:28
每个函数调用都要压栈, 
栈用来存放局部变量和形参以及状态指针,
可是很多时候变量是按程序逻辑动态创建的, 编译器不可能知道函数块需要多少局部变量啊?

比如这样一个函数: 
void test()
{
    if(someCondition) {double a = 123.0;} else {int b = 456;}  // 栈的大小应按sizeof(a) 还是 sizeof(b)计算? 
}

或者, 是我理解有误, 栈的大小可以伸缩?

66 个解决方案

#1


应该是默认分配给你一个大小的栈吧。 如你超过这个大小,就会运行报错. 

#2


sizeof(a) + sizeof(b)

#3


编译器做的工作: 1. 词法提取和分析  2.语法分析 3 链接  4生成可执行程序代码
其中词法分析是将整个代码扫描一遍,提取变量……

堆栈的实际长度随着局部变量的增多而变长

可以确定的是, 编译器先提取变量, 后压入堆栈中。

置于填充堆栈内容,我觉得是 “生成可执行程序代码” 这一步要完成的事情。

#4


栈的大小可以通过编译器选项指定。

在集成环境中,也可以可以做到,比如在VS2010中,可以:
(菜单)Project->工程名 Properties->Configuration Properties->Linker->System->***

也可以在代码中:
#pragma comment(linker, "/STACK:5000000 ")//用该语句设置/STACK:后面数字为容量

#5


我不知道楼主所理解的“栈”是怎么个东西
反正我没听说过“确定栈的大小”这种行为

#6


引用楼主  的回复:
void test()
{
    if(someCondition) {double a = 123.0;} else {int b = 456;}  // 栈的大小应按sizeof(a) 还是 sizeof……
关于函数占用栈的空间大小,并没有一个硬性规定.编译器通常都是假设栈空间足够大.在你这个例子里,函数运行时使用的栈空间可以是sizeof(double)+sizeof(int),也可以是max( sizeof(double), sizeof(int) ).更可以根据运行的情况动态调整,即用的时候从栈中申请,用完就还回去.
函数中,需要放在栈中数据在代码敲定了以后,就是确定的了.当然你也可以手工从栈中分配内存,但这并不会影响函数中直接占用栈空间的对象数量.所以,编译器是明确知道一次函数调用需要多少栈空间的.
一个程序所能用的栈空间的大小也不是确定的.编译器一般允许用户设置栈的提交大小和保留大小.提交大小是指程序启动时,OS必须分配给程序的栈空间.而保留大小是表示保留一段连续的地址空间给栈,便于栈的生长.
而os在栈超出的时候还有可能动态增大栈的空间(甚至可以超出保留地址空间).但内存总有限制,栈溢出就会出错.这是也递归调用必须要限制深度的原因.

#7


引用楼主  的回复:
每个函数调用都要压栈, 
栈用来存放局部变量和形参以及状态指针,
可是很多时候变量是按程序逻辑动态创建的, 编译器不可能知道函数块需要多少局部变量啊?

比如这样一个函数: 
void test()
{
    if(someCondition) {double a = 123.0;} else {int b = 456;}  // 栈的大小应按sizeof(a) 还是 sizeof……

堆栈的容量一般由编译器指定,很多编译器也留了选项供程序员设定。但编译器出于性能考虑一般不检测栈边界,那么当栈溢出的时候会发生什么呢?不同的执行环境、不同的栈实现会有所不同,可能啥事也不会发生,可能会覆盖了其它有效数据,也可能产生CPU异常等等。

#8


6楼说得很明白了!

#9


引用 1 楼  的回复:
应该是默认分配给你一个大小的栈吧。 如你超过这个大小,就会运行报错.


我的意思是, 每次调用函数时压栈, 被压入的那个元素的大小. 而不是程序总的栈空间.
不可能每个栈元素的大小是一样的吧? 

#10


引用 2 楼  的回复:
sizeof(a) + sizeof(b)

看错了。。。
栈的大小一般可以通过编译器开关/STACK:设置,不然默认的是 1 MB
==============================
如果你想说的是,为:
void test()
{
  if(someCondition) {double a = 123.0;} else {int b = 456;} // 栈的大小应按sizeof(a) 还是 sizeof(b)计算?  
}
这样一段程序,分配了多少空间,
那就是sizeof(a) + sizeof(b)

#11


引用 9 楼  的回复:
引用 1 楼  的回复:

应该是默认分配给你一个大小的栈吧。 如你超过这个大小,就会运行报错.


我的意思是, 每次调用函数时压栈, 被压入的那个元素的大小. 而不是程序总的栈空间.
不可能每个栈元素的大小是一样的吧?

push进入的当然不是“总的栈空间”,完全不是同一个概念。而push的也不一定是对象的大小,就是说,不一定是sizeof(a)+sizeof(b),因为出于性能考虑,压入栈的元素通常都会进行内存对齐,压入栈的东西可能会包含内存对齐产生的空隙,空隙的大小要看对齐系数。

#12


栈的内容在不断的变化,只要留出足够的空间即可。

#13


void test()
{
  if(someCondition) {double a = 123.0;} else {int b = 456;} // 栈的大小应按sizeof(a) 还是 sizeof(b)计算?  
}

----------------------
你要明确, 函数的所有局部变量都是进入函数的时候分配的. 并不是你定义的时候才分配!
所以, 无论 someCondition 是否成立, double a 和 int b 变量都会被分配. 而不是根据这个条件, 满足的时候分配 a, 不满足的时候分配 b.

#14


看汇编 12 + 4
_test	PROC
; Line 2
push ebp
mov ebp, esp
sub esp, 12 ; 0000000cH
; Line 3
cmp DWORD PTR _b$[ebp], 0
je SHORT $LN2@test
; Line 5
fld QWORD PTR __real@405ec00000000000
fstp QWORD PTR _a$907[ebp]
; Line 6
lea eax, DWORD PTR _a$907[ebp]
push eax
push OFFSET $SG909
call _printf
add esp, 8
; Line 8
jmp SHORT $LN1@test
$LN2@test:
; Line 10
mov DWORD PTR _b$911[ebp], 456 ; 000001c8H
; Line 11
lea ecx, DWORD PTR _b$911[ebp]
push ecx
push OFFSET $SG912
call _printf
add esp, 8
$LN1@test:
; Line 13
mov esp, ebp
pop ebp
ret 0
_test ENDP

#15


栈的大小肯定不是估算出来的,而且所谓足够的,我很怀疑。

WCHAR szFileName[MAX_PATH] = {}
我经常这么用,这个栈就大了,栈的大小肯定是编译器通过函数的参数,局部变量的和来处理的。

只可能比这个和大,因为调用一个函数的时候,有些寄存器的值也要PUSH的。

---------------------------
C++和C判断栈需要多大的空间也是相同的逻辑,只不过C++可以不在函数的开始就声明,但是不影响计算栈的空间。

---------------------------
而且汇编指令ret N,也说明这个空间的大小是编译的时候就知道的。



#16


google stack, frame, 

or 

找本 深入理解计算机系统(2004)扫描版 3.7.1 Stack Frame Structure

#17


操作栈的都属于处理器底层命令了,我觉得应该会通过一些列命令进行动态分配,栈本身有一个容量,应该是1MB,在这个容量内会不断动态分配,多了就加一个空间在放。 但是要是超了就会崩溃。

此情形参照VECTOR<>

#18


偶尔会遇到栈不够用的情况,只需要更改编译器的配置即可。

#19


无汇编,无真相。
《深度探索C++对象模型》

#20


引用楼主  的回复:
每个函数调用都要压栈,  
栈用来存放局部变量和形参以及状态指针,
可是很多时候变量是按程序逻辑动态创建的, 编译器不可能知道函数块需要多少局部变量啊?

比如这样一个函数:  
void test()
{
  if(someCondition) {double a = 123.0;} else {int b = 456;} // 栈的大小应按sizeof(a) 还是 sizeof(b)计算?  
}

或者, 是我理解有误, 栈的大小可以伸缩?


当someCondition为真,a入栈,否则b入栈。出作用域时,再分别出栈即可

完全无需知道需要多少栈空间

因为寄存器ebp记录着当下函数的栈空间的起始值,而此起始处里就记录着上次ebp的值和当下函数返回后应该回去的地址

本次函数结束时,依次出栈,或者根据ebp定位一次性跳到所需的位置即可

总之,入多少,出多少,即可

#21


引用 4 楼  的回复:
栈的大小可以通过编译器选项指定。

在集成环境中,也可以可以做到,比如在VS2010中,可以:
(菜单)Project->工程名 Properties->Configuration Properties->Linker->System->***

也可以在代码中:
#pragma comment(linker, "/STACK:5000000 ")//用该语句设置/STACK:后面数……

good point, 在链接时确定。

#22


引用 4 楼  的回复:
栈的大小可以通过编译器选项指定。

在集成环境中,也可以可以做到,比如在VS2010中,可以:
(菜单)Project->工程名 Properties->Configuration Properties->Linker->System->***

也可以在代码中:
#pragma comment(linker, "/STACK:5000000 ")//用该语句设置/STACK:后面数……

good point, 在链接时指定(如果未指定,就用默认值,默认值大小跟编译器厂商有关)。

#23


引用 20 楼  的回复:
当someCondition为真,a入栈,否则b入栈。出作用域时,再分别出栈即可

完全无需知道需要多少栈空间

因为寄存器ebp记录着当下函数的栈空间的起始值,而此起始处里就记录着上次ebp的值和当下函数返回后应该回去的地址

本次函数结束时,依次出栈,或者根据ebp定位一次性跳到所需的位置即可

总之,入多少,出多少,即可


有意思的观点. 
那么, static局部变量呢, 它们被存放在全局数据区. 
是编译时在全局数据区预留了空间, 还是全局数据区运行时可伸缩?

#24


引用 23 楼  的回复:
那么, static局部变量呢, 它们被存放在全局数据区.  
是编译时在全局数据区预留了空间, 还是全局数据区运行时可伸缩?


注意区分两种存在方式:一个是在文件里,一个是在虚拟内存里

静态和全局变量保存在.exe/.dll文件的数据区,运行时则被加载到虚拟内存的某个区域,既不在堆,也不在栈

局部变量实质上是以代码的形式,即相关的push/pop指令,保存在.exe/.dll文件的代码区,运行时,代码被加载到虚拟内存的某个区。而执行代码里的push/pop时,局部变量被压入/弹出栈

编译时,.exe/.dll文件里保存了相关的数据,以告诉操作系统加载它的时候怎样分配栈和堆,默认都是1MB。于是,系统加载该文件时,在虚拟内存里分别预留1MB给栈和堆。运行时,这1MB栈用完了就会溢出。堆则不然,用完了预留的,则会自动再向系统申请,直到申请不到

#25


引用 20 楼  的回复:
当someCondition为真,a入栈,否则b入栈。出作用域时,再分别出栈即可

完全无需知道需要多少栈空间

因为寄存器ebp记录着当下函数的栈空间的起始值,而此起始处里就记录着上次ebp的值和当下函数返回后应该回去的地址

本次函数结束时,依次出栈,或者根据ebp定位一次性跳到所需的位置即可

总之,入多少,出多少,即可


更正#20楼

编译器需要知道局部变量所需的总空间,即所有局部变量所需空间的和。楼主的例子里就是8+4=12

#26


更正#24楼

注意区分两种存在方式:一种是在文件里,一种是在虚拟内存里

静态和全局变量保存在.exe/.dll文件的数据区,运行时则被加载到虚拟内存的某个区域,既不在堆,也不在栈

文件里不存在局部变量区

局部变量实质上是以代码的形式,即相关的等价于push/pop的指令,保存在.exe/.dll文件的代码区,运行时,代码被加载到虚拟内存的某个区。而执行代码里的等价于push/pop的指令,局部变量被压入/弹出栈

所谓等价是指,比如楼主的例子里,编译器直接用sub esp,12指令,这相当于一次性push了12字节

编译时,.exe/.dll文件里保存了相关的数据,以告诉操作系统加载它的时候怎样分配栈和堆,默认都是1MB。于是,系统加载该文件时,在虚拟内存里分别预留1MB给栈和堆。运行时,这1MB栈用完了就会溢出。堆则不然,用完了预留的,则会自动再向系统申请,直到申请不到

#27


引用 25 楼  的回复:
更正#20楼

编译器需要知道局部变量所需的总空间,即所有局部变量所需空间的和。楼主的例子里就是8+4=12


也就是说, 还是要为所有变量预留空间,不管它会不会在运行时被创建? 怎样用代码去窥探/证实这一点呢?

另外, 局部变量应该没有"退栈"一说吧, 函数退栈是不会做任何清理的.
void A(){int a = 123;}
void B(){int b; cout<<b<<endl;}

int main(int argc, char** argv)
{
  A();
  B();
return 0;
}
输出为 123;

#28


编译器可以控制栈大小,一些用默认值。所以写递归函数之类的东西得注意点。

#29


引用 27 楼  的回复:
也就是说, 还是要为所有变量预留空间,不管它会不会在运行时被创建? 怎样用代码去窥探/证实这一点呢?


如果不优化的话,一般是这样

#14楼给出了你的例子的汇编

下面给个vs2008例子,release下编译 /Od 即禁用优化
int u=0,v=0;

int main()
{
_asm
{
mov u,esp;
mov v,ebp;
}

cout <<v-u <<endl; // 预留了24字节,即6个int

if(true)
{
int x=5,y;
int z=x;
}
else
{
int a=2,b;
int c=a-a;
}

return 0;
}

#30


引用 27 楼  的回复:
另外, 局部变量应该没有"退栈"一说吧, 函数退栈是不会做任何清理的.


退栈只是增加esp,如果需要的话,把当下esp处的值弹到需要的地方,但并不清理栈

#31


该回复于2012-09-06 08:25:33被版主删除

#32


 函数的返回值地址 所占用的空间 都没有加吗?
怎么可能是简答的 sizeof (a +b) 呢? 
someCondition 如果是局部变量的话, 也得加上它的值,
如果是函数的话, 加上 跳转地址吧?

#33


引用 26 楼  的回复:
更正#24楼

注意区分两种存在方式:一种是在文件里,一种是在虚拟内存里

静态和全局变量保存在.exe/.dll文件的数据区,运行时则被加载到虚拟内存的某个区域,既不在堆,也不在栈

文件里不存在局部变量区

局部变量实质上是以代码的形式,即相关的等价于push/pop的指令,保存在.exe/.dll文件的代码区,运行时,代码被加载到虚拟内存的某个区。而执行代码里的等价于push……

阁下的观点不敢苟同,对于cpu来说根本没有什么所谓的代码形式,全是二进制数据而已。
而且阁下存在方式的区分也并不正确,无论局部变量或者全局变量亦或者静态变量,只要初始化的都在.data段,或者.rdata段,未初始化的全局变量在.bss段。
其他部分不做评价,请google变量及函数的重定位。

#34


不论是什么对象,总有作用域范围,在C++里就是一对大括号(全局、静态除外)里,在声明变量时push,在出作用域时pop,这种处理对于编译器应该是小菜一碟。

不知道我理解对不对

#35


引用 33 楼  的回复:
阁下的观点不敢苟同,对于cpu来说根本没有什么所谓的代码形式,全是二进制数据而已。
而且阁下存在方式的区分也并不正确,无论局部变量或者全局变量亦或者静态变量,只要初始化的都在.data段,或者.rdata段,未初始化的全局变量在.bss段。
其他部分不做评价,请google变量及函数的重定位。


你的意思是下例的{1,2,3}保存在.data段?

int func()
{
int temp[]={1,2,3}; // {1,2,3}保存在.data段?
return temp[2];
}

int main()
{
int x=func();

return 0;
}

#36


引用 34 楼  的回复:
不论是什么对象,总有作用域范围,在C++里就是一对大括号(全局、静态除外)里,在声明变量时push,在出作用域时pop,这种处理对于编译器应该是小菜一碟。

不知道我理解对不对


似乎是进入函数时一次性push全部局部变量所需的空间,而不是用一个push一个

#37


引用 33 楼  的回复:


下例的字符串just a teststring保存在.rdata区,这是因为编译器将字符串做为全局只读数据保存,然后在运行时再把此数据从.rdata区读入局部变量

倘若写成char c[]={'j','u','s','t',…… '\0'};的形式,也就不存在了。正如#35楼的例子

但是无论哪种形式,绝不会有一个局部变量名c和x保存在.exe/.dll文件里

int func()
{
char c[]="just a teststring";
return strlen(c);
}

int main()
{
int x=func();

return 0;
}

#38


引用 37 楼  的回复:
引用 33 楼  的回复:

下例的字符串just a teststring保存在.rdata区,这是因为编译器将字符串做为全局只读数据保存,然后在运行时再把此数据从.rdata区读入局部变量

倘若写成char c[]={'j','u','s','t',…… '\0'};的形式,也就不存在了。正如#35楼的例子

但是无论哪种形式,绝不会有一个局部变量名c和x保存在.exe/.dl……

链接视图和装载视图不是一个概念,编译完了之后已经在.rdata了,链接视图的段和装载视图的段是有区别的

#39


楼上认为非静态局部变量是保存在文件里的?

就举最简单的例子吧
int func()
{
    int temp=7;
temp+=temp;
    return temp;
}


这里的 7 保存在文件的.rdata段,temp 名称保存在符号表里?然后文件里是不是还有一个4字节的空间留给它?

呵呵

否则的话,实在不知道和我说的有什么冲突

#40


int func()
{
    int temp=7;
    temp+=temp;
    return temp;
}


用OD查看反汇编
Address   Hex dump          Command
                                  
00401000  /$  55            PUSH EBP                                 
00401001  |.  8BEC          MOV EBP,ESP
00401003  |.  51            PUSH ECX
00401004  |?  C745 FC 07000000 MOV DWORD PTR SS:[EBP-4],7
0040100B  |?  8B45 FC       MOV EAX,DWORD PTR SS:[EBP-4]
0040100E  |?  0345 FC       ADD EAX,DWORD PTR SS:[EBP-4]
00401011  |?  8945 FC       MOV DWORD PTR SS:[EBP-4],EAX
00401014  |.  8B45 FC       MOV EAX,DWORD PTR SS:[LOCAL.1]
00401017  |?  8BE5          MOV ESP,EBP
00401019  |?  5D            POP EBP
0040101A  |?  C3            RETN


显然7在文件上的存在形式就是代码区的00401004处这一行指令,即机器语言的 C745 FC 07000 里的 07000000
而temp符号根本不存在,因为一个非静态的局部变量需要符号做什么呢!?
运行时,内存里用来存放7的栈位置就是ebp-4
对temp和7来说,这应该就是全部了,哪里有什么.data,.rdata,.bss ?!

我前面说,局部变量是以代码的方式保存在文件的代码区的,虽然说法很粗糙,但意思没什么错吧?

#41


引用 40 楼  的回复:
C/C++ code
int func()
{
    int temp=7;
    temp+=temp;
    return temp;
}


用OD查看反汇编
Assembly code
Address   Hex dump          Command
                                  
00401000  /$  55       ……

 SS:[EBP-4] 这个是temp的地址,7只是个立即数。
变成汇编之后本来就不存在.rdata或者其他什么,你看不到而已
你可以使用工具查看你的可执行文件,windows下面我不太清楚是什么,pedump应该可以
linux下
objdump
readelf
都可以查看
对这个问题只是个探讨而已,有的我也不是很熟,书刚看到这里,还没完全了然于胸,共同学习。

#42


引用 41 楼  的回复:
SS:[EBP-4] 这个是temp的地址,7只是个立即数。
变成汇编之后本来就不存在.rdata或者其他什么,你看不到而已
你可以使用工具查看你的可执行文件,windows下面我不太清楚是什么,pedump应该可以
linux下
objdump
readelf
都可以查看
对这个问题只是个探讨而已,有的我也不是很熟,书刚看到这里,还没完全了然于胸,共同学习。


windows下dumpbin即可

编译/汇编之后,文件里当然存在.rdata(如果有只读数据的话)。加载到内存里也有对应的一个区

只要有初始化的全局/静态数据,编译/汇编后就存放在文件里的.data区。加载到内存里也是对应的一个区

但是,文件里没有局部变量区

不是看不到,是根本没有

另外,ss,ebp,是运行时用于定位局部变量在栈的位置,而不是局部变量在磁盘文件里的位置

#43


有线程栈,我自己理解的“函数栈”是线程栈的子站
线程栈是编译阶段定死的,windows默认貌似4MB,这个在编译选项中可调的
“函数栈”又分两个部分,第一是参数,第二是函数的局部变量。
参数是需要随着函数调用入栈出栈的,一个函数的局部变量也可以看成是一个元素,函数调用的时候入栈

在一个内部来说,每个句柄变量也是在栈上,位置在编译阶段就定好了

#44


该回复于2012-09-06 12:59:19被版主删除

#45


没错函数调用的确是是个压栈的过程; 函数返回后,压栈的数据被弹出。
栈的大小,应该由编译器和os共同决定吧。
编译器中,有frame 和 record的概念。来控制函数调用和返回。以及发生异常时,栈的回退(unwinded)等。

函数调用中其相关信息和其局部变量的空间都在栈上分配,而且是编译器自动分配。

至于你说的是sizeof(a) 还是sizeof(b);个人觉得应该大于sizeof(a) + sizeof(b);

因为函数调用的过程,是一个上下文切换的过程,其间,首先要保存现场、然后将函数的返回地址及局部变量压栈、执行函数、返回、恢复现场。 如果写过汇编程序,这个就很容易理解了。
这些都是有编译器实现的,具体细节我们不必关心。

alloc 可以在栈上分配内存。你可以通过在函数内部定义一个足够大数组,来看看,你的编译器分配的stack size。

#46


引用 19 楼  的回复:
无汇编,无真相。
《深度探索C++对象模型》


哥决定先去学汇编.

#47


栈的大小也可以动态设置吧。CreateThread(...)就可以设置栈的大小

#48


...
简单点  花点时间了解下ebp,esp寄存器的用途,用Ollydbg随便打开一个程序,单步跟踪下函数调用的过程,观察栈的变化  就明白了

#49


"或者, 是我理解有误, 栈的大小可以伸缩?"

这里边还有个栈大小,和实际用了的栈大小。
正是应为编译器不知道程序运行的时候栈有多大,才会有栈溢出这一说,就是实际用的栈空间超过了编译器分配的栈空间,程序就崩溃了。

#50


引用 43 楼  的回复:
有线程栈,我自己理解的“函数栈”是线程栈的子站
线程栈是编译阶段定死的,windows默认貌似4MB,这个在编译选项中可调的
“函数栈”又分两个部分,第一是参数,第二是函数的局部变量。
参数是需要随着函数调用入栈出栈的,一个函数的局部变量也可以看成是一个元素,函数调用的时候入栈

在一个内部来说,每个句柄变量也是在栈上,位置在编译阶段就定好了


没有所谓的函数栈

线程栈就是栈,只不过是不同的上下文中的术语而已

栈,针对编译原理

线程栈,针对操作系统

现代系统,都是一个线程一个栈,所以叫做线程栈,windows默认一个线程1MB的栈,每新开一个线程,就配套分配1MB的栈,当然也可以指定大小

#1


应该是默认分配给你一个大小的栈吧。 如你超过这个大小,就会运行报错. 

#2


sizeof(a) + sizeof(b)

#3


编译器做的工作: 1. 词法提取和分析  2.语法分析 3 链接  4生成可执行程序代码
其中词法分析是将整个代码扫描一遍,提取变量……

堆栈的实际长度随着局部变量的增多而变长

可以确定的是, 编译器先提取变量, 后压入堆栈中。

置于填充堆栈内容,我觉得是 “生成可执行程序代码” 这一步要完成的事情。

#4


栈的大小可以通过编译器选项指定。

在集成环境中,也可以可以做到,比如在VS2010中,可以:
(菜单)Project->工程名 Properties->Configuration Properties->Linker->System->***

也可以在代码中:
#pragma comment(linker, "/STACK:5000000 ")//用该语句设置/STACK:后面数字为容量

#5


我不知道楼主所理解的“栈”是怎么个东西
反正我没听说过“确定栈的大小”这种行为

#6


引用楼主  的回复:
void test()
{
    if(someCondition) {double a = 123.0;} else {int b = 456;}  // 栈的大小应按sizeof(a) 还是 sizeof……
关于函数占用栈的空间大小,并没有一个硬性规定.编译器通常都是假设栈空间足够大.在你这个例子里,函数运行时使用的栈空间可以是sizeof(double)+sizeof(int),也可以是max( sizeof(double), sizeof(int) ).更可以根据运行的情况动态调整,即用的时候从栈中申请,用完就还回去.
函数中,需要放在栈中数据在代码敲定了以后,就是确定的了.当然你也可以手工从栈中分配内存,但这并不会影响函数中直接占用栈空间的对象数量.所以,编译器是明确知道一次函数调用需要多少栈空间的.
一个程序所能用的栈空间的大小也不是确定的.编译器一般允许用户设置栈的提交大小和保留大小.提交大小是指程序启动时,OS必须分配给程序的栈空间.而保留大小是表示保留一段连续的地址空间给栈,便于栈的生长.
而os在栈超出的时候还有可能动态增大栈的空间(甚至可以超出保留地址空间).但内存总有限制,栈溢出就会出错.这是也递归调用必须要限制深度的原因.

#7


引用楼主  的回复:
每个函数调用都要压栈, 
栈用来存放局部变量和形参以及状态指针,
可是很多时候变量是按程序逻辑动态创建的, 编译器不可能知道函数块需要多少局部变量啊?

比如这样一个函数: 
void test()
{
    if(someCondition) {double a = 123.0;} else {int b = 456;}  // 栈的大小应按sizeof(a) 还是 sizeof……

堆栈的容量一般由编译器指定,很多编译器也留了选项供程序员设定。但编译器出于性能考虑一般不检测栈边界,那么当栈溢出的时候会发生什么呢?不同的执行环境、不同的栈实现会有所不同,可能啥事也不会发生,可能会覆盖了其它有效数据,也可能产生CPU异常等等。

#8


6楼说得很明白了!

#9


引用 1 楼  的回复:
应该是默认分配给你一个大小的栈吧。 如你超过这个大小,就会运行报错.


我的意思是, 每次调用函数时压栈, 被压入的那个元素的大小. 而不是程序总的栈空间.
不可能每个栈元素的大小是一样的吧? 

#10


引用 2 楼  的回复:
sizeof(a) + sizeof(b)

看错了。。。
栈的大小一般可以通过编译器开关/STACK:设置,不然默认的是 1 MB
==============================
如果你想说的是,为:
void test()
{
  if(someCondition) {double a = 123.0;} else {int b = 456;} // 栈的大小应按sizeof(a) 还是 sizeof(b)计算?  
}
这样一段程序,分配了多少空间,
那就是sizeof(a) + sizeof(b)

#11


引用 9 楼  的回复:
引用 1 楼  的回复:

应该是默认分配给你一个大小的栈吧。 如你超过这个大小,就会运行报错.


我的意思是, 每次调用函数时压栈, 被压入的那个元素的大小. 而不是程序总的栈空间.
不可能每个栈元素的大小是一样的吧?

push进入的当然不是“总的栈空间”,完全不是同一个概念。而push的也不一定是对象的大小,就是说,不一定是sizeof(a)+sizeof(b),因为出于性能考虑,压入栈的元素通常都会进行内存对齐,压入栈的东西可能会包含内存对齐产生的空隙,空隙的大小要看对齐系数。

#12


栈的内容在不断的变化,只要留出足够的空间即可。

#13


void test()
{
  if(someCondition) {double a = 123.0;} else {int b = 456;} // 栈的大小应按sizeof(a) 还是 sizeof(b)计算?  
}

----------------------
你要明确, 函数的所有局部变量都是进入函数的时候分配的. 并不是你定义的时候才分配!
所以, 无论 someCondition 是否成立, double a 和 int b 变量都会被分配. 而不是根据这个条件, 满足的时候分配 a, 不满足的时候分配 b.

#14


看汇编 12 + 4
_test	PROC
; Line 2
push ebp
mov ebp, esp
sub esp, 12 ; 0000000cH
; Line 3
cmp DWORD PTR _b$[ebp], 0
je SHORT $LN2@test
; Line 5
fld QWORD PTR __real@405ec00000000000
fstp QWORD PTR _a$907[ebp]
; Line 6
lea eax, DWORD PTR _a$907[ebp]
push eax
push OFFSET $SG909
call _printf
add esp, 8
; Line 8
jmp SHORT $LN1@test
$LN2@test:
; Line 10
mov DWORD PTR _b$911[ebp], 456 ; 000001c8H
; Line 11
lea ecx, DWORD PTR _b$911[ebp]
push ecx
push OFFSET $SG912
call _printf
add esp, 8
$LN1@test:
; Line 13
mov esp, ebp
pop ebp
ret 0
_test ENDP

#15


栈的大小肯定不是估算出来的,而且所谓足够的,我很怀疑。

WCHAR szFileName[MAX_PATH] = {}
我经常这么用,这个栈就大了,栈的大小肯定是编译器通过函数的参数,局部变量的和来处理的。

只可能比这个和大,因为调用一个函数的时候,有些寄存器的值也要PUSH的。

---------------------------
C++和C判断栈需要多大的空间也是相同的逻辑,只不过C++可以不在函数的开始就声明,但是不影响计算栈的空间。

---------------------------
而且汇编指令ret N,也说明这个空间的大小是编译的时候就知道的。



#16


google stack, frame, 

or 

找本 深入理解计算机系统(2004)扫描版 3.7.1 Stack Frame Structure

#17


操作栈的都属于处理器底层命令了,我觉得应该会通过一些列命令进行动态分配,栈本身有一个容量,应该是1MB,在这个容量内会不断动态分配,多了就加一个空间在放。 但是要是超了就会崩溃。

此情形参照VECTOR<>

#18


偶尔会遇到栈不够用的情况,只需要更改编译器的配置即可。

#19


无汇编,无真相。
《深度探索C++对象模型》

#20


引用楼主  的回复:
每个函数调用都要压栈,  
栈用来存放局部变量和形参以及状态指针,
可是很多时候变量是按程序逻辑动态创建的, 编译器不可能知道函数块需要多少局部变量啊?

比如这样一个函数:  
void test()
{
  if(someCondition) {double a = 123.0;} else {int b = 456;} // 栈的大小应按sizeof(a) 还是 sizeof(b)计算?  
}

或者, 是我理解有误, 栈的大小可以伸缩?


当someCondition为真,a入栈,否则b入栈。出作用域时,再分别出栈即可

完全无需知道需要多少栈空间

因为寄存器ebp记录着当下函数的栈空间的起始值,而此起始处里就记录着上次ebp的值和当下函数返回后应该回去的地址

本次函数结束时,依次出栈,或者根据ebp定位一次性跳到所需的位置即可

总之,入多少,出多少,即可

#21


引用 4 楼  的回复:
栈的大小可以通过编译器选项指定。

在集成环境中,也可以可以做到,比如在VS2010中,可以:
(菜单)Project->工程名 Properties->Configuration Properties->Linker->System->***

也可以在代码中:
#pragma comment(linker, "/STACK:5000000 ")//用该语句设置/STACK:后面数……

good point, 在链接时确定。

#22


引用 4 楼  的回复:
栈的大小可以通过编译器选项指定。

在集成环境中,也可以可以做到,比如在VS2010中,可以:
(菜单)Project->工程名 Properties->Configuration Properties->Linker->System->***

也可以在代码中:
#pragma comment(linker, "/STACK:5000000 ")//用该语句设置/STACK:后面数……

good point, 在链接时指定(如果未指定,就用默认值,默认值大小跟编译器厂商有关)。

#23


引用 20 楼  的回复:
当someCondition为真,a入栈,否则b入栈。出作用域时,再分别出栈即可

完全无需知道需要多少栈空间

因为寄存器ebp记录着当下函数的栈空间的起始值,而此起始处里就记录着上次ebp的值和当下函数返回后应该回去的地址

本次函数结束时,依次出栈,或者根据ebp定位一次性跳到所需的位置即可

总之,入多少,出多少,即可


有意思的观点. 
那么, static局部变量呢, 它们被存放在全局数据区. 
是编译时在全局数据区预留了空间, 还是全局数据区运行时可伸缩?

#24


引用 23 楼  的回复:
那么, static局部变量呢, 它们被存放在全局数据区.  
是编译时在全局数据区预留了空间, 还是全局数据区运行时可伸缩?


注意区分两种存在方式:一个是在文件里,一个是在虚拟内存里

静态和全局变量保存在.exe/.dll文件的数据区,运行时则被加载到虚拟内存的某个区域,既不在堆,也不在栈

局部变量实质上是以代码的形式,即相关的push/pop指令,保存在.exe/.dll文件的代码区,运行时,代码被加载到虚拟内存的某个区。而执行代码里的push/pop时,局部变量被压入/弹出栈

编译时,.exe/.dll文件里保存了相关的数据,以告诉操作系统加载它的时候怎样分配栈和堆,默认都是1MB。于是,系统加载该文件时,在虚拟内存里分别预留1MB给栈和堆。运行时,这1MB栈用完了就会溢出。堆则不然,用完了预留的,则会自动再向系统申请,直到申请不到

#25


引用 20 楼  的回复:
当someCondition为真,a入栈,否则b入栈。出作用域时,再分别出栈即可

完全无需知道需要多少栈空间

因为寄存器ebp记录着当下函数的栈空间的起始值,而此起始处里就记录着上次ebp的值和当下函数返回后应该回去的地址

本次函数结束时,依次出栈,或者根据ebp定位一次性跳到所需的位置即可

总之,入多少,出多少,即可


更正#20楼

编译器需要知道局部变量所需的总空间,即所有局部变量所需空间的和。楼主的例子里就是8+4=12

#26


更正#24楼

注意区分两种存在方式:一种是在文件里,一种是在虚拟内存里

静态和全局变量保存在.exe/.dll文件的数据区,运行时则被加载到虚拟内存的某个区域,既不在堆,也不在栈

文件里不存在局部变量区

局部变量实质上是以代码的形式,即相关的等价于push/pop的指令,保存在.exe/.dll文件的代码区,运行时,代码被加载到虚拟内存的某个区。而执行代码里的等价于push/pop的指令,局部变量被压入/弹出栈

所谓等价是指,比如楼主的例子里,编译器直接用sub esp,12指令,这相当于一次性push了12字节

编译时,.exe/.dll文件里保存了相关的数据,以告诉操作系统加载它的时候怎样分配栈和堆,默认都是1MB。于是,系统加载该文件时,在虚拟内存里分别预留1MB给栈和堆。运行时,这1MB栈用完了就会溢出。堆则不然,用完了预留的,则会自动再向系统申请,直到申请不到

#27


引用 25 楼  的回复:
更正#20楼

编译器需要知道局部变量所需的总空间,即所有局部变量所需空间的和。楼主的例子里就是8+4=12


也就是说, 还是要为所有变量预留空间,不管它会不会在运行时被创建? 怎样用代码去窥探/证实这一点呢?

另外, 局部变量应该没有"退栈"一说吧, 函数退栈是不会做任何清理的.
void A(){int a = 123;}
void B(){int b; cout<<b<<endl;}

int main(int argc, char** argv)
{
  A();
  B();
return 0;
}
输出为 123;

#28


编译器可以控制栈大小,一些用默认值。所以写递归函数之类的东西得注意点。

#29


引用 27 楼  的回复:
也就是说, 还是要为所有变量预留空间,不管它会不会在运行时被创建? 怎样用代码去窥探/证实这一点呢?


如果不优化的话,一般是这样

#14楼给出了你的例子的汇编

下面给个vs2008例子,release下编译 /Od 即禁用优化
int u=0,v=0;

int main()
{
_asm
{
mov u,esp;
mov v,ebp;
}

cout <<v-u <<endl; // 预留了24字节,即6个int

if(true)
{
int x=5,y;
int z=x;
}
else
{
int a=2,b;
int c=a-a;
}

return 0;
}

#30


引用 27 楼  的回复:
另外, 局部变量应该没有"退栈"一说吧, 函数退栈是不会做任何清理的.


退栈只是增加esp,如果需要的话,把当下esp处的值弹到需要的地方,但并不清理栈

#31


该回复于2012-09-06 08:25:33被版主删除

#32


 函数的返回值地址 所占用的空间 都没有加吗?
怎么可能是简答的 sizeof (a +b) 呢? 
someCondition 如果是局部变量的话, 也得加上它的值,
如果是函数的话, 加上 跳转地址吧?

#33


引用 26 楼  的回复:
更正#24楼

注意区分两种存在方式:一种是在文件里,一种是在虚拟内存里

静态和全局变量保存在.exe/.dll文件的数据区,运行时则被加载到虚拟内存的某个区域,既不在堆,也不在栈

文件里不存在局部变量区

局部变量实质上是以代码的形式,即相关的等价于push/pop的指令,保存在.exe/.dll文件的代码区,运行时,代码被加载到虚拟内存的某个区。而执行代码里的等价于push……

阁下的观点不敢苟同,对于cpu来说根本没有什么所谓的代码形式,全是二进制数据而已。
而且阁下存在方式的区分也并不正确,无论局部变量或者全局变量亦或者静态变量,只要初始化的都在.data段,或者.rdata段,未初始化的全局变量在.bss段。
其他部分不做评价,请google变量及函数的重定位。

#34


不论是什么对象,总有作用域范围,在C++里就是一对大括号(全局、静态除外)里,在声明变量时push,在出作用域时pop,这种处理对于编译器应该是小菜一碟。

不知道我理解对不对

#35


引用 33 楼  的回复:
阁下的观点不敢苟同,对于cpu来说根本没有什么所谓的代码形式,全是二进制数据而已。
而且阁下存在方式的区分也并不正确,无论局部变量或者全局变量亦或者静态变量,只要初始化的都在.data段,或者.rdata段,未初始化的全局变量在.bss段。
其他部分不做评价,请google变量及函数的重定位。


你的意思是下例的{1,2,3}保存在.data段?

int func()
{
int temp[]={1,2,3}; // {1,2,3}保存在.data段?
return temp[2];
}

int main()
{
int x=func();

return 0;
}

#36


引用 34 楼  的回复:
不论是什么对象,总有作用域范围,在C++里就是一对大括号(全局、静态除外)里,在声明变量时push,在出作用域时pop,这种处理对于编译器应该是小菜一碟。

不知道我理解对不对


似乎是进入函数时一次性push全部局部变量所需的空间,而不是用一个push一个

#37


引用 33 楼  的回复:


下例的字符串just a teststring保存在.rdata区,这是因为编译器将字符串做为全局只读数据保存,然后在运行时再把此数据从.rdata区读入局部变量

倘若写成char c[]={'j','u','s','t',…… '\0'};的形式,也就不存在了。正如#35楼的例子

但是无论哪种形式,绝不会有一个局部变量名c和x保存在.exe/.dll文件里

int func()
{
char c[]="just a teststring";
return strlen(c);
}

int main()
{
int x=func();

return 0;
}

#38


引用 37 楼  的回复:
引用 33 楼  的回复:

下例的字符串just a teststring保存在.rdata区,这是因为编译器将字符串做为全局只读数据保存,然后在运行时再把此数据从.rdata区读入局部变量

倘若写成char c[]={'j','u','s','t',…… '\0'};的形式,也就不存在了。正如#35楼的例子

但是无论哪种形式,绝不会有一个局部变量名c和x保存在.exe/.dl……

链接视图和装载视图不是一个概念,编译完了之后已经在.rdata了,链接视图的段和装载视图的段是有区别的

#39


楼上认为非静态局部变量是保存在文件里的?

就举最简单的例子吧
int func()
{
    int temp=7;
temp+=temp;
    return temp;
}


这里的 7 保存在文件的.rdata段,temp 名称保存在符号表里?然后文件里是不是还有一个4字节的空间留给它?

呵呵

否则的话,实在不知道和我说的有什么冲突

#40


int func()
{
    int temp=7;
    temp+=temp;
    return temp;
}


用OD查看反汇编
Address   Hex dump          Command
                                  
00401000  /$  55            PUSH EBP                                 
00401001  |.  8BEC          MOV EBP,ESP
00401003  |.  51            PUSH ECX
00401004  |?  C745 FC 07000000 MOV DWORD PTR SS:[EBP-4],7
0040100B  |?  8B45 FC       MOV EAX,DWORD PTR SS:[EBP-4]
0040100E  |?  0345 FC       ADD EAX,DWORD PTR SS:[EBP-4]
00401011  |?  8945 FC       MOV DWORD PTR SS:[EBP-4],EAX
00401014  |.  8B45 FC       MOV EAX,DWORD PTR SS:[LOCAL.1]
00401017  |?  8BE5          MOV ESP,EBP
00401019  |?  5D            POP EBP
0040101A  |?  C3            RETN


显然7在文件上的存在形式就是代码区的00401004处这一行指令,即机器语言的 C745 FC 07000 里的 07000000
而temp符号根本不存在,因为一个非静态的局部变量需要符号做什么呢!?
运行时,内存里用来存放7的栈位置就是ebp-4
对temp和7来说,这应该就是全部了,哪里有什么.data,.rdata,.bss ?!

我前面说,局部变量是以代码的方式保存在文件的代码区的,虽然说法很粗糙,但意思没什么错吧?

#41


引用 40 楼  的回复:
C/C++ code
int func()
{
    int temp=7;
    temp+=temp;
    return temp;
}


用OD查看反汇编
Assembly code
Address   Hex dump          Command
                                  
00401000  /$  55       ……

 SS:[EBP-4] 这个是temp的地址,7只是个立即数。
变成汇编之后本来就不存在.rdata或者其他什么,你看不到而已
你可以使用工具查看你的可执行文件,windows下面我不太清楚是什么,pedump应该可以
linux下
objdump
readelf
都可以查看
对这个问题只是个探讨而已,有的我也不是很熟,书刚看到这里,还没完全了然于胸,共同学习。

#42


引用 41 楼  的回复:
SS:[EBP-4] 这个是temp的地址,7只是个立即数。
变成汇编之后本来就不存在.rdata或者其他什么,你看不到而已
你可以使用工具查看你的可执行文件,windows下面我不太清楚是什么,pedump应该可以
linux下
objdump
readelf
都可以查看
对这个问题只是个探讨而已,有的我也不是很熟,书刚看到这里,还没完全了然于胸,共同学习。


windows下dumpbin即可

编译/汇编之后,文件里当然存在.rdata(如果有只读数据的话)。加载到内存里也有对应的一个区

只要有初始化的全局/静态数据,编译/汇编后就存放在文件里的.data区。加载到内存里也是对应的一个区

但是,文件里没有局部变量区

不是看不到,是根本没有

另外,ss,ebp,是运行时用于定位局部变量在栈的位置,而不是局部变量在磁盘文件里的位置

#43


有线程栈,我自己理解的“函数栈”是线程栈的子站
线程栈是编译阶段定死的,windows默认貌似4MB,这个在编译选项中可调的
“函数栈”又分两个部分,第一是参数,第二是函数的局部变量。
参数是需要随着函数调用入栈出栈的,一个函数的局部变量也可以看成是一个元素,函数调用的时候入栈

在一个内部来说,每个句柄变量也是在栈上,位置在编译阶段就定好了

#44


该回复于2012-09-06 12:59:19被版主删除

#45


没错函数调用的确是是个压栈的过程; 函数返回后,压栈的数据被弹出。
栈的大小,应该由编译器和os共同决定吧。
编译器中,有frame 和 record的概念。来控制函数调用和返回。以及发生异常时,栈的回退(unwinded)等。

函数调用中其相关信息和其局部变量的空间都在栈上分配,而且是编译器自动分配。

至于你说的是sizeof(a) 还是sizeof(b);个人觉得应该大于sizeof(a) + sizeof(b);

因为函数调用的过程,是一个上下文切换的过程,其间,首先要保存现场、然后将函数的返回地址及局部变量压栈、执行函数、返回、恢复现场。 如果写过汇编程序,这个就很容易理解了。
这些都是有编译器实现的,具体细节我们不必关心。

alloc 可以在栈上分配内存。你可以通过在函数内部定义一个足够大数组,来看看,你的编译器分配的stack size。

#46


引用 19 楼  的回复:
无汇编,无真相。
《深度探索C++对象模型》


哥决定先去学汇编.

#47


栈的大小也可以动态设置吧。CreateThread(...)就可以设置栈的大小

#48


...
简单点  花点时间了解下ebp,esp寄存器的用途,用Ollydbg随便打开一个程序,单步跟踪下函数调用的过程,观察栈的变化  就明白了

#49


"或者, 是我理解有误, 栈的大小可以伸缩?"

这里边还有个栈大小,和实际用了的栈大小。
正是应为编译器不知道程序运行的时候栈有多大,才会有栈溢出这一说,就是实际用的栈空间超过了编译器分配的栈空间,程序就崩溃了。

#50


引用 43 楼  的回复:
有线程栈,我自己理解的“函数栈”是线程栈的子站
线程栈是编译阶段定死的,windows默认貌似4MB,这个在编译选项中可调的
“函数栈”又分两个部分,第一是参数,第二是函数的局部变量。
参数是需要随着函数调用入栈出栈的,一个函数的局部变量也可以看成是一个元素,函数调用的时候入栈

在一个内部来说,每个句柄变量也是在栈上,位置在编译阶段就定好了


没有所谓的函数栈

线程栈就是栈,只不过是不同的上下文中的术语而已

栈,针对编译原理

线程栈,针对操作系统

现代系统,都是一个线程一个栈,所以叫做线程栈,windows默认一个线程1MB的栈,每新开一个线程,就配套分配1MB的栈,当然也可以指定大小