《Windows核心编程》读书笔记

这篇笔记是我在读《Windows核心编程》第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多个人的思考和对实现的推断,因此不少条款和Windows实际机制可能有出入,但应该是合理的。开头几章由于追求简洁,往往是很多单独的字句,后面的内容更为连贯。

  海量细节。

 

 

1    错误处理

1.         GetLastError返回的是最后的错误码,即更早的错误码可能被覆盖。

2.         GetLastError可能用于描述成功的原因(CreatEvent)。

3.         VS监视窗口err,hr

4.         FormatMessage。

5.         SetLastError。

2    字符和字符串处理

1.         ANSI版本的API全部是包装Unicode版本来的,在传参和返回是多了一次编码转换。

2.         MS的C库的ANSIUnicode版本之间也是没有互相调用关系的,没有编码转换开销。

3.         宽字符函数:_tcscpy_tcscat_tcslen

4.         UNICODE宏是Windows API使用的,而MSC库中,对于非标准的东西用_前缀区分,所以_UNICODE宏是MSC API使用的。

5.         MS提供的避免缓冲区溢出攻击的函数在<StrSafe.h>文件中,包括StringCbCatStringCchCat等函数(其中Cb表示Count of ByteCch表示Count of Character,都用于表示衡量目标缓冲大小的单位);另外<TChar.h>中有_tcscpy_s_s后缀的函数。在源串过短时,<StrSafe.h>的函数截断,<TChar.h>的函数断言。

6.         要想接管CRT的错误处理(比如assert),使用_set_invalid_parameter_handler设置自己的处理函数,然后使用_CrtSetReportMode(_CRT_ASSERT, 0);来禁止CRT弹出对话框。

7.         Windows也提供了字符串处理函数,但lstrcatlstrcpy(针对T字符的)已经过时了,因为没考虑缓冲区溢出攻击。考虑使用StrFormatKBSizeStrFormatByteSizeCompareString(有很多比较选项)、CompareStringOrdinal(相当于_tcscmp)。

8.         GetThreadLocale返回线程的语言信息:LCIDLocale ID),供很多函数使用(包括使用CompareString针对语言来比较的时候)。

9.         宽字节转多字节WideCharToMultiByte,反之MultiByteToWideChar。其中,在宽字节转多字节的时候,如果有Unicode字符在多字节编码中没有对应项,那宽字节会被替换成参数lpDefaultChar,并且lpUsedDefaultChar会被标记为TRUE。当用这两个函数计算结果串的大小时,返回的是字符数。

10.     IsTextUnicode。

3    内核对象

1.         简单区分内核对象和其他对象的方法:创建需要安全信息的多半是内核对象。

2.         每个进程有一个内核对象表,表的每一项是一个简单结构,包括真实内核对象地址和访问权限等。用户代码持有的内核对象句柄其实是对象表中对应项的索引。因此如果CloseHandle关闭一个对象后没有清空变量,且在对象表的同样位置恰好又创建了一个新的内核对象,对之前没清空的无效变量的访问会造成bug。(比如对同一个句柄多调用了一次CloseHandle导致另一个内核对象被关闭。)

3.         进程退出时,会释放各种内存、内核对象、GDI对象等。

4.         跨进程使用内核对象的理由:跨进程传输:用文件映像对象实现共享内存、邮件槽和命名管道实现数据通信、信号量和互斥量进行同步等。

5.         跨进程使用内核对象的三种方式:对象句柄继承、命名内核对象、复制对象句柄。

6.         对象句柄继承:创建内核对象的时候可以指定SECURITY_ATTRIBUTES. bInheritHandle表示可继承(任何时候可以使用SetHandleInformation修改可继承性等属性),创建子进程时指定CreateProcess的参数bInheritHandlesTRUE,则子进程从父进程的对象表中拷贝所有可继承的对象到自己的对象表的相同表位置中(并增加引用计数),因为表项结构被完全拷贝且内核对象实际地址在地址空间后2G的内核地址段中,所以拷贝过来的表项完全有效,进而父子进程的可继承内核对象的句柄值完全相同,于是只要以任何方式将要继承的对象的句柄值跨进程交给子进程(创建子进程时的命令行参数、环境变量、共享内存、消息等手段),则后者可以使用。

7.         命名内核对象:要访问已经存在的命名内核对象,可以使用CreateXXX或者OpenXXX,后者在对象不存在的时候返回NULL。如果打开了一个已经存在的命名对象,在打开时为API指定的对象名以外的参数被忽略。注意,一个进程打开同一对象两次,除了增加引用两次外,返回的句柄值是不同的,需要分别关闭一次,即打开和关闭完全对称(很合理的行为)。在Vista及以上的系统,对象名可以包括在命名空间下,避免被低授权用户访问。

8.         复制对象句柄:DuplicateHandle

4    进程

1.         进程是执行文件的运行时形态。包括两部分:内核数据(对应内核对象)、地址空间(包括执行文件代码和栈堆等动态内存)。

2.         VC的“系统-子系统”值删除掉,即不指定控制台或GUI,则编译器会根据代码中存在main或者WinMain来自动选择子系统(这里不谈Unicode了),很方便。

3.         启动程序:根据子系统执行mainCRTStartup/WinMainCRTStartup,在该函数中干几件事(1)准备命令行和环境变量(用于char *argv[]char *env[])(2)初始化CRT的全局变量(包括_osver_winmajor_winver__argc_environ等)(3)初始化CRT运行库的内存分配(mallocfree)、IO函数等(4)初始化全局对象调用C++构造函数。

4.         退出程序:main返回后mainCRTStartup会调用exitexit干以下几件事:(1)执行通过_onexit注册的函数(2)执行全局对象的C++析构函数(通过atexit注册的)(3)判断_CrtDumpMemoryLeaks设置的内存泄漏检测标志,尝试检测内存泄漏(4)调用ExitProcess

5.         HINSTANCE和HMOUDLE完全相同,都是表示映像文件加载到内存后的基址(链接器中可以配置)。GetModuleHandle传入文件名可以获得模块基址;传入NULL可以得到执行文件的HINSTANCE(即使调用者位于某个模块中同样返回应用程序基址);GetModuleHandleEx可以根据函数地址得到模块基址

6.         访问环境变量:char *env[]参数、GetEnvironmentStringsGetEnvironmentVariableExpandEnvironmentStrings(将一个使用了类似”%USERPROFILE%”环境变量的字符串中的变量替换成值)。

7.         系统环境变量:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Enviroment。用户环境变量:HKEY_CURRENT_USER\Enviroment

8.         修改环境变量后可以通知相关的系统窗口(如控制面板等):SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM) “Enviroment”)

9.         可以设置特定线程在一个CPU核心集合上执行。

10.     SetErrorMode。设置该进程如何响应各种错误。

11.     关于相对路径:在通过GetEnvironmentStrings返回的环境变量中,有一部分不是真正的环境变量,比如“=C:=C:\Windows”“ =F:=F:\Projects\Test05”,他们表示一种进程相关配置“本进程在特定驱动器下对应的当前文件夹”。一个进程除了有以上配置外,还有一个当前驱动器,最终GetCurrentDirectory返回的当前路径就是当前驱动器+当前驱动器对应的当前文件夹。使用SetCurrentDirectory会改变该驱动器的当前文件夹,还会改变进程的当前驱动器(但这个API的改变并不会在GetEnvironmentStrings上体现出来,使用C函数_chdir可以同时改变两者,故C函数更优)。进程刚启动时,如果不考虑从父进程继承的环境,则只有进程当前驱动有当前文件夹,其他驱动都无配置。使用相对路径访问文件的时候,其绝对路径可以用GetFullPathName得到。文件名这样的相对路径的绝对路径是GetCurrentDirectory() + “文件名驱动器盘符:文件名(注意不是驱动符:/文件名)这样的相对路径的绝对路径就是该驱动器的当前文件夹(如果无配置,则是根目录) + “文件名

看如下代码:

_chdir("D:/Downloads"); // 修改D:的当前路径为Downloads,且进程当前驱动器为D:

_chdir("F:/Projects"); // 修改F:的当前路径为Projects,且进程当前驱动器为F:

std::ofstream("1.txt"); // 当前驱动器是F:,所以绝对路径是F:/Projects/1.txt

std::ofstream("d:1.txt"); // D:的当前路径是Downloads,所以绝对路径是D:/Downloads/1.txt

这种行为从cmdcd命令也可以看得出点端倪。

归纳:相对路径访问文件的时候,首先将相对路径展开成绝对路径,使用GetFullPathName,后者分两步:首先判断是否包含驱动器(以X:开头),如果没有,则在开头添加进程当前驱动器;然后检查是否以”X:/”开头,如果没有,则将”X:”展开成”X:/” + “对应驱动的当前文件夹。两步过后得到绝对路径。

12.     GetVersionEx获取系统版本信息。VerifyVersionInfo检测当前系统是否满足版本需要。

13.     CreateProcess的参数:关于lpApplicationNamelpCommandLine,有两种用法:(1)前者指定应用程序路径,后者指定参数(第一个参数前面要有一个空格,似乎底层会直接连接两个串)(2)前者为NULL,后者指定路径和参数,空格隔开。常用第二种方法。注意,lpCommandLine中由于是用空格分隔参数的,所以对其中含有空格的路径一定要用内层引号括起来。另外CreateProcessW有一个奇怪的行为,它会修改参数lpCommandLine(似乎只在lpApplicationName为空的时候会修改),所以使用Unicode版本的时候传入的该参数不能是常字符串(如L”Nodepad 1.txt”),而应该另外准备缓冲传给该API供其修改,因为ANSI版本是调用Unicode版本的且在编码转换的时候内置了缓冲,所以CreateProcessAlpCommandLine参数可以是常串(最终API会修改转换编码的临时缓冲)。默认情况下,CUICUI型子进程会和父进程共享控制台,在参数dwCreationFlags中添加DETACHED_PROCESSCREATE_NEW_CONSOLE标志可以阻止这种行为。在dwCreationFlags中添加CREATE_NEW_PROCESS_GROUP标志,可以控制进程组的组织,用户按下Ctrl+C的时候同一进程组的所有进程得到通知。lpEnvironment指定为NULL的时候,底层为用GetEnvironmentStrings来填充。lpCurrentDirectoryNULL的时候,子进程继承父进程的当前目录。lpStartupInfo不能为空,至少要初始化结构为0并将cb赋为sizeof。使用STARTUPINFOEX结构作为lpStartupInfo参数,还可以具体指定子进程要继承哪些父进程的可继承内核对象(即使bInheritHandles参数为FALSE)。

14.     cmd进程输入命令行前显示的路径,就是其当前路径(GetCurrentDirectory)。在CreateProcess时,cmd没有设置子进程当前路径,而资源管理器将路径设置成子进程镜像目录。因为cmd的子进程会继承cmd的当前路径(lpCurrentDirectory为空的结果),因此最好在用cmd启动程序的时候先将cmd的当前路径设置为新进程的镜像路径。

15.     进程和线程结束后,句柄对象被标记为激活, WaitForSingleObject会返回。

16.     CreateProcess后,可以使用WaitForInputIdle或类似函数来等待新进程初始化环境完毕开始运行。

17.     WoW64:Windows 32 On Windows 64。所有64windows运行着这个虚拟机,用来执行32位程序。判断一个32位程序是否是运行在64位系统的32位虚拟机中:IsWow64Process

18.     父进程创建子进程时使用的lpStartupInfo,在子进程中可以使用GetStartupInfo来查询。

19.     创建一个子进程时,进程和主线程本身的存在就有了引用1,而调用CreateProcess的父进程又会有他们的引用所以计数到了2。要完全销毁进程和线程,需要计数为0,所以除了需要进程本身结束外,引用的该进程的其他线程也要释放引用。当然,CreateProcess过后父进程马上CloseHandle并不会结束子进程,只是释放自己的引用,使其计数为1,这是正常的行为。要确保某个进程或线程不被销毁,不调用CloseHandle即可。如果进程本身已经退出了,但还有其他进程引用它,则它的地址空间被回收,只有内核对象还存在(比如这时再对句柄使用API查看内存,则内存信息为空),这也是为什么可以查看已经退出的进程的退出码的原因(退出码保存在内核对象中)。

20.     进程和线程的ID位于同一个系统顶层名空间。即任意进程的任意线程ID绝不可能和任意进程ID相同。这个ID会被系统循环利用。

21.     GetProcessIDOfThread。

22.     进程只有在它所有线程都结束后才会结束。ExitProcess会杀死所有线程,所以可以直接结束进程,在主线程中调用ExitThread只会结束主线程(即,主线程创建一个死循环线程后自己_exitthreadex,这个进程不会退出。)。main返回后CRT调用exit后者再调用ExitProcess,所以在mainreturn可以直接结束进程。

23.     通过ExitProcessExitThread(单线程时)结束进程,由于这些APICRT更底层,他们只能保证正确的释放Windows资源(内存、内核对象引用),并不保证释放C++资源(CRT底层资源、全局对象的析构函数),故一定要从main中返回自然的结束进程(其他原因在后面章节说明)。TerminateProcess也出于相同的原因应该避免使用。

24.     CreateProcess创建的子进程会继承父进程的Security Token权限,而ShellExecuteEx可以提高子进程的权限(令lpVerb参数为”runas”)。资源管理器使用前者创建子进程,所以通过它开打的程序都具有和资源管理器相同的权限。

25.     关于Vista及更高系统的UACUser Account Control):Vista以前的系统如果以管理员账号登陆,资源管理器(Explorer)会获得一个管理员权限的Security Token,然后从资源管理器打开的子进程都会继承这个最高权限,这种行为非常危险。Vista以后,即使以管理员账号登陆,资源管理器仍然只持有一个一般权限的TokenFiltered Token),子进程如果想提升权限,有两种途径:(1)用户“以管理员身份运行”启动该进程(2)子进程自己提出请求要求用户提升权限(子进程是安装程序、或者子进程配置有.manifest文件说明权限需求)。另外,在很多软件中出现有小盾牌图标的按钮,也是要求提高权限,点击过后会结束当前进程,重启一个高权限进程(如资源管理器中“显示所有用户的进程”按钮)。其实这三种提高权限都是父进程调用了ShellExecuteEx

26.     IsUserAnAdmin判断当前用户是否是管理员。在Vista及以上的系统中,即使是管理员,进程也有可能因为筛选Token而不具备最高权限。

27.     枚举所有进程:Process32FirstProcess32NextEnumProcesses

28.     可以从HMOUDLE中读取IMAGE_DOS_HEADERIMAGE_NT_HEADERS,进而从这些PE头中取得模块的推荐加载地址等信息。

29.     PEB(Process Enviroment Block)包含了进程的启动命令行、当前路径等数据。该字段可以通过NtQueryInformationProcessPROCESS_BASIC_INFORMATION参数取得。

30.     可以通过WinDbgdt命令,查看一些结构的具体成员布局,如PEB等。

31.     Windows完整性机制(Windows Integrity Mechanism):这是UAC之外的另一套安全机制,Windows通过在系统访问控制表(SACL, System Access Control List)中增加访问控制项(ACE, Access Control Entry)实现,每一种受保护的资源都有对应的完整性级别(Integrity Level),每个进程都有一个基于Token计算的完整性级别,如果进程的级别小于资源的级别,则不能访问资源。提升Token权限之前的进程级别为中,提升后为高,而像IE这样可以能执行网络代码的进程为低。可以通过GetTokenInfomation查看一些和完整性级别相关的策略。窗口系统也根据完整性级别,拒绝低级别者向高级别使用PostMessageSendMessageAPI

32.     Vista以上有一些进程是特殊的受保护进程,ToolHelp API对他们无效,因此无法查看进程信息。

33.     GetProcessTime查看进程时间,GetProcessIoCounters查看IO次数。

34.     GetProcessImageFileName返回内核格式的文件名。

第5章               作业

1.         Job(作业),也就是进程组的概念,添加进同一个作业的进程能够通过作业内核对象来集中控制,设置一些额外的属性等。添加进一个作业就不能再移出。

2.         IsProcessInJob、CreateJobObjectOpenJobObject

3.         作业内核对象在它内部的所有进程都结束后才会被销毁。

4.         细节:当客户的作业句柄变量都被关闭后,即使作业对象还存在(因为进程没有全部结束),也不能再通过作业名打开作业再操作了。

5.         Vista以上,通过任务管理器创建的进程,都被添加进了一个独立的作业;从命令行(cmd)创建的进程则不然。

6.         能够对作业添加的限制:基本限制(限制进程时间、优先级、物理内存占用等)、扩展限制(基础限制之上,还能限制内存使用总量,以及查看峰值内存使用)、UI限制(限制关机/重启、访问剪切板、切换桌面、改变显示器设置、访问作业外进程的句柄等)、安全限制(安全限制一旦设置,则不能修改)。SetInformationJobObjectQueryInformationJobObject用于设置和查询限制。

7.         AssignProcessToJobObject添加进程到作业。

8.         父进程位于某一作业中,子进程创建后也自动加入同一作业。除非作业的基本限制中包含JOB_OBJECT_LIMIT_BREAKAWAY_OK(允许进程时脱离作业),并且CreateProcess时指定CREATE_BREAKAWAY_FROM_JOB标记。

9.         TerminateJobObject强制结束作业,同时结束作业内所有进程(等价于对作业内每个进程调TerminateProcess)。

10.     QueryInformationJobObject除了查看作业限制外,也可以查看作业信息,包括总进程数、活跃进程数、总时间、总IO次数、进程ID列表等。

11.     作业结束后(所有内部进程结束),内核对象处于激活态,WaitForSingleObject返回。

12.     作业通知机制:将作业对象和IO完成端口绑定,作业中的事件(进程结束、时间到期、内存达到限制等)将通过完成端口事件来通知。

第6章               线程基础

1.         像进程一样,线程在数据上也分为两个部分:线程内核对象(包括统计信息)、栈。(进程的两个部分是,内核对象和地址空间)。­­­

2.         比起ExitThreadTerminateThread,应该让线程的主函数返回来结束线程,否则一些栈对象不能正常析构(这里不再考虑CRT函数)。

3.         C/C++编程中不要使用CreateThreadExitThread,应该使用编译器厂商提供的包装函数,如MS_beginthreadex_endthreadex。因为使用前者,C/C++CRT不能正常初始化和释放线程相关资源(C/C++中有一些全局变量如errno和一些有内部状态的函数strtokasctime都需要通过TLS来正确实现,毕竟C库函数的诞生早于多线程)。事实上,如果在C/C++中使用了CreateThreadEndThread,部分有内部状态的函数还是可以正常使用的,因为这些函数内部会尝试取得TLS,发现还未分配的话会自动分配,CRTDll版本库也会在得到线程退出通知时尝试释放TLS,只是因为这份TLS是中途分配的信息不够全面,部分状态函数还是会有问题,因此在C/C++中还是要尽量使用后者。

4.         线程栈最大为CreateThreaddwStackSize参数和/STACK链接选项(VC中默认为1MB)两者中的较大值。

5.         TerminateThread的一些细节:该函数是异步的,函数返回时,线程还没有结束,需要WaitForSingleObjectDllMain不会收到被Terminate线程的结束通知。

6.         只有当线程函数结束(正常返回或Exit掉)后,该线程的栈空间才会被回收(也就是说TerminateThread函数刚返回时被杀死线程栈空间还在,直到线程对象处于激活态)。

7.         对进程中的各个线程来说,ExitProcessTerminateProcess都将导致对线程的TerminateThread调用,因此进程的main函数结束前,尽量确保工作线程都正常退出。

8.         大部分的资源都是进程相关的,窗口句柄和hook句柄是线程相关的,线程退出时会释放他们(在C/C++中还有CRTTLS变量)。

9.         GetCurrentProcess、GetCurrentThread返回的都是伪句柄,如果想要把这个句柄保存下来在其他线程、进程中使用的话,是有歧义的,可以用保存ID来代替,如果一定要保存句柄的话,两种方法:(1DuplicateHandle2)先GetCurrentThreadID,再OpenThread

7   线程调度、优先级和关联性

1.         Windows线程调度的时间间隔(发生上下文切换的时间片)大概是15毫秒(GetSystemTimeAdjustmentlpTimeIncrement参数)。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值