Windows核心编程 第三章 内核对象

1. 什么是内核对象?

 

a)内核对象是一个内存块,它只能由操作系统内核分配管理,并由操作系统内核访问。但Windows系统一系列的调用接口供我们操纵这些内核对象。 这个内存块是一个数据结构,其成员维护着与该内核对象相关的信息。

 

b) 为了增强操作系统的可靠性,当一个进程创建一个内核对象后,函数返回的内核对象句柄只能在本进程中使用,因为这个句柄值是和进程相关的。但可以根据某种机制跨边界共享内核对象。

 

c)内核对象使用计数:

  内核对象的所有者是操作系统内核,而不是进程。因此一个内核对象的生命周期可能长于创建它的那个进程。操作系统通过内核对象里的使用计数成员感知当前有多少个进程在使用这个内核对象,当这个内核对象计数器为0的时候,操作系统才会销毁该对象。

 

d)内核对象的安全性:

1) 用于创建内核对象的所有函数几乎指向一个SECURITY_ATTRIBUTES 用于保护该内核对象,如果这个参数传入NULL,则使用当前进程的安全令牌的默认安全属性。SECURITY_ATTRIBUTES中的lpSecurityDescriptor参指向一个SECURITY_DESCRIPTOR安全描述符结构,该结构用于设定内核对象的安全属性。

2) 对于需要通过安全访问标志访问的对象,应该正确地用访问标志来访问它,而不是使用ALL_ACCESS

 

3) 区分内核对象和普通对象的方法 :

  几乎所欲创建内核对象的函数有指定安全属性信息的参数。

 

2. 进程内核对象句柄表

a) 每一个进程都维护一个内核对象句柄表,表中每一个项都是一个数据结构,这个数据结构的成员包含真实的内核对象地址和访问权限等。用户代码持有的句柄(这个句柄仅供这个进程的所有用户线程使用, 但可以通过某种机制实现跨边界共享内核对象)其实是这个进程内核对象句柄表对应内核对象项的索引值。因此当关闭一个内核对象之后没清空变量,而这个内核对象句柄表中在这个索引位置又重新创建一个内核对象,对之前的没清空变量的句柄进行访问就会造成BUG。而在其它进程访问这个句柄的内核对象,其实是扫描其它进程对应的内核对象句柄表的索引值,也会造成BUG

 

b)  调用一个创建内核对象的函数时,若是调用失败,其返回值可能是或是 INVALID_HANDLE_VALUE-1)。因此单一判断返回值是0或是INVALID_HANDLE_VALUE是错误的。

 

c)  无论以什么方式创建内核对象,都要调用CloseHandle向系统表明已经结束使用这个对象。CloseHandle内部先检查进程内核句柄表,验证该句柄是否有效,若是有效,则将索引到该内核对象数据结构的地址,并将结构中的使用计数器减一。若是使用计数器为0,操作系统内核则将该内核对象销毁。CloseHandle 函数在返回之前还会清除进程内核对象句柄表中对应的记录项。调用CloseHandle之后,即使该内核对象还存在,本进程都将该句柄视为无效。因此该进程不能再使用这个句柄。使用CloseHandle之后,该句柄应该设置为NULL

 

d) 进程退出时,会释放所有对象等。系统会确保进程结束后不会留下任何东西。(内核对象是根据内核对象中的使用计数器来判定是否回收资源,如果计数器为0, 则释放该对象, 否则只是将计数器减一操作而已

 

3. 跨边界共享内核对象

a) 对象句柄继承

i. 只有进程之间存在父子关系时,才可以使用对象句柄继承。而且只有句柄才可以继承,内核对象本身是不能继承的。

ii. 对象句柄的继承只会发生在生成子进程的时候发生。若是父进程后来又创建了新的内核对象,并将它们的句柄设置为可继承的句柄,那么正在运行的子进程是不会继承这些新的句柄的。

iii. 对象句柄继承的步骤:

1. 初始化一个SECURITY_ATTRIBUTES 结构体,把成员中 bInheritHandle 指定为TRUE来表明该内核对象可继承,并传给内核对象创建函数。或者使用SetHandleInformation设置一个内核对象句柄的可继承标志。对应的还有一个GetHandleInformation,用于返回句柄的继承标志。

2. 调用CreateProcess创建一个子进程,并将CreateProcess中的bInheritHandles参数设置为TRUE。表明子进程将继承父进程中可被继承的内核对象句柄。

3. 完成以上两步,操作系统便会将父进程中可被继承的对象句柄继承到子进程之中,而且其句柄值与父进程保持一直。但是,子进程并不会知道它已经继承了父进程的句柄。因此,子进程若是想要获得从父进程继承过来的句柄就必须通过某种机制来获取。

a) 向子进程传入命令行参数

b) 让父进程向其环境块添加一个环境变量,子进程通过环境变量取得。注意的是,要在生成子进程之前添加环境变量,否则子进程继承不到父进程的环境变量。

c) 通过其他进程通讯技术将继承的内核对象句柄值从父进程传入子进程。

 

iv. 改变句柄的标志

1. 通过SetHandleInformation可以修改内核对象句柄的继承标志

a) 第一个参数标识一个内核对象的有效句柄

b) 第二个参数dwMask告诉函数想要更改哪个或哪些标志

c) 第三个参数dwFlags指定希望把标志设置为什么。

2. 通过GetHandleInformation 可以取得内核对象句柄当前标志

  父进程源码:

 

 1 #define _CRT_SECURE_NO_DEPRECATE
 2 #include <iostream>
 3 #include <windows.h>
 4 using namespace std;
 5 
 6 DWORD _stdcall Thread(LPVOID param)
 7 {
 8     while (1)
 9     {
10         cout << "loop" << endl;
11         Sleep(500);
12     }
13     return 0;
14 }
15 
16 int AnsiToUnicode (char *str_ansi, wchar_t *str_wide, unsigned int chcount)
17 {
18     return MultiByteToWideChar(CP_ACP, 0, str_ansi, -1, str_wide, chcount);
19 }
20 
21 int main(void)
22 {
23     /*
24     SECURITY_ATTRIBUTES sa;
25     sa.nLength = sizeof(sa);
26     sa.bInheritHandle = TRUE;
27     sa.lpSecurityDescriptor = NULL;*/
28 
29     HANDLE hThread = CreateThread (NULL, 0, Thread, NULL, 0, NULL);
30     //CloseHandle(hThread);        //如果关闭句柄的话,子进程将不能访问父进程的内核对象
31 
32     SetHandleInformation (hThread, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
33 
34     Sleep(1000);
35 
36 
37     STARTUPINFO info = {0};
38     PROCESS_INFORMATION pi = {0};
39     char comline[256];
40     TCHAR p[256];
41 
42     sprintf(comline, "%d", hThread);
43     AnsiToUnicode(comline, p, sizeof(p));
44 
45     SetEnvironmentVariable(L"hThread", p);            //要在生成子进程之前添加环境变量,否则子进程继承不到父进程的环境变量
46     
47     CreateProcess (L"..\\debug\\child.exe", p, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &info, &pi);
48     CloseHandle(pi.hProcess);
49     CloseHandle(pi.hThread);
50 
51 
52     cin.get();
53     return 0;
54 }

    子进程源码:

 1 #include <iostream>
 2 #include <windows.h>
 3 using namespace std;
 4 
 5 int UnicodeToAnsi(wchar_t *str_wide, char *str_ansi, unsigned int cbsize)
 6 {
 7     return WideCharToMultiByte(CP_ACP, 0, str_wide, -1, str_ansi, cbsize, NULL, NULL);
 8 }
 9 
10 int main (int argv, char ** argc)
11 {
12 
13 
14     // 命令行获取继承句柄。
15     /*
16     HANDLE hThread = (HANDLE)atol(argc[0]);  
17     cout << "get kernal handle: " << argc[0] << endl;
18     TerminateThread(hThread, 0);
19     cout << "kill my parent process thread " << endl;
20     */
21 
22     
23     HANDLE hThread;
24     TCHAR buf[256] = {0};
25     char str[256] = {0};
26     GetEnvironmentVariable(L"hThread", buf, sizeof(buf));
27     UnicodeToAnsi(buf, str, sizeof(str));
28     cout << str << endl;
29     hThread = (HANDLE) atoi(str);
30     TerminateThread(hThread, 0);
31     
32 
33     cin.get();
34     return 0;
35 }

b) 为对象命名

i. 大部分内核对象都可以进行命名,通过对内核对象的命名可以实现共享一个对象。一般可以命名的内核对象创建函数在最后一个参数都是pszName。通过它可以实现内核对象的命名。如果此参数传入NULL,则表明创建一个匿名的内核对象。通过为对象命名来实现共享时,是否可以继承并非一个必要条件。

ii. 所有内核对象都共享同一个命名空间,即使它们的类型并不相同。

iii. 当一个进程调用Create*  创建内核对象函数时,如果已经存在一个pszName参数指定的内核对象名称,它会忽略其他参数。如果不存在pszName参数指定的内核对象名称,其它参数则会被传递进去。

iv. 除了Create* 还可以使用Open* 打开共享的内核对象。Open*pszName 参数不能传入NULLCreate*Open*函数的主要区别:如果对象不存在,Create*会创建它,Open*则会以调用失败告终。

v. 一个进程打开同一个共享内核对象N次,其共享内核对象使用计数器会增加,所以需要分别对它们进行关闭。另外,不同的进程中,其返回句柄可能会不同。

 

vi. 终端服务命名空间

1. 运行于终端服务的计算机中,有多个用于内核对象的命名空间,其中一个是全局命名空间。所有客户端都能访问的内核对象都放在这个命名空间中,这个命名空间主要由系统服务使用。而每个客户端会话都有一个自己的命名空间。这样使得同一个程序在不同的绘画中避免相互的干扰。即使对象名称相同。

2. 一个服务的命名内核对象应该始终位于全局命名空间内的。默认情况下,应用程序的命名内核对象在当前会话的命名空间内。若是想要让它加入全局命名空间,可以在其名称加上”Global\’前缀。而把一个命名内核对象放到当前会话的命名空间可以在其名称加上”Local\”前缀。同时可以使用”Session\<会话id>\”限制命名内核对象只能在哪个会话运行。注意,这些前缀大小写区分。

  终端服务命名空间例子:

 1 #include <iostream>
 2 #include <windows.h>
 3 using namespace std;
 4 
 5 int main (void)
 6 {
 7     // 创建一个全局可使用的Mutex
 8     HANDLE hMutex;
 9     hMutex = CreateMutex (NULL, FALSE, L"Global\\Mutex");
10     if (GetLastError() == ERROR_ALREADY_EXISTS)
11     {
12         cout << "this is old mutex" << endl;
13     }
14     else if (NULL == hMutex)
15     {
16         
17         cout << "create object error" << endl;
18     }
19     else
20         cout << "this is new mutex" << endl;
21     
22     cin.get();
23     return 0;
24 }

 

 

vii. 私有命名空间

1. 为了防止一个共享对象的名称被劫持,可以使用私有命名空间。即在命名内核对象名称的前面加一个自定义最缀加以保护。使用私有命名空间,负责创建内核对象的服务器进程将定义一个边界描述符以对私有命名空间的保护。

2. 创建私有命名空间的步骤:

a) 创建一个边界描述符 CreateBoundaryDescriptor

b) 创建一个SID标识用户、组和计算机帐户的唯一的号码) CreateWellKnownSid

c) 将SID与边界描述符关联起来。 AddSIDToBoundaryDescriptor

d) 初始化一个安全描述符 //书中使用ConvertStringSecurityDescriptorToSecurityDescriptor

e) 创建一个私有命名空间 CreatePrivateNamespace

3. 注意,CreatePrivateNamespace 和 OpenPrivateNamespace 返回的HANDLE 不是内核对象句柄,应该使用ClosePrivateNamspace来关闭它们返回的伪句柄。

4. 如果不希望私有命名空间在进程关闭后仍然可被其他进程引用,应该调用ClosePrivateNamespace,并在第二个参数传入PRIVATE_NAMESPACE_FLAG_DESTROY

5. 边界将在进程终止或调用DeleteBoundaryDescriptor的情况下关闭。

6. 如果还有内核对象正在使用,私有命名空间一定不能关闭。

7. 我们为私有命名空间指定的名称是一个别名,只有在进程内可见。其他进程(甚至是同一个进程)可以打开同一个私有命名空间,并为它指定一个不同的别名。

   私有命名空间例子:

  

 1 #include <windows.h>
 2 #include <sddl.h>
 3 #include <strsafe.h>
 4 #include <iostream>
 5 using namespace std;
 6 
 7 
 8 
 9 void CheckInstances()
10 {
11 
12     BOOL bIsNamespaceOpen = FALSE;
13 
14     //创建一个边界描述符
15     HANDLE hBoundary = CreateBoundaryDescriptor(L"my_private_name", 0);
16 
17 
18     //创建一个SID
19     BYTE localAdminSID[SECURITY_MAX_SID_SIZE];
20     DWORD cbSID = sizeof(localAdminSID);
21     if ( !CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, localAdminSID, &cbSID) )
22     {
23         cout << "CreateWellKnownSID error : " << GetLastError() << endl;
24         return ;
25     }
26 
27     //将SID与边界描述符关联起来
28     if ( !AddSIDToBoundaryDescriptor (&hBoundary, localAdminSID) )
29     {
30         cout << "AddSIDToBoundaryDescriptor error : " << GetLastError() << endl; 
31     }
32 
33     //将一个安全描述符字符串转为安全描述符
34     SECURITY_ATTRIBUTES sa;
35     sa.nLength = sizeof(sa);
36     sa.bInheritHandle = FALSE;
37     if ( !ConvertStringSecurityDescriptorToSecurityDescriptor (L"D:(A;;GA;;;BA)", SDDL_REVISION_1, &sa.lpSecurityDescriptor, NULL) )
38     {
39         cout << "ConvertStringSecurityDescriptorToSecurityDescriptor error : " << GetLastError() << endl;
40         return ;
41     }
42 
43     //创建一个专有命名空间
44     TCHAR szNamespace[256] = L"my_private_name";
45     HANDLE hNamespace = CreatePrivateNamespace (&sa, hBoundary, szNamespace);
46     LocalFree (sa.lpSecurityDescriptor);
47     DWORD dwError = GetLastError();
48 
49     if (hNamespace == NULL)
50         if (dwError == ERROR_ALREADY_EXISTS)
51         {
52             // 如果存在了这个命名空间则打开这个命名空间
53             cout << "CreatePrivateNamespace failed : " << GetLastError() << endl;
54             hNamespace = OpenPrivateNamespace (hBoundary, szNamespace);
55 
56             bIsNamespaceOpen = TRUE;
57         }
58 
59 
60     TCHAR szMutex[256];
61     StringCchPrintf (szMutex, _countof(szMutex), L"%s\\%s", szNamespace, L"MyMutex");
62 
63     HANDLE hMutex = CreateMutex (NULL, FALSE, szMutex);
64     if (GetLastError () == ERROR_ALREADY_EXISTS)
65     {
66         cout << "已存在这个内核对象" << endl;
67     }
68     else if (hMutex == NULL)
69     {
70         cout << "创建错误" << endl;
71     }
72     else
73     {
74         cout << "内核对象创建成功" << endl;
75     }
76 
77 
78     if (hNamespace != NULL)
79     {
80         if (bIsNamespaceOpen)
81             ClosePrivateNamespace (hNamespace, PRIVATE_NAMESPACE_FLAG_DESTROY);
82         else
83             ClosePrivateNamespace (hNamespace, PRIVATE_NAMESPACE_FLAG_DESTROY);
84     }
85     
86     DeleteBoundaryDescriptor (hBoundary);
87 
88     CloseHandle (hMutex);
89 }
90 
91 int main (void)
92 {
93 
94     CheckInstances();
95 
96     while (1);
97     return 0;
98 }

 

c). 使用DuplicateHandle 可以复制一个对象句柄

1. DuplicateHandle获得一个进程句柄表中的一个记录项,然后在另一个进程的句柄表中创建这个记录项的一个副本。

2. DuplicateHandle dwOptions参数可以指定DUPLICATE_SAME_ACCESSDUPLICATE_CLOSE_SOURCE标志。如果指定DUPLICATE_SAME_ACCESS标志将希望目标句柄拥有与源进程的句柄一样的访问掩码。如果指定DUPLICATE_CLOSE_SOURCE标志,会关闭源进程的句柄。使用这个标志,内核对象计数不会受到影响。

3. DuplicateHandle 函数与继承一样,目标进程并不知道它现在能访问一个新的内核对象,所以源进程以某种方式通知目标进程。与继承不一样的是,源进程不能使用命令行参数或更改目标进程的环境变量。

4. 可以利用DuplicateHandle修改内核对象的访问权限

5.绝对不能使用CloseHandle函数关闭通过phTargetHandle参数返回的句柄。

    DuplicateHandle 三个进程 例子:

     其中,S进程是源进程,T进程是目标进程,C进程将S进程的内核对象句柄复制到T进程中。

      S进程源码:

 

 1 #define _CRT_SECURE_NO_DEPRECATE
 2 #include <iostream>
 3 #include <windows.h>
 4 using namespace std;
 5 
 6 
 7 int AnsiToUnicode (char *str_ansi, wchar_t *str_wide, unsigned int chcount);
 8 DWORD _stdcall Thread(LPVOID param);
 9 
10 int main(void)
11 {
12     cout << "我是进程S" << endl;
13 
14 
15     HANDLE hThread = CreateThread (NULL, 0, Thread, NULL, 0, NULL);
16     Sleep(1000);
17 
18     // 将句柄值添加到环境块中
19     char comline[256];
20     TCHAR p[256];
21     sprintf(comline, "%d", hThread);
22     AnsiToUnicode(comline, p, sizeof(p));
23     SetEnvironmentVariable(L"hThread", p);    
24 
25 
26     // 将线程句柄继承给C进程。
27     STARTUPINFO info = {0};
28     PROCESS_INFORMATION pi = {0};
29     CreateProcess (L"..\\debug\\C.exe", NULL, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &info, &pi);
30 
31 
32     CloseHandle(pi.hProcess);
33     CloseHandle(pi.hThread);
34     cin.get();
35     return 0;
36 }
37 
38 
39 DWORD _stdcall Thread(LPVOID param)
40 {
41     while (1)
42     {
43         Sleep(500);
44         cout << "loop" << endl;
45     }
46     return 0;
47 }
48 
49 int AnsiToUnicode (char *str_ansi, wchar_t *str_wide, unsigned int chcount)
50 {
51     return MultiByteToWideChar(CP_ACP, 0, str_ansi, -1, str_wide, chcount);
52 }

    T进程源码:

 1 #include <iostream>
 2 #include <Windows.h>
 3 using namespace std;
 4 
 5 int main (void)
 6 {
 7     cout << "我是进程T" << endl;
 8 
 9     HANDLE hThread;
10 
11     cout << "请输入复制过来内核对象句柄的值, 我将关闭S进程的线程:" << endl;
12     while (cin >> hThread)
13         TerminateThread(hThread, 0);
14 
15     cin.get();
16     return 0;
17 }

    C进程源码:

 1 #include <iostream>
 2 #include <windows.h>
 3 #include <Tlhelp32.h>
 4 using namespace std;
 5 
 6 int UnicodeToAnsi(wchar_t *str_wide, char *str_ansi, unsigned int cbsize);
 7 HANDLE GetProcessHandle (LPCTSTR szProceName);
 8 
 9 int main (void)
10 {
11     cout << "我是进程C" << endl;
12 
13 
14     //取得从S进程的线程内核对象句柄
15     HANDLE hThread;
16     TCHAR buf[256] = {0};
17     char str[256] = {0};
18 
19     GetEnvironmentVariable(L"hThread", buf, sizeof(buf));
20     UnicodeToAnsi(buf, str, sizeof(str));
21     hThread = (HANDLE) atoi(str);
22     cout << "取得从S进程的线程内核对象句柄: " << hThread << endl;
23 
24 
25     // 复制句柄
26     HANDLE hobj;
27     HANDLE hSProcess, hTProcess;
28     hSProcess = GetProcessHandle(L"S.exe");
29     hTProcess =  GetProcessHandle(L"T.exe");
30     if (TRUE == DuplicateHandle (hSProcess, hThread, hTProcess, &hobj, 0, TRUE, DUPLICATE_SAME_ACCESS) )
31         cout << "句柄: " <<  hobj << " 复制成功现在可以在T进程进行操作S进程的内核对象!" << endl;
32 
33     
34     CloseHandle (hSProcess);
35     CloseHandle (hTProcess);
36 
37     cin.get();
38     return 0;
39 }
40 
41 
42 HANDLE GetProcessHandle (LPCTSTR szProceName)
43 {
44     HANDLE hSanpshot = CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0);
45     if (hSanpshot == INVALID_HANDLE_VALUE  || hSanpshot == NULL)
46     {
47         return NULL;
48     }
49 
50     PROCESSENTRY32 pe;
51     BOOL ok;
52     pe.dwSize = sizeof(pe);
53     ok = Process32First (hSanpshot, &pe);
54     if ( !ok )
55         return NULL;
56 
57     do{
58         if ( 0 == wcscmp( pe.szExeFile , szProceName) )
59         {
60             return OpenProcess (PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID);
61         }
62         ok = Process32Next (hSanpshot, &pe);
63     }while (ok);
64 
65     return NULL;
66 }
67 
68 int UnicodeToAnsi(wchar_t *str_wide, char *str_ansi, unsigned int cbsize)
69 {
70     return WideCharToMultiByte(CP_ACP, 0, str_wide, -1, str_ansi, cbsize, NULL, NULL);
71 }

 

    DuplicateHandle  两个进程 例子

      S进程拥有一个内核对象,将S进程中的内核对象句柄复制到T进程进程句柄表中。

  S进程源码:

 1 #include <iostream>
 2 #include <windows.h>
 3 #include <process.h>
 4 #include <TlHelp32.h>
 5 
 6 using namespace std;
 7 
 8 
 9 unsigned __stdcall thread (void * lpPragma);
10 HANDLE GetProcessHandle(LPCTSTR szName);
11 
12 
13 int main (void)
14 {
15 
16  
17     HANDLE hThread;
18     hThread = (HANDLE)_beginthreadex(NULL, 0, thread, NULL, 0, NULL);
19     cout << "my thread handle: " << hThread << endl;
20 
21 
22 
23     HANDLE hTarget;
24     if (DuplicateHandle (GetCurrentProcess(), hThread, GetProcessHandle(L"Target.exe"), &hTarget, 0, FALSE, DUPLICATE_SAME_ACCESS ) )
25         cout << "句柄复制成功, 其句柄值为:" << hTarget << endl;
26          
27 
28 
29     cin.get();
30     return 0;
31 }
32 
33 
34 unsigned __stdcall thread (void * lpPragma)
35 {
36     while (1)
37     {
38         Sleep (500);
39         cout << "terminal me" << endl;
40     }
41 
42     return 0;
43 }
44 
45 HANDLE GetProcessHandle(LPCTSTR szName)
46 {
47     HANDLE hSanpshot;
48     hSanpshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
49     if ( INVALID_HANDLE_VALUE == hSanpshot )
50     {
51         return NULL;
52     }
53 
54     PROCESSENTRY32 pe;
55     BOOL bOk;
56     pe.dwSize = sizeof(pe);
57 
58     bOk = Process32First (hSanpshot, &pe);
59     if (!bOk)
60         return NULL;
61 
62     do {
63         if ( !wcscmp (pe.szExeFile, szName) )
64         {
65             return OpenProcess (PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID);
66         }
67         bOk = Process32Next (hSanpshot, &pe);
68     }while (bOk);
69 
70     return NULL;
71 }

  T进程源码:

#include <iostream>
#include <windows.h>
#include <stdlib.h>
#include <process.h>
using namespace std;

unsigned __stdcall thread (void * lpPragma);

int main (void)
{
	HANDLE hRecv;


 
	HANDLE hThread;
	hThread = (HANDLE)_beginthreadex(NULL, 0, thread, NULL, 0, NULL);

	cout << "my thread handle: " << hThread << endl;
	cout << "请输入复制过来的句柄 : "<< endl;
	cin >> hRecv;


	TerminateThread(hRecv, 0);




	while (1);
	return 0;
}


unsigned __stdcall thread (void * lpPragma)
{
	while (1)
	{
	
		Sleep (1500);
		MessageBox(NULL, L"run", L"", 0);
	}

	return 0;
}

你可能感兴趣的:(windows)