2023年8月1日发(作者:)
《Windows核⼼编程》笔记(⼀)字符及字符串处理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,以递减对象的使⽤计数。
发布者:admin,转转请注明出处:http://www.yc00.com/news/1690874335a452262.html
评论列表(0条)