建立SSCLI运行和调试环境
从微软网站上(http://msdn.microsoft.com/net/sscli)下载回来的SSCLI是一个15M的压缩包。本文介绍如编译,运行,调试SSCLI和如何察看它的代码。下文所述都是笔者使用的运行环境和方法。有可能有更好的方法,欢迎交流,我的电子邮件:xxx@msn.com。
SSCLI是一个可以跨平台的实现,可以运行在Winodws,FreeBSD和Mac OS上,据说有些高手已经成功的把SSCLI跑在了Linux上。但是后面几个环境笔者不熟悉,所以Windows就成了不二之选。
安装必备的软件:
操作系统: Microsoft Windows XP
其它软件: Visual Studio.NET 2003专业版 (用来编译SSCLI,至少安装VC++.NET)
Active Perl(Perl的引擎,用来编译SSCLI)
Source Insight (不错的源代码查看工具,可以方便的在代码之间进行符号跳转。用来查看SSCIL源代码)
Windbg(微软的调试工具,用来调试SSCLI的运行情况)
编译SSCLI:
-
把下载来的压缩文件解压缩到某个盘的根目录,笔者为F:\sscli。
-
打开命令行窗口cmd.exe。把当前目录切换到F:\sscli。
-
输入:
env.bat checked
设置编译所需要的环境变量。SSCLI有三个编译版本。分别为:checked, fastchecked和free。free大概相当于C++中的release版本,有编译器优化,不能调试。fastchecked也有编译器优化,但是可以调试。checked是没有优化可以调试的版本。默认env.bat会把环境变量设置为fastchecked。但是调试经过编译器优化过的代码会有一些令人迷惑的地方,例如碰上循环消解……。所以选择checked版本最容易调试跟踪。
- 输入:
buildall
编译SSCLI。编译过程大概分为六个步骤,分别是:
a) 平台抽象层PAL和非托管工具,例如binplace。
b) 其他的非托管工具,例如资源编译器resourcecompiler等等。
c) SSCLI的实现,还有C#编译器。
d) 精简过的.net framework类库。
e) 其他程序集,主要是序列化和Remoting支持。
f) 托管的编译器。Javascript的编译器jsc.exe等
如果运气足够好,经过半个小时左右,就可以大功告成了。如果不幸出错了,可以通过build log来解决。文件名为buildd.log,buildd.wrn和buildd.wrn。分散在各个子目录下。
- 测试:编译的结果会放在名为build的目录下。用cd转到该目录输入
csc /?
如果看到下面的输出,则基本上编译成功:
Microsoft (R) Visual C# Shared Source CLI Compiler version 1.0.0003
for Microsoft (R) Shared Source CLI version 1.0.0
Copyright (C) Microsoft Corporation 2002. All rights reserved.
运行和调试第一个程序
用notepad建立Hello.cs,文件内容如下:
using System;
class Hello
{
static void Main(string[] args)
{
int x = 10;
System.Console.WriteLine("Hello World, x is {0}", x);
}
}
把文件复制到F:\sscli\build\v1.x86chk.rotor,然后输入下面的命令行来编译该程序:
csc /debug+ /t:exe Hello.cs
没有报错就说明编译成功(出了错的话,买本《C#入门到精通》什么的看看吧……)。会生成两个文件Hello.exe和Hello.ildb。前者就是包含托管代码的程序集,后者是用来调试用的,类似于VC生成的PDB文件。
这时直接双击Hello.exe或者在命令行里输入Hello.exe都可以看到想要得执行结果。但是我们却不能这样做,因为这样子是利用了Windows操作系统把Hello.exe加载进系统,然后在.net Framework中运行了Hello.exe。在MacOS或Unix下是不会有这种便宜赚的。SSCLI提供了一个工具clix,专门用来帮助我们代替完成加载工作。因此我们需要在命令行下输入:
clix Hello.exe
这时,clix会帮助我们加载和执行Hello.exe,这才是只依赖SSCLI的方式。输出结果:
Hello World, x is 10
SSCLI提供了cordbg调试器(debugger)用来调试托管代码。编译结束后cordbg会放在F:\sscli\build\v1.x86chk.rotor\sdk\bin目录下。
在命令行下输入如下命令启动cordbg
sdk\bin\cordbg.exe
系统进入cordbg命令行状态,显示如下:
Microsoft (R) Shared Source CLI Test Debugger Shell Version 1.0.0003.0
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
(cordbg)
这时,输入?可以看到cordbg的帮助信息。
Usage: ? [
Displays debugger command descriptions. If no arguments
are passed, a list of debugger commands is displayed. If
one or more command arguments is provided, descriptions
are displayed for the specified commands. The ? command
is an alias for the help command.
The following commands are available:
ap[pdomainenum] Display appdomains/assemblies/modules in the current process
a[ttach] Attach to a running process
…………
cordbg是一个基于命令行的调试器,提供了设置断点,单步跟踪,查看变量值,查看源代码等调试器所必备的所有功能。有可能使用IDE习惯了的开发者一开始会不太习惯。但是熟悉了以后,效率也不会比IDE图形界面操作差多少。
这时,输入下面命令来启动要调试的程序:
(cordbg) run Hello.exe
Process 19432/0x4be8 created.
[thread 0x4a7c] Thread created.
011: int x = 10;
(cordbg) _
我们可以看到程序停在了要执行的第一条代码之前,等待下面的输入。通过命令,就可以进行一系列调试了。我们让程序直接执行到结束,然后退出cordbg:
(cordbg) g
Hello Wold, x is 10
[thread 0x4830] Thread created.
[thread 0x4a7c] Thread exited.
Process exited.
(cordbg) quit
F:\sscli\build\v1.x86chk.rotor>
调试SSCLI的代码
cordbg是一个调试托管代码的好工具,然而整个CLI执行引擎中最精彩的部分例如垃圾回收,元数据等都是用非托管代码编写的。跟踪调试这一部分代码cordbg就无能为力了。目前为止,似乎还没有一个调试器可以既调试托管代码又调试非托管代码(有些调试器可以加载extension以实现少量支持)。好在在Windows下,SSCLI也不过就是一个标准windows程序,否则SSCLI也没法在Windows上跑啊。所以我们仍然可以用调试windows程序的办法去调试SSCLI。
调试一个Windows程序无非需要这么几样东西:调试器,pdb符号文件和源代码。源代码不必多说,符号文件在编译SSCLI的时候已经自动生成了。放在F:\sscli\build\v1.x86chk.rotor\Symbols下面。调试器有多种选择,最简单的方法就直接使用Visual Studio.net。鉴于功能和实用,笔者选用了Windbg,该软件可以从http://www.microsoft.com/ddk/debugging/下载。
安装Windbg完毕后,建立一个小的批处理程序,内容如下:
F:
cd F:\sscli\build\v1.x86chk.rotor
call F:\debug\windbg clix Hello.exe
其中F:\debug是Windbg的安装目录。下次每次Double Click这个小批处理程序,就可以直接启动Windbg,并调试通过clix运行的Hello.exe程序。
这时在命令窗口输入:
0:000</a>>bp main
0:000>g
其中第一条命令的意思是在主函数main处设置断点,g命令是使程序继续执行,当然,程序执行到main的时候,就停下来了。如下图所示。
这里有必要澄清一个问题,有人或许会问,main不就是程序执行的第一行代码么?为什么要还要go才能撞上main。其实Windows下一个应用程序执行的第一条指令写在PE文件头的某个位置,一般是0x0400000,这个地方的函数的名字官方叫WinMainCRTStartup。这个函数作一些初始化工作,然后才会去调用main或者WinMain。也就是我们看到的程序入口点函数。
下面我们简单的分析一下clix的代码。Clix是个很简单的程序,首先,main函数解析命令行参数,收集三样东西CLI运行时所在的库文件名,托管可执行文件的名称和命令行参数,分别放在pRuntimeName, pModuleName和pActualCmdLine中,然后调用Launch函数。下面是简化的main函数代码。
int __cdecl main(int argc, char **argv)
{
WCHAR* pRuntimeName;
WCHAR* pModuleName;
WCHAR* pActualCmdLine;
// First, parse the program name. Anything up to the first whitespace outside
// a quoted substring is accepted (algorithm from clr/src/vm/util.cpp)
pRuntimeName = pdst;
// Now, load the runtime from the clix directory
// Parse the first arg – the name of the module to run
pModuleName = pdst;
nExitCode = Launch(pRuntimeName, pModuleName, pActualCmdLine);
return nExitCode;
}
使用bp命令在Launch一句设置断点,然后go,当断点hit的时候,输入下列命令察看三个参数的值:
0:000> dt pRuntimeName
Local var @ 0x6ff30 Type unsigned short*
0x002b2670
-> 0x73
0:000> du 0x002b2670
002b2670 “sscoree.dll”
其中dt是显示pRuntimeName的类型和地址,du是显示一个unicode的字符串。依此,我们可以得到三个参数的值分别是:”sscoree.dll”,”Hello.exe”和”Hello.exe”。后面两个重复的原因是我们Hello.exe后面并没有其他参数,否则pActualCmdLine的值会加上其他参数。
然后我们用t命令跟踪到Launch函数里面去。下面是精简过的Launch代码。
DWORD Launch(WCHAR* pRunTime, WCHAR* pFileName, WCHAR* pCmdLine)
{
// open the file & map it
hFile = ::CreateFile(pFileName, GENERIC\_READ, FILE\_SHARE_READ,
0, OPEN_EXISTING, 0, 0);
hMapFile = ::CreateFileMapping(hFile, NULL, PAGE_WRITECOPY, 0, 0, NULL);
pModule = ::MapViewOfFile(hMapFile, FILE\_MAP\_COPY, 0, 0, 0);
// check the DOS headers
pdosHeader = (IMAGE\_DOS\_HEADER*) pModule;
// check the NT headers
pNtHeaders = (IMAGE\_NT\_HEADERS32\*) ((BYTE\*)pModule + VAL32(pdosHeader->e_lfanew));
// check the COR headers
pSectionHeader = (PIMAGE\_SECTION\_HEADER) Cor_RtlImageRvaToVa(pNtHeaders, (PBYTE)pModule,
// load the runtime and go
hRuntime = ::LoadLibrary(pRunTime);
__int32 (STDMETHODCALLTYPE * pCorExeMain2)(
& nbsp;PBYTE pUnmappedPE, // -> memory mapped code
DWORD cUnmappedPE, // Size of memory mapped code
LPWSTR pImageNameIn, // -> Executable Name
LPWSTR pLoadersFileName, // -> Loaders Name
LPWSTR pCmdLine); // -> Command Line
\*((VOID\**)&pCorExeMain2) = (LPVOID) ::GetProcAddress(hRuntime, "_CorExeMain2");
nExitCode = (int)pCorExeMain2((PBYTE)pModule, dwSize,
pFileName, // -> Executable Name
NULL, // -> Loaders Name
pCmdLine); // -> Command Line
return nExitCode;
}
函数的功能很简单,首先把Hello.exe作为内存映射文件加载到进程的地址空间内。然后对合法性作必要的检查,最后加载sscoree.dll,然后找到入口函数_CorExeMain2的地址,并且转到那个函数去执行。
我们可以在最后一句设断点,然后用t命令跟进_CorExeMain2的函数里面去。这时,程序离开了clix,转到了F:\sscli\clr\src\vm\ceemain.cpp中执行。欢迎来到.NET虚拟机内部。
跟踪SSCLI的动态执行
由于虚拟机是一个非常复杂的环境,.NET Assembly的执行也相当复杂,牵扯到多线程,同步等等,有些问题很难通过调试器一行一行跟踪代码来发现,这时候所需要的是动态的环境,也就是要得到程序真正执行的时候的一些信息。
好在SSCLI提供了功能极为强大的Log功能,可以动态的记录很多代码的执行。其基本思想就是设置一些环境变量,然后SSCLI的代码会根据这些预先设置的环境变量来输出一些调试信息。例如我们在命令行窗口下输入下面的命令,就可以跟踪JIT编译器所即时编译的所有函数:
set COMPlus_JitTrace=1
clix Hello.exe
程序执行的时候就会输出大量的信息,如下所示:
Method SetupDomain Class System.AppDomain
Method .cctor Class System.Runtime.Remoting.Proxies.RealProxy
Method .ctor Class System.AppDomainSetup
Method .ctor Class System.Object
Method SetupFusionStore Class System.AppDomain
Method get_Value Class System.AppDomainSetup
Method .cctor Class System.String
Method LastIndexOfAny Class System.String
Method get_Length Class System.String
…………
随机文档\docs\techinfo\logging.html提供了对Log功能的完整介绍。在此就不再多做敷述。
结束语
除了上面介绍的一些工具方法外,在几十万行代码中来回穿梭,Source Insight是一个不可缺少的好帮手,由于介绍Source Insight的使用超出了本文的范畴,读者可以查阅相关的资料。有了上面的环境,方法,工具,我们下一步就可以向SSCLI的深处探秘了。