全局变量初始化顺序探究

简介: 全局变量初始化顺序探究

缘起

我在上一篇文章——《调试实战 —— dll 加载失败之全局变量初始化篇》中,跟大家分享了一个由于全局变量初始化顺序导致的 dll 加载失败的例子。感兴趣的小伙伴儿可以点击阅读。

虽然我们知道了是由于全局变量初始化顺序导致的问题,也给出了解决方案。但是有一点却没有刨根问底——为什么改变文件在工程文件中的顺序就可以改变全局变量初始化顺序?是怎么影响的呢?本篇文章力求解决这个问题。

了解 vs 编译

我们可以简单的把整个构建过程分成三个步骤(当然实际还有其它步骤,我们一般不关心):预编译,编译,链接。

预编译: 处理宏,#include 展开等。

编译: 以编译单元为单位生成对应的 .obj 文件。

链接: 把生成的.obj 文件和必要的文件链接成最后的应用程序。

猜想

因为编译是把符号放到对应的 .obj 中,链接的时候才把对应的 .obj 文件链接成最后的应用程序。链接的时候应该是按照 .obj 文件出现的先后顺序依次把 .obj 中的符号放到对应的位置。

思路

对比观察调整顺序前和调整顺序后的编译参数,链接参数。因为猜测是链接导致的问题,我们主要关注链接参数。

编译过程初探

当我们在 vs 中执行 build 时的整个过程如下图(使用 process monitor 捕获的):

vs-msbuild-cl-link.png

可以清晰的看到,vs 在内部会启动 msbuild.exe 执行后续的操作。msbuild.exe 会间接启动 cl.exe 进行编译,link.exe 进行链接。我们还发现黄色高亮部分的 Tracker.exe ,这个进程主要用来加速编译的。具体可以参考《Inside the Microsoft Build Engine Using MSBuild and Team Foundation Build》 这本书的介绍,简单截图如下:

filetracker-introduce.png

简化编译过程

因为 vs 会通过 msbuild.exe 执行操作,我们可以直接使用 msbuild.exe 进行构建。msbuild 有一个选项 TrackFileAccess 可以用来控制是否使用 FileTracker。为 false 时,不启用 FileTracker

为了简化问题,我们直接执行 msbuild.exe -p:TrackFileAccess=false project_file_to_build.vcxproj

msbuild-cl-link-param.png

我们发现,传递给 cl.exelink.exe 的参数都是文件。猜测,应该是把参数保存到文件中传递的。据观察,这些文件会在执行完后被清理。得想办法在这些文件被删除之前保存一份,各位小伙伴儿有什么好办法吗?

我们先看下这些参数文件是谁创建和删除的,什么时候删除的。创建很简单,肯定是 msbuild.exe。删除呢?是 cl.exe / link.exe 还是 msbuild.exe 呢?又是什么时候删除的呢?相信下图能很好的回答这些问题了。

msbuild-remove-param-file.png

我想到两个思路:

  1. 因为这些文件是 msbuild.exe 创建/删除的,可以在 msbuild.exe 中文件操作的地方加断点。
  2. 可以暂停 cl.exe/link.exe 的执行,拷贝我们需要的文件到桌面。

第一个思路相对来说比较复杂,今天我们尝试第二个思路。我们该如何暂停呢?请出 gflags.exe

中断 link.exe

我们可以在 gflags.exe 中进行如下设置,这样当 link.exe 启动时就会中断到 windbg.exe 中了。

gflags-set-debug-link.png

断下来后,我们可以在 windbg 中输入 !peb 观察参数,里面包含了我们需要拷贝的文件路径。

windbg-command-line.png

有了文件路径,我们就可以手动复制对应的文件到桌面慢慢研究了。

对比链接参数

调整 Test1.cpp Test2.cpp.vcxproj 中的顺序,按上面的方法分别保存传递给 link.exe 的参数文件,对比如下图(格式有调整):

obj-link-order.png

发现在两次链接过程中,Test1.obj Test2.obj 出现的顺序是不一样的。

结论

哪个源码文件在 .vcxproj 中先出现,其对应的 .obj 文件在传递给 link.exe 的参数文件(.rsp)中越靠前,会被优先处理。.obj 中包含的全局变量会被优先处理。当进程启动时,执行全局变量初始化的时候会按照先后顺序初始化。

总结

  • vs 内部会使用 msbuild.exe 编译,我们也可以直接使用 msbuild.exe 进行编译。
  • 使用 msbuild.exe -p:TrackFileAccess=false 可以在编译的过程中不启动 Tracker.exe,对我们调查问题有帮助。
  • 我们可以在一个进程启动时就中断到调试器,可以使用 gflags.exe 帮我们实现这一点。
  • !peb 可以查看启动参数,环境变量等信息。
  • .vcxproj 中文件的顺序会影响最后链接时的顺序。

参考资料

《Inside the Microsoft Build Engine Using MSBuild and Team Foundation Build》

https://docs.microsoft.com/en-us/cpp/c-runtime-library/crt-initialization?redirectedfrom=MSDN&view=vs-2019

http://www.cppblog.com/xlshcn/archive/2007/12/07/37088.html

相关文章
|
23天前
|
存储 安全 Java
5分钟读懂成员变量和局部变量的所有区别
本文介绍了Java面试中常见的成员变量与局部变量的区别,包括定义位置、生命周期、默认值、修饰符支持、存储位置以及在多线程环境中的表现。成员变量存储在堆内存,生命周期与对象绑定;局部变量存储在栈内存,生命周期较短。在多线程中,成员变量线程不安全,局部变量线程安全。掌握这些区别有助于应对面试中的相关问题。
|
8月前
|
存储 Java
【Java开发指南 | 第七篇】静态变量生命周期、初始化时机及静态变量相关性质
【Java开发指南 | 第七篇】静态变量生命周期、初始化时机及静态变量相关性质
158 4
|
8月前
|
存储 Java 编译器
Java面向对象编程:成员变量与局部变量
Java面向对象编程:成员变量与局部变量
70 0
|
8月前
|
存储 人工智能 编译器
【重学C++】【引用】一文看懂引用的本质与右值引用存在的意义
【重学C++】【引用】一文看懂引用的本质与右值引用存在的意义
180 0
|
8月前
|
编译器 C语言 C++
C++类和对象的细节原理:this指针、构造函数和析构函数、深浅拷贝、运算符重载、初始化列表、类的各种成员和方法
C++类和对象的细节原理:this指针、构造函数和析构函数、深浅拷贝、运算符重载、初始化列表、类的各种成员和方法
90 0
|
安全 Java
Java多线程对于成员变量和局部变量的影响
Java多线程对于成员变量和局部变量的影响
|
存储 C++
C++基础语言之(一)static关键字的作用
C++基础语言之(一)static关键字的作用
151 0
|
编译器 C++
C++类和对象终章——友元函数 | 友元类 | 内部类 | 匿名对象 | 关于拷贝对象时一些编译器优化
C++类和对象终章——友元函数 | 友元类 | 内部类 | 匿名对象 | 关于拷贝对象时一些编译器优化
158 0
何为构造函数
Java构造函数,也叫构造方法,是Java中一种特殊的函数。函数名与对应类名相同,无返回值。
150 0
|
存储 程序员 编译器
容易混淆的基本概念 成员变量 局部变量 全局变量
在实际开发与学习中,特别容易混淆几个基本概念:成员变量、局部变量、全局变量。了解这些概念的属性,存储在实际编码中非常有用。
144 0
容易混淆的基本概念 成员变量 局部变量 全局变量