字符及字符串处理
- UTF-16将每个字符编码为2个字节(或者说16位)。UTF-8将一些字符编码为1个字节,一些字符编码为2个字节,一些字符编码为3个字节,一些字符编码为4个字节。UTF-32将每个字符都编码为4个字节。
- C运行库中现有的字符串处理函数,在应用程序中包含StrSafe.h时,String.h也会包含进来。比如_tcscpy宏背后的那些函数,已标记为废弃不用。如果使用了这些函数,编译时就会发出警告。注意,必须在包含了其他所有文件之后,才包含StrSafe.h。
- Windows也提供了各种字符串处理函数。其中许多函数(比如lstrcat和lstrcpy)已经不赞成使用了,因为它们无法检测缓冲区溢出问题。与此同时,ShlwApi.h定义了大量方便好用的字符串函数,可以用来对操作系统有关的数值进行格式化操作,比如StrFormatKBSize和StrFormatByteSize。我们经常都要比较字符串,以便进行相等性测试或者进行排序。为此,最理想的函数是CompareString(Ex)和CompareStringOrdinal。
- 修改字符串算术问题。例如,函数经常希望你传给它缓冲区的字符数,而不是字节数。这意味着你应该传入_countof(szBuffer),而不是sizeof(szBuffer)。而且,如果需要为一个字符串分配一个内存块,而且知道字符串中的字符数,那么记住内存是以字节来分配的。这意味着你必须调用malloc(nCharacters * sizeof(TCHAR)),而不是调用malloc(nCharacters)。如果出错,编译器不会提供任何警告或错误信息。所以,最好定义一个宏来避免犯错:#define chmalloc(nCharacters) (TCHAR*)malloc(nCharacters * sizeof(TCHAR)).
- 始终使用安全的字符串处理函数,比如那些后缀为_s的,或者前缀为StringCch的。后者主要在你想明确控制截断的时候使用;如果不想明确控制截断,则首选前者。
内核对象
- 要想判断一个对象是不是内核对象,最简单的方式是查看创建这个对象的函数。几乎所有创建内核对象的函数都有一个允许你指定安全属性信息的参数。
- 记住,对象句柄的继承只会在生成子进程的时候发生。假如父进程后来又创建了新的内核对象,并同样将它们的句柄设为可继承的句柄。那么正在运行的子进程是不会继承这些新句柄的。
- 子进程获取继承来的父进程句柄值的方法:
1、命令行传参
2、进程间通信
3、通过环境变量
进程
- 一般将进程定义成一个正在运行的程序的一个实例,它由以下两个组件构成:
- 一个内核对象,操作系统用它来管理进程。内核对象也是系统保存进程统计信息的地方。
- 一个地址空间,其中包含所有执行体(executable)或DLL模块的代码和数据。此外,它还包含动态内存分配,比如线程堆栈和堆的分配。
- 操作系统实际并不调用你所写的入口函数。相反,它会调用由C/C++运行库实现并在链接时使用-entry:命令行选项来设置的一个C/C++运行时启动函数。该函数将初始化C/C++运行库,使你能调用malloc和free之类的函数。
它还确保了在你的代码开始执行之前,你声明的任何全局和静态C++对象都被正确地构造。 - 一个鲜为人知的事实是,完全可以从自己的项目中移除/SUBSYSTEM链接器开关。一旦这样做,链接器就会自动判断应该将应用程序设为哪一个子系统。链接时,链接器会检查代码中包括4个函数中的哪一个(WinMain,wWinMain,main或wmain),并据此推算你的执行体应该是哪个子系统,以及应该在执行体中嵌入哪个C/C++启动函数。
- 许多应用程序都会将(w)WinMain的hInstanceExe参数保存在一个全局变量中,使其很容易由执行体文件的所有代码访问。
- (w)WinMain的hInstanceExe参数的实际值是一个基内存地址;在这个位置,系统将执行体文件的映像加载到进程的地址空间中。
- 可以使用C运行库函数_chdir而不是Windows SetCurrentDirectory函数来更改当前目录。_chdir函数在内部调用SetCurrentDirectory,但_chdir还可以调用SetEnvironmentVariable来添加或修改环境变量,从而使不同驱动器的当前目录得以保留。
- 注意,pszCommandLine参数被原型化为一个PTSTR。这意味着CreateProcess期望你传入的是一个非“常量字符串”的地址。在内部,CreateProcess实际上会修改你传给它的命令行字符串。但在CreateProcess返回之前,它会将这个字符串还原为原来的形式。
- 创建一个新的进程,会导致系统创建一个进程内核对象和一个线程内核对象。在创建时,系统会为每个对象指定一个初始的使用计数1。然后,就在CreateProcess返回之前,它会使用完全访问权限来打开进程对象和线程对象,并将各自的与进程相关的(相对于进程的)句柄放入PROCESS_INFORMATION结构的hProcess和hThread成员中。当CreateProcess在内部打开这些对象时,每个对象的使用计数就变为2。这意味着系统要想释放进程对象,进程必须终止(使用计数递减1),而且父进程必须调用CloseHandle(使用计数再次递减1,变成0)。
- 许多开发人员都有这样的一个误解:关闭到一个进程或线程的句柄,会强迫系统杀死此进程或线程。但这是大谬不然的。关闭句柄只是告诉系统你对进程或线程的统计数据不再感兴趣了。进程或线程会继续执行,直至自行终止。
- 可以使用GetCurrentProcessId来得到当前进程的ID,使用GetCurrentThreadId来获得当前正在运行的线程的ID。另外,还可以使用GetProcessId来获得与指定句柄对应的一个进程的ID,使用GetThreadId来获得与指定句柄对应的一个线程的ID。最后,根据一个线程句柄,你可以调用GetProcessIdOfThread来获得其归属进程的ID。
- TerminateProcess函数是异步的——换言之,它告诉系统你希望进程终止,但到函数返回的时候,并不能保证进程已经被“杀死”了。所以,为了确定进程是否已经终止,应该调用WaitForSingleObject(详见第9章)或者一个类似的函数,并将进程的句柄传给它。
- Windows只允许在进程边界上进行权限提升。一旦进程启动,再要求更多的权限就已经迟了。不过,一个未提升权限的进程可以生成另一个提升了权限的进程,后者将包含一个COM服务器。这个新进程将保持活动状态。这样一来,老进程就可以向已经提升了权限的新进程发出IPC调用,而不必启动一个新实例再终止它自身。
- 由于管理任务必须由另一个进程或者另一个进程中的COM服务器来执行,所以你应该在另一个应用程序中收集好需要管理员权限的所有任务,并通过调用ShellExecuteEx(为lpVerb 传递“runas”)来提升它的权限。然后,具体要执行的特权操作应该作为新进程的命令行上的一个参数来传递。
- GetProcessElevation的helper函数能返回提升类型和一个指出你是否正在以管理员身份运行的布尔值。
BOOL GetProcessElevation(TOKEN_ELEVATION_TYPE* pElevationType, BOOL* pIsAdmin) {
HANDLE hToken = NULL;
DWORD dwSize;
// Get current process token
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
return(FALSE);
BOOL bResult = FALSE;
// Retrieve elevation type information
if (GetTokenInformation(hToken, TokenElevationType,
pElevationType, sizeof(TOKEN_ELEVATION_TYPE), &dwSize)) {
// Create the SID corresponding to the Administrators group
byte adminSID[SECURITY_MAX_SID_SIZE];
dwSize = sizeof(adminSID);
CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, &adminSID,
&dwSize);
if (*pElevationType == TokenElevationTypeLimited) {
// Get handle to linked token (will have one if we are lua)
HANDLE hUnfilteredToken = NULL;
GetTokenInformation(hToken, TokenLinkedToken, (VOID*)
&hUnfilteredToken, sizeof(HANDLE), &dwSize);
// Check if this original token contains admin SID
if (CheckTokenMembership(hUnfilteredToken, &adminSID, pIsAdmin)) {
bResult = TRUE;
}
// Don't forget to close the unfiltered token
CloseHandle(hUnfilteredToken);
} else {
*pIsAdmin = IsUserAnAdmin();
bResult = TRUE;
}
}
// Don't forget to close the process token
CloseHandle(hToken);
return(bResult);
}
作业
- Windows提供了一个作业(job)内核对象,它允许你将进程组合在一起并创建一个“沙箱”来限制进程能够做什么。最好将作业对象想象成一个进程容器。但是,即使作业中只包含一个进程,也是非常有用的,因为这样可以对进程施加平时不能施加的限制。
- 创建好一个作业之后,接着一般需要限制作业中的进程能做的事情;换言之,现在要设置
一个“沙箱”。可以向作业应用以下几种类型的限制:- 基本限制和扩展基本限制,防止作业中的进程独占系统资源。
- 基本的UI限制,防止作业内的进程更改用户界面。
- 安全限制,防止作业内的进程访问安全资源(文件、注册表子项等)。
线程
-
CreateThread函数是用于创建线程的Windows函数。不过,如果写的是C/C++代码,就绝对不要调用CreateThread。相反,正确的选择是使用Microsoft C++运行库函数_beginthreadex。如果使用的不是Microsoft C++编译器,你的编译器的提供商应该提供类似的函数来替代CreateThread。不管这个替代函数是什么,都必须使用它。
-
线程可以通过以下4种方法来终止运行。
- 线程函数返回(这是强烈推荐的)。
- 线程通过调用ExitThread函数“杀死”自己(要避免使用这种方法)。
- 同一个进程或另一个进程中的线程调用TerminateThread函数(要避免使用这种方法)。
- 包含线程的进程终止运行(这种方法避免使用)。
-
终止线程运行的推荐方法是让它的线程函数返回 。但是,务必注意ExitThread函数是用于“杀死”线程的Windows函数。如果你要写C/C++代码,就绝对不要调用ExitThread。相反,应该使用C++运行库函数_endthreadex。如果使用的不是Microsoft的C++编译器,那么你的编译器提供方应该提供它们自己的ExitThread替代函数。不管这个替代函数是什么,都必须使用它。
-
Windows提供了一些函数来方便线程引用它的进程内核对象或者它自己的线程内核对象:
- HANDLE GetCurrentProcess();
- HANDLE GetCurrentThread();
这两个函数都返回到主调线程的进程或线程内核对象的一个伪句柄(pseudohandle)。它们不会在主调进程的句柄表中新建句柄。而且,调用这两个函数,不会影响进程或线程内核对象的使用计数。如果调用CloseHandle,将一个伪句柄作为参数传入,CloseHandle只是简单地忽略此调用,并返回FALSE。在这种情况下,GetLastError将返回ERROR_INVALID_HANDLE。
-
有时或许需要一个真正的线程句柄,而不是一个伪句柄。 所谓“真正的句柄”,指的是能明确、无歧义地标识一个线程的句柄。
DuplicateHandle函数可以执行这个转换:
BOOL DuplicateHandle(
HANDLE hSourceProcess,
HANDLE hSource,
HANDLE hTargetProcess,
PHANDLE phTarget,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions);
- 正常情况下,利用这个函数,你可以根据与进程A相关的一个内核对象句柄来创建一个新句柄,并让它同进程B相关。因为DuplicateHandle递增了指定内核对象的使用计数,所以在用完复制的对象句柄后,有必要把目标句柄传给CloseHandle,以递减对象的使用计数。
网友评论