跟我学C++中级篇—debug和release

一、编译配置

在实际的开发中,开发者可能经常遇到将代码编译成两种模式即debug和release模式。或说可以这样说,在编译时开发者可以指定编译的配置方式。这种情况在使用命令编译时非常明显,如果有用过g++指定调试选项和优化选项的经验,则可以更容易理解。

可以举一个不太恰当的例子,如果学习对一个机器的拆解安装,在未完全掌握时,往往会在每个零件上贴上标签,并排好编号,甚至对于一些复杂的零件可以进行拍照录相。以期方便在以后进行重新组装时保证机器安装的准确性。但在真正掌握机器的拆解安装后,就不必再如此大费周章的进行处理,可以很丝滑的组装后直接交付。

而对于代码开发来说,更是如此。代码与机器打交道,反而不如机器零件那么容易看得清楚。那么在实际的开发过程中,如果出现各种问题,如何将代码与机器产生的问题映射起来呢?这就需要类似打标签,将机器码产生的问题与实际的代码互相映射,保证问题与代码产生类似绑定的情况,容易对问题进行定位进而解决它。但是如果已经将问题解决后,这些标签就没有了存在的意义或者说意义不大。反而它们的存在会导致运行速度和效率等的性能的损失,这时候就不需要把这些标签删除。

二、debug和release版本

通过上面的分析,其实就可以很好的理解debug和release配置了,前者用来对应着前面的有"标签"信息的编译版本,后者对应着无"标签"信息的编译版本。

debug模式面向开发者应用,通过在编译时插入详细的调试信息,让开发者可以更好理解代码与机器结合运行时的状态流转,用来发现和定位错误、定位各种性能问题。

release模式则面向用户应用。其通过代码优化来提高运行效率和资源占用率。它可以更好的体现代码的质量,进而反映开发者对代码和机器运行的内在关系的理解度。

debug版本对开发者友好,release版本对使用者友好。从更上层的设计角度来看,它是在不同的应用场景下,对实际需求的一种平衡。前者更强调开发者对代码和机器指令的把控,而后者则更强调代码与机器最大的性能契合度。

三、区别

对于debug和release版本的不同,主要包括以下几点:

  1. 编译目标不同
    debug的编译目标是为了调试而release是为了用户应用
  2. 代码优化不同
    debug的代码一般不优化,代码和编译结果一一映射;而release则可以根据条件进行优化,进行指令重排、内联、循环展开等等处理
  3. 编译结果不同
    debug的编译结果体积因为包含各种信息可能很大而release版本则因为优化,编译出来的结果一般比较小。二者的区别一般都存在着量级的差距
  4. 包含内容不同
    debug为了能够进行调试,相关的调试信息非常完全,可以生成相关的调试信息文件(.pdb文件)而release是为了效率一般不会有调试信息(有时也可以有),即使有也无法做到正确的代码映射
  5. 编译依赖不同
    debug版本依赖的当然是debug相关的库,如果使用错误则有可能引起程序的崩溃而release使用的是release版本的相关的库,同样引用错误也会产生问题
  6. 断言和检查处理不同
    debug会启用断言和运行时检查,可以保证代码的安全控制而release则因为代码已经经过了调试,默认是关闭断言和运行时检查的

一般可以这样认为,debug模式就是开发者自用的而release是为了程序发布使用的。这样,开发者可以在debug模式下进行相应的开发处理,而在真正发布时使用release模式,方便用户的使用。

四、release版本带调试信息

虽然说debug和release两种模式各自分工解决了不同情况下的问题。但在实际应用的过程中,开发者往往遇到过这种问题,发布的版本异常(崩溃等)。这种问题是广泛存在的,即使是大公司甚至大牛编写的程序这种情况也不少见。而release版本的编译特点又决定了其无法为开发者提供更多的错误信息,导致了问题的定位和解决的困难。这也是实际开发过程中的一个难题。

出现这种问题的原因是很容易理解的,即编译器的优化可能导致代码执行逻辑的细节的错误,特别是指令重排导致的并行问题。而这种现象更麻烦的在于,它往往不是必现的,而是以一种随机的形式出现,这就更增加了问题的复杂度。

那么怎么解决这个问题呢?一般来说,有两种方式:

  1. 凭经验和日志等进行反复的分析和定位,利用各种工具帮助查找相关问题。这需要开发者有着丰富的开发和调试经验以及细心和耐心
  2. 开发一个介于二者之间的一个版本,即在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明显要多很多。

六、总结

两种版本的配置,对于开发者来说,其实很多人并不敏感甚至说不重视。可能觉得就是一个选项或配置一下的过程,但这其中其实有着底层巨大的差异。只有真正掌握这两种模式,才能从机器的视角去看待代码,才能真正的理解代码语言与机器语言的转换。