Widows核心编程第一章:错误处理
Windows API同linux一样, 在返回值的基础上可以通过错误码确认具体的程序错误原因。当一个 Windows 函数检测到错误时,它会使用一个名为“线程本地存储区” (thread-local storage)的机制将恰当的错误码与“主调线程”(或者说发出调用的线程,即 calling thread)关联到一起(windows核心编程)。在vs调试时刻可以监视(watch)$err,hr变量的值查看系统调用具体错误原因。
这个错误码也可以在代码中通过DWORD GetLastError() 函数获取。
获取到的错误码可以通过
DWORD FormatMessage( DWORD dwFlags,
LPCVOID pSource,
DWORD dwMessageId,
DWORD dwLanguageId,
PTSTR pszBuffer,
DWORD nSize,
va_list *Arguments);
函数转换为具体的文本信息。具体参数如下:
dwFlags : 格式化选项
低位值指定了函数如何处理输出缓冲区处理行转换,也可以指定格式化输出字符串输出行的最大宽度。
Value |
Meaning |
FORMAT_MESSAGE_ALLOCATE_BUFFER |
lpBuffer参数是一个PVOID指针,nSize参数指定按TCHARs为单位的分配给输出消息缓冲区的最小值。当你不适用这个缓冲区的时候也就是lpBuffer的时候需要用LocalFree将其释放
|
FORMAT_MESSAGE_ARGUMENT_ARRAY |
Arguments参数不是一个va_list结构,但是它表示一个数组指针。这个标识符不能在64位整数值时使用,你如果要使用64位整数值,那么你必须使用va_list结构体
|
FORMAT_MESSAGE_FROM_HMODULE |
lpSource参数是一个包含了消息表资源(Message-table resources)模块(dll)句柄。如果lpSource句柄为NULL,系统会自动搜索当前进程文件的消息资源。 这个标示符不可以和FORMAT_MESSAGE_FROM_STRING共用。 如果模块中没有资源表,这个函数执行失败并且返回ERROR_RESOURCE_TYPE_NOT_FOUND错误值。
|
FORMAT_MESSAGE_FROM_STRING |
lpSource参数指向一个包含了消息定义的字符串.这个消息定义里面可能包含了插入序列(insert sequence),像消息表资源中包含消息文本一样.和这个标示符不和FORMAT_MESSAGE_FROM_HMODULE或者FORMAT_MESSAGE_FROM_SYSTEM一起使用.
|
FORMAT_MESSAGE_FROM_SYSTEM |
函数将会搜索系统消息表资源来寻找所需消息资源。如果这个标示符同时定义了FORMAT_MESSAGE_FROM_HMODULE,那么如果函数在模块中没有搜索到所需消息的话将会在系统中搜索。这个标示符不能和FORMAT_MESSAGE_FROM_STRING一起使用. 当这个标示符设置的时候,可以使用GetLastError函数返回值来搜索这个错误码在系统定义错误中相应的消息文本。
|
FORMAT_MESSAGE_IGNORE_INSERTS |
在消息定义中的插入序列将会被忽略,这个标示符在获取一个格式化好的消息十分有用,如果这个标示符设置好了,那么Arguments参数将被忽略。
|
lpSource:
这个值是消息表资源来自哪里,这个值依靠dwFlags,详细请看FORMAT_MESSAGE_FROM_HMODULE和FORMAT_MESSAGE_FROM_STRING,如果这两个标示符都没设置,那么lpSource将会被忽略。
dwMessageId :
所需格式化消息的标识符。当dwFlags设置了FORMAT_MESSAGE_FROM_STRING,这个参数将会被忽略
dwLanguageId:
格式化消息语言标识符。
lpBuffer:
一个缓冲区指针来接受格式化后的消息。当dwFlags包括了FORMAT_MESSAGE_ALLOCATE_BUFFER标志符,这个函数将会使用LocalAlloc函数分配一块缓冲区,lpBuffer需要接受一个地址来使用这个缓冲区。(这里要注意传参一定要传地址)。
nSize:
如果FORMAT_MESSAGE_ALLOCATE_BUFFER没有设置,那么这个参数指定了输出缓冲区的消息,以TCHARs为单位。如果FORMAT_MESSAGE_ALLOCATE_BUFFER设置了,这个参数设置以TCHARs为单位的输出缓冲区的最小值。这个输出缓冲区不能大于64KB。
Arguments:
一个数组中的值在格式化消息中作为插入值,根据消息文本的格式里面的内容(详见mc.exe使用),可以知道%n[!format_specifier!]为这个参数的指定形式。
比如说在格式字符串中的%1为数组中的第一个值,%2为第二个值。n就代表数组第几个值。
那么[!format_specifier!]如何解释呢?这个格式化指定具体解释在 Format Specification Fields,也就是printf的格式形式安排。
错误码被定义在WinError.h头文件中, 是一个32位数。
用户可以通过VOID SetLastError(DWORD dwErrCode); 函数定义自己的错误码。
线程本地存储(thread-local storage):
https://www.cnblogs.com/zhoug2020/p/6497709.html
在同一进程中的线程,全局或静态的局部变量通常时共有的。其中一个线程进行了修改, 就会影响到其他线程。
TLS技术将数据和执行的特定线程联系起来。分为静态和动态。
静态TLS:
__declspec(thread) DWORD myTLSData=0;
__declspec(thread)的前缀是Microsoft添加给Visual C++编译器的一个修改符。它告诉编译器,对应的变量应该放入可执行文件或DLL文件中它的自己的.tls节中。__declspec(thread)后面的变量必须声明为函数中(或函数外)的一个全局变量或静态变量。
然后,为了使含有静态TLS的程序能够运行,操作系统必须参与其操作。当TLS应用程序加载到内存中时,系统要寻找可执行文件中的.tls节,并且动态地分配一个足够大的内存块,以便存放所有的静态TLS变量。应用程序中的代码每次引用其中的一个变量时,就要转换为已分配内存块中包含的一个内存位置。因此,编译器必须生成一些辅助代码来引用该静态TLS变量,这将使你的应用程序变得比较大而且运行的速度比较慢。在x86 CPU上,将为每次引用的静态TLS变量生成3个辅助机器指令。如果在进程中创建了另一个线程,那么系统就要将它捕获并且自动分配另一个内存块,以便存放新线程的静态TLS变量。新线程只拥有对它自己的静态TLS变量的访问权,不能访问属于其他线程的TLS变量。
如果隐式链接包含静态TLS变量的DLL, ,当系统加载该应用程序时,它首先要确定应用程序的.tls节的大小,并将这个值与应用程序链接的DLL中的所有.tls节的大小相加。当在你的进程中创建线程时,系统自动分配足够大的内存块来存放所有应用程序声明的和所有隐含链接的DLL包含的TLS变量。
如果显式链接包含静态TLS变量的DLL,系统必须查看该进程中已经存在的所有线程,并扩大它们的TLS内存块,以便适应新DLL对内存的需求。另外,如果调用FreeLibrary来释放包含静态TLS变量的DLL,那么与进程中的每个线程相关的的TLS内存块又都应该被压缩。 对于操作系统来说,这样的管理任务太重了。所以,虽然系统允许包含静态TLS变量的库在运行期进行显式加载,但是其包含TLS数据却没有进行相应的初始化。如果试图访问这些数据,就可能导致访问违规!
动态TLS:
1、使用动态TLS
动态TLS在程序实现中比静态TLS要稍微麻烦一些,需要通过一组函数来实现:
DWORD TlsAlloc();//返回TLS数组可用位置的索引
BOOL TlsSetValue(DWORD dwTlsIndex, LPVOID lpTlsValue); //将调用线程的TLS数组索引dwTlsIndex处设为值lpTlsValue
LPVOID TlsGetValue(DWORD dwTlsIndex); //返回调用线程的TLS数组dwTlsIndex索引处的值
BOOL TlsFree(DWORD dwTlsIndex); //释放所有线程的TLS数组位置索引dwTlsIndex,将该位置标记为未使用。
有了以上四个函数,我们可以发现使用动态TLS其实还是很容易很方便的。
2、动态TLS原理
windows用来管理TLS的内部数据结构如下:
线程本地存储器的位标志显示了该进程中所有运行的线程正在使用的一组标志。每个标志均可设置为FREE或者INUSE,表示TLS插槽(slot)是否正在使用。Microsoft保证至少TLS_MINIMUM_AVAILABLE位标志是可供使用的。另外,TLS_MINIMUM_AVAILABLE在WinNT.h中被定义为64。Windows2000将这个标志数组扩展为允许有1000个以上的TLS插槽。
而每一个线程拥有一个自己独立的TLS slot数组,用于存储TLS数据。
为了使用动态TLS,我们首先调用TlsAlloc()来命令系统对进程的位标志进行扫描,找到一个可用的位置,并返回该索引;如果找不到,就返回TLS_OUT_OF_INDEXES。事实上,除此之外,TlsAlloc函数还会自动清空所有线程的TLS数组的对应索引的值。这避免以前遗留的值可能引起的问题。
然后,我们就可以调用TlsSetValue函数将对应的索引位保存一个特定的值,可以调用TlsGetValue()来返回该索引位的值。注意,这两个函数并不执行任何测试和错误检查,我们必须要保证索引是通过TlsAlloc正确分配的。
当所有线程都不需要保留TLS数组某个索引位的时候,应该调用TlsFree。该函数告知系统将进程的位标志数组的index位置为FREE状态。如果运行成功,函数返回TRUE。注意,如果试图释放一个没有分配的索引位,将产生一个错误。
动态TLS的使用相对静态TLS稍微麻烦一点,但是无论是将其用在可执行文件中还是DLL中,都还是很简单的。而且当用在DLL中时,没有由于DLL链接方式而可能产生的问题,所以,如果要在DLL中用TLS,又不能保证客户始终采用隐式链接方式,那么请采用动态TLS的实现。