不懂汇编也可以从底层理解C/C++中各种变量(上篇)

内存中的结构分布:

 

块变量

块变量声明周期为块内,一旦出块声明周期将立马停止,所以第11行直接报错,因为块内的i已经被销毁。

接下来在块外面也添加一个i变量。在块内时,如下图所示可以看到块外的i首先在栈帧内被赋值

进入块后

进入块后也是在main的栈帧内存储块内变量i。并且在块内时i的值是在块内的i值10

当出块后, 块内的i已经不复存在了,虽然该值在栈帧内还有,但实际上栈帧压栈和弹栈会把该值覆盖掉。

第二次打印的实际上是5。所以块变量的生命周期仅仅是在块内

局部变量

关于局部变量的分析,首先看这张图:

在进入main函数时,实际上编译器已经做了非常多的工作,main函数也不是真正的入口点,我们一个个来:

进入main函数并且开辟局部变量空间时首先会进行一下压栈操作:

  1. 形参压栈, 对于main函数而言, 形参就是argc, char **argv, 以及envp等变量。由于栈从高地址往低地址生长,所以可以看到内存中是反向的
  2. 返回地址压栈
  3. ebp压栈保存调用方的栈底指针
  4. 进入main函数内

来看一下真正的入口点:

通过调用栈可以知道,main函数实际上是有mainCRTStartup()调用的,而mainCRTStartup是有kernel32.dll导出的。在这个函数的开始下一个断点:

可以看到最上方是很多条件宏,实际上是选择对应的入口函数。如果是编写Windows应用程序则是上面那两个,如果是控制台的话就是下面那两个,由于我这里是控制台并且是ANSI版本的所以在调用堆栈内可以看到我是使用了mainCRTStartup作为入口函数。

看吧,这才是真正的入口函数。接下去找一找main函数的位置。

经过一系列的初始化后,具体细节不关心。找到了main函数

接着我们进入后就到了我们熟悉的main函数内部:

看见了把,这就是为什么main函数还会压入返回地址以及一些形参,原因在于它本来就是被别人调用的:

argc == 1: 因为就一个*.exe没有命令行

argv == 0x00380FD0

envp == 0x00381068

好了,到这里已经偏离主题了,来看一下main函数内部局部变量是如何运行的以及他们的生命周期又是如何

当初始化第一个变量时,由于VC++6.0 Debug版本会预留一大片局部变量空间并全部初始化成0xCC所以能看到绿色框框里全是0xCC实际上这些都是main函数在栈帧内预留的局部变量。0x0012FFC0是调用者的栈底指针,红色框中0x00401339该地址指向mainCRTStartup中调用main的下一条指令,我们可以来看一看这个地址里到底有什么:

看见没,这个地址是call main的下一条,不用关注汇编指令,call就是调用的意思,接下去就会执行add esp, 0Ch, 意思是清空main函数的栈帧。由于C语言是__cdecl的调用约定,该约定规定是由调用者清空堆栈所以才会有这条指令。

跑远了,来看看局部变量的生命周期!

当我们为局部变量赋值的时候,局部空间内的4字节0xCC就变成了我们赋的内容0x00000014就是十进制的20。

接下来进入test函数:

可以明显看到栈内压入了test()函数的返回地址,该地址是调用test后的下一条指令。往下走就是test函数定义局部变量了。

test函数做了与main函数相同的事:

  1. 灰色是main函数的栈底指针和test的返回地址
  2. 0XCC都是局部空间,并且iTmp1和iTmp2都会在其中
  3. 蓝色框是保存的寄存器

看局部变量的生存周期实际上只要关注0xCC这部分何时消亡即可。再往下走两步:

看吧,执行完赋值语句后栈内的0xCC就变了,实际上这在底层是push操作

来看看test函数返回后的结果:

很奇怪,为何test函数返回后由test创建的栈帧局部变量还是存在? 不是应该被销毁么,实际上已经被销毁了,注意ESP的值是0x0012ff30指向的是main函数的栈帧了(黑色部分),所以test函数的局部变量名存实亡。

到这里给出一个结论,局部变量的生命周期就是当前函数块的作用于,超出了本函数块的作用域局部变量也就消亡了。实际上块变量也是一种局部变量。

全局变量

来看看全局变量的生命周期,先看看它存在哪里:

发现他的地址是0x00426d9c, 进去看一看:

果然是在这里,实际上全局变量并不存储在栈内,而是存在了0x0042xxxx的位置,不信? 在来一个全局变量:

看吧,这次g_iNum虽然存在了0x00426e54处但是g_iNumx也存在这篇区域,说明全局变量在VC6.0,32位环境下是存储在0x0042xxxx左右的。全局变量的生命周期是整个进程的存在时间, 准确的来说是模块载入到模块卸载。因为可能是通过动态库载入的方式获取全局变量的

静态的(全局/局部)变量

下面来看看静态全局变量和静态局部变量, 这是非常有意思的两种。首先可以告诉他们的几个特点:

  1. 与全局变量一样,整个进程都存在(如果在库内的话就是模块载入到模块卸载)
  2. 只需要一次初始化,如果再碰到该初始化代码,不会重复初始化
  3. 静态局部变量和静态全局变量的唯一区别就在于访问权限,静态局部是在本函数内而静态全局是整个文件可以访问,与全局变量不同。静态全局不能跨文件访问

来看看第二点:

我这里定义了静态局部变量siTest该变量位于test函数内,以及静态全局变量g_siNum和main中的静态局部变量siNumM。发现他不管是静态的还是全局的都是存放在一起的,也就是说实际上他们并没有什么差别。

再来看看下面的现象:

当我进入Test并且向执行第10条初始化局部静态变量siTest时发现操作系统根本没有执行第10条语句,而是跳过执行了13条printf语句,这到底是是怎么回事?

那他们之间到底是怎么区分的呢?

当前刚刚进入main函数时,这些全局以及静态等变量就已经存在了... 也就是说这些变量是由编译器进行定义生成的,而非运行时。

这里跑远了,我想证明的是:

第一次进入test函数内,siTest被初始化为5同样被打印出来也是5接下去siTest执行了自增操作,按照这种道理,siTest应该是6了。但是不管如何,再次进入Test()后siTest会重新被赋值成5吧,来看看结果:

结果打印出来还是6,代表静态局部变量根本没有被初始化,这也证明了之前的观点,静态变量,全局变量,静态全局变量都是在编译期间完成了定义和初始化操作。在进程运行时是不会执行的,所以这也就达到了第二点:

只需要一次初始化,如果再碰到该初始化代码,不会重复初始化

(完成)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值