我们在编写 .NET 程序时,经常会在该程序的“关于本软件”对话框中给出这个程序的编译时间,如下图所示:
上图中的编译时间是如果得到的呢?其实是在其 C# 源程序中有这么一句:
[assembly: AssemblyVersion("1.3.*")]
上述语句使用了 System.Reflection.AssemblyVersionAttribute 类,该类用于指定正在特性化的程序集的版本。在 MSDN 文档中有以下描述:
程序集版本号是程序集标识的一部分,在绑定到程序集时以及在版本策略中扮演着关键的角色。版本号包含以下四部分:<主版本(Major)>.<次版本(Minor)>.<内部版本号(Build)>.<修订号(Revision)>
您可以指定所有这些值,也可使用星号 (*) 表示接受默认的内部版本号、修订号,或者接受二者。 例如,
[assembly:AssemblyVersion("2.3.25.1")] 指示主版本为 2,次版本为 3,内部版本号为 25,修订号为 1。 版本号 [assembly:AssemblyVersion("1.2.*")] 指定主版本为 1,次版本为 2,并接受默认的内部版本号和修订号。 版本号 [assembly:AssemblyVersion("1.2.15.*")] 指定主版本为 1,次版本为 2,内部版本号为 15,并接受默认的修订号。
默认的内部版本号每日增加。 默认修订号是随机的。
具体来说,默认的内部版本号表示自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文件头由Microsoft MS-DOS®占位程序、PE文件签名、COFF文件头以及可选文件头组成,COFF目标文件头由COFF文件头和可选文件头组成。在这两种情况下,文件头后面紧跟着的都是节头。MS-DOS占位程序是一个运行于MS‑DOS下的合法应用程序,它被放在EXE映像的最前面。在位置0x3C处,这个占位程序包含PE文件签名的偏移地址。
在MS-DOS占位程序后面、在偏移0x3C指定的文件偏移处,是一个4字节的签名,它用来标识文件为一个PE格式的映像文件。这个签名是“PE\0\0”(字母“P”和“E”后跟着两个空字节)。
紧跟着映像文件签名之后,是一个标准COFF文件头。在这个COFF文件头的偏移为4开始的4个字节就是从UTC时间1970年1月1日00:00起的总秒数(一个C运行时time_t类型的值)的低32位,它指出文件何时被创建。
一个PE文件的例子如下图所示:
使用 DumpBin.exe 可得到如下信息:
Microsoft (R) COFF/PE Dumper Version 10.00.30319.01 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file PowerWord2Snb.exe PE signature found File Type: EXECUTABLE IMAGE FILE HEADER VALUES 14C machine (x86) 3 number of sections 4D0165D7 time date stamp Fri Dec 10 07:27:19 2010 0 file pointer to symbol table 0 number of symbols E0 size of optional header 102 characteristics Executable 32 bit word machine
此外,在操作系统的文件系统中,也记录了每个文件(不要求是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: }
这个程序的运行结果如下所示:
E:\work> BuildTime PowerWord2Snb.exe OS Version: Microsoft Windows NT 6.0.6002 Service Pack 2 CLR Version: 2.0.50727.4206 PowerWord2Snb.exe 文件系统 : 2010-12-10 07:32:14 PE32 : 2010-12-10 07:27:19 装配件版本: 2010-12-10 07:27:18 E:\work> BuildTime OS Version: Microsoft Windows NT 6.0.6002 Service Pack 2 CLR Version: 2.0.50727.4206 E:\work\BuildTime.exe 文件系统 : 2010-12-18 20:26:24 PE32 : 2010-12-18 19:56:41 装配件版本: 2010-12-18 19:56:40
我们来看看著名的 .NET Reflector 的信息吧:
E:\work> BuildTime d:\bin\reflector\reflector.exe OS Version: Microsoft Windows NT 6.0.6002 Service Pack 2 CLR Version: 2.0.50727.4206 d:\bin\reflector\reflector.exe 文件系统 : 2008-07-10 19:36:45 PE32 : 2010-07-15 00:42:36 装配件版本: 2000-01-01 00:00:00 E:\work>
再来更多的例子:
E:\work> BuildTime C:\windows\regedit.exe OS Version: Microsoft Windows NT 6.0.6002 Service Pack 2 CLR Version: 2.0.50727.4206 C:\windows\regedit.exe 文件系统 : 2008-04-23 19:35:29 PE32 : 2008-01-19 13:30:16 装配件版本: 未能加载文件或程序集“file:///C:\windows\regedit.exe” 或它的某一个依赖项。该模块应包含一个程序集清单。 E:\work> BuildTime BuildTime.cs OS Version: Microsoft Windows NT 6.0.6002 Service Pack 2 CLR Version: 2.0.50727.4206 BuildTime.cs 文件系统 : 2010-12-18 20:50:03 PE32 : 非法的PE32文件 装配件版本: 未能加载文件或程序集“file:///E:\work\BuildTime.cs” 或它的某一个依赖项。该模块应包含一个程序集清单。 E:\work>
我们再看看在 Linux 操作系统中的情况(openSUSE 11.3, mono 2.8.1):
ben@ben1520:~/work> dmcs BuildTime.cs ben@ben1520:~/work> mono BuildTime.exe OS Version: Unix 2.6.34.7 CLR Version: 4.0.30319.1 /home/ben/work/BuildTime.exe 文件系统 : 2010-12-18 21:29:14 PE32 : 2010-12-18 21:29:14 装配件版本: 2000-01-01 00:00:00 ben@ben1520:~/work> mono BuildTime.exe /usr/lib/mono/4.0/dmcs.exe OS Version: Unix 2.6.34.7 CLR Version: 4.0.30319.1 /usr/lib/mono/4.0/dmcs.exe 文件系统 : 2010-11-12 22:52:48 PE32 : 2010-11-12 22:44:24 装配件版本: 2000-01-02 00:00:00 ben@ben1520:~/work> mono BuildTime.exe /usr/bin/gcc-4.5 OS Version: Unix 2.6.34.7 CLR Version: 4.0.30319.1 /usr/bin/gcc-4.5 文件系统 : 2010-07-02 02:29:53 PE32 : 非法的PE32文件 装配件版本: Could not load file or assembly '/usr/bin/gcc-4.5' or one of its dependencies. An attempt was made to load a program with an incorrect format. ben@ben1520:~/work>
由上可见,在 Linux 操作系统中 mono 对 System.Reflection.AssemblyVersionAttribute 类的支持很成问题。在 Microsoft 的 MSDN 文档中说可以用星号表示接受默认的内部版本号、修订号,默认的内部版本号每日增加。 默认修订号是随机的。而 mono 的 C# 编译器直接将默认的内部版本号和默认的修订号都设置为零了。
综上所述,要得到 .NET 程序的编译时间,还是去读该程序的PE文件头中的文件创建时间最靠谱。