欢迎光临散文网 会员登陆 & 注册

观察C编译器对局部变量作用域的约束作用

2022-09-17 16:29 作者:28283844972_bili  | 我要投稿

我们知道,C局部(自动)变量的作用域和生命周期仅限于其自身所在的语句块,此处所说的语句块包括函数代码块,由 { } 构成的一般代码块,至于局部静态变量,虽说其生命周期存在于整个程序运行期间,但其作用域和局部变量无异,同样受限于身处的语句块。

不难理解两者生命周期的差异,C进程中局部(自动)变量被分配在内存调用栈中,随着程序的运行,这片内存区域所管理的内容被频繁地分配和销毁。例如函数在被调用时会自动分配栈帧,离开时栈帧被自动释放,同时函数中所定义的局部变量就分配在这些栈帧中,因此当函数退出时栈帧被自动释放,局部变量所在的内存空间也将被回收,容身之所不复存在,作用域和生命周期也无从谈起。当然对于C这种对边界检查较为宽松的语言,如果熟悉内存栈局部的话,短时间内通过越界访问指定位置,也是有可能在局部变量的作用域外进行一些操作,当然这种行为的结果是未定义的,取决于编译器的约束行为。

至于局部静态变量,同其他全局变量一样都是存放在全局数据区,存在于程序的整个运行期间,因此每次进入或离开其所在的语句块并不会重新为其分配内存空间,不过受限于编译器的约束行为,局部静态变量自身在作用域以外是不可见的。事实上,即使是在作用域中操作局部静态变量,也是紧紧地围绕它在全局数据区的存储地址展开的,并不会在调用栈中新开辟额外的内存空间。

为了进一步观察C编译器对局部变量作用域的约束作用,我们可以从最简单的语句块入手——由 { } 构成的一般语句块,通过观察学习,我个人认为C局部变量,包括局部静态变量的作用域,从狭义上来说是一种受编译器约束的行为;广义上就是由C编译器、操作系统、硬件等因素共同作用之下的行为,即使是在函数构成的语句块内也不例外。

在此之前,给出本次实验环境,不能保证在不同环境下最终结果的一致性,特别是涉及在作用域外访问局部变量的行为,取决于平台软硬件和C编译器:

平台软硬件信息:

GCC编译器信息:

objdump反汇编工具信息:

首先我们用一段非常简单的代码演示来验证作用域约束行为的存在,如果你的编辑器足够智能的话,在编写阶段就会得到错误提示“identifier "xxx" is undefined”,可惜我的Vim没有那么智能,不会给予错误提示,妹说那就是一遍过,照常编译并观察报错信息。

GCC非常贴心地贴出了编译错误地信息,快速定位问题所在,原因是使用了未定义的符号,进一步分析可知,想要正常访问作用域外的局部变量,仅在程序编译的环节就被C编译器阻止了,编译器对这些局部变量的作用域给出了约束,在作用域外是不可见状态,因此自然无法访问。这是非常重要的错误提示,可以避免大部分情况下意外操作作用域之外的内存空间导致的各种未定义行为。

现在我们验证了作用域约束行为的确存在,另外一个常识是当我们在语句块之前定义一个同名变量,进入语句块尝试在局部变量之后操作这个同名变量,则会出现局部变量暂时覆盖同名变量可见状态的行为,表现为所有操作将作用于局部变量,直到离开语句块之后原先的同名变量重新对用户可见,如下所示,编译后观察变量值变化:

结果符合预期,尽管语句块之前的同名变量作用域是能够延伸至语句块内部,不过当遇到重名局部变量之后作用域临时被覆盖,上述行为同样受到编译器的约束。

让我们剔除一些额外的代码,邀请局部静态变量一同参加接下来的汇编盛宴,场地布局(源码)如下:

活动场地都准备好之后,让我们看看客人是如何入场的吧?使用GCC仅编译源码,不进行汇编、链接,gcc -S test.c

没怎么学习过汇编语言,看着信息量有点大,剔除些说明信息会不会好一些呢?

这样看上去就好多了,凭感觉删去了一些貌似和话题无关的信息,粗略的观察一下汇编代码,大致也能猜到 movl $123, -4(%rbp) 和 main 代码块中的 i_local 变量有关,下一条 movl $456, -8(%rbp) 指令和 { } 代码块中的 i_local 局部变量有关,仅在最下方的描述信息中找到有关局部静态变量的踪迹,其他的大概率和 main 函数栈帧的分配、释放有关,加上一些信息描述。

结合理论知识分析可知,局部静态变量无需像自动变量那样直到运行时才分配内存空间,只需要在编译时记录类型、大小、偏移量等信息,变量值存放在全局数据段,偏移量放在全局偏移表(GOT)中便于访问。而局部(自动)变量需要借助调用栈进行空间分配和释放,但不同于函数栈帧中的局部变量,在我的这个例子中,仅有 { } 构成的语句块在离开其作用域后并没有释放和回收空间,紧接着的下一条汇编指令是 movl $0, %eax ,显然这是为 return 0; 语句保存返回值0做准备,这也就意味着在这个例子中,离开局部变量作用域之后我们依然有办法可以访问、操作这片区域。

不过在此之前,我们还是来完整的观察一下该如何访问、操作局部静态变量,在原有代码的基础上,我们在语句块内增加一行,类似这样,再次观察编译之后的汇编代码:

前后发生变化的部分如下, i_static_local++; 被分成了4条汇编指令,无论是获取局部静态变量值还是向存储空间内写入运算结果,是通过x64模式下RIP相对寻址实现的,也就是说在作用域以外,依旧是可以通过这种相对寻址访问局部静态变量,只不过编译器没有提供、也不会提供相对应的操作指令,因为规矩就是规矩。

为了看清 i_static_local.0 标签的真面目,将上述编译好的可执行文件反汇编得到可执行段汇编代码,找到 main 符号所在部分,此时所有的标签都已经被替换成地址偏移值了,且随着指令地址的改变而变化,不过最终都指向 0x0000000000004028 (本身也是偏移值,套娃了属于是),可以用十六进制计算器手工模拟RIP相对寻址检验一下。

既然RIP相对寻址在作用域以外的地方不起作用,还有什么办法可以“借用”一下局部静态变量呢?既然生命周期那么长,又不必担心地址失效的问题,取址,简单粗暴🤣,间接地拓展作用域。

我已出仓,感觉良好😎

回到局部(自动)变量的例子中来,貌似在短时间内同样也是可以使用语句块之外的指针来“拓展”作用域,只要我们还未从 main 函数中返回,整个栈帧空间未被回收。确实可以做到,不过这次我们尝试另外一种办法,在C语言中嵌入汇编代码,在作用域外模仿对局部变量的读写操作。第一次接触C和汇编的联动场面,照猫画虎地写了一个样例:

因为这个例子中的调用栈布局还算简单,通过模仿正常情况下,语句块中局部变量访问的汇编代码,尝试在作用域之外实现对局部变量的取址和赋值操作,并跳转回原作用域验证,输出如下:

不难看出,作用域的约束行为确实存在,但在一定条件下,我们又可以通过某些C编译器认为合法的操作去突破这样的约束,这一切得益于C语言信任程序员,而且不检测越界程序运行更快的哲学?以及编译器相对宽松的检查机制?

举以上例子并不是要想尽办法打破这种“桎梏”,然后往大坑里面跳,相反地,我们既要严格遵守C编译器的各种约束行为,也不能完全依赖于C编译器的约束检查,要以更加谨慎的约束要求审视自己的代码。所以从狭义上来说,我个人以为符号作用域是一种受C编译器的约束行为,是一种软件层面的约束,是一种较为宽松的约束行为。不过这并不会妨碍我喜欢C😋

写在最后:

注意,注意,注意,重要的事情说3遍!首先我个人才学浅薄,暂时还在尝试摸到C的入门门槛,写代码的格式还不够规范,更别说之前几乎没怎么接触过汇编,肯定会有不少纰漏和谬误,望各位道行高深的大佬轻喷,欢迎在评论区或私信中友好交流和指正🙂

其次,我不能保证遵照上述操作流程,在不同平台下都能复现出相同的结果,这取决于你的平台软硬件环境,或许有些编译器的行为约定即使是普通语句块在执行结束后,其中的局部变量需要立即释放和回收,其行为结果也是未定义的。我也不建议通过某些“耍小聪明”的方式刻意去破坏局部变量作用域的约束,如果你不想徒增某些莫名其妙的Bug的话🙃

最后,专栏仅作为入门学习使用,实验结果和文章观点自行甄别,附上所有的参考资料。

参考资料:

  • https://www.bilibili.com/video/BV1at411j7vrC代码中嵌入汇编代码的写法

  • https://blog.csdn.net/littlehedgehog/article/details/2259665GCC内嵌汇编

  • https://www.cnblogs.com/whutzhou/articles/2638498.htmlGCC嵌入汇编代码

  • https://blog.csdn.net/pizi0475/article/details/6301660更多有关C代码嵌入汇编

  • https://www.cnblogs.com/sky-heaven/p/7561625.html同上

  • https://www.zhihu.com/question/270485830另外来说说x86-64的rip相对数据寻址

  • https://blog.csdn.net/hit_shaoqi/article/details/108063166汇编:静态变量与RIP

  • https://www.polarxiong.com/archives/x64%E4%B8%8BPIC%E7%9A%84%E6%96%B0%E5%AF%BB%E5%9D%80%E6%96%B9%E5%BC%8F-RIP%E7%9B%B8%E5%AF%B9%E5%AF%BB%E5%9D%80.htmlx64下PIC的新寻址方式:RIP相对寻址

  • https://blog.csdn.net/qq_43401808/article/details/86476526Intel 64/x86_64/x86/IA-32处理器的指令指针(IP/EIP/RIP)



观察C编译器对局部变量作用域的约束作用的评论 (共 条)

分享到微博请遵守国家法律