我们在编写 .NET 程序时,经常会在该程序的“关于本软件”对话框中给出这个程序的编译时间,如下图所示:

上图中的编译时间是如果得到的呢?其实是在其 C# 源程序中有这么一句:
[assembly: AssemblyVersion("1.3.*")]
上述语句使用了 System.Reflection.AssemblyVersionAttribute 类,该类用于指定正在特性化的程序集的版本。在 MSDN 文档中有以下描述:
具体来说,默认的内部版本号表示自2000年1月1日以来的天数,而默认修订号也不是随机的,表示自该天午夜零时以来的秒数的一半。于是就可以使用下面的表达式获得 .NET 程序的编译时间:
new DateTime(2000, 1, 1).AddDays(version.Build).AddSeconds(version.Revision * 2)
但是,还有很多 .NET 程序的程序集版本号没有使用星号来接受默认的内部版本号、修订号,就不能使用这个方法了。我们知道,.NET 程序也是一个标准的32位或64位的 Microsoft Windows 可执行体(PE32,Portable Executable)文件。而PE文件头中包含一个时间标记来指出文件的生成时间,请参见:“Microsoft可移植可执行文件和通用目标文件格式文件规范v8.1修订版”和“Microsoft Portable Executable and Common Object File Format Specification”。具体来说就是:
一个PE文件的例子如下图所示:

使用 DumpBin.exe 可得到如下信息:
此外,在操作系统的文件系统中,也记录了每个文件(不要求是PE文件)的创建和修改时间,如下图所示:

我们写一个 C# 程序来获取这三个时间吧:
01: using System;
02: using System.IO;
03: using System.Reflection;
04:
05: [assembly: AssemblyVersion("1.0.*")]
06:
07: namespace Skyiv.BuildTime
08: {
09: sealed class Program
10: {
11: delegate DateTime GetTime(string fileName);
12:
13: TextWriter writer;
14:
15: Program(TextWriter writer)
16: {
17: this.writer = writer;
18: }
19:
20: static void Main(string[] args)
21: {
22: Console.WriteLine("OS Version: " + Environment.OSVersion);
23: Console.WriteLine("CLR Version: " + Environment.Version);
24: Console.WriteLine();
25: var fileName = (args.Length > 0) ? args[0] : Assembly.GetExecutingAssembly().Location;
26: new Program(Console.Out).Write(fileName);
27: }
28:
29: void Write(string fileName)
30: {
31: writer.WriteLine(fileName);
32: Write("文件系统 ", GetFileCreationTime, fileName);
33: Write("PE32 ", GetPe32Time, fileName);
34: Write("装配件版本", GetAssemblyVersionTime, fileName);
35: }
36:
37: void Write(string msg, GetTime getTime, string fileName)
38: {
39: string time;
40: try
41: {
42: time = getTime(fileName).ToString("yyyy-MM-dd HH:mm:ss");
43: }
44: catch (Exception ex)
45: {
46: time = ex.Message;
47: }
48: writer.WriteLine("{0}: {1}", msg, time);
49: }
50:
51: DateTime GetFileCreationTime(string fileName)
52: {
53: return new FileInfo(fileName).CreationTime;
54: }
55:
56: DateTime GetAssemblyVersionTime(string fileName)
57: {
58: var version = Assembly.LoadFrom(fileName).GetName().Version;
59: return new DateTime(2000, 1, 1).AddDays(version.Build).AddSeconds(version.Revision * 2);
60: }
61:
62: DateTime GetPe32Time(string fileName)
63: {
64: int seconds;
65: using (var br = new BinaryReader(new FileStream(fileName, FileMode.Open, FileAccess.Read)))
66: {
67: var bs = br.ReadBytes(2);
68: var msg = "非法的PE32文件";
69: if (bs.Length != 2) throw new Exception(msg);
70: if (bs[0] != 'M' || bs[1] != 'Z') throw new Exception(msg);
71: br.BaseStream.Seek(0x3c, SeekOrigin.Begin);
72: var offset = br.ReadByte();
73: br.BaseStream.Seek(offset, SeekOrigin.Begin);
74: bs = br.ReadBytes(4);
75: if (bs.Length != 4) throw new Exception(msg);
76: if (bs[0] != 'P' || bs[1] != 'E' || bs[2] != 0 || bs[3] != 0) throw new Exception(msg);
77: bs = br.ReadBytes(4);
78: if (bs.Length != 4) throw new Exception(msg);
79: seconds = br.ReadInt32();
80: }
81: return DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).
82: AddSeconds(seconds).ToLocalTime();
83: }
84: }
85: }
这个程序的运行结果如下所示:
我们来看看著名的 .NET Reflector 的信息吧:

再来更多的例子:
我们再看看在 Linux 操作系统中的情况(openSUSE 11.3, mono 2.8.1):
由上可见,在 Linux 操作系统中 mono 对 System.Reflection.AssemblyVersionAttribute 类的支持很成问题。在 Microsoft 的 MSDN 文档中说可以用星号表示接受默认的内部版本号、修订号,默认的内部版本号每日增加。 默认修订号是随机的。而 mono 的 C# 编译器直接将默认的内部版本号和默认的修订号都设置为零了。
综上所述,要得到 .NET 程序的编译时间,还是去读该程序的PE文件头中的文件创建时间最靠谱。
评论