一、编译配置
在实际的开发中,开发者可能经常遇到将代码编译成两种模式即debug和release模式。或说可以这样说,在编译时开发者可以指定编译的配置方式。这种情况在使用命令编译时非常明显,如果有用过g++指定调试选项和优化选项的经验,则可以更容易理解。
可以举一个不太恰当的例子,如果学习对一个机器的拆解安装,在未完全掌握时,往往会在每个零件上贴上标签,并排好编号,甚至对于一些复杂的零件可以进行拍照录相。以期方便在以后进行重新组装时保证机器安装的准确性。但在真正掌握机器的拆解安装后,就不必再如此大费周章的进行处理,可以很丝滑的组装后直接交付。
而对于代码开发来说,更是如此。代码与机器打交道,反而不如机器零件那么容易看得清楚。那么在实际的开发过程中,如果出现各种问题,如何将代码与机器产生的问题映射起来呢?这就需要类似打标签,将机器码产生的问题与实际的代码互相映射,保证问题与代码产生类似绑定的情况,容易对问题进行定位进而解决它。但是如果已经将问题解决后,这些标签就没有了存在的意义或者说意义不大。反而它们的存在会导致运行速度和效率等的性能的损失,这时候就不需要把这些标签删除。
二、debug和release版本
通过上面的分析,其实就可以很好的理解debug和release配置了,前者用来对应着前面的有"标签"信息的编译版本,后者对应着无"标签"信息的编译版本。
debug模式面向开发者应用,通过在编译时插入详细的调试信息,让开发者可以更好理解代码与机器结合运行时的状态流转,用来发现和定位错误、定位各种性能问题。
release模式则面向用户应用。其通过代码优化来提高运行效率和资源占用率。它可以更好的体现代码的质量,进而反映开发者对代码和机器运行的内在关系的理解度。
debug版本对开发者友好,release版本对使用者友好。从更上层的设计角度来看,它是在不同的应用场景下,对实际需求的一种平衡。前者更强调开发者对代码和机器指令的把控,而后者则更强调代码与机器最大的性能契合度。
三、区别
对于debug和release版本的不同,主要包括以下几点:
- 编译目标不同
debug的编译目标是为了调试而release是为了用户应用 - 代码优化不同
debug的代码一般不优化,代码和编译结果一一映射;而release则可以根据条件进行优化,进行指令重排、内联、循环展开等等处理 - 编译结果不同
debug的编译结果体积因为包含各种信息可能很大而release版本则因为优化,编译出来的结果一般比较小。二者的区别一般都存在着量级的差距 - 包含内容不同
debug为了能够进行调试,相关的调试信息非常完全,可以生成相关的调试信息文件(.pdb文件)而release是为了效率一般不会有调试信息(有时也可以有),即使有也无法做到正确的代码映射 - 编译依赖不同
debug版本依赖的当然是debug相关的库,如果使用错误则有可能引起程序的崩溃而release使用的是release版本的相关的库,同样引用错误也会产生问题 - 断言和检查处理不同
debug会启用断言和运行时检查,可以保证代码的安全控制而release则因为代码已经经过了调试,默认是关闭断言和运行时检查的
一般可以这样认为,debug模式就是开发者自用的而release是为了程序发布使用的。这样,开发者可以在debug模式下进行相应的开发处理,而在真正发布时使用release模式,方便用户的使用。
四、release版本带调试信息
虽然说debug和release两种模式各自分工解决了不同情况下的问题。但在实际应用的过程中,开发者往往遇到过这种问题,发布的版本异常(崩溃等)。这种问题是广泛存在的,即使是大公司甚至大牛编写的程序这种情况也不少见。而release版本的编译特点又决定了其无法为开发者提供更多的错误信息,导致了问题的定位和解决的困难。这也是实际开发过程中的一个难题。
出现这种问题的原因是很容易理解的,即编译器的优化可能导致代码执行逻辑的细节的错误,特别是指令重排导致的并行问题。而这种现象更麻烦的在于,它往往不是必现的,而是以一种随机的形式出现,这就更增加了问题的复杂度。
那么怎么解决这个问题呢?一般来说,有两种方式:
- 凭经验和日志等进行反复的分析和定位,利用各种工具帮助查找相关问题。这需要开发者有着丰富的开发和调试经验以及细心和耐心
- 开发一个介于二者之间的一个版本,即在release中增加相应的调试信息。它可以保留关键或敏感位置的调试信息和崩溃转储。为定位和解决问题提供重要的信息支持。但需要说明的是,由于是release版本,其产生的信息可能有所偏差,这就需要有一定的经验进行相关的分析,而不能盲目信任这些信息的位置
从上面的分析可以很清楚的发现,对于开发者来说,任何问题都是可以通过一定的手段进行监控,关键就看应用的场景和需要的成本如何。特别强调的是,一定要把问题与场景紧密的结合在一起,才有可能较为快速的解决相关问题。开发需要抽象,解决问题需要具体。
五、例程对比
下面对一个最简单的基础的C++程序的编译目标进行对比,如下:
c
#include <iostream>
int Add(int a, int b) {
return a + b;
}
int main() {
int sum = Add(5, 3);
std::cout << "The sum of 5 and 3 is: " << sum << std::endl;
return 0;
}
其汇编代码的显著不同在于:
//debug
00000000000011e1 <main>:
11e1: f3 0f 1e fa endbr64
11e5: 55 push %rbp
11e6: 48 89 e5 mov %rsp,%rbp
11e9: 48 83 ec 10 sub $0x10,%rsp
11ed: be 03 00 00 00 mov $0x3,%esi
11f2: bf 05 00 00 00 mov $0x5,%edi
11f7: e8 cd ff ff ff call 11c9 <_Z3Addii> #Add函数调用
11fc: 89 45 fc mov %eax,-0x4(%rbp)
11ff: 48 8d 05 fe 0d 00 00 lea 0xdfe(%rip),%rax # 2004 <_IO_stdin_used+0x4>
1206: 48 89 c6 mov %rax,%rsi
1209: 48 8d 05 30 2e 00 00 lea 0x2e30(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
0000000000001297 <_GLOBAL__sub_I__Z3Addii>:
1297: f3 0f 1e fa endbr64
129b: 55 push %rbp #明显的栈动作
129c: 48 89 e5 mov %rsp,%rbp
129f: be ff ff 00 00 mov $0xffff,%esi
12a4: bf 01 00 00 00 mov $0x1,%edi
12a9: e8 93 ff ff ff call 1241 <_Z41__static_initialization_and_destruction_0ii>
12ae: 5d pop %rbp
12af: c3 ret
//release
00000000000011e1 <main>:
11e1: f3 0f 1e fa endbr64
11e5: 55 push %rbp
11e6: 48 89 e5 mov %rsp,%rbp
11e9: 48 83 ec 10 sub $0x10,%rsp
11ed: be 03 00 00 00 mov $0x3,%esi
11f2: bf 05 00 00 00 mov $0x5,%edi
11f7: e8 cd ff ff ff call 11c9 <_Z3Addii>
11fc: 89 45 fc mov %eax,-0x4(%rbp)
11ff: 48 8d 05 fe 0d 00 00 lea 0xdfe(%rip),%rax # 2004 <_IO_stdin_used+0x4>
1206: 48 89 c6 mov %rax,%rsi
1209: 48 8d 05 30 2e 00 00 lea 0x2e30(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
0000000000001300 <_Z3Addii>:
1300: f3 0f 1e fa endbr64
1304: 8d 04 37 lea (%rdi,%rsi,1),%eax #直接操作
1307: c3 ret
上面的区别还是很明显的,在主函数中,debug版本中直接调用了Add(_Z3Addii)函数,而release版本中则没有显示调用(当然也有可能其它折叠等原因造成)。更显著的是在Add函数实现中,debug版本使用了明显的栈创建处理,而release直接使用了lea命令就返回了。
当然,如果能够把两个文件的整体的反编译代码进行比较,可以发现.plt及.text段中,debug版本中的信息比release明显要多很多。
六、总结
两种版本的配置,对于开发者来说,其实很多人并不敏感甚至说不重视。可能觉得就是一个选项或配置一下的过程,但这其中其实有着底层巨大的差异。只有真正掌握这两种模式,才能从机器的视角去看待代码,才能真正的理解代码语言与机器语言的转换。