从微软网站上(http://msdn.microsoft.com/net/sscli)下载回来的SSCLI是一个15M的压缩包。本文介绍如编译,运行,调试SSCLI和如何察看它的代码。下文所述都是笔者使用的运行环境和方法。有可能有更好的方法,欢迎交流,我的电子邮件:[email protected]

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:

  1. 把下载来的压缩文件解压缩到某个盘的根目录,笔者为F:\sscli。

  2. 打开命令行窗口cmd.exe。把当前目录切换到F:\sscli。

  3. 输入:

env.bat checked

设置编译所需要的环境变量。SSCLI有三个编译版本。分别为:checked, fastchecked和free。free大概相当于C++中的release版本,有编译器优化,不能调试。fastchecked也有编译器优化,但是可以调试。checked是没有优化可以调试的版本。默认env.bat会把环境变量设置为fastchecked。但是调试经过编译器优化过的代码会有一些令人迷惑的地方,例如碰上循环消解……。所以选择checked版本最容易调试跟踪。

  1. 输入:

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。分散在各个子目录下。

  1. 测试:编译的结果会放在名为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>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的深处探秘了。