Windows核心编程-学习笔记1
错误处理
常见windows函数返回值类型
- VOID
- BOOL
- HANDLE
- PVOID
- LONG/DWORD
Microsoft编辑了一个列表为所有错误代码分配了一个32位的编号,当Windows函数检测到错误时会使用线程本地存储区机制将相应的错误代码于主调线程关联到一起。要查看具体错误可以用GetLastError函数
每个错误有三种表示:消息ID、消息文本、编号;vs有个error lookup可以查询错误信息
位 | 内容 | 含义 |
---|---|---|
31-30 | 严重性 | 0=成功,1=信息,2=警告,3=错误 |
29 | Microsoft/客户 | 0=Microsoft定义的代码,1=客户定义的代码 |
28 | 保留 | 必须为0 |
27-16 | Facility代码 | 前256个值由Microsoft保留 |
15-0 | 异常代码 | Microsoft/客户定义的代码 |
因此看到C开头的十六进制错误码就知道有报错
字符和字符串处理
字符有单字节也有双字节
- UTF-8:可以将字符编码为1、2、3、4字节,对于值为0x0800及以上的大量字符编码时不如UTF-16有效
- UTF-32:每个字符都编码为4字节,但内存使用不够高效
C语言用char来表示8位ANSI,默认情况下代码里声明一个字符串时,c编译器会把字符串中的字符转换成由8位char数据类型构成的一个数组
Microsoft的c/c++编译器定义了内建的数据类型wchar_t,表示一个16位的Unicode字符
wchar_t c = L'A';
wchar_t szBuffer[100] = L"A String";
字符串前的大写字母L通知编译器该字符串应编译为unicode字符串,而Windows头文件WinNT.h中定义了以下数据类型
typedef char CHAR; // 8-bit
typedef wchar_t WCHAR; // 16-bit
测试下宽字节
#include <stdio.h>
#include <string.h>
int main() {
char c[] = "XYZ";
printf("%s\n", c);
wchar_t wc[] = L"ABC";
wprintf(L"%s\n", wc);
}
反编译后如下,可以看到v4是2字节表示一个字符
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+20h] [rbp-28h] BYREF
__int64 v5; // [rsp+28h] [rbp-20h]
v4 = 'ZYX';
sub_140001070("%s\n", (const char *)&v4);
v5 = 'C\0B\0A';
sub_140001010((wchar_t *)L"%s\n");
return 0;
}
CreateWindowExW接受Unicode字符串,W代表wide,而CreateWindowExA的A表示接受ANSI字符串
实际上vs默认UNICODE所以CreateWindowEx都会调用CreateWindowExW
C语言中strlen返回ANSI字符串长度,wcslen返回Unicode字符串长度
安全字符串函数
Unicode的好处
推荐的字符和字符串处理方式
Windows MultiByteToWideChar函数将多字节字符串转换为宽字符字符串
int MultiByteToWideChar(
[in] UINT CodePage,
[in] DWORD dwFlags,
[in] _In_NLS_string_(cbMultiByte)LPCCH lpMultiByteStr,
[in] int cbMultiByte,
[out, optional] LPWSTR lpWideCharStr,
[in] int cchWideChar
);
这是该章节我自己学习写的部分测试代码,初步掌握了下相关库的调用以及各种字符串操作函数用法
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <tchar.h>
int main() {
char c[] = "XYZ";
printf("%s %lld\n", c, strlen(c));
wchar_t wc[] = L"ABC";
wprintf(L"%s %lld\n", wc, _tcslen(wc)); // _tcslen等价于wcslen,ifdef _UNICODE, tchar.h
wchar_t wc_c[10];
wcscpy(wc_c, L"DEF");
wprintf(L"%s %lld\n", wc_c, wcslen(wc_c));
wcscpy_s(wc_c, _countof(wc), wc); // stdlib.h
wprintf(L"%s %lld\n", wc_c, wcslen(wc_c));
if (!wcscmp(wc_c, wc)) {
printf("Same!\n");
}
}
内核对象
内核对象-kernel object、句柄-handle
内核对象有:访问令牌对象、事件对象、文件对象、文件映射对象、IO完成端口对象、作业对象、邮槽对象、互斥量对象、管道对象、进程对象、信号量对象、线程对象。。。。
进程对象有一个进程ID、一个基本的优先级和一个退出代码;文件对象有一个字节偏移量、一个共享模式和一个打开模式
内核对象的数据结构只能由操作系统内核访问,所以应用程序不能再内存中定位这些数据结构并直接更改内容。Microsoft有意强化了限制以确保内核对象结构保持一致性状态。
由于不能直接更改这些结构,因此需要利用Windows提供的一组函数来操纵结构。调用一个会创建内核对象的函数后,函数会返回一个句柄,其标识了所创建的对象。32位Windows进程句柄就是一个32位值,64位-64。
使用计数:内核操作对象所有这是OS内核而非进程,内核对象的生命周期可能长于创建它的进程。操作系统内核知道当前有多少进程正在使用一个特定的内核对象,因为每个对象都包含一个使用计数(内核对象类型都有的一个数据成员)。初次创建一个对象时,其<u>使用技术被设置为1</u>。<u>另一个进程获取对现有内核对象的访问后,使用计数就会递增</u>。<u>进程终止后OS内核自动递减此进程仍然打开的所有内核对象的使用计数</u>。一旦对象的<u>使用计数变为0,OS内核就会销毁该对象</u>,以确保系统中不存在没有被任何进程引用的内核对象
安全性:内核对象使用安全描述符(security descriptor,SD)来保护,其描述了谁(通常是对象的创建者)拥有对象;哪些组和用户被允许访问或使用此对象;哪些组和用户被拒绝访问此对象
用于创建内核对象的所有函数几乎都有指向一个SECURITY_ATTRIBUTES结构的指针作为参数,如下CReateFileMapping
HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSIzeLow,
PCTSTR pszName);
进程
如何定义
正在运行的程序实例,由下面两部分构成
- 一个内核对象:OS用它来管理进程
- 一个地址空间:包含所有可执行文件或DLL模块的代码和数据,还包含动态内存分配,如线程栈和堆的分配
进程和线程联系
- 进程做任何事都必须让一个线程在它的上下文中运行 ,该线程负责执行进程地址空间包含的代码
- 一个进程可以多个线程,每个线程都有自己的一组CPU寄存器和栈
- 系统创建进程,会自动为进程创建第一个线程(aka主线程),然后这个线程再创建更多线程,后者继续...
- 如果没有线程执行进程地址空间包含的代码,进程及其地址空间就会被系统自动销毁
CPU和线程
- OS会采用轮询的方式为每个线程分配时间片(CPU时间),看起来像并发
- 多CPU或CPU核心时,windows采用更复杂的算法来为线程分配CPU时间,实现真正并发
GUI和CUI
- GUI:图形用户界面,创建窗口、拥有菜单、对话框交互,例如记事本、计算器等
- CUI:控制台用户界面,只有文本,例如CMD
二者界限模糊,如CMD可以运行命令调出图形化对话框,GUI同样也可以创建控制台窗口(如查看调试信息、日志等)
用户运行程序时,OS加载程序(loader)会检查可执行文件映像的文件头,获取链接器开关值,为SUBSYSTEM:CONSOLE
时是CUI程序(自动确保有一个可用的文本控制台窗口),为SUBSYSTEM:WINDOWS
时是GUI程序(不会创建控制台窗口,只加载程序交给程序自己处理UI)
进程实例句柄
应用程序类型和对应的入口函数
应用程序类型 | 入口点函数 | 嵌入可执行文件的启动函数 |
---|---|---|
ANSI+GUI | _tWinMain (WinMain) | WinMainCRTStartup |
Unicode+GUI | _tWinMain (wWinMain) | wWinMainCRTStartup |
ANSI+CUI | _tmain (Main) | mainCRTStartup |
Unicode+CUI | _tmain (Wmain) | wmainCRTStartup |
啥是进程实例句柄,怎么用?
加载到进程地址空间的每个可执行文件或DLL都被赋予了唯一的实例句柄。可执行文件的实例被当作(w)WinMain函数的第一个参数hInstanceExe传入
需要加载资源的函数调用一般需要提供句柄值,例如加载图标资源
HICON LoadIcon(HINSTANCE hInstance, PCTSR pszIcon);
第一个参数指出哪个文件包含了想要加载的资源,因此很多程序都将hInstanceExe参数保存在一个全局变量中,使其容易被可执行文件的所有代码访问
有的函数也需要HMODULE类型参数,例如
DWORD GetModuleFileName(HMODULE hInstModule, PTSTR pszPath, DWORD cchPath);
HMODULE和HINSTANCE实际上一致,他们的值都是一个内存基地址;系统将可执行文件的映像加载到进程地址空间的该位置(VS默认基地址是0x400000)
要获取一个可执行文件或DLL被加载到进程地址空间的什么位置可以
法一:GetModuleHandle,传入一个已在调用进程地址空间中的可执行文件或DLL名称字符串
HMODULE GetModuleHandle(PCTSTR pszModule);
找到就会返回该加载基地址,反之NULL
法二:GetModuleHandleEx,将GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS作为第一个参数,将当前函数的地址作为第二个参数,最后一个参数指向一个HMODULE指针。GetModuleHandleEx会用第二个参数所在DLL的基地址来填写指针
#include<stdio.h>
#include<windows.h>
#include<tchar.h>
extern "C" const IMAGE_DOS_HEADER __ImageBase;
void DumpModule() {
HMODULE hModule = GetModuleHandle(NULL);
_tprintf(TEXT("with GetModuleHandle(NULL) = 0x%x\r\n", hModule));
_tprintf(TEXT("with __ImageBase = 0x%x\r\n", (HINSTANCE)&__ImageBase));
hModule = NULL;
GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (PCTSTR)DumpModule, &hModule);
_tprintf(TEXT("with GetModuleHandleEx = 0x%x\r\n", hModule));
}
int _tmain() {
DumpModule();
return 0;
}
进程的命令行
系统创建新进程都会传一个命令行,几乎总是非空的
可以使用GetCommandLine来获取完整命令行
#include<stdio.h>
#include<windows.h>
#include<tchar.h>
int _tmain() {
PTSTR a = GetCommandLine();
_tprintf(TEXT("%s\n"), a);
return 0;
}
而C运行库的启动代码开始执行一个GUI程序时,会调用GetCommandLine获取完整命令行,然后忽略可执行文件的名称,将指向剩余部分的一个指针传给WinMain的pszCmdLine参数
可以利用CommandLineToArgvW获取参数数目以及指向参数的指针
#include<stdio.h>
#include<windows.h>
#include<tchar.h>
#include<ShellAPI.h>
int _tmain() {
PTSTR a = GetCommandLine();
_tprintf(TEXT("%s\n"), a);
int numArgs;
PWSTR* ppArgv = CommandLineToArgvW(a, &numArgs);
for (int i = 0; i < numArgs; i++) {
_tprintf(TEXT("%s\n"), ppArgv[i]);
}
return 0;
}
进程的环境变量
每个进程都有一个与之关联的环境块,这是在进程地址空间内分配的一个内存块,类似下面
=::=::\ ...
VarName1=VarValue1\0
VarName2=VarValue2\0
VarName3=VarValue3\0...
VarNameX=VarValueX\0
\0
很明显环境变量名称和值,但是可能有其他字符串等号开头,这种不作为环境变量使用
怎么读取环境块
法1:GetEnvironmentStrings
#include<stdio.h>
#include<windows.h>
#include<tchar.h>
#include<strsafe.h>
void DumpEnvStrings() {
PTSTR pEnvBlock = GetEnvironmentStrings();
TCHAR szName[MAX_PATH]; // 260
TCHAR szValue[MAX_PATH];
PTSTR pszCurrent = pEnvBlock;
HRESULT hr = S_OK;
PCTSTR pszPos = NULL;
int current = 0;
while (pszCurrent != NULL) {
if (*pszCurrent != TEXT('=')) {
pszPos = _tcschr(pszCurrent, TEXT('='));
pszPos++;
size_t cbNameLength = (size_t)pszPos - (size_t)pszCurrent - sizeof(TCHAR);
hr = StringCbCopyN(szName, MAX_PATH, pszCurrent, cbNameLength);
if (FAILED(hr)) {
break;
}
hr = StringCchCopyN(szValue, MAX_PATH, pszPos, _tcslen(pszPos) + 1);
if (SUCCEEDED(hr)) {// >=0
_tprintf(TEXT("[%u] %s=%s\r\n"), current, szName, szValue);
}
else if (hr == STRSAFE_E_INSUFFICIENT_BUFFER) {
_tprintf(TEXT("[%u] %s=%s...\r\n"), current, szName, szValue);
}
else {
_tprintf(TEXT("[%u] %s=???\r\n", current, szName));
break;
}
}
else {
_tprintf(TEXT("[%u] %s\r\n", current, pszCurrent));
}
current++;
while (*pszCurrent != TEXT('\0'))
pszCurrent++;
pszCurrent++;
if (*pszCurrent == TEXT('\0'))
break;
};
FreeEnvironmentStrings(pEnvBlock);
}
int _tmain() {
DumpEnvStrings();
return 0;
}
StringCbCopyN等价于strncpy、wcsncpy、_tcsncpy,用于将指定的字节数从一个字符串复制到另一个字符串,目标缓冲区大小需要提供以防止缓冲区溢出;它的返回值包括
STRSAFEAPI StringCbCopyNW(
[out] STRSAFE_LPWSTR pszDest,
[in] size_t cbDest,
[in] STRSAFE_PCNZWCH pszSrc,
[in] size_t cbToCopy
);
- S_OK:源数据存在,从 pszSrc 复制数据而不截断,生成的目标缓冲区为 null 终止。
- STRSAFE_E_INVALID_PARAMETER:cbDest 中的值大于
STRSAFE_MAX_CCH * sizeof(TCHAR)
,或者目标缓冲区已满。 - STRSAFE_E_INSUFFICIENT_BUFFER:复制操作由于缓冲区空间不足而失败。目标缓冲区包含预期结果的截断、以 null 结尾的版本。 在截断是可接受的情况下,这不一定被视为失败条件。(相当于太长了提前截断了)
StringCchCopyNA同上, 只不过复制的是字符(wchar_t就是2字节,比方说5个字符StringCbCopyNW要复制10字节;用sizeof计算缓冲区总字节数,用_countof计算缓冲区字符数)
STRSAFEAPI StringCchCopyNA(
[out] STRSAFE_LPSTR pszDest,
[in] size_t cchDest,
[in] STRSAFE_PCNZCH pszSrc,
[in] size_t cchToCopy
);
这会打印出所有环境变量,根据我的观察其中甚至还有系统环境变量
法2:CUI专用,通过main入口点函数所接收的TCHAR* env[]参数实现
#include<windows.h>
#include<tchar.h>
void DumpEnvVariables(PTSTR pEnvBlock[]) {
int current = 0;
PTSTR* pElement = (PTSTR*)pEnvBlock;
PTSTR pCurrent = NULL;
while (pElement != NULL) {
pCurrent = (PTSTR)(*pElement);
if (pCurrent == NULL) {
pElement = NULL;
}
else {
_tprintf(TEXT("[%u] %s\r\n"), current, pCurrent);
current++;
pElement++;
}
}
}
int _tmain(int argc, _TCHAR* argv[], TCHAR* env[]) {
DumpEnvVariables(env);
return 0;
}
需要注意空格是有意义的
注册表中和环境变量相关
- 系统所有环境变量:HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession ManagerEnvironment
- 当前登录用户的所有环境变量:HKEY_CURRENT_USEREnvironment
环境变量相关操作
判断环境变量是否存在:
DWORD GetEnvironmentVariable(PCTSTR pszName, PTSTR pszValue, DWORD cchValue);
pszName指向预期的变量名称,pszValue指向保存变量值的缓冲区,cchValue指出缓冲区大小(字符数);如果找到则返回复制到缓冲区的字符数,反之返回0
由于不知道需要多少字符来保存环境变量的值,因此通常调用2次GetEnvironmentVariable,第一次传入cchValue值为0
#include<windows.h>
#include<tchar.h>
void PrintEnvironmentVariable(PCTSTR pszVariableName) {
PTSTR pszValue = NULL;
DWORD dwResult = GetEnvironmentVariable(pszVariableName, pszValue, 0);
if (dwResult != 0) {
DWORD size = dwResult * sizeof(TCHAR);
pszValue = (PTSTR)malloc(size);
GetEnvironmentVariable(pszVariableName, pszValue, size);
_tprintf(TEXT("%s=%s\n"), pszVariableName, pszValue);
}
else {
_tprintf(TEXT("'%s'=<unknown value>\n"), pszVariableName);
}
}
int _tmain(int argc, _TCHAR* argv[], TCHAR* env[]) {
PrintEnvironmentVariable(TEXT("path"));
return 0;
}
扩展可替换字符串:在前面我们查看环境变量时可以发现很多百分号围起来的变量,这种是可替换字符串
,Windows也是提供了相关api来获取完整的字符串,
DWORD ExpandEnvironmentStrings(PCTSTR pszSrc, PTSTR pszDst, DWORD chSize);
pszSrc是包含可替换环境变量字符串的一个字符串地址,pszDst是接收扩展字符串的缓冲区地址,chSize是缓冲区最大大小(字符数);返回值是保存扩展字符串所需的缓冲区大小,如果chSize小于该值%%就不会扩展而是被换成空字符串,所以通常调用2次ExpandEnvironmentStrings
这里去尝试把打印了一个,同时测试了下如果%%里面的不存在打印的就是直接字符串
#include<windows.h>
#include<tchar.h>
int _tmain() {
DWORD chValue = ExpandEnvironmentStrings(TEXT("%DriverData%"), NULL, 0);
PTSTR pszBuffer = new TCHAR[chValue];
chValue = ExpandEnvironmentStrings(TEXT("%DriverData%"), pszBuffer, chValue);
_tprintf(TEXT("%s\r\n"), pszBuffer);
return 0;
}
添加/删除/修改变量值:SetEnvironmentVariable将pszName标识的一个变量设为pszValue参数所标识的值
BOOL SetEnvironmentVariable(PCTSTR pszName, PCTSTR pszValue);
- 添加:直接新pszName
- 删除:pszValue设为NULL
- 修改:指定已有名称变量
进程当前所在的驱动器和目录
Q:不提供完整路径名怎么查找文件
A:当前驱动器当前目录进行查找,例如CreateFile打开一个文件
系统内部跟踪记录着一个进程的当前驱动器和目录,由于以进程为单位维护,因此进程中的一个线程更改了当前驱动器或目录,进程中所有线程信息均被修改
如何获取和设置所在进程的当前驱动器和目录
DWORD GetCurrentDirectory(DWORD cchCurDir, PTSTR pszCurDir);
BOOL SetCurrentDirectory(PCTSTR pszCurDir);
如果缓冲区不够大,GetCurrentDirectory将返回保存此文件夹所需字符数(包括末尾的0字符),且不会向缓冲区复制任何内容。调用成功则会返回字符串的长度,但不包括末尾的0
#include<windows.h>
#include<tchar.h>
int _tmain(int argc, _TCHAR* argv[], TCHAR* env[]) {
DWORD size = GetCurrentDirectory(0, NULL);
PTSTR curDir = new TCHAR[size];
GetCurrentDirectory(size, curDir);
_tprintf(curDir);
return 0;
}
小结
初步入门了windows的一些核心概念如错误处理、字符串、内核对象、进程等,进程还有很多没学完,继续抽空学习