走读OpenSSL代码----从一张奇怪的证书说起(八)

上节末尾,我们提到 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 这个大神,谁要文本处理是它的强项呢?

不再多说,直接上代码

View Code
  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,可想而知的是,剩下的路仍不平坦。

转载于:https://www.cnblogs.com/efzju/archive/2012/08/23/2651807.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PC-Lint是一个历史悠久、功能强大的C/C++静态代码检测工具,其使用历史可以追溯到计算机编程的远古时代(30多年前)。经过多年的发展,它不仅可以检查出一般的语法错误,还可以检查出那些虽然符合语法要求但不易发现的潜在错误,还能够有效地帮助开发人员提出许多程序在空间利用、运行效率上的改进点,从而提高软件的质量。 PC-Lint是GIMPEL SOFTWARE公司的产品,许多国外的大型专业软件公司,如微软公司,都把它作为程序检查工具,在程序合入正试版本或交付测试之前一定要保证通过了LINT检查。要求软件工程师在使用LINT时要打开所有的编译开关,如果一定要关闭某些开关,那么要给出关闭这些开关的正当理由。 PC-Lint能够将C/C++程序中的遁词、特性、问题和缺陷等找出来。这种分析的目的是为了在程序整合或移植前确定程序中潜在的问题,找出可能是敏感源未被发现的错误的特殊结构。PC-Lint能够在多个模块中查找,因此比编译器更能发现问题。 可以把PC-Lint看作是一种更加严格的编译器。但它仅使用程序源代码和头文件工作,不需要编译器的参与。本质上PC-Lint是进行严格的词法语法和语义分析工作。能否通过PC-Lint的检测将成为程序开发人员的严峻挑战。当然,有些没有通过PC-Lint的程序照样能够运行,通过了也不能保证没有问题,但是PC-Lint代表了一致性和可移植性以及良好的风格是无可厚非的。 可想而知,如果从编码后第一次编译程序时就使用LINT来检查程序,并且保证消除所有的LINT告警,那么软件编码结束后整个工程再编译时就不会遇到很多的告警信息。即使整个工程在编译时,如果能抽出一定的精力来消除程序中的LINT告警,以后再维持这种无告警状态就很容易了。程序质量的提高也是不言而喻的。 PC-LINT的内容非常广泛,光是选项就有300多个,涉及到程序编译及语法使用中的方方面面。它的全称是PC-Lint/FlexeLint for C/C++。PC-Lint能够在Windows、MS-DOS和OS/2平台上使用,以二进制可执行文件的形式发布,而FlexeLint 运行于Linux/Unix平台,以源代码的形式发布。 PC-Lint包中包含3个可执行文件:一个Windows可执行文件(在Windows下运行的32位控制台程序)、一个DOS扩展的可执行文件(在MS-DOS下运行,利用80386 DOS扩展技术来访问所有可利用的扩展内存)和一个OS/2 32位的可执行文件(只在OS/2下运行)。 使用PC-Lint在代码走读和单元测试之前进行检查,可以提前发现程序隐藏错误,提高代码质量,节省测试时间,并提供编码规则检查,规范软件人员的编码行为。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值