第7章 INI配置文件和注册表操作

操作系统和各种应用程序,通常需要使用某种方式来保存配置信息。.ini文件是Initialization File的缩写,即初始化文件,是Windows中配置文件所采用的存储格式,例如Windows目录中的Win.ini文件保存了桌面设置和与应用程序运行有关的信息,System.ini文件保存了与硬件配置有关的信息。INI文件是文本文件,可以使用任何文本编辑器对其进行修改,所以安全性不是很好。另外,INI文件的结构比较简单,无法保存格式复杂的数据,例如很长的二进制数据或换行的字符串等。最主要的缺点是单个INI文件的大小不能超过64KB,如果不同的应用程序都将自己的配置信息保存在Win.ini或System.ini中,那么这些文件的大小很快就会超过限制,如果不同应用程序都使用自己的INI文件,那么集中管理又会成为一个问题。

后来,操作系统改用了一种全新的方式来管理配置信息,即注册表(Registry)。在 Windows 3.x操作系统中,注册表是一个极小的文件,其文件名为Reg.dat,其中只存放了某些文件类型的应用程序关联,大部分设置被存放在Win.ini、System.ini等多个INI文件中,由于这些初始化文件不便于管理和维护,时常出现一些因INI文件遭到破坏而导致系统无法启动的问题。为了使系统运行得更为稳定、健壮,Windows NT操作系统开始广泛使用注册表,但是直到Windows 95操作系统后,注册表才真正成为Windows用户经常接触的内容,并在其后的操作系统中继续沿用至今。

注册表是Windows操作系统中的一个核心数据库,其内容存放于几个格式由系统定义的二进制文件中,NT系统的注册表通常由Windows\System32\Config目录中的多个文件构成,操作系统将这些不同的文件虚拟成整个注册表供系统自身及应用程序使用。注册表中存放有各种参数,直接控制Windows的启动、硬件驱动程序的装载以及一些Windows应用程序的运行,从而在整个系统中起核心作用,这些作用包括软、硬件的相关配置和状态信息,例如注册表中保存有应用程序和资源管理器外壳的初始条件、首选项和卸载数据等,联网计算机的整个系统的设置和各种许可,文件扩展名与应用程序的关联,硬件部件的描述、状态和属性,性能记录和其他底层的系统状态信息,以及其他数据等。具体来说,在Windows启动时,注册表会对照已有硬件配置数据,检测硬件信息;系统内核从注册表中读取信息,包括要载入驱动程序的设备信息、载入次序、内核传送回它自身的信息(例如版权号)等;同时设备驱动程序也向注册表传送数据,并从注册表接收载入和配置参数,一个好的设备驱动程序会告诉注册表它正在使用的系统资源,例如硬件中断或DMA通道等;另外,设备驱动程序还要报告所发现的配置数据;为应用程序或硬件的运行提供增加新的配置数据的服务。如果注册表遭遇破坏,轻则使Windows的启动过程出现异常,重则可能会导致整个Windows系统完全瘫痪,因此正确地认识、使用,特别是及时备份以及有问题时恢复注册表,对Windows用户来说非常重要。

实际上,Windows系统对注册表文件的保护非常严格。系统在运行时,注册表文件被操作系统以独占方式打开,其他应用程序无法使用最基本的读权限打开它们,更不用说对它们进行写操作。要对注册表文件进行操作,必须使用操作系统提供的接口,Windows为此提供了一系列的注册表操作函数,应用程序可以通过它们来完成注册表编辑器(Regedit程序)能够完成的全部功能,甚至包括远程操作注册表以及对.reg文件进行导入和导出等操作。

为了提供向下兼容性,系统在支持注册表操作的同时也支持INI文件的操作,对某些小程序来说,需要保存的配置信息并不复杂,使用INI文件可能更加简单实用,而且保存于注册表中的配置信息无法随文件复制到其他计算机中,如果某些应用程序希望在复制程序的同时复制配置信息,则可以使用INI文件。

7.1 INI配置文件

INI文件是一种文本格式的配置文件,文件中的数据组织格式如下:

;注释
[SectionName1]
KeyName1=value1
KeyName2=value2
...
;注释
[SectionName2]
KeyName1=value1
KeyName2=value2
...

INI文件中可以存放多个小节(Section),小节名称包含在一对方括号[]中,一个小节的内容从小节名称的下一行开始,直到下一个小节开始为止,一个程序可以根据需要创建多个小节,但是需要注意不同的小节不能重名。

每个小节中可以定义多个键(Key),每个键由一个“键名=键值”格式的字符串组成,每个键独自占用一行,同一个小节中不能存在同名的键,但是不同的小节中可以存在同名的键。

INI文件的注释以;开始,放在单独的一行中,注释可以放在INI文件的任何一行中。

大多数情况下应用程序是在自己的目录中创建一个独立的INI文件,而不是在系统的INI文件中添加一个小节来存放程序信息。

7.1.1 键值对的创建、更新与删除

WritePrivateProfileString函数用于在指定INI文件的指定小节中创建、更新或删除键值对,该函数还可以删除指定INI文件中的小节(包括小节名称和其下面的所有键值对),函数声明如下:

BOOL WINAPI WritePrivateProfileString(
     _In_ LPCTSTR lpAppName,         // 小节名称字符串,不区分大小写
     _In_ LPCTSTR lpKeyName,         // 键名字符串
     _In_ LPCTSTR lpString,           // 键值字符串
     _In_ LPCTSTR lpFileName);        // INI文件的名称字符串
  • lpAppName参数表示小节名称字符串,不区分大小写。如果指定的小节不存在,则函数会自动创建该小节。

  • lpKeyName参数表示键名字符串。如果指定的键名不存在,函数会自动创建该键;如果该参数设置为NULL,则函数会删除lpAppName参数指定的小节(包括小节名称和其下面的所有键值对)。

  • lpString参数表示键值字符串,如果该参数设置为NULL,则函数会删除lpKeyName参数指定的键。键名不能以;开始,但是键值可以使用;,另外键值不能定义为多行文本,即字符串中不可以包含换行符,因为在INI文件中一行表示一个键值对。

  • lpFileName参数表示INI文件的名称。如果lpFileName参数不包含INI文件的完整路径,则函数会在Windows目录中搜索该文件。如果该文件不存在,则函数会在Windows目录中自动创建该文件;如果lpFileName参数包含完整路径和文件名,并且该文件不存在,则函数会自动创建该文件,但是指定的目录必须已经存在。

当指定的INI文件、文件中的小节和小节中的键名都已经存在时,函数使用新键值替换掉原来的键值;当指定的INI文件存在而小节不存在时,函数自动创建小节并将键值对写入;当指定的INI文件不存在时,函数会自动创建INI文件。程序不必考虑INI文件是否存在、小节是否存在或键值定义是否已经存在的情况,只要调用WritePrivateProfileString函数就可以保证配置信息被正确保存。

INI文件区分Unicode和ANSI,如果文件是使用Unicode字符创建的,则函数会将Unicode字符写入文件;否则函数将写入ANSI字符。

该函数的用法有以下几种情况。

(1)在指定INI文件的指定小节中创建或更新键值对:

WritePrivateProfileString(lpAppName, lpKeyName, lpString, lpFileName);

(2)在指定INI文件的指定小节中删除键值对:

WritePrivateProfileString(lpAppName, lpKeyName, NULL, lpFileName);

(3)删除指定INI文件的指定小节(包括小节名称和所有键值对):

WritePrivateProfileString(lpAppName, NULL, NULL, lpFileName);

INI文件以文本方式保存,键值也只是一个字符串,如果需要保存一个数值类型的值,则程序可以使用wsprintf函数将数值转换成字符串后再保存。

7.1.2 获取键值

GetPrivateProfileString函数用于获取指定INI文件中指定小节的键名对应的键值字符串,还可以实现枚举指定INI文件中所有小节名称的功能,也具有枚举指定INI文件中指定小节名称中所有键名的功能,函数声明如下:

DWORD WINAPI GetPrivateProfileString(
     _In_  LPCTSTR lpAppName,            // 小节名称字符串,不区分大小写
     _In_  LPCTSTR lpKeyName,            // 键名字符串
     _In_  LPCTSTR lpDefault,            // 默认字符串,可以设置为NULL
     _Out_ LPTSTR  lpReturnedString,    // 接收获取到的字符串缓冲区的指针
     _In_  DWORD   nSize,              // lpReturnedString参数指向的缓冲区大小,以字符为单位
     _In_  LPCTSTR lpFileName);        // INI文件的名称字符串
  • lpAppName参数表示小节名称字符串,不区分大小写。如果该参数设置为NULL,则函数会将INI文件中的所有小节名称都复制到lpReturnedString参数指定的缓冲区中,每个小节名称以0结尾,最后一个小节名称的后面会额外附加一个0,即最后一个小节名称的后面以2个0字符结尾,可以通过这种方法枚举指定INI文件中的所有小节名称。

  • lpKeyName参数表示键名字符串。如果该参数设置为NULL,函数会将lpAppName参数指定的节中的所有键名都复制到lpReturnedString参数指定的缓冲区中,每个键名以0结尾,最后一个键名的后面会额外附加一个0,即最后一个键名的后面以2个0字符结尾,可以通过这种方法枚举指定节中的所有键名。

  • lpDefault参数表示默认字符串。如果INI文件中没有lpKeyName键,函数会将默认字符串复制到lpReturnedString缓冲区,如果该参数设置为NULL,则默认为空字符串。

  • lpReturnedString参数指定为接收获取到的字符串缓冲区的指针。

  • nSize参数表示lpReturnedString参数指向的缓冲区大小,以字符为单位。

  • lpFileName参数表示INI文件的名称字符串,如果该参数不包含文件的完整路径,则系统在Windows目录中搜索INI文件。

函数返回值是复制到缓冲区中的字符数,不包括终止的空字符。如果lpAppName和lpKeyName都不为NULL,并且提供的目标缓冲区太小而无法容纳请求的字符串,则该字符串将被截断并后跟一个空字符,此时返回值等于nSize−1。如果lpAppName或lpKeyName为NULL,并且提供的目标缓冲区太小而无法容纳所有字符串,则最后一个字符串将被截断,后跟两个空字符,此时返回值等于nSize−2。

INI文件以文本方式保存,键值也只是一个字符串,当时可能保存的是一个数值型字符串,这时可以调用GetPrivateProfileInt函数,该函数返回UINT类型的键值:

UINT WINAPI GetPrivateProfileInt(
     _In_ LPCTSTR lpAppName,        // 小节名称字符串,不区分大小写
     _In_ LPCTSTR lpKeyName,        // 键名字符串
     _In_ INT     nDefault,          // 默认数值,INT类型,如果在INI文件中找不到键名,则返回该默认值
     _In_ LPCTSTR lpFileName);     // INI文件的名称字符串

7.1.3 管理小节

GetPrivateProfileString函数用于在键名已知的情况下获取键值,可能有时小节中的键名以及键名个数未知,比如一个文本编辑软件需要保存近期编辑过的文件名列表,它可以创建一个小节如下:

[History]
File[0]=C:\Users\SuperWang\Desktop\FWMApp\Release\FloatingWaterMark.txt
File[1]=F:\Source\Windows\Chapter6\FWMApp\Release\FloatingWaterMark.txt
File[2]=C:\Users\SuperWang\Desktop\FWMApp\加密系统加密.txt
File[3]=C:\Users\SuperWang\Desktop\FWMDll\Release\FWMDll.txt
File[4]=F:\Source\Windows\Chapter6\FloatingWaterMark\Release\FloatingWaterMark.txt
File[5]=C:\Users\SuperWang\Desktop\FWMApp\Release\加密系统加密.txt
...

另外,在INI文件中小节的名称和个数未知的情况下,需要对小节或键名进行枚举,上一节中已经介绍了在GetPrivateProfileString函数中通过将lpAppName或lpKeyName参数设置为NULL来枚举小节名称列表或键名列表的方法。实际上,Windows中还有专门用来实现该功能的函数,这些函数可以用来枚举小节和键,还有用来一次性修改整个小节内容的函数。

GetPrivateProfileSectionNames函数用于枚举指定INI文件中的所有小节名称:

DWORD WINAPI GetPrivateProfileSectionNames(
     _Out_ LPTSTR  lpszReturnBuffer,     // 指向接收小节名称列表缓冲区的指针
     _In_  DWORD   nSize,                  // lpszReturnBuffer参数指向的缓冲区大小,以字符为单位
     _In_  LPCTSTR lpFileName);         // INI文件名称字符串
  • lpszReturnBuffer参数指定为指向接收小节名称列表缓冲区的指针,返回的每个小节名称以0结尾,最后一个小节名称的后面会额外附加一个0,即最后一个小节名称的后面以2个0字符结尾。

  • nSize参数指定lpszReturnBuffer参数指向的缓冲区大小,以字符为单位。

  • lpFileName参数指定INI文件名称字符串,不包含完整路径则在Windows目录中搜索文件,如果该参数设置为NULL,则函数枚举Win.ini文件中的所有小节名称。

返回值是复制到缓冲区中的字符数,不包括终止空字符。如果缓冲区的大小不足以容纳所有小节名称列表,则返回值等于nSize−2。

GetPrivateProfileSection函数用于获取指定INI文件的指定小节中的所有键值对(键名=键值):

DWORD WINAPI GetPrivateProfileSection(
     _In_  LPCTSTR lpAppName,            // 小节名称字符串,不区分大小写
     _Out_ LPTSTR  lpReturnedString,    // 指向接收键值对列表缓冲区的指针
     _In_  DWORD   nSize,            // lpReturnedString参数指向的缓冲区大小,以字符为单位
     _In_  LPCTSTR lpFileName);        // INI文件名称字符串
  • lpAppName参数指定小节名称字符串,不区分大小写。

  • lpReturnedString参数指定指向接收键值对列表缓冲区的指针,返回的每个键值对以0结尾,最后一个键值对的后面会额外附加一个0,即最后一个键值对的后面以2个0字符结尾。

  • nSize参数指定lpReturnedString参数指向的缓冲区大小,以字符为单位。

  • lpFileName参数指定INI文件名称字符串,不包含完整路径则在Windows目录中搜索文件。

返回值是复制到缓冲区中的字符数,不包括终止空字符。如果缓冲区的大小不足以容纳所有键值对列表,则返回值等于nSize−2。

GetPrivateProfileSection函数获取的是指定INI文件的指定小节中的所有键值对(“键名=键值”),实际使用中如果觉得处理"键名=键值"字符串来分解键名和键值比较麻烦,可以调用GetPrivateProfileString函数枚举键名并再次调用它来获取指定键的键值。

WritePrivateProfileSection函数用于向指定INI文件的指定小节中批量写入键值对:

BOOL WINAPI WritePrivateProfileSection(
     _In_ LPCTSTR lpAppName,         // 小节名称字符串,不区分大小写
     _In_ LPCTSTR lpString,             // 要写入指定小节中的键值对缓冲区,最大64KB
     _In_ LPCTSTR lpFileName);      // INI文件名称字符串
  • lpAppName参数指定小节名称字符串,不区分大小写。

  • lpString参数指定要写入指定小节中的键值对缓冲区,最大64KB,定义缓冲区时需要注意每个键值对以0结尾,最后一个键值对的后面应该再额外附加一个0,即最后一个键值对的后面以2个0字符结尾。

  • lpFileName参数指定INI文件名称字符串,如果不包含完整路径,则函数在Windows目录中搜索文件,如果该文件不存在,并且lpFileName不包含完整路径,则函数将在Windows目录中创建该文件。

函数执行后,指定小节中原来的键值对定义会被全部删除,然后写入lpString参数指定的所有新键值对。

INI文件是为了与16位应用程序兼容,微软建议新的应用程序应该将初始化信息存储在注册表中,系统将很多.ini文件例如Control.ini、System.ini和Win.ini等映射到注册表:HKEY_LOCAL_MACHINE\ SOFTWARE\Microsoft\Windows NT\Current Version\IniFileMapping,但是对于一些小的应用程序,如果需要存储一些简单的配置数据,可以使用INI文件,因为简单易用。

我们使用软件时经常需要把软件拖动到屏幕中一个合适的位置并调整程序窗口为一个合适的大小,后期再次运行程序时我们希望该程序窗口的位置和大小与之前相同。本节将实现这样一个对话框程序示例,对话框程序也可以具有最大化按钮、最小化按钮以及调整程序窗口大小的功能,这可以分别通过对话框的Maximize Box、Minimize Box和Border(设置为Resizing)属性来设置。

INIDemo.cpp源文件的内容如下:

#include <windows.h>
#include <tchar.h>
#include <strsafe.h>
#include "resource.h"

// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
    return 0;
}

INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    static TCHAR szFileName[MAX_PATH] = { 0 };           // INI文件名称
    LPCTSTR lpAppName = TEXT("INIDemoPositionSize");    // 小节名称
    LPCTSTR lpKeyNameX = TEXT("X");
    LPCTSTR lpKeyNameY = TEXT("Y");
    LPCTSTR lpKeyNameWidth = TEXT("Width");
    LPCTSTR lpKeyNameHeight = TEXT("Height");
    UINT unX = 0, unY = 0, unWidth = 0, unHeight = 0;
    RECT rect;
    TCHAR szBuf[16] = { 0 };

    switch (uMsg)
    {
    case WM_INITDIALOG:
        // 获取当前进程的可执行文件完整路径,然后拼接出INI文件完整路径
        GetModuleFileName(NULL, szFileName, _countof(szFileName));
        StringCchCopy(_tcsrchr(szFileName, TEXT('\\')) + 1, _countof(szFileName), TEXT("INIDemo.ini"));

        // 获取X、Y、Width、Height键的键值
        unX = GetPrivateProfileInt(lpAppName, lpKeyNameX, NULL, szFileName);
        unY = GetPrivateProfileInt(lpAppName, lpKeyNameY, NULL, szFileName);
        unWidth = GetPrivateProfileInt(lpAppName, lpKeyNameWidth, NULL, szFileName);
        unHeight = GetPrivateProfileInt(lpAppName, lpKeyNameHeight, NULL, szFileName);

        // 设置程序窗口位置、大小
        if (unWidth && unHeight)
            SetWindowPos(hwndDlg, HWND_TOP, unX, unY, unWidth, unHeight, SWP_ SHOWWINDOW);
        return TRUE;

    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDCANCEL:
            // 保存程序窗口位置、大小
            GetWindowRect(hwndDlg, &rect);
            wsprintf(szBuf, TEXT("%d"), rect.left);
            WritePrivateProfileString(lpAppName, lpKeyNameX, szBuf, szFileName);
            wsprintf(szBuf, TEXT("%d"), rect.top);
            WritePrivateProfileString(lpAppName, lpKeyNameY, szBuf, szFileName);
            wsprintf(szBuf, TEXT("%d"), rect.right - rect.left);
            WritePrivateProfileString(lpAppName, lpKeyNameWidth, szBuf, szFileName);
            wsprintf(szBuf, TEXT("%d"), rect.bottom - rect.top);
            WritePrivateProfileString(lpAppName, lpKeyNameHeight, szBuf, szFileName);

            EndDialog(hwndDlg, 0);
            break;
        }
        return TRUE;
    }

    return FALSE;
}

因为当前目录是可变的,所以程序中没有使用GetCurrentDirectory函数,而是通过GetModuleFileName函数获取当前进程的可执行文件完整路径,然后拼接出INI文件完整路径。GetWindowRect函数用于获取指定窗口的位置与大小,以相对于屏幕左上角的屏幕坐标表示。

7.2 注册表操作

前面已经介绍过注册表的重要性,Windows中的许多场合都需要使用注册表存储数据,因此注册表是一个巨大的数据迷宫。注册表中的数据类似于磁盘目录的多层组织形式,与文件系统中根目录、子目录和文件的层次划分类似,注册表中的数据层次分为根键、子键和键值项,其中根键相当于文件系统中的根目录,子键相当于子目录,键值项相当于文件。根键和子键是为了将不同的键值项分类组织而定义的,只有键值项中才包含真正的数据。

单击桌面左下角的开始或搜索内容,输入regedit,打开注册表编辑器,可以发现注册表中的根键有5个,其名称是Windows规定的,并且是固定不变的,它们分别是HKEY_CLASSES_ROOT、HKEY_CURRENT_USER、HKEY_LOCAL_MACHINE、HKEY_USERS和HKEY_CURRENT_CONFIG。每个根键中都有一些子键,以HKEY_LOCAL_MACHINE根键为例,下面有BCD00000000、HARDWARE、SAM、SECURITY、SOFTWARE和SYSTEM子键,HARDWARE子键下面有ACPI、DESCRIPTION、DEVICEMAP和RESOURCEMAP等子键,子键和子键的关系是相对的,例如一个目录既可以是其上层目录的子目录,又可以是其下层目录的父目录。一个子键中既可以创建多个子键,也可以同时创建多个键值项,就像一个目录中既可以创建多个子目录,同时也可以存放多个文件一样。注册表编辑器的程序界面如原书的图7.1所示。

每个键值项由键名和键值数据两部分组成(与文件名和文件中数据的关系相似),例如某台计算机中HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\BIOS下的键名BaseBoardManufacturer对应的键值是字符串"LENOVO",而键名BiosMajorRelease对应的键值是DWORD类型的0x00000001。每个子键下面通常有一个没有名称的键值项,称为默认键,默认键通常是REG_SZ或REG_EXPAND_SZ类型。

与INI文件中的键值只能定义为字符串不同,注册表键值数据类型要丰富得多,可用的键值数据类型见表7.1。

表7.1

键值数据类型含义
REG_SZ以零结尾的字符串,根据使用的是Unicode或ANSI函数,可以是Unicode或ANSI字符串
REG_DWORD一个32位数字
REG_QWORD一个64位数字
REG_BINARY任何形式的二进制数据
REG_MULTI_SZ字符串序列,格式为String1\0String2\0String3\0…LastString\0\0
REG_EXPAND_SZ以零结尾的字符串,根据使用的是Unicode或ANSI函数,可以是Unicode或ANSI字符串,其中包含对环境变量(例如"%PATH%")的未扩展引用,要扩展环境变量引用,可以使用ExpandEnvironmentStrings函数
REG_DWORD_LITTLE_ENDIAN小尾数格式的32位数字,在Intel系列处理器中等于REG_DWORD
REG_DWORD_BIG_ENDIAN大尾数格式的32位数字
REG_QWORD_LITTLE_ENDIAN小尾数格式的64位数字,在Intel系列处理器中等于REG_QWORD
REG_LINK以零结尾的Unicode字符串,其中包含符号链接的目标路径,该符号链接是通过使用REG_OPTION_CREATE_LINK调用RegCreateKeyEx函数创建的
REG_NONE没有定义的值类型

注册表编辑器中的5个根键分散存放在不同的文件中,操作系统将这些不同的文件虚拟成整个注册表供系统自身及应用程序使用。HKEY_LOCAL_MACHINE和HKEY_USERS根键是注册表中的两大根键,其他根键都是它们派生出来的,是这两大根键下面某些子键的映射,例如HKEY_CLASSES_ROOT根键是HKEY_LOCAL_MACHINE根键下SOFTWARE\Classes子键的映射。

注册表是一个巨大的数据迷宫,大部分子键和键值项的含义是未知的,下面对这5个根键进行简要介绍。

  • HKEY_LOCAL_MACHINE根键中存放的是系统和软件的设置,这些设置针对所有使用Windows系统的用户,是一个公共配置信息,与具体用户无关。

  • HKEY_USERS根键中存放的是默认用户(.DEFAULT)、当前登录用户与软件(Software)等信息。其中最重要的是.DEFAULT子键,.DEFAULT子键的配置针对未来将会被创建的新用户,新用户根据默认用户的配置信息来生成自己的配置文件(包括环境、屏幕、声音等多种信息)。

  • HKEY_CLASSES_ROOT根键中存放的是系统中所有数据文件的信息,主要记录不同文件名后缀的文件和与之关联的应用程序,当用户双击一个文件时,系统通过这些信息启动相应的应用程序。HKEY_CLASSES_ROOT根键中存放的信息与HKEY_LOCAL_MACHINE\Software\ Classes子键中存放的信息一致。

  • HKEY_CURRENT_USER根键中存放的信息是当前用户的信息。

  • HKEY_CURRENT_CONFIG根键中存放的是硬件配置文件,该根键很少使用,如果在Windows中设置了两套或两套以上的硬件配置文件,则在系统启动时会提示用户选择使用其中一套配置文件,HKEY_CURRENT_CONFIG根键中存放的正是当前配置文件的所有信息。

程序的很多信息通常保存在注册表中,包括加密程序的用户名注册码信息,在加密解密领域经常用到注册表相关操作函数。

7.2.1 子键的打开、关闭、创建和删除

与操作文件类似,要对某个子键下面的子键或键值项进行操作前,首先需要调用RegCreateKeyEx或RegOpenKeyEx函数创建或打开子键以获得一个子键句柄,然后可以通过该子键句柄在其下面创建、删除子键,创建或设置、查询、删除键值项。

RegOpenKeyEx函数用于打开子键以获取一个子键句柄:

LONG WINAPI RegOpenKeyEx(
     _In_     HKEY      hKey,           // 父键句柄
     _In_opt_ LPCTSTR lpSubKey,       // 子键名称字符串
     _In_     DWORD     ulOptions,      // 通常设置为0
     _In_     REGSAM    samDesired,     // 子键的打开方式,即访问权限
     _Out_    PHKEY     phkResult);     // 返回打开的子键句柄
  • hKey参数指定父键句柄。

  • lpSubKey参数指定子键名称字符串,子键名称不区分大小写。

与目录名的表示方法类似,一个子键的完整名称是以“根键\第1层子键\第2层子键\第n层子键”类型的字符串来表示的。既然子键的完整名称以这种方式表示,那么当打开一个子键时,下面的两种表示方法有什么不同呢?

  • 父键=HKEY_LOCAL_MACHINE,子键=Software\RegTest\MySubkey

  • 父键=HKEY_LOCAL_MACHINE\Software,子键=RegTest\MySubkey

实际上这两种表示方法是完全相同的,在使用RegOpenKeyEx函数打开子键时,既可以将hKey参数设置为HKEY_LOCAL_MACHINE根键的句柄,并将lpSubKey参数设置为“Software\ RegTest\MySubkey”字符串;也可以将hKey参数设置为“HKEY_LOCAL_MACHINE\Software”的句柄,并将lpSubKey参数设置为“Reg Test\MySubkey”字符串,得到的结果是相同的。但是,使用第一种方法时,hKey参数可以直接使用常量HKEY_LOCAL_MACHINE来表示,  5 个根键的名称分别代表其句柄,不需要打开,也不需要关闭根键句柄;而使用第二种方法时,需要先打开“HKEY_LOCAL_MACHINE\Software”子键来获取它的句柄以作为父键句柄,所以具体使用哪种方法还要根据具体情况灵活选用。

  • ulOptions参数通常设置为0。

  • samDesired参数指定子键的打开方式,即访问权限,常用的访问权限如表7.2所示。

表7.2

常量含义
KEY_QUERY_VALUE可以查询键值项数据
KEY_CREATE_SUB_KEY可以创建下一层子键
KEY_ENUMERATE_SUB_KEYS可以枚举子键
KEY_NOTIFY当子键以及下面的子键发生更改时可以接收到通知
KEY_SET_VALUE可以创建、修改和删除键值项
KEY_READ(同KEY_EXECUTE)`等于STANDARD_RIGHTS_READ
KEY_WRITE等于`STANDARD_RIGHTS_WRITE
KEY_WOW64_32KEY表示64位Windows上的应用程序应在32位注册表视图上运行,32 位Windows忽略此标志
KEY_WOW64_64KEY表示64位Windows上的应用程序应在64位注册表视图上运行,32 位Windows忽略此标志
KEY_ALL_ACCESS等于`STANDARD_RIGHTS_REQUIRED
  • phkResult参数用于返回打开的子键句柄。

如果函数执行成功,则返回值为ERROR_SUCCESS,并在phkResult参数指向的HKEY类型变量中返回子键句柄。如果注册表中不存在指定的子键,则RegOpenKeyEx函数不会创建指定的子键,函数执行失败。

当不再需要打开的子键句柄时,应该调用RegCloseKey函数关闭子键句柄:

LONG WINAPI RegCloseKey(_In_ HKEY hKey);     // 关闭RegOpenKeyEx或RegCreateKeyEx(打开或创建)的  
                                       // 子键句柄

如果函数执行成功,则返回值为ERROR_SUCCESS。

RegCreateKeyEx函数用于创建一个子键并返回子键句柄,如果指定的子键已经存在,则该函数会打开该子键并返回子键句柄:

LONG WINAPI RegCreateKeyEx(
     _In_            HKEY                      hKey,                 // 父键句柄
     _In_            LPCTSTR                   lpSubKey,             // 子键名称字符串
     _Reserved_     DWORD                    Reserved,             // 保留参数,必须为0
     _In_opt_       LPTSTR                    lpClass,              // 用户定义的子键类名,通常设置为NULL
     _In_          DWORD                   dwOptions,            // 子键的创建选项,通常设置为  
                                                           // REG_OPTION_NON_VOLATILE
     _In_           REGSAM                     samDesired,            // 子键的打开方式,即访问权限
     _In_opt_      LPSECURITY_ATTRIBUTES lpSecurityAttributes,    // 指向安全属性结构的指针,     
                                                                           // 通常设置为NULL
     _Out_          PHKEY                    phkResult,                // 返回创建或打开的子键句柄
     _Out_opt_      LPDWORD                   lpdwDisposition);       // 返回函数的处理结果,可以设置为NULL

hKey、lpSubKey、samDesired和phkResult参数的含义与RegOpenKeyEx函数相同。下面介绍其他参数。

  • dwOptions参数表示创建子键时的选项,常用的值如表7.3所示。

  • lpSecurityAttributes参数是一个指向安全属性SECURITY_ATTRIBUTES结构的指针,该参数通常可以设置为NULL,表示使用默认的安全属性,返回的句柄不可以被子进程继承。

  • lpdwDisposition参数返回函数处理的结果,返回的值如表7.4所示。

表7.3

常量含义
REG_OPTION_NON_VOLATILE默认值,子键将被保存在注册表中,并在系统重新启动时保留
REG_OPTION_VOLATILE创建易失性的子键,子键被保存在内存中,系统重新启动时子键消失
REG_OPTION_BACKUP_RESTORE如果设置了该标志,函数将忽略samDesired参数,并尝试使用备份或还原子键所需的访问权限来打开子键

表7.4

含义
REG_CREATED_NEW_KEY子键不存在并且已经被创建
REG_OPENED_EXISTING_KEY子键已经存在,打开该子键并返回子键句柄

如果lpdwDisposition参数设置为NULL,则不返回任何函数处理结果。

如果函数执行成功,则返回值为ERROR_SUCCESS。注意,程序无法在HKEY_USERS或HKEY_LOCAL_MACHINE根键下面创建子键,但是可以在这两个根键的子键下面创建子键。

假设要创建“HKEY_LOCAL_MACHINE\SOFTWARE\Key1\Key2\Key3”子键,既可以将hKey参数设置为HKEY_LOCAL_MACHINE,将lpSubKey参数设置为“SOFTWARE\Key1\Key2\Key3”字符串;也可以先打开“HKEY_LOCAL_MACHINE\SOFTWARE”子键,将hKey设置为上面打开的子键句柄,然后将lpSubKey参数设置为“Key1\Key2\Key3”字符串,这与RegOpenKeyEx函数的用法类似。在第二种用法中,打开父键时要包含KEY_CREATE_SUB_KEY权限。当被创建的子键的上层子键不存在时,函数会自动创建上层子键,例如上面的例子中,假如 Key2 子键不存在,函数会先在“HKEY_LOCAL_MACHINE\SOFTWARE\Key1”下创建 Key2 子键,然后在 Key2 子键下面继续创建Key3子键。

同样,当不再需要创建或打开的子键句柄时,应该调用RegCloseKey函数关闭子键句柄。

RegDeleteKey函数用于删除一个子键和该子键中的所有键值项:

LONG WINAPI RegDeleteKey(
     _In_ HKEY    hKey,        // 父键句柄,根键或RegCreateKeyEx、RegOpenKeyEx函数返回的子键句柄
     _In_ LPCTSTR lpSubKey); // 子键名称字符串

如果函数执行成功,则返回值为ERROR_SUCCESS。要删除的子键下面必须无子键,否则函数执行会失败。

要删除一个键及其下面的所有子键,可以枚举子键并分别删除它们。要递归删除子键,可以使用RegDeleteTree或SHDeleteKey函数,这两个函数的函数参数与RegDeleteKey完全相同。这两个函数可以删除指定子键下面的所有子键和键值项,须谨慎使用。

程序的一些信息通常可以保存在HKEY_CURRENT_USER\Software子键或HKEY_LOCAL_MACHINE\ SOFTWARE子键下面,例如INIDemo程序的配置信息可以在HKEY_CURRENT_USER\Software子键下面创建一个INIDemo子键,然后在该子键下面创建相关键值项。例如:

HKEY hKey;
LPCTSTR lpSubKey = TEXT("Software\\INIDemo");
LONG lRet;

lRet = RegCreateKeyEx(HKEY_CURRENT_USER, lpSubKey, 0, NULL, REG_OPTION_NON_VOLATILE,
     KEY_WRITE, NULL, &hKey, NULL);
if (lRet != ERROR_SUCCESS)
{
     MessageBox(hwndDlg, TEXT("创建或打开子键失败!"), TEXT("提示"), MB_OK);
     return FALSE;
}

// 键值项的操作

7.2.2 键值项的创建或设置、查询和删除

创建或打开一个子键后,即可利用该子键句柄在其中管理键值项,包括键值项的创建或设置、查询和删除。RegSetValueEx函数用于在指定的子键中创建或设置键值项:

LONG WINAPI RegSetValueEx(
     _In_                  HKEY     hKey,          // RegCreateKeyEx、RegOpenKeyEx等函数返回的子键句柄
     _In_opt_             LPCTSTR lpValueName,    // 键名字符串
     _Reserved_           DWORD    Reserved,      // 保留参数,必须为0
     _In_                 DWORD    dwType,         // lpData参数指向的数据类型,前面介绍过可用的键值数据类型
     _In_       const BYTE*     lpData,         // 要存储的键值数据
     _In_                 DWORD     cbData);       // lpData参数指向的数据的大小,以字节为单位
  • lpValueName参数指定键名字符串,如果指定的键名不存在,函数会创建该键名,如果指定的键名已经存在,则函数会更新该键名对应的键值;如果该参数设置为空字符串或NULL,则表示创建或设置子键中的默认键。

  • Reserved参数是保留参数,必须为0。

  • dwType参数指定lpData参数指向的数据类型,前面介绍过可用的键值数据类型。

  • lpData参数指定要存储的键值数据。

  • cbData参数表示lpData参数指向的数据的大小,以字节为单位。注意,如果键值数据类型为REG_SZ、REG_EXPAND_SZ或REG_MULTI_SZ,那么cbData必须包含1个或2个终止空字符的大小;如果键值数据类型为REG_DWORD,那么该参数可以设置为sizeof(DWORD);如果键值数据类型为REG_QDWORD,那么该参数可以设置为sizeof(QDWORD)。

如果函数执行成功,则返回值为ERROR_SUCCESS。要存储的键值数据的大小受可用内存的限制,但是在注册表中存储较大的数据可能会影响性能,因此数据大小大于2KB的键值数据应作为文件来存储,然后把文件的完整路径存储在注册表中。

RegQueryValueEx函数用于获取指定子键中指定键名的键值数据或键值数据类型:

LONG WINAPI RegQueryValueEx(
     _In_         HKEY    hKey,            // RegCreateKeyEx、RegOpenKeyEx等函数返回的子键句柄
     _In_opt_    LPCTSTR lpValueName,    // 键名字符串
     _Reserved_  LPDWORD lpReserved,     // 保留参数,必须为NULL
     _Out_opt_   LPDWORD lpType,             // 返回键值数据的数据类型,可为NULL
     _Out_opt_   LPBYTE  lpData,           // 返回键值数据的缓冲区指针,可为NULL
     _Inout_opt_ LPDWORD lpcbData);      // 指定缓冲区大小,以字节为单位,函数返回时是复制到lpData的数据大小
  • lpValueName参数指定键名字符串。如果该参数设置为空字符串""或NULL,则表示获取子键中的默认键的键值数据或键值数据类型。

  • lpReserved参数是保留参数,必须为NULL。

  • lpType参数指向的DWORD类型变量用于返回键值数据的数据类型,如果不需要获取键值数据类型,则该参数可以设置为NULL。

  • lpData参数指向的缓冲区用于返回键值数据,如果不需要获取键值数据,则该参数可以设置为NULL。

  • lpcbData参数指定缓冲区大小,以字节为单位,函数返回以后该参数是复制到lpData缓冲区中的数据大小。只有lpData参数设置为NULL时,lpcbData参数才能设置为NULL。该参数是一个输入输出参数,因此,如果是在一个循环中调用RegQueryValueEx函数,每一次循环都应该重新初始化该参数。如果仅需要查询键值数据类型,lpData和lpcbData参数都可以设置为NULL。

如果函数执行成功,则返回值为ERROR_SUCCESS。如果lpData参数指定的缓冲区不足以容纳数据,则函数返回ERROR_MORE_DATA并将所需的缓冲区大小存储在lpcbData指向的变量中。为了分配大小合适的缓冲区,可以把lpData参数设置为NULL,而lpcbData参数设置为一个指向DWORD类型变量的指针,函数会返回ERROR_SUCCESS,并将所需缓冲区的大小(以字节为单位)存储在lpcbData指向的变量中,然后分配大小合适的缓冲区并再次调用RegQueryValueEx函数以获取键值数据。

如果键值数据类型为REG_SZ、REG_EXPAND_SZ或REG_MULTI_SZ,则当初用户保存到注册表中时,可能没有正确设置1个或2个终止空字符,这种情况下操作lpData参数返回的键值数据可能会导致越界操作,RegGetValue函数的功能与RegQueryValueEx类似,但是RegGetValue函数会检查终止的空字符,如果用户当初没有正确设置终止空字符并且缓冲区大小可以容纳额外的空字符,则函数会自动添加;否则,函数执行失败并返回ERROR_MORE_DATA。

RegDeleteValue函数用于删除指定子键中的指定键值项:

LONG WINAPI RegDeleteValue(
     _In_      HKEY     hKey,          // RegCreateKeyEx、RegOpenKeyEx等函数返回的子键句柄
     _In_opt_ LPCTSTR lpValueName);    // 键名字符串

如果函数执行成功,则返回值为ERROR_SUCCESS。

例如INIDemo程序的配置信息使用注册表来存取,可以按如下方式使用:

#include <windows.h>
#include <tchar.h>
#include "resource.h"

// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
     DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
     return 0;
}

INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
     HKEY hKey;
     LPCTSTR lpSubKey = TEXT("Software\\INIDemo");
     LONG lRet;
     LPCTSTR lpValueNameX = TEXT("X");
     LPCTSTR lpValueNameY = TEXT("Y");
     LPCTSTR lpValueNameWidth = TEXT("Width");
     LPCTSTR lpValueNameHeight = TEXT("Height");
     DWORD dwcbData;
     DWORD dwX = 0, dwY = 0, dwWidth = 0, dwHeight = 0;
     RECT rect;

     switch (uMsg)
     {
     case WM_INITDIALOG:
          // 打开HKEY_CURRENT_USER\Software\INIDemo子键
          lRet = RegOpenKeyEx(HKEY_CURRENT_USER, lpSubKey, 0, KEY_READ, &hKey);
          if (lRet != ERROR_SUCCESS)
               return TRUE;

          // 获取键值数据
          dwcbData = sizeof(DWORD);
          RegQueryValueEx(hKey, lpValueNameX, NULL, NULL, (LPBYTE)&dwX, &dwcbData);
          dwcbData = sizeof(DWORD);
          RegQueryValueEx(hKey, lpValueNameY, NULL, NULL, (LPBYTE)&dwY, &dwcbData);
          dwcbData = sizeof(DWORD);
          RegQueryValueEx(hKey, lpValueNameWidth, NULL, NULL, (LPBYTE)&dwWidth, &dwcbData);
          dwcbData = sizeof(DWORD);
          RegQueryValueEx(hKey, lpValueNameHeight, NULL, NULL, (LPBYTE)&dwHeight, &dwcbData);
          RegCloseKey(hKey);

          // 设置程序窗口位置、大小
          if (dwWidth && dwHeight)
               SetWindowPos(hwndDlg, HWND_TOP, dwX, dwY, dwWidth, dwHeight, SWP_SHOWWINDOW);
          return TRUE;

     case WM_COMMAND:
          switch (LOWORD(wParam))
          {
          case IDCANCEL:
               // 保存程序窗口位置、大小
               GetWindowRect(hwndDlg, &rect);
               lRet = RegCreateKeyEx(HKEY_CURRENT_USER, lpSubKey, 0, NULL,   
                    REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL);
               if (lRet == ERROR_SUCCESS)
               {
                    dwX = rect.left;
                    dwY = rect.top;
                    dwWidth = rect.right - rect.left;
                    dwHeight = rect.bottom - rect.top;

                    RegSetValueEx(hKey, lpValueNameX, 0, REG_DWORD, (LPBYTE)&dwX, sizeof(DWORD));
                    RegSetValueEx(hKey, lpValueNameY, 0, REG_DWORD, (LPBYTE)&dwY, sizeof(DWORD));
                    RegSetValueEx(hKey, lpValueNameWidth, 0, REG_DWORD, (LPBYTE) &dwWidth, sizeof(DWORD));
                    RegSetValueEx(hKey, lpValueNameHeight, 0, REG_DWORD, (LPBYTE) &dwHeight, sizeof(DWORD));
                    RegCloseKey(hKey);
               }

               EndDialog(hwndDlg, 0);
               break;
          }
          return TRUE;
     }

     return FALSE;
}

也可以使用RegGetValue函数获取指定子键中指定键名的键值数据或键值数据类型,该函数不需要子键句柄:

LONG WINAPI RegGetValue(
     _In_            HKEY     hkey,           // 父键句柄
     _In_opt_        LPCTSTR lpSubKey,       // 子键名称字符串
     _In_opt_        LPCTSTR lpValueName,    // 键名字符串
     _In_opt_        DWORD    dwFlags,      // 限制要查询的键值数据类型标志,通常设置为RRF_RT_ANY
     _Out_opt_       LPDWORD lpType,         // 返回键值数据的数据类型,可为NULL
     _Out_opt_       PVOID    pvData,         // 返回键值数据的缓冲区指针,可为NULL
     _Inout_opt_    LPDWORD lpcbData);      // 指定缓冲区大小,以字节为单位,函数返回时是复制到pvData  
                                      // 的数据大小
  • lpValueName参数指定键名字符串。如果该参数设置为空字符串或NULL,则表示获取子键中的默认键的键值数据或键值数据类型。

  • dwFlags参数指定限制要查询的键值数据类型标志,可取的值如表7.5所示。

表7.5

常量含义
RRF_RT_ANY无键值数据类型限制
RRF_RT_REG_SZ将类型限制为REG_SZ
RRF_RT_REG_DWORD将类型限制为REG_DWORD
RRF_RT_REG_QWORD将类型限制为REG_QWORD
RRF_RT_REG_BINARY将类型限制为REG_BINARY
RRF_RT_DWORD将类型限制为32位`RRF_RT_REG_BINARY
RRF_RT_QWORD将类型限制为64位`RRF_RT_REG_BINARY
RRF_RT_REG_EXPAND_SZ将类型限制为REG_EXPAND_SZ
RRF_RT_REG_MULTI_SZ将类型限制为REG_MULTI_SZ
RRF_RT_REG_NONE将类型限制为REG_NONE

还可以同时指定以下一个或多个值,如表7.6所示。

表7.6

常量含义
RRF_NOEXPAND如果键值的数据类型为REG_EXPAND_SZ,则不要自动扩展环境变量字符串
RRF_ZEROONFAILURE如果pvData参数不为NULL,则在函数执行失败时将缓冲区中的数据清零
RRF_SUBKEY_WOW6464KEY如果lpSubKey参数不为NULL,则打开lpSubKey参数指定的具有KEY_WOW64_64KEY访问权限的子键
RRF_SUBKEY_WOW6432KEY如果lpSubKey参数不为NULL,则打开lpSubKey参数指定的具有KEY_WOW64_32KEY访问权限的子键
  • lpType参数指向的DWORD类型变量用于返回键值数据的数据类型,如果不需要获取键值数据类型,则该参数可以设置为NULL。

  • pvData参数指向的缓冲区用于返回键值数据,如果不需要获取键值数据,则该参数可以设置为NULL。

  • lpcbData参数指定缓冲区大小,以字节为单位,函数返回时该参数是复制到pvData缓冲区中的数据大小。只有pvData参数设置为NULL时,lpcbData参数才能设置为NULL。如果键值数据类型是REG_SZ、REG_EXPAND_SZ或REG_MULTI_SZ,则lpcbData参数返回的缓冲区大小包含1个或2个终止空字符。

如果函数执行成功,则返回值为ERROR_SUCCESS。如果pvData参数指定的缓冲区不足以容纳数据,则函数返回ERROR_MORE_DATA并将所需的缓冲区大小存储在lpcbData指向的变量中。为了分配大小合适的缓冲区,可以把pvData参数设置为NULL,而lpcbData参数设置为一个指向DWORD类型变量的指针,函数会返回ERROR_SUCCESS,并将所需缓冲区的大小(以字节为单位)存储在lpcbData指向的变量中,然后分配大小合适的缓冲区并再次调用RegGetValue函数以获取键值数据。例如:

RegGetValue(HKEY_CURRENT_USER, lpSubKey, lpValueNameX, RRF_ RT_ANY, NULL, &dwX, &dwcbData)

7.2.3 子键、键值项的枚举

有时候可能需要枚举指定子键下的所有子键或键值项,例如上一章的DIPSHookDll项目用到了RegEnumValue函数枚举指定子键下的所有键值项,枚举指定子键下的所有子键使用的是RegEnumKeyEx函数,这与FindFirstFile等函数可以一起遍历一个目录中的子目录和文件不同。

RegEnumKeyEx函数用于枚举指定子键下的所有子键的名称、类类型和最后写入时间:

LONG RegEnumKeyEx(
     _In_            HKEY         hKey,            // 子键句柄,枚举该子键下面的所有子键
     _In_            DWORD        dwIndex,        // hKey下子键的索引,初始设置为0,后续每次调用加1
     _Out_opt_       LPTSTR       lpName,         // 返回子键名称,包括终止的空字符
     _Inout_         LPDWORD      lpcchName,    // 指定lpName缓冲区的大小,以字符为单位,返回时是实际大小
     _Reserved_      LPDWORD      lpReserved,     // 保留参数,必须为NULL
     _Out_opt_       LPTSTR       lpClass,        // 返回子键类名,通常设置为NULL
     _Inout_opt_    LPDWORD      lpcchClass,    // 指定lpClass缓冲区的大小,以字符为单位,返回时是实际大小
     _Out_opt_       PFILETIME lpftLastWriteTime);// 子键的最后写入时间,不需要可以设置为NULL
  • dwIndex参数指定 hKey 下子键的索引,要枚举所有子键应该循环调用该函数,第 1 次调用RegEnumKeyEx函数时该参数设置为0,后续每次调用增加1,直到该函数返回ERROR_NO_MORE_ITEMS。

  • lpName参数返回子键名称,包括终止的空字符。

  • lpcchName参数指定lpName缓冲区的大小,以字符为单位,指定的缓冲区大小应该包括终止的空字符,函数返回时该参数是复制到lpName缓冲区中的字符数,但返回的字符个数不包括终止空字符。

  • lpReserved参数是保留参数,必须为NULL。

  • lpClass参数返回子键类名,不需要可以设置为NULL。

  • lpcchClass参数指定lpClass缓冲区的大小,以字符为单位,指定的缓冲区大小应该包括终止的空字符,函数返回时该参数是复制到lpClass缓冲区中的字符数,但返回的字符个数不包括终止空字符。只有lpClass参数设置为NULL时,该参数才可以设置为NULL。

  • lpftLastWriteTime参数返回子键的最后写入时间,不需要可以设置为NULL。

如果函数执行成功,则返回值为ERROR_SUCCESS。如果提供的缓冲区太小无法容纳返回的子键名称或子键类名,则返回值为ERROR_MORE_DATA,要解决这个问题,可以先调用RegQueryInfoKey函数获取hKey子键的相关信息。

通常可以按如下方式枚举指定子键下的所有子键:

DWORD dwIndex;
TCHAR szName[MAX_PATH] = { 0 };
DWORD dwchName;

dwIndex = 0;
while (TRUE)
{
     dwchName = _countof(szName);
     lRet = RegEnumKeyEx(hKey, dwIndex, szName, &dwchName, NULL, NULL, NULL, NULL);
     if (lRet == ERROR_NO_MORE_ITEMS)
          break;

     // 处理枚举到的子键

     dwIndex++;
}

RegEnumValue函数用于枚举指定子键下的所有键值项:

LONG WINAPI RegEnumValue(
     _In_            HKEY        hKey,             // 子键句柄,枚举该子键下面的所有键值项
     _In_            DWORD       dwIndex,         // hKey下键值项的索引,初始设置为0,后续每次调用加1
     _Out_           LPTSTR      lpValueName,        // 返回键名,包括终止的空字符
     _Inout_        LPDWORD     lpcchValueName,    // 指定lpValueName缓冲区的大小,以字符为单位,返回  
                                        // 时是实际大小
     _Reserved_      LPDWORD     lpReserved,         // 保留参数,必须为NULL
     _Out_opt_       LPDWORD     lpType,             // 返回键值数据类型,不需要可以设置为NULL
     _Out_opt_       LPBYTE      lpData,             // 返回键值数据,不需要可以设置为NULL
     _Inout_opt_    LPDWORD     lpcbData);          // 指定lpData缓冲区的大小,以字节为单位,返回时是实际大小
  • dwIndex参数指定hKey下键值项的索引,要枚举所有键值项应该循环调用该函数,第1次调用RegEnumValue函数时该参数设置为0,后续每次调用增加1,直到该函数返回ERROR_NO_MORE_ITEMS。

  • lpValueName参数返回键名,包括终止的空字符。

  • lpcchValueName参数指定lpValueName缓冲区的大小,字符单位,指定的缓冲区大小应该包括终止的空字符,函数返回时该参数是复制到lpValueName缓冲区中的字符数,但是返回的字符个数不包括终止空字符。

  • lpReserved参数是保留参数,必须为NULL。

  • lpType参数指向的DWORD类型变量用于返回键值数据类型,不需要可以设置为NULL。

  • lpData参数指向的缓冲区用于返回键值数据,不需要可以设置为NULL。

  • lpcbData参数指定lpData缓冲区的大小,以字节为单位,函数返回时该参数是复制到lpData缓冲区中的字节数。只有lpData参数设置为NULL时,该参数才可以设置为NULL。如果键值数据类型为REG_SZ、REG_EXPAND_SZ或REG_MULTI_SZ,则lpcbData参数返回的大小包括1个或2个终止空字符的大小,但是当初用户保存到注册表中时,可能没有正确设置1个或2个终止空字符,这种情况下操作lpData参数返回的键值数据可能会导致越界操作,因此应该判断并处理返回的键值数据以确保字符串具有正确的终止空字符。

如果函数执行成功,则返回值为ERROR_SUCCESS。如果lpData参数指定的缓冲区不足以容纳数据,则函数返回ERROR_MORE_DATA并将所需的缓冲区大小存储在lpcbData指向的变量中。为了分配大小合适的缓冲区,可以把lpData参数设置为NULL,而lpcbData参数设置为一个指向DWORD类型变量的指针,函数会返回ERROR_SUCCESS,并将所需缓冲区的大小(以字节为单位)存储在lpcbData指向的变量中,然后分配合适大小的缓冲区并再次调用RegEnumValue函数以获取键值数据。

通常可以按如下方式枚举指定子键下的所有键值项:

DWORD dwIndex;
TCHAR szValueName[MAX_PATH] = { 0 };
BYTE bData[512] = { 0 };
DWORD dwchValueName, dwcbData;

dwIndex = 0;
while (TRUE)
{
     dwchValueName = _countof(szValueName);
     dwcbData = sizeof(bData);
     lRet = RegEnumValue(hKey, dwIndex, szValueName, &dwchValueName, NULL, NULL, 
          bData, &dwcbData);
     if (lRet == ERROR_NO_MORE_ITEMS)
          break;

     // 处理枚举到的键值项

     dwIndex++;
}

在实际编程过程中,应该合理设计缓冲区大小,并合理处理返回的键值数据。现在读者可以重新查看DIPSHookDll项目。

在枚举子键和键值项时往往会遇到这样一个问题:注册表函数对键值数据的长度没有限制,在分配缓冲区时如果申请太大的内存比较浪费,申请太小的内存则无法枚举成功,对返回的子键名称和键名也是如此。那么究竟应该分配多大的缓冲区呢?在枚举前可以先调用RegQueryInfoKey函数查询指定子键的相关信息。RegQueryInfoKey函数返回的信息有:一个子键下面的子键的数量、键值项的数量、子键名称和键名字符串的最大长度以及键值数据的最大长度等,根据这些信息能够方便地申请合适的缓冲区来保证枚举成功。RegQueryInfoKey函数声明如下:

LONG WINAPI RegQueryInfoKey(
     _In_            HKEY         hKey,                // 子键句柄
     _Out_opt_       LPTSTR       lpClass,           // 返回子键类名
     _Inout_opt_    LPDWORD      lpcClass,          // lpClass参数指向的缓冲区的大小,以字符为单位
     _Reserved_      LPDWORD      lpReserved,          // 保留参数,必须为NULL
     _Out_opt_       LPDWORD      lpcSubKeys,          // 返回hKey下面所有子键的数量
     _Out_opt_      LPDWORD     lpcMaxSubKeyLen,    // 返回子键名称的最大长度(按Unicode),不包含终止字符
     _Out_opt_      LPDWORD     lpcMaxClassLen,      // 返回子键类名的最大长度(按Unicode),不包含终止字符
     _Out_opt_      LPDWORD     lpcValues,            // 返回hKey下面所有键值项的数量
     _Out_opt_      LPDWORD     lpcMaxValueNameLen,    // 返回键名的最大长度(按Unicode),不包含终止字符
     _Out_opt_      LPDWORD     lpcMaxValueLen,     // 返回键值数据的最大长度,以字节为单位
     _Out_opt_      LPDWORD     lpcbSecurityDescriptor,    // 返回hKey的安全描述符的大小,以字节为单位
     _Out_opt_      PFILETIME    lpftLastWriteTime);     // 返回hKey的最后写入时间

如果函数执行成功,则返回值为ERROR_SUCCESS。

还有一些不常用的注册表函数,例如RegSaveKey函数用来将子键信息保存到指定的文件中,RegLoadKey和RegReplaceKey函数用来从指定的文件中恢复注册表的子键信息等。

7.2.4 注册表应用:程序开机自动运行设置文件关联

在以下子键中创建一个键值项,键值数据设置为一个程序的完整路径,该程序可以在开机后自动运行,键值数据类型可以是REG_SZ或REG_EXPAND_SZ,需要注意的是键名不能与已存在的键名冲突:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce (仅运行一次)
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce  (仅运行一次)

示例代码如下:

HKEY hKey;
LPCTSTR lpSubKey = TEXT("Software\\Microsoft\\Windows\\CurrentVersion\\Run");
LPCTSTR lpValueName = TEXT("INIDemo");                                                    // 键名
LPTSTR lpData = TEXT("F:\\Source\\Windows\\Chapter7\\INIDemo\\Debug\\INIDemo. exe"); // 键值
DWORD dwcbData = (_tcslen(lpData) + 1) * sizeof(TCHAR);

RegOpenKeyEx(HKEY_LOCAL_MACHINE, lpSubKey, 0, KEY_WRITE, &hKey);
RegSetValueEx(hKey, lpValueName, NULL, REG_SZ, (LPBYTE)lpData, dwcbData);
RegCloseKey(hKey);

注意,如果程序编译为32位,即32位程序运行在WOW64时,实际上是在以下位置创建键值项:

HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run

如果程序编译为64位,则是在我们指定的子键中创建键值项:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run

出现这个问题的原因是 WOW64 对注册表做了重定向。如果我们使用 32 位的注册表编辑(C:\ Windows\SysWOW64\Regedit.exe)在HKEY_LOCAL_MACHINE/Software子键下面新建一个键值项,然后使用 64 位的注册表编辑器(C:\Windows\Regedit.exe)查看,会发现这个键值项只会出现在HKEY_LOCAL_MACHINE/Software/WOW6432Node子键下面,不会出现在HKEY_LOCAL_MACHINE/Software子键下面,因为HKEY_LOCAL_MACHINE/Software子键是专门用于存放64位程序所使用的注册表数据的,而HKEY_LOCAL_MACHINE/Software/WOW6432Node子键是专门用于存放32位程序所使用的注册表数据的。实际上,编写出上述代码通常是没有问题的,操作系统内部怎么去重定向、映射我们的键值项可以不予理会,如果一定要在指定的位置操作键值项,可以在创建或打开子键的时候指定KEY_WOW64_64KEY或KEY_WOW64_32KEY访问权限。

如果将一个类型的数据文件与一个可执行文件相关联,可以通过双击该类型的数据文件来运行可执行文件并打开数据文件,例如双击以.txt为扩展名的文本文件,就会自动运行Notepad.exe并打开.txt文本文件。文件关联可以通过在注册表的HKEY_CLASSES_ROOT根键中设置,要为某种扩展名的数据文件设置关联程序,需要在HKEY_CLASSES_ROOT根键下设置2个子键,第1个子键的名称是“.扩展名”,在“.扩展名”子键下设置一个默认键,默认键的键值数据类型是REG_SZ,键值数据是HKEY_CLASSES_ROOT根键下另一个子键的名称,在第2个子键下设置与“.扩展名”类型数据文件相关联的可执行文件名。

如果关联的操作方式是“打开”,则可以在第 2 个子键中继续创建“shell\open\command”子键,然后为该子键设置默认键,默认键的键值数据类型可以是REG_SZ或REG_EXPAND_SZ,键值数据设置为可执行文件的完整路径,双击数据文件即可自动运行这个可执行文件。如果关联的操作方式是“打印”,则可以在第2个子键中继续创建“shell\print\command”子键,同样将command子键的默认键设置为执行打印操作的可执行文件名。

HKEY_CLASSES_ROOT.txt子键的默认键的键值为txtfile,HKEY_CLASSES_ROOT\txtfile\shell\ open\command子键的默认键的键值为%System Root%\system32\NOTEPAD.EXE %1,%1表示该程序执行时的第1个参数。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐