标签 思考 下的文章

跨过BUG查找的”最后一公里”

如果你看到一个C程序员在通宵熬夜神情紧张地对着电脑敲代码或阅读代码,多数只有两种可能:一是为了赶进度;二就是查找内存Bug。
                                                                                                                              — 个人感悟
 
昨晚搞到凌晨一点多,终于算是把一个棘手的Bug的来龙去脉搞清楚了。截至到今天,这个Bug已经困扰了项目组两个核心开发同事达三周之久了。

这个Bug的确很难查找:

   – 首先模拟环境下无法复现该Bug;
   – 生产环境下该Bug是随机出现的,发生频率十分低;
   – Bug出现时并未有dump core等明显异常现象出现,系统依旧运行良好。

得到Bug报告后,我的两位同事就开始对bug引发的问题现象进行了分析,得出了内存被污染的初步结论。之后又在生产环境做了GDB attach到进程的调试,甚至替换了生产环境的版本,利用传统的print语句在关键路径上输出提示信息,试图找到引发Bug的真正原因。但做过这些 后,所能得到的结论依旧停留在内存被污染,至于怎么被污染的、在哪个业务流程上被污染的却无从得知。无奈之下,两位同事开始根据 subversion的commit history进行代码比对和分析,试图查找到哪些新增或修改的代码引发了Bug。代码修改量小还好,如果修改数量巨大,这种代码比对就好比大海捞针,我 们无法保证注意力自始自终是集中的,结果两位同事也的确没有从代码变更中发现什么蛛丝马迹。这类Bug会让你有一种有力无处施展的感觉,面对这样 的Bug,我的两位开发人员似乎也失去了信心和思路。

下面简要描述一下这个Bug:

有这样一个字段数目众多的结构体foo_t,这里仅列出bug相关的几个字段e、c、flag、pdata:

struct foo_t {
    … …
    char e[XX_SIZE];
    char c[XX_SIZE];
    char flag;
    data_t *pdata;
    … …
};

业务逻辑是:

if (flag) {
    处理e、c两个字段;
}

   
bug现象:值本是1的flag字段被污染,值变成了0,导致e、c两个字段没有被做处理,从而引发业务异常,导致客户投诉。我的同事曾经做过如 下尝试,以确定内存污染的行为特点,她在flag之前又加了一个字段flag1:

struct foo_t {
    … …
    char e[XX_SIZE];
    char c[XX_SIZE];
    unsigned int flag1;
    char flag;
    data_t *pdata;
    … …
};

在生产环境下运行得到的结果是flag1和flag值正常,但字段c的尾部字节遭到了污染。现象已经十分明确,离真相就差那最后一公里了。

对于上面的内存污染问题,我首先会怀疑在处理flag或c之前的字段时出现了缓冲区溢出,导致后面字段的内容被整体或局部覆盖。不过从bug现象 来看,这个思路也有说不通的地方,那就是为何是c的尾部字段被污染,而不是从头部开始呢?不过我们依旧沿着这个思路追查了e以及e的诸多前驱字 段,细致的分析了代码,但没有发现溢出点。

c或flag的后继字段比如pdata要想污染c或flag则必须具备更多条件,至少要有操作&pdata的代码,之前基本认为这不太可 能。但现在仅有这一条路可以继续走下去了,也只能沿着这条路走下去。事实证明我们走的没错。在后续的处理流程中有这样的一个函数:

int func(void *p, int size)

这个函数本来是用于处理data_t*变量的,但由于编码者的疏忽,将&pdata传给了p,另外size这个参数也传了一个错误的值, 估计是滥用了copy&paste。而func函数体中对p指向的内存地址做了修改,这个修改直接污染了 ((char*)&pdata + size)起始的那片内存块儿,这就是问题的真正原因所在。这样看来pdata并未污染其所在的foo_t实例中的flag或c字段,而是污染了其他foo_t实例中的flag或c字段,因为这些实例都放在一个mem block pool中的,所以这还是一个随机的远距离内存污染^_^。

我走完了BUG查找的最后那一公里,到达了终点。这个BUG的查找确实不易,但并非遥不可及,为何我的两位同事就停在离真相只有一公里的地方而踌 躇不前了呢?对此我也做了一些考量,希望能在日后的BUG查找方面给予帮助。

要跨过BUG查找的那最后一公里,可从如下几个方面着手努力:

* 收罗证据,不放过一处可疑之处

这是准备工作,就好比警察查看罪案现场,哪怕是一根毛发,一处异物也不能放过。一般来说我们至少要收集到Bug发生时的各方面信息,包括:

 - 系统日志
        这个时间点上各个模块的日志都要搜罗到;

 - core文件
        如果bug引发core dump,那core文件是bug查找的最佳入口;

 - 通信数据包内容
        对于很多后端服务程序而言,不合法的通信数据包常常会引发Bug,我经手的类似Bug就不止一起了。必要时通过抓包工具将通信包抓到文件中以备后用。

 - CPU/内存/磁盘实时状况
       千万不要小视这些信息。如果发现CPU过高,则很可能代码存在死循环的可能(后pstack进程号,则可直接找到问题所在);如果磁盘满,则可以很好解释 数据不完整的异常;如果mem占用过高,则可以解释分配内存异常或性能下降等问题。

 - 系统操作日志
       如果有管理员的操作行为的话,我们也不要放过,将操作日志(一般系统都有保存,并需要对这些日志进行定期审核)截取并保留,以备后用。

 - 操作系统/硬件相关异常信息等。
       如果是因为OS或硬件异常导致的Bug,那搜集到这些信息就太重要了,否则你将付出惨重的Bug查找代价。

Bug查多了你就会有这种感悟:证据用时方恨少啊!

* 沉下心,保持清晰思路

BUG有难有易,简单的Bug大家都能应付,而困难的Bug,就要比拼能力和经验了。要想解决掉Bug,务必要沉下心,不急不躁,这是保持大脑始 终有清晰思路的前提。

能用工具(比如GDB)调试出来的Bug,都不是最难的问题,因为现场就摆在你的面前,你可以看到一切蛛丝马迹。最难的问题最终都是要通过脑力分 析出来的。

解决问题前,要根据之前搜罗的证据,形成自己的查找思路。没有思路是可怕的。没有思路的时候,也不要急于开始查,那样只会乱套。应根据已有的蛛丝 马迹,行成一些思路,哪怕这个思路你自己都不是很肯定,先按这个思路做做看,也许走出一步后,你又能收获新的信息,形成新的思路。就这样敏捷地向 前进,边向前探索边定期回顾。

* 知晓原理,缩小查找范围,形成正确思路

要保持清晰正确的思路,开发人员对系统的运行原理要做到十分清楚,这样可以缩小查找范围,重点突破。就好比上面的那个bug例子,我们要知道 c/flag被污染有几种潜在的可能,并形成多种思路,然后沿着这几种可能的思路继续走下去。在这次查找过程中,想必两位同事恰恰是在原理这方面 没有理解透彻吧。

* 质疑,从自己的代码开始

查Bug就要抛弃“不可能”,拥抱“质疑一切”。而质疑要从自己的代码开始。程序员或多或少都有一种“自负”的心态,骨子里会认为自己的代码肯定 是正确的。如果出现问题,一定是其他人代码的问题,哪怕是OS这样总体来说十分稳定的平台也会成为被首先质疑的对象。不过事实证明,错误多出在我 们自己的代码中,毫无理由的去怀疑操作系统、怀疑你使用的第三方库,多半会南辕北辙,浪费你宝贵的查找时间。

* 拥抱调试技巧和工具

必要的调试技巧是Bug查找的基本功底,这些技巧在涉及内存问题查找过程中相当有用。

  — print语句
        不用多说,print语句是最简单、最常用的调试手段,在代码任意位置,根据你的需要,输出信息,帮助你分析bug原因。其唯一的缺点就是可能需要你重新 build代码和部署你的应用。

  — gdb切入进程地址空间查看堆栈
         利用gdb一类的专用调试工具可在代码运行时切入进程地址空间,实时查看数据变化。你也可以在gdb下执行应用,获得同样的效果(适合单进程应用)。
 
  — 调试版中采用magic number + assert
         C程序的bug多为内存问题。常见的内存越界访问或污染的调试手段是在代码中为内存块添加magic number,并在特定环节用assert保证该magic number的值是没有被修改的。一旦值改变了,则说明问题发生在执行流的两次assert之间的某个地方,后续可进一步缩小assert间隙,直到定位 到问题。

  — 让bug尽可能的容易复现
         一个可以在模拟环境下复现的Bug总是比较好查的。出于这个考虑,我们可通过放大问题区域来尽可能更容易的复现bug,比如将一个字节的字段改为4个字 节,这样可能占据更多被污染的区域,比较利于Bug的复现(但这不总是ok的)。

* 把握节奏,避免陷入惯性思维

一些比较难fix的Bug,其查找过程可能会十分漫长,就像这次我们遇到的这个问题。这就需要我们的开发人员把握好Bug查找的节奏,因为长时间 调试和查问题容易让人陷入惯性思维,反倒不利于Bug的查找。一旦意识到自己进入惯性思维后,可考虑换种活动做做,比如出去散散步、洗个热水澡 等。或者给其他人员讲解你的查找思路,这个过程中自己可能会发现思路上的缺陷,或者由他人指出你思路方面的问题。

感觉Bug查找是一门手艺活,要学会慢工出细活,这总比不出活儿的要好,尤其是在面对那些十分诡异的内存Bug时。

也谈Commit log

版本控制工具大行其道的今天,作为程序员,势必要每天与各种版本控制系统(比如SubversionGitMercurial等)打交道, 每天不commit几次代码都不好意思说自己是专业程序员^_^。不过commit代码可不止敲入commit命令这么简单,对于一个专业程序员 来说,我们还要关注每次commit所携带的背景信息,这里暂且称之为“commit context”。在每次commit时,这些上下文信息只能通过commit log来体现。

一、Commit Context

今日的软件复杂度日益增加,软件开发模式也早已从单打独斗的英雄模式变成了团队协作模式了,而在团队模式下,版本控制系统发挥着至关重要的作用, 它让开发过程变得有序,将冲突解决的成本尽可能地降低到最低。但版本控制系统毕竟不是智能的,它只是机械地记录着每次提交前后的内容的raw差 异,至于这个差异究竟代表了什么,版本管理系统是不得而知的,这就需要我们开发者们来提供,这就算是产生commit context的动机吧。即便是一个人开发维护的项目,个人的记忆也是有时效性的,时间久了,以前的代码变更context势必也就淡忘了,良好且规范的 commit context有助于更好的维护项目,追踪历史思路和行为,甚至在查找bug时也是能帮得上大忙的,比如确认bug引入的时段边界、代码范围等。

前面说了,commit context最终是以commit log形式提供的,这才是我在这篇文章中真正要说的内容^_^。评价一个项目的好坏,无论是商业项目,还是开源项目,代码本身质量是一个重要的方面,代码 维护的规范性则是另外不可忽略的一个重要因素,而在代码维护规范性方面,commit log的规范是一项重要内容。做了这么多年Coding工作,到目前为止部门内部还没有哪一个项目在commit log规范方面是让我满意和欣赏的。另外本人在亲为commit log方面也是不能让自己满意的,这也是促使我思考commit log这块内容的一个初衷。

commit log承载着每次commit动作的context。一般来说context中至少要有一项内容,那就是此次代码变更的summary,这是最基本的要 求。如果你的commit log还是空着的,那你真该反思反思了,那是对自己和他人的不负责任。但无论是商业公司内部开发还是开源项目,commit context涉及到的因素往往不止一个,很多情况下commit context还与项目过程、质量保证流程以及项目使用的一些工具系统有 关联。我们来看两个知名开源项目的commit log样例吧。

[example1 - Linux Kernel]

audit: catch possible NULL audit buffers
It's possible for audit_log_start() to return NULL.  Handle it in the
various callers.

Signed-off-by: Kees Cook <keescook@chromium.org>
Cc: Al Viro <viro@zeniv.linux.org.uk>
Cc: Eric Paris <eparis@redhat.com>
Cc: Jeff Layton <jlayton@redhat.com>
Cc: "Eric W. Biederman" <ebiederm@xmission.com>
Cc: Julien Tinnes <jln@google.com>
Cc: Will Drewry <wad@google.com>
Cc: Steve Grubb <sgrubb@redhat.com>
Cc: Andrea Arcangeli <aarcange@redhat.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>

这是Linux Kernel项目的一个commit log的内容。从这个log携带的context信息来看,我们能够清楚地了解如下一些内容:

- 修改的内核模块范围audit
- 修改的原因summary: to catch possible NULL audit buffers
- 这个patch从诞生到被merge到trunk过程中涉及到的相关的人员列表
- 这个patch由Who sign-off的。

将mail list放入到commit log中,这是Linux Kernel开发过程规范所要求的,同样也是质量保证的一个方法。在《如何加入Linux内核开发社区》系列文章中你可以了解到一些有关Linux Kernel开发过程的内容。从这个例子中我们主要可以看出commit context与Project过程、质量保证链条方面的相关性。

[example2 - Apache Subversion]

Fix issue #3498 – Subversion password stores freeze Eclipse

* subversion/libsvn_auth_gnome_keyring/gnome_keyring.c
  (simple_gnome_keyring_first_creds, simple_gnome_keyring_save_creds,
   ssl_client_cert_pw_gnome_keyring_first_creds,
   ssl_client_cert_pw_gnome_keyring_save_creds): If the keyring is locked
    and we are in interactive mode but have no unlock prompt function, don't
    throw a "GNOME Keyring is locked and we are non-interactive" error;
    instead, continue without unlocking it, so that the unlocking may be
    handled by the default GNOME Keyring unlock dialog box.

这是Apache Subversion项目的一个commit log的内容。同样从这个log携带的context信息来看,我们能够清楚地了解如下一些内容:

- 修改的代码范围subversion/libsvn_auth_gnome_keyring/gnome_keyring.c,包括括号中的函数名列表, 这个显然更为细致。
- 修改的原因summary: Fix issue #3498 – Subversion password stores freeze Eclipse
- 这个patch与问题跟踪系统的关联性 -issue #3498

通过这个commit log,我们可以快速找到此patch对应的问题跟踪系统中的条目#3498,这样可以查看到一些更为细致的context信息。从这个例子我们主要能够 看出commit context与项目所使用的一些工具系统的关联。

综合以上可以看出良好的commit log是可以清楚全面反映commit context的。这里的“全面”是project-dependent的,是需要能够体现出涉及project的一切必要信息的:过程的、质量的、工具 的。

二、Commit log格式

Commit log没有放之四海而皆准的统一格式,而是project-dependent的。就我个人而言,我会在下面的几个问题上有纠结。

* 语言

不得不承认在创造编程语言方面,西方文化占了主导,语言中的关键字也多取自英语。虽然目前主流的语言以及新兴的语言都号称源码原生支持utf8或 unicode其他字符集格式,但却是很少见到在源文件中使用非英语命名变量或函数的,这也影响了我在commit log中对语言的选择 – 我基本上都是用英文编写commit log的。目前主流的版本控制工具都是支持unicode字符集的,你用中文提交也是没有任何问题的,尤其是在国内商业项目中,使用中文描述起来,理解上快且歧义少。我是不反对用中文写commit log的,但反感的是中英文混合写commit log(有些人用中文,有些人用英文)。每当批量看commit log时,中英文混在一起,一点美感都没有了。

commit log不是给最终用户看的,而是给开发维护人员看的。因此选择语言种类时要看这种语言是否能给开发维护人员的工作带来便利,精确全面地传达context。即便 应用是要发布给非洲人民,但若开发人员都是中国人,一样可以用中文编写commit log。

* 地道

说到“地道”,主要是针对你选择外语(大多数情况是英语)作为你commit log的承载语言时。就像生活在国外要用外国人熟悉的语言习惯与人交流似的,我们在用英语编写commit log时也要学会选用“地道”的词汇,远离Chinglish。当然想立即做到“地道”也不是那么容易,毕竟我们一直以来就按照Chinglish的思维去学 习英语的,一个比较好的方式就是多看看知名开源项目(比如linux kernel)的commit log,看看人家是如何选择词汇和组织句子的。其实Commit log中用到的词汇和句型很少,看多了也就找猫画虎的学会了。

* 规范

“没有规矩,不成方圆”,无论是商业软件项目,还是大型开源项目,莫不如此。如果要想很好的传达commit context,一个设计规范,内容全面的commit log格式是必不可少的。我们无需从头做起,很多开源项目在这方面都已经有一些良好的实践,比如上面提到的linux kernel的commit log convention,再比如这里有Apache Subversion的Commit log要求。TYPO3和FLOW3也有自己详细的Commit log说明

制定规范时总体来说,注意以下几点:
– 格式简明扼要,只保留必要的项;
– 注意与项目过程、质量保证流程的结合,以及与第三方工具的关联(注意序号或ID的唯一性);
– 对于规模较大的系统,可以考虑在log中体现影响的涉及的“子模块”或“子目录”名字或者逻辑功能的名字(比如前面linux kernel例子中的audit),这样便于快速定位本地commit的影响范畴。

三、Commit模板

如果像linux kernel或subversion那样涉及到过程、质量控制以及第三方工具的集成(比如问题跟踪系统、代码评审系统等)时,建议设置Commit log template(模板)以简化开发者commit log编写的工作。

* Subversion命令行客户端支持commit log模板

Subversion在命令行客户端侧暂无对模板的支持。不过可以通过一些trick模拟实现这个功能:

- 创建commit log模板log.tmpl,放在特定目录下,本例中放在用户的$HOME目录下
- 添加并导出环境变量SVN_EDITOR
         export SVN_EDITOR="rm svn-commit.tmp && cp ~/log.tmpl svn-commit.tmp && vi "

svn commit时,svn客户端会在当前路径下会执行类似$SVN_EDITOR svn-commit.tmp的命令,而svn-commit.tmp文件已经被替换为我们的模板文件,开发者只需按模板填写内容,并保存退出即可。如果 commit成功,svn客户端会删除当前目录下的svn-commit.tmp,否则svn-commit.tmp不会被删除,这将导致下次再提交 时,svn客户端检测到svn-commit.tmp的存在,从而新建立一个svn-commit.2.tmp的新文件,导致模板失效,这也是这个方法的 一个瑕疵。

* Git命令行支持commit log模板

Git是目前very hot的分布式版本管理工具,起步晚,但起点高,因此已经内置了对模板的支持,只需将模板文件配置一下即可。
         git config –global commit.template ~/log.tmpl

四、良好格式commit log的实施

即便有了良好格式的commit log的模板定义,但就我经验而言,实施起来也还会遇到诸多问题。commit行为是客户端发起的,要让所有开发者都能很好的使用模板并主动按模板提交需 要一些流程以及工具支持。比如在server段部署pre-commit hook,对提交的log格式进行检查,不符合模板格式的予以拒绝等。

对于与问题跟踪系统有关联的log格式,还要注意保持问题跟踪系统id或序号的唯一性,这显然是管理和过程方面的工作。

对于开源项目,一般merge到trunk需要owner的检查,所以反倒实施起来容易了些,只要有一篇内容丰富的 developer/community guide或convention之类的文档即可,多数知名的opensource project(比如linux kernel、subversion、apache httpd server、python等)都是有这类文档的,为这些project提交patch前是要好好阅读这些文档的,不能坏了规矩^_^。     
 

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats