上节,我们利用函数运行树对错误进行了初步定位

读者可能会问: VC 的调试功能很强大,为什么不利用单步跟踪来定位?

这是因为,对现有代码单步跟踪时,函数帧栈会发生意想不到的变化

乍一听很不可思议,那就让我们在函数 ASN1_item_ex_d2i 调用 asn1_template_ex_d2i 的地方
(case ASN1_ITYPE_SEQUENCE 分支)下断点, F5 启动,程序在中断处的调用栈为
> openssl-0.9.8e.exe!ASN1_item_ex_d2i
  openssl-0.9.8e.exe!ASN1_item_d2i
  openssl-0.9.8e.exe!d2i_X509
  openssl-0.9.8e.exe!PEM_X509_INFO_read_bio
接着再按 F10, 本来我们认为调试光标会指向下一行,但是光标仍停留在原处,调用栈反而变成
> openssl-0.9.8e.exe!ASN1_item_ex_d2i
  openssl-0.9.8e.exe!asn1_template_noexp_d2i
  openssl-0.9.8e.exe!asn1_template_ex_d2i
  openssl-0.9.8e.exe!ASN1_item_ex_d2i
  openssl-0.9.8e.exe!ASN1_item_d2i
  openssl-0.9.8e.exe!d2i_X509
顿时让你陷入不知所措 -- 至于为什么会这样,请读者思考

另外,由于函数之间嵌套调用关系复杂,同一函数经常进入多次(参见上节中的函数运行树)
很容易让人产生似曾相识的感觉:函数相同但上下文不同,难以对整体的代码运行有完整的理解和把握

碰到这种情况,我们该怎么办,能否另辟蹊径?

函数运行树就是笔者想到的一个尝试
它可以解决上面碰到的问题:将函数的执行路径以树的形式呈现出来,克服了“只见树木,不见森林”的不足

为什么要这样做?因为再复杂的业务逻辑,也可以通过硬啃代码来熟悉,但这只是时间的问题,算不上什么高明
我们更关心的是,能否在这种代码走读的 routine 中有所提高

问题紧接而来:有没有可能得到函数运行树?
答案是明确的:当然

我们来考察以函数 ASN1_item_ex_d2i(调用路径: d2i_X509->ASN1_item_d2i->ASN1_item_ex_d2i)为根的运行树
下面是得到此运行树的简要说明,由于此过程比较 ad hoc, 就不再列出每一步的详细过程而只给出思路
当然 Perl 在这之中仍扮演了重要的角色

1、准备工作 -- 代码实现:获取正在运行函数的调用栈
我们选择不重新造轮子,而是站在别人的肩膀上 -- 再次感谢伟大的 Internet!
访问 http://www.codeproject.com/Articles/11132/Walking-the-callstack, 下载源码
将代码改为 C 语言风格,核心函数改为 int ShowCallStack(char* str),加入 VC 工程

2、在待显示调用栈的函数开头插入对 ShowCallStack 的函数调用,我们关心的函数有:
crypto\asn1\tasn_dec.c
    ASN1_item_ex_d2i
    asn1_template_ex_d2i
    asn1_template_noexp_d2i
    asn1_d2i_ex_primitive
    asn1_ex_c2i
crypto\asn1\tasn_new.c
    ASN1_item_new
    ASN1_item_ex_new
    asn1_item_ex_combine_new
    ASN1_template_new
    ASN1_primitive_new
crypto\asn1\x_name.c
    x509_name_ex_d2i
为区分同一函数的不同调用,待显示调用栈函数的入参信息作为 ShowCallStack 的入参

3、F5 启动调试,结束后将 VC 输出窗口的调用栈显示结果,保存在文件 vc_callstack.txt 中
其中的典型输出格式如下:

asn1_item_ex_combine_new(sname=X509_CINF#itype=1#utype=16#type=NEW)
ASN1_template_new
asn1_item_ex_combine_new
ASN1_item_ex_new
ASN1_item_ex_d2i
ASN1_item_d2i
d2i_X509

ASN1_template_new(field_name=version#flags=OPT_EXP_CONT_#item=ASN1_INTEGER)
asn1_item_ex_combine_new
ASN1_template_new
asn1_item_ex_combine_new
ASN1_item_ex_new
ASN1_item_ex_d2i
ASN1_item_d2i
d2i_X509

4、重排调用栈显示格式
例如,将调用栈
    fun3
    fun2
    fun1
重新排列成一行内显示的正常格式
fun1  fun2  fun3

使用下列 Perl 脚本即可完成

# 说明:$/=qq/\n\n表示以连续两个换行符【空行】作为记录分隔标记,一次读一个调用栈
#       split 分隔调用栈为列表(\n 为分隔标记),并逆序在一行内显示列表内容
perl -ne "BEGIN{$/=qq/\n\n/;}; print qq/\n/;@a=split(qq/\n/,$_);@a=reverse @a;foreach (@a){print qq/$_  /;}" vc_callstack.txt > normal_call.txt

5、生成的 normal_call.txt, 其函数排列类似如下(A,B,C...表示函数名)
    A
    A B
    A B C
    A B C D
    A B C D E
    A B C F
    A B C G
    A B H
    A B I
    A J
    A J K
    A L
    A M
用 Perl 脚本,以列为单位,从上向下扫描,将遇到的与上面重复的函数去掉
并转换成 XML 文件,最终得到以 A 为根的函数运行树
    A
      B
        C
          D
            E
          F
          G
        H
        I
      J
        K
      L
      M

6、需要强调,在以上步骤,还会碰到某些问题,比如
对函数 ShowCallStack 的微调,包括:调用栈不显示 ShowCallStack 本身、不显示函数 d2i_X509 以下的调用函数
ASN1_item_ex_d2i(sname=X509#itype=1#utype=16) 中 # 改为空格
对函数 ASN1_TYPE_new 的特殊处理,等等
当然这些问题最终都 work around,细节就不再表述了

最后,我们来回答上节留下的问题: asn1_ex_c2i 在哪里出错了?
如下,根据当前运行的上下文,将调用函数 c2i_ASN1_INTEGER

int asn1_ex_c2i(ASN1_VALUE **pval, const unsigned char *cont, int len,
      int utype, char *free_cont, const ASN1_ITEM *it)
{
    switch(utype)
    {
      ......
      case V_ASN1_INTEGER:
      case V_ASN1_NEG_INTEGER:
      case V_ASN1_ENUMERATED:
      case V_ASN1_NEG_ENUMERATED:
          tint = (ASN1_INTEGER **)pval;
          if (!c2i_ASN1_INTEGER(tint, &cont, len))
            goto err;
          /* Fixup type to match the expected form */
          (*tint)->type = utype | ((*tint)->type & V_ASN1_NEG);
          break;
      ......
    }
}

F11 跟进函数 c2i_ASN1_INTEGER(crypto\asn1\a_int.c), 并继续单步跟踪,最终发现
当运行到第 244 行时,由于判断条件满足,将执行 if 块里面的语句
    if ((*p == 0) && (len != 1)) // 序列号以 0x00 开头,且序列号长度 != 1
        {
        p++; // p 指向当前证书的序列号
        len--; // len 表示序列号长度
        }
    memcpy(s,p,(int)len); // if 条件满足,将导致序列号少复制一字节

上面复制的缓冲区 s 构成 X509_CINF 的成员 ASN1_INTEGER *serialNumber
由于内部表示少了一个字节,在后面的 i2d 环节现了原形,导致验证不通过

马上验证一下,很简单:将 if 块里面的两行语句注释掉
编译、链接、运行,历经千辛万苦,屏幕终于输出了 OK