生产环境软件的调试信息

  虽然把系统做的坚若磐石是每个程序员的理想和坚持不懈的目标,但往往现实就是现实,就是必须面对的东西。每当服务被引入新的组件或者特性的时候,即使在本地环境下各种测、各种压,但是上线后还是可能会遇到各种问题导致服务挂掉。这不,前两天刚引入的两个更改(syslog日志以及Redis队列服务解耦),导致核心服务crash了两次,当然在每天几十万笔交易中出现两次问题(分属两个特性),说明不是普遍性Bug,很有可能是在特定的条件、特定数据情况下才被触发的。
  线上的服务器打开了core dump的功能,这点还是要表扬的。出现任何问题都要保留尸体,只有这样才能追踪问题,不在相同的位置再跌倒第二次。不过线上运行的程序都是发行版,缺乏调试信息,所以用发行版的软件虽然能加载调用栈信息,知道是哪里出了问题,但是具体的调用参数就找不到了,而这种偶发性的问题大多都是因为调用参数的异常导致的,所以这种情况下的尸体是没法用的。
  使用-g编译过的程序,在可执行程序的体积和占用内存方面会耗费比较大,至于执行速度方面则不得而知了。
  网上搜了一下,针对生产版本软件调试符号的处理,主要有两个方法:(1) 编译带调试版本的软件,然后将调试符号strip调后,用于生产环境;(2) 编译不带调试版本的软件,然后记录其软件版本号,出了问题后现编带调试版本的软件进行调试。

一、记录版本号法

  主要是记录线上运行软件对应的软件分支和版本号,出了问题后将代码树checkout到指定的版本信息,然后编译出一个带调试符号的版本用于问题的跟踪调试。我们知道在Linux下,对于常亮字符串可执行区域有一个专门的区域用于保存这些字符串常量,通过strings工具可以取出这些字符串常量。
  很显然,这个保存版本等信息的源代码本身不能够存在于版本控制中,否则就是鸡和蛋的逻辑问题了。在Makefile中可以指定一个伪目标,然后编译的时候提取代码库的信息生成一个源代码文件,最终编译链接到可执行文件中去;如果你用的是CMake工具,那就更简单了,通过使用exec_program就可以执行特定的命令,而且工具明确了这些命令是在编译步骤开始的时候执行。

1
2
3
4
5
6
exec_program( "export BUILD_VAR=`git log -1 --pretty=%H` && echo 'const char *build_commit = \"VCS: Commit:' $BUILD_VAR '\";' > build_version.cpp ")
exec_program( "export BUILD_VAR=`git log -1 --pretty=%cd` && echo 'const char *build_date = \"VCS: Date:' $BUILD_VAR '\";' >> build_version.cpp ")
exec_program( "export BUILD_VAR=`git log -1 --pretty=\"%an %ae\"` && echo 'const char *build_author = \"VCS: Author:' $BUILD_VAR '\";' >> build_version.cpp ")
exec_program( "export BUILD_VAR=`git symbolic-ref HEAD` && echo 'const char *build_branch = \"VCS: Branch:' $BUILD_VAR '\";' >> build_version.cpp ")

add_executable( tibank main.cpp build_version.cpp ${DIR_SRCS} )

  在最终生成的可执行文件中,通过strings工具可以检索代码版本相关的信息。

1
2
3
4
5
[nicol@yeahka-test build]$ strings -a tibank | grep VCS 
VCS: Commit: 190d5ed5732f22138824b97a0e56d598058b2911
VCS: Date: Sun Sep 3 19:37:22 2017 +0800
VCS: Author: taozhijiang t@taozj.org
VCS: Branch: refs/heads/master

  通过checkout特定版本的源代码然后编译生成调试版软件,在加载core的时候一次成功了,还有一次提示“warning: core file may not match specified executable file.”加载失败了。我也不确定是这种方式可能会有固定的缺陷,还是当时线上代码编译的时候做过一些临时更改所致,不过我认为将版本信息嵌入到可执行文件中是一个很好的习惯!

二、strip符号法

  这种方法是比较推荐的方法,因为在GDB中,调试时候允许可执行程序和调试信息独立放置,同时其还约定了一些固定路径,而在GDB调试需要调试符号的时候,会自动在这些路径下面进行查找和加载。正由于这种形式比较的方便而且被约定俗成,所以CentOS等发行版也根据这种情况把可执行程序和其调试信息(debuginfo)进行分别打包,当某个程序或者库出现异常需要调试的时候,可以使用yum直接安装其debuginfo软件包(比如curl-debuginfo),也可以直接使用debuginfo-install curl来安装,安装后就生成了/usr/lib/debug/usr/bin/curl.debug文件。
  对于我们自己编译的软件,可以先编译带调试符号的版本,然后借助GNU Binutils的objcopy或者elfutils的strip工具生成不带调试符号的发行版用于线上执行,出现问题后使用带有调试信息的原始版用于调试。
  (1) objcopy工具

1
2
3
4
5
6
7
nicol@yeahka-test:~  ls -l tibank
-rwxrwxr-x 1 nicol nicol 11M 9月 22 11:58 tibank
nicol@yeahka-test:~ objcopy --only-keep-debug tibank tibank.debug
nicol@yeahka-test:~ objcopy --strip-debug tibank
nicol@yeahka-test:~ ls -l tibank tibank.debug
-rwxrwxr-x 1 nicol nicol 3.7M 9月 22 11:59 tibank
-rwxrwxr-x 1 nicol nicol 8.7M 9月 22 11:59 tibank.debug

  (2) strip工具

1
2
3
4
5
6
nicol@yeahka-test:~  cp tibank tibank.debug
nicol@yeahka-test:~ strip --only-keep-debug tibank.debug
nicol@yeahka-test:~ strip --strip-debug --strip-unneeded tibank
nicol@yeahka-test:~ ls -l tibank tibank.debug
-rwxrwxr-x 1 nicol nicol 2.4M 9月 22 12:03 tibank
-rwxrwxr-x 1 nicol nicol 8.7M 9月 22 12:03 tibank.debug

  上面两种方式都可以将可执行程序和调试信息进行分离。后面在测试验证的时候,发现无论是使用gdb -s tibank.debug,还是在gdb中直接使用symbol-file tibank.debug的方式加载调试符号都不得成功,需要查看变量的时候提示“can’t compute CFA for this frame”错误。
debug-failed1
  然后,如果使用下面的方式,将可执行程序和调试信息进行合并,然后用得到的完整的带有调试符号的可执行程序进行跟踪,就发现没有任何问题了。

1
2
nicol@yeahka-test:~  cp tibank tibank.full
nicol@yeahka-test:~ objcopy --add-gnu-debuglink=tibank.debug tibank.full

debug-failed2
  网上有人说可能是格式的问题,估计打开一个rpmbuild的spec文件可以探个究竟,我也没深究了。

本文完!

参考