上节末尾,我们提到 d2i_X509 函数,该函数在证书验证过程中的一个调用栈是
d2i_X509
d2i_X509_AUX
PEM_ASN1_read_bio
PEM_read_bio_X509_AUX
load_cert
check
这是上节中提到的证书验证步骤(1) -- 将证书内容转换为内部结构 -- 的必经之路,但是我们在原始代码中找不到 d2i_X509 的实现过程。
事实上,包括它在内的一大群函数(最著名的是 d2i/i2d 系列)都在 OpenSSL 中找不到函数定义的源码,下面是双击函数调用栈中 d2i_X509 函数的截图
上图告诉我们, d2i_X509 函数定义在宏 IMPLEMENT_ASN1_FUNCTIONS(X509) 中,它是如何定义的?
这个很简单,我们可以通过编译器提供的预处理功能还原其本来的面目。
VC 中在 cl 编译器后加上 /E 选项就可以得到。下面是宏 IMPLEMENT_ASN1_FUNCTIONS(X509) 进行展开的结果
X509 *d2i_X509(X509 **a, const unsigned char **in, long len) {
return (X509 *)ASN1_item_d2i((ASN1_VALUE **)a, in, len, (X509_it()));
}
int i2d_X509(X509 *a, unsigned char **out) {
return ASN1_item_i2d((ASN1_VALUE *)a, out, (X509_it()));
}
X509 *X509_new(void) {
return (X509 *)ASN1_item_new((X509_it()));
}
void X509_free(X509 *a) {
ASN1_item_free((ASN1_VALUE *)a, (X509_it()));
}
再用宏展开后的代码替换原文件中的宏定义,就可以满足我们的要求。
当然,这种手工方式仅仅适用于文件比较少的情况,如果文件有几十、上百个,那就成为一个纯体力活。
不幸的是, OpenSSL 中恰好充斥了类似下面的各种各样的宏。
ASN1_SEQUENCE/ASN1_SEQUENCE_END、ASN1_ITEM_TEMPLATE/ASN1_ITEM_TEMPLATE_END、IMPLEMENT_ASN1_FUNCTIONS_fname、IMPLEMENT_ASN1_FUNCTIONS_name
这些宏定义的代码/数据,在后面的代码走读中经常遇到。
自然想到,如果能够把“宏定义替换为宏展开后的结果”这一手动过程变成脚本自动执行,那该多好?
又一次,我们想到 Perl 这个大神,谁要文本处理是它的强项呢?
不再多说,直接上代码
1 # 名称: auto_expand_macro.pl 2 # 功能: 将 C 源代码中的指定宏替换为预处理后的结果, 宏在宏定义文件指定, 这些宏在匹配它们的源文件中被替换为预处理后的内容 3 # 使用方法: perl auto_expand_macro.pl 宏定义文件[全路径] 被处理源文件所在目录[全路径] 编译源文件所在的主目录[全路径] 4 # 举 例: perl auto_expand_macro.pl d:\openssl-0.9.8e\macro_def.txt d:\openssl-0.9.8e\crypto d:\openssl-0.9.8e 5 # 实现思路: 对比源文件和预处理后的内容,用宏展开后的内容替换原来的宏定义 6 # 说明: 读者需要基本掌握 Perl 7 # 脚本以 quick and dirty 的方式完成 8 # 适用于 VC2008, 如使用 VC 的其他版本, 请修改相应批处理命令 9 # 对宏展开后的代码用 uncrustify 进行了美化, 如果不需要可以修改脚本, 将其去掉 10 # 前提: uncrustify.exe 与配置文件 defaults.cfg 要放在源文件所在的主目录[主编译目录], 请自行下载并存放 11 # 12 # 联系: chen_yan_hua@163.com 13 14 use v5.10; 15 16 # 参数检测 17 if ($#ARGV != 2) 18 { 19 say "Usage: perl auto_expand_macro.pl macro_define_file[full_path] sourcefile_dir[full_path] project_dir[full_path]"; 20 exit 0; 21 } 22 chdir $ARGV[1] or die "$ARGV[1] : $!\n"; # 检测 宏展开源文件 目录 23 chdir $ARGV[2] or die "$ARGV[2] : $!\n"; # 检测 编译 主目录 24 25 if (! -e "uncrustify.exe") # 检测 uncrustify.exe 26 { 27 say "please confirm uncrustify.exe in $ARGV[2]"; 28 exit 0; 29 } 30 if (! -e "defaults.cfg") # 检测 defaults.cfg 31 { 32 say "please confirm defaults.cfg in $ARGV[2]"; 33 exit 0; 34 } 35 36 # 说明: 宏定义文件以 # 开头表示注释, 定义的宏分为两种 37 # 38 # 成对宏, 例如 39 # ASN1_SEQUENCE ASN1_SEQUENCE_END 40 # 41 # 单个宏, 例如 42 # IMPLEMENT_ASN1_FUNCTIONS 43 44 # 打开宏定义文件, 读入宏到相应数组 45 $macro_def_file = $ARGV[0]; 46 open(MACRO_DEF_FILE, "<$macro_def_file") or die "unable to open $macro_def_file : $!\n"; 47 while ($line = <MACRO_DEF_FILE>) 48 { 49 if ($line =~ /^#/) # 跳过 # 开头的注释 50 { 51 next; 52 } 53 elsif ($line =~ /^\s*(\w+)\s+(\w+)\s*$/) # 成对宏, 放在 macro_pair_first[]/macro_pair_second[] 54 { # 分别保存 开始宏 和 结束宏 55 $macro_pair_first[$#macro_pair_first+1] = $1; # ASN1_SEQUENCE 56 $macro_pair_second[$#macro_pair_second+1] = $2; # ASN1_SEQUENCE_END 57 } 58 elsif ($line =~ /^\s*(\w+)\s*$/) # 单个宏, 放在 macro_single_list[] 59 { 60 chomp $line; 61 $macro_single_list[$#macro_single_list+1] = $1; 62 } 63 } 64 close(MACRO_DEF_FILE); 65 66 # 显示读取的宏 67 say "single MACRO"; 68 say " ",$_ for @macro_single_list; 69 say "pair MACRO"; 70 for ($i = 0; $i <= $#macro_pair_first; $i++) 71 { 72 say " [", $macro_pair_first[$i],"\t",$macro_pair_second[$i], "]"; 73 } 74 75 $count = 0; 76 $temp_file = "temp.file"; 77 78 # 递归查找匹配宏的 C 源文件,并在匹配的宏前后加上标记 79 MarkSourceMacro($ARGV[1]); 80 81 sub MarkSourceMacro 82 { 83 my $curdir = $_[0]; # 获取递归的目录 84 opendir DH, $curdir or die "Can't open directory: $!\n"; 85 my @dirs = readdir DH; 86 # say qq/Dir: / . $curdir; 87 foreach my $dir_item (@dirs) 88 { 89 if ($dir_item =~ /^(\.|\.\.)$/) # 跳过 . 和 .. 90 { # 不能去掉括号() 91 next; # /^\.|\.\.$/ 的意思是匹配 ^\. 或 \.\.$ 92 } 93 $full_path = $curdir . "/" . $dir_item; 94 if (-d $full_path) # 是目录 95 { 96 MarkSourceMacro($full_path); 97 } 98 else # 是文件 99 { 100 if ($dir_item =~ /\.c$/i) # 源文件, 后缀为 .c(.C) 101 { 102 $full_path =~ s{\/}{\\}g; 103 match_func($full_path); # 处理匹配的源文件 -- 用 自定义对 标识宏定义 104 } 105 } 106 } 107 closedir DH; 108 } 109 110 # MARK 源文件中的指定宏 -- 在其前后加上 MY_MAGIC_MARK_START_XXX/MY_MAGIC_MARK_END_XXX 对 111 # 指定的宏放在全局变量 macro_single_list macro_pair_first macro_pair_second 112 sub match_func{ 113 local $/; # $/ 作为局部变量, 用于一次性读出所有内容 -- 跳出作用域后,$/ 将恢复原来的值 114 my $file = $_[0]; # 以下将源文件全部内容读入变量 115 open(ORIGIN_SOURCE_FILE, "<$file") or die "unable to open $file : $!\n"; 116 $current_content = <ORIGIN_SOURCE_FILE>; 117 $origin_content = $current_content; 118 close(ORIGIN_SOURCE_FILE); 119 120 # MARK 成对宏: 在 121 # ASN1_SEQUENCE_ref(X509, x509_cb, CRYPTO_LOCK_X509) = { 122 # ASN1_SIMPLE(X509, cert_info, X509_CINF), 123 # ASN1_SIMPLE(X509, sig_alg, X509_ALGOR), 124 # ASN1_SIMPLE(X509, signature, ASN1_BIT_STRING) 125 # } ASN1_SEQUENCE_END_ref(X509, X509) 126 # 外围包上 MY_MAGIC_MARK_START_XXX/MY_MAGIC_MARK_END_XXX 对, 成为如下格式 127 # MY_MAGIC_MARK_START_XXX 128 # ASN1_SEQUENCE_ref(X509, x509_cb, CRYPTO_LOCK_X509) = { 129 # ...... 130 # } ASN1_SEQUENCE_END_ref(X509, X509) 131 # MY_MAGIC_MARK_END_XXX 132 for ($i = 0; $i <= $#macro_pair_first; $i++) 133 { # 所有成对宏 对源文件处理一次 134 $start = $macro_pair_first[$i]; 135 $end = $macro_pair_second[$i]; 136 137 $current_content =~ s 138 { 139 ( # 捕捉括号开始 140 $start # ASN1_SEQUENCE_ref 141 \( # ( -- 我们关注的成对宏都有括号(), 这有点 ad hoc 的意味, 至于要关注哪些宏, 取决于具体需求 142 [^()]* # 括号内的任意文本 143 \) # ) -- 更理想的是匹配嵌套括号对, 参见: <<Perl Cookbook(2nd Edition)>> 6.17 Matching Nested Patterns 144 .*? # 任意宏定义体 ASN1_SIMPLE(X509, cert_info, X509_CINF)/ASN1_SIMPLE(X509, sig_alg, X509_ALGOR) ...... 145 $end # ASN1_SEQUENCE_END_ref 146 \( # ( 147 [^()]* # 括号内的任意文本 148 \) # ) 149 ) # 捕捉括号结束 150 } 151 { 152 MY_MAGIC_MARK_START_ .# 字符串 MY_MAGIC_MARK_START_ 与连接符. 153 ++$count . # $count 是全局变量,每次的替换值都不同 -- MY_MAGIC_MARK_START_1/2/3/... 154 "\n$1\n" . # 匹配的 成对宏定义处 前后用换行符隔开 155 MY_MAGIC_MARK_END_ . # MY_MAGIC_MARK_END_ 与连接符. 156 $count # 第 $count 个宏定义 157 }egsx; # e 把 REPLACEMENT 作为 Perl 代码块(而不是一个硬编码的字符串)执行,执行的结果作为替换字串 158 # g 替换所有 159 # s 匹配换行 160 # x 排版美观 161 } 162 163 # MARK 单个宏: 在 164 # IMPLEMENT_ASN1_FUNCTIONS(X509) 165 # 外围包上 MY_MAGIC_MARK_START_XXX/MY_MAGIC_MARK_END_XXX 对, 成为如下格式 166 # MY_MAGIC_MARK_START_XXX 167 # IMPLEMENT_ASN1_FUNCTIONS(X509) 168 # MY_MAGIC_MARK_END_XXX 169 for ($i = 0; $i <= $#macro_single_list; $i++) 170 { # 所有单个宏 对源文件处理一次 171 $start = $macro_single_list[$i]; 172 $current_content =~ s 173 { 174 ( # 捕捉括号开始 175 $start # IMPLEMENT_ASN1_FUNCTIONS 176 \( # ( -- 关注的单个宏都有括号() 177 [^()]* # 括号内的任意文本 178 \) # ) 179 ) # 捕捉括号结束 180 } 181 { 182 MY_MAGIC_MARK_START_ .# "MY_MAGIC_MARK_START_" . 183 ++$count . # 不同的 $count 确保匹配时可以正确定位 184 "\n$1\n" . # 匹配的 单个宏定义处 前后用换行符隔开 185 MY_MAGIC_MARK_END_ . # "MY_MAGIC_MARK_END_" . 186 $count # 第 $count 个宏定义 187 }egsx; # 见上面说明 188 } 189 190 if( $current_content ne $origin_content ) # 只有真正匹配并发生替换,才保存替换结果 191 { 192 open(TEMP_SOURCE_FILE,">$temp_file") or die "unable to open $temp_file : $!\n"; 193 print TEMP_SOURCE_FILE $current_content; 194 close(TEMP_SOURCE_FILE); # 替换后结果保存到临时文件 195 rename $file, $file . ".bak"; # 将源文件备份.bak 196 rename $temp_file, $file; # 临时文件覆盖源文件 197 $macro_match_src_file[$#macro_match_src_file+1] = $file; 198 say "File: " , $file; 199 } 200 } 201 202 # 显示匹配宏的 C 源文件 203 for ($i = 0; $i <= $#macro_match_src_file; $i++) 204 { 205 say "preprocess $macro_match_src_file[$i] ..."; 206 207 # 以下适用 openssl 0.9.8e 208 # 调用编译器对 OpenSSL 源文件进行预处理, 编译选项来自 make_nt_dll_output.txt(又有点 ad hoc), 因为是预处理, 即使编译选项有些偏差, 也不会影响最后结果 209 $cflag = "-Iinc32 -Itmp32dll.dbg /MDd /Od -DDEBUG -D_DEBUG /W3 /WX /Gs0 /GF /Gy /nologo -DOPENSSL_SYSNAME_WIN32 -DWIN32_LEAN_AND_MEAN -DL_ENDIAN 210 -DDSO_WIN32 -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE -DOPENSSL_USE_APPLINK -I. /Fdout32dll -DOPENSSL_NO_CAMELLIA -DOPENSSL_NO_RC5 211 -DOPENSSL_NO_MDC2 -DOPENSSL_NO_KRB5 -DOPENSSL_NO_DYNAMIC_ENGINE -D_WINDLL -DOPENSSL_BUILD_SHLIBCRYPTO"; 212 $cflag =~ s{\n}{}g; # 上面分成多行是为了排版美观, 实际在命令中需要处理成一行 213 214 # 打开 Visual Studio 2008 命令提示窗口, 切换到主编译目录, 执行预处理命令 215 system('call "C:\Program Files\Microsoft Visual Studio 9.0\VC\vcvarsall.bat" x86' . # 其他 VC 版本请更改为对应的批处理 216 " && cd $ARGV[2] && cl /E $cflag $macro_match_src_file[$i] > $macro_match_src_file[$i].preprocess"); 217 218 update_macro("$macro_match_src_file[$i].preprocess", $macro_match_src_file[$i]); # 用宏展开后结果更新源文件 219 } 220 221 # 将【MY_MAGIC_MARK_START/END_XXX 对】包围的宏定义替换为预处理(宏展开)后的结果 222 sub update_macro 223 { 224 $src_file = $_[0]; # .preprocess 后缀 225 $dst_file = $_[1]; # .c/.C 后缀 226 local $/; 227 local $temp_file = $dst_file.".tmp"; 228 open(SOURCE_FILE, "<$src_file") or die "unable to open $src_file : $!\n"; 229 $content = <SOURCE_FILE>; # 一次性读入预处理后的源文件 230 close(SOURCE_FILE); 231 local %macro_define_list; 232 233 while ( $content =~ /(MY_MAGIC_MARK_START_\d+)(.*?)(MY_MAGIC_MARK_END_\d+)/gs) # 匹配代码块 234 { 235 # 对宏的预处理结果进行美化 -- 预处理生成的代码有些 ugly, 原因是宏定义未考虑, 如下 236 # define IMPLEMENT_ASN1_FUNCTIONS_fname(stname, itname, fname) \ 237 # IMPLEMENT_ASN1_ENCODE_FUNCTIONS_fname(stname, itname, fname) \ 238 # IMPLEMENT_ASN1_ALLOC_FUNCTIONS_fname(stname, itname, fname) // 全部放在一行 239 240 # 本来用下面的一行语句搞定,不知为什么没起作用,期盼指点 241 # $macro_output = ` echo $2 | uncrustify.exe -q -c defaults.cfg `; 242 # 所以替换为下面的实现 243 my $temp_mac_file = "temp_mac_file.txt"; 244 open(TEMP_MACRO_FILE,">$temp_mac_file") or die "unable to open $temp_mac_file : $!\n"; 245 print TEMP_MACRO_FILE $2; # MY_MAGIC_MARK_START/END_XXX 内部的宏展开结果保存到临时文件 246 close TEMP_MACRO_FILE; 247 248 # 调用 uncrustify 进行代码美化 -- uncrustify 如何使用请自行 google 249 $macro_output = `uncrustify.exe -q -c defaults.cfg -f $temp_mac_file`; 250 251 $macro_define_list{$1} = $macro_output; # 记录每个 MY_MAGIC_MARK_START/END_XXX 对与宏展开结果的关系 252 } 253 254 open(DEST_FILE, "<$dst_file") or die "unable to open $dst_file : $!\n"; 255 $content = <DEST_FILE>; # 一次性读入用 MY_MAGIC_MARK_START/END_XXX 标记过的源文件 256 close(DEST_FILE); 257 258 # 在宏定义处展开,并将原来的宏定义以注释的形式保留 259 open(TEMP_FILE,">$temp_file") or die "unable to open $temp_file : $!\n"; 260 $content =~ s 261 { 262 (MY_MAGIC_MARK_START_\d+) # $1 263 (.*?) # $2 264 (MY_MAGIC_MARK_END_\d+) 265 } 266 { 267 "#if 0$2#endif" . # 注释原定义 268 $macro_define_list{$1} # 根据 MY_MAGIC_MARK_START/END_XXX 对, 输出宏展开结果 269 }egsx; 270 print TEMP_FILE $content; 271 close(TEMP_FILE); 272 273 # 删除 .bak 和 .preprocess 后缀文件 274 unlink "$dst_file.bak", "$dst_file.preprocess"; 275 276 # 用最终结果刷新源文件 277 rename $temp_file, $dst_file; 278 }
脚本 auto_expand_macro.pl 需要与宏定义文件配合使用,下面是笔者用到的一个典型的文件
# 本文件包含需要自动展开的宏定义,与脚本 auto_expand_macro.pl 配合使用
# 宏定义共有两种格式,见下面说明
# 以符号 # 开头的文本是注释内容,脚本将忽略
# 格式一:成对宏定义
ASN1_SEQUENCE ASN1_SEQUENCE_END
ASN1_CHOICE ASN1_CHOICE_END
ASN1_ITEM_TEMPLATE ASN1_ITEM_TEMPLATE_END
ASN1_SEQUENCE_ref ASN1_SEQUENCE_END_ref
ASN1_SEQUENCE_cb ASN1_SEQUENCE_END_cb
ASN1_SEQUENCE_enc ASN1_SEQUENCE_END_enc
ASN1_CHOICE ASN1_CHOICE_END_selector
ASN1_NDEF_SEQUENCE ASN1_NDEF_SEQUENCE_END
# 格式二:单个宏定义
IMPLEMENT_ASN1_FUNCTIONS_fname
IMPLEMENT_ASN1_FUNCTIONS_name
IMPLEMENT_ASN1_TYPE_ex
IMPLEMENT_ASN1_MSTRING
IMPLEMENT_ASN1_FUNCTIONS
IMPLEMENT_ASN1_DUP_FUNCTION
IMPLEMENT_ASN1_TYPE
OPENSSL_IMPLEMENT_GLOBAL
IMPLEMENT_PEM_rw
IMPLEMENT_EXTERN_ASN1
IMPLEMENT_ASN1_ENCODE_FUNCTIONS_const_fname
# IMPLEMENT_STACK_OF 不再用
# IMPLEMENT_ASN1_SET_OF 不再用
此外,还需要开源的代码美化工具 uncrustify 及配置文件配合 -- 这不是必选项,可以修改脚本去掉
使用方法: perl auto_expand_macro.pl 宏定义文件[全路径] 被处理源文件所在目录[全路径] 编译源文件所在的主目录[全路径]
使用举例: perl auto_expand_macro.pl d:\openssl-0.9.8e\macro_def.txt d:\openssl-0.9.8e\crypto d:\openssl-0.9.8e
处理后源代码仍可正常编译,并保留了原来的宏定义(与展开后的代码相比较),格式如下
#if 0
IMPLEMENT_ASN1_FUNCTIONS(X509)
#endif
X509 *d2i_X509(X509 **a, const unsigned char **in, long len) {
return (X509 *)ASN1_item_d2i((ASN1_VALUE **)a, in, len, (X509_it()));
}
int i2d_X509(X509 *a, unsigned char **out) {
return ASN1_item_i2d((ASN1_VALUE *)a, out, (X509_it()));
}
X509 *X509_new(void) {
return (X509 *)ASN1_item_new((X509_it()));
}
void X509_free(X509 *a) {
ASN1_item_free((ASN1_VALUE *)a, (X509_it()));
}
到目前为止,阻碍代码走读的所有外围因素都扫光了。下节起我们开始啃 hard core,可想而知的是,剩下的路仍不平坦。