Windows编程笔记

前言

  记录下自己学习Windows编程的知识点。做个笔记,方便自己查阅。

进程

什么是进程

  进程是程序的实体,是资源分配的最小单位。进程是一种空间上的概念,它的责任就是提供资源。每一个进程都有一个4GB大小的虚拟空间,范围从0x0-0xFFFFFFFF。其中内核占高2GB,低2GB给程序的堆栈使用

  在Windows中,系统通过句柄管理进程中的资源,句柄存储在内核空间中的一个全局句柄表中,而每个进程也都有一个句柄表,这个句柄表是私有的。

  PID 是指的是全局句柄表的值。

进程执行的加载过程

  1.映射EXE文件(低2G)
  2.创建内核对象EPROCESS(高2G)
  3.映射系统DLL(ntdll.dll)
  4.创建线程内核对象ETHREAD
  5.系统启动线程
    a.映射DLL(ntdll.LdrInitalizeThunk)
    b.线程开始执行

创建进程

  任何进程都是别的进程创建的,当用户双击exe时,实际上是Explorer.exe这个进程所创建的,使用了CreateProcess函数

BOOL CreateProcess(
    LPCWSTR lpApplicationName, 					//文件名 完整文件路径
    LPWSTR lpCommandLine, 						//命令行参数
    LPSECURITY_ATTRIBUTES lpProcessAttributes, 	//SD 进程句柄
    LPSECURITY_ATTRIBUTES lpThreadAttributes, 	//SD 线程句柄
    BOOL bInheritHandles, 						//句柄是否可继承
    DWORD dwCreationFlags, 						//创建进程方式
    LPVOID lpEnvironment, 						//父进程环境变量
    LPCWSTR lpCurrentDirectory, 				//子进程工作路径
    LPSTARTUPINFOW lpStartupInfo, 				//启动进程相关信息
    LPPROCESS_INFORMATION lpProcessInformation	//子进程的相关信息 (进程ID,线程ID,进程句柄,线程句柄)
);												//返回值BOOL

  这里讲解CreateProcess()的前两个参数和后两个参数。第一个lpApplicationName启动进程的绝对路径,比如记事本的路径为:C:\Windows\System32\notepad.exe。第二个lpCommandLine对命令行进行传参。

  例如下面的例子生成test.exe

#include <stdio.h>

int main(int argc,char *argv[])
{
    printf("%s - %s",argv[0],argv[1]);
    return 0;
}

  传入hello得到

D:\>test.exe hello
test.exe - hello

  第9个lpStartupInfo,函数原型

typedef struct _STARTUPINFOW {
    DWORD   cb;
    LPWSTR  lpReserved;
    LPWSTR  lpDesktop;
    LPWSTR  lpTitle;
    DWORD   dwX;
    DWORD   dwY;
    DWORD   dwXSize;
    DWORD   dwYSize;
    DWORD   dwXCountChars;
    DWORD   dwYCountChars;
    DWORD   dwFillAttribute;
    DWORD   dwFlags;
    WORD    wShowWindow;
    WORD    cbReserved2;
    LPBYTE  lpReserved2;
    HANDLE  hStdInput;
    HANDLE  hStdOutput;
    HANDLE  hStdError;
} STARTUPINFOW, *LPSTARTUPINFOW;

   有非常多的成员,我们只需要关注第一个成员cb,意思是包含STARTUPINFO结构体的大小(需要的原因,方便windows未来更新换代)。cb必须初始化sizeof(STARTUPINFO)。其他属性都置0即可。

   第10个lpProcessInformation,函数原型

typedef struct _PROCESS_INFORMATION {
    HANDLE hProcess;	// 进程句柄
    HANDLE hThread;		// 线程句柄
    DWORD dwProcessId;	// 进程ID
    DWORD dwThreadId;	// 线程ID
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;

   创建进程例子

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

int main()
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
	
    // 填充置0
    ZeroMemory(&pi, sizeof(pi));
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
	
    // 打开cmd
    char processName[] = "C:\\Windows\\System32\\cmd.exe";
    // 打开cmd后 ping百度
    char cmdLine[] = "/c ping www.baidu.com -n 1";

    if (CreateProcess(processName, cmdLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
    {
        printf("CreateProcess Success\n");
    }
    else
    {
        printf("CreateProcess Error:%d\n", GetLastError());
    }
}

  运行结果

D:\>test.exe
CreateProcess Success

D:\>
正在 Ping www.a.shifen.com [14.215.177.39] 具有 32 字节的数据:
来自 14.215.177.39 的回复: 字节=32 时间=34ms TTL=54

14.215.177.39 的 Ping 统计信息:
    数据包: 已发送 = 1,已接收 = 1,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 34ms,最长 = 34ms,平均 = 34ms

  上面的GetLastError函数来获取问题编号,具体编号对应内容可以参考百度百科

结束进程

  使用TerminateProcess函数,传入一个句柄和退出状态码

BOOL WINAPI TerminateProcess(HANDLE hProcess,UINT uExitCode);

  使用TerminateProcess结束打开的记事本

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

int main()
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&pi, sizeof(pi));
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
	
    // 打开记事本
    char processName[] = "C:\\Windows\\System32\\notepad.exe";
    if (CreateProcess(processName, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
    {
    	// 等待按键
        system("pause");
        // 结束子进程
        TerminateProcess(pi.hProcess,0);
        printf("success exit childProcess\n");
    }
    else
    {
        printf("CreateProcess Error:%d\n", GetLastError());
    }
}

  使用OpenProcess函数,通过pid去操作进程对象

HANDLE WINAPI OpenProcess(
    DWORD dwDesiredAccess,	// 此进程访问权限
    BOOL bInheritHandle,	// 进程句柄是否可以被继承
    DWORD dwProcessId		// 进程ID
);							// 返回句柄对象

  dwDesiredAccess 有以下几种

PROCESS_ALL_ACCESS							获取所有权限
PROCESS_CREATE_PROCESS						创建进程
PROCESS_CREATE_THREAD						创建线程
PROCESS_DUP_HANDLE							使用DuplicateHandle()函数复制一个新句柄
PROCESS_QUERY_INFORMATION					获取进程的令牌、退出码和优先级等信息
PROCESS_QUERY_LIMITED_INFORMATION			获取进程特定的某个信息
PROCESS_SET_INFORMATION						设置进程的某种信息
PROCESS_SET_QUOTA							使用SetProcessWorkingSetSize函数设置内存限制
PROCESS_SUSPEND_RESUME						暂停或者恢复一个进程
PROCESS_TERMINATE							使用Terminate函数终止进程
PROCESS_VM_OPERATION						在进程的地址空间执行操作
PROCESS_VM_READ								使用ReadProcessMemory函数在进程中读取内存
PROCESS_VM_WRITE							使用WriteProcessMemory函数在进程中写入内存
SYNCHRONIZE									使用wait函数等待进程终止

   使用以下代码查看记事本的pid,也可以使用任务管理器或者是tasklist等等工具

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

int main()
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&pi, sizeof(pi));
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);

    char processName[] = "C:\\Windows\\System32\\notepad.exe";

    if (CreateProcess(processName, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
    {
        printf("PID:%d",pi.dwProcessId);
    }
    else
    {
        printf("CreateProcess Error:%d\n", GetLastError());
    }
}

  运行上面代码,得到记事本PID:32,之后使用OpenProcess来操作进程

#include <stdio.h>
#include <Windows.h>

int main()
{
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 32);	// 32就是记事本的PID
    if (!TerminateProcess(hProcess, 0))
    {
        printf("error %d",GetLastError());
    }
    return 0;
}

挂起方式创建进程

  对应第六个参数dwCreationFlags,进程的创建方式,如果想让新的进程使用一个新的控制台,而不是继承父进程的控制台。使用CREATE_NEW_CONSOLE即可

  运行后子进程以新窗口的方式打开

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

int main()
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&pi, sizeof(pi));
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    
    char processName[] = "C:\\Windows\\System32\\cmd.exe";
    char cmdLine[] = " /c net user && pause";
    
    if (CreateProcess(processName, cmdLine, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi))
    {
        printf("PID:%d",pi.dwProcessId);
    }
    else
    {
        printf("CreateProcess Error:%d\n", GetLastError());
    }
}

  对我们最有用的是以挂起方式创建进程,值为CREATE_SUSPENDED。本质上还是挂起的主线程。下面代码按任意键后,弹出记事本

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>

int main()
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&pi, sizeof(pi));
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);

    char processName[] = "C:\\Windows\\System32\\notepad.exe";

    if (CreateProcess(processName, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
    {
        system("pause");
        ResumeThread(pi.hThread);	// 继续运行
    }
    else
    {
        printf("CreateProcess Error:%d\n", GetLastError());
    }
}

遍历进程

  以快照的方式遍历进程CreateToolhelp32Snapshot,需包含头文件TlHelp32.h。快照的意思是先给当前的进程截个图,之后再告诉图里的内容。是非实时的

// 为特定的进程拍系统快照,也可以为进程使用的堆、模块或线程拍快照。
HANDLE WINAPI CreateToolhelp32Snapshot(
 	DWORD dwFlags,			// 设置快照对象 值为:TH32CS_SNAPPROCESS 表示获取进程
    DWORD th32ProcessID		// 快照进程号 要获取当前进程快照时设为0
);							// 成功 返回句柄  失败 返回INVALID_HANDLE_VALUE

  PROCESSENTRY32用来存放快照进程信息的一个结构体

typedef struct tagPROCESSENTRY32
{
    DWORD   dwSize;					// 结构体的大小 必须初始化为sizeof(PROCESSENTRY32)
    DWORD   cntUsage;				// 已废弃
    DWORD   th32ProcessID;          // 进程ID
    ULONG_PTR th32DefaultHeapID;	// 已废弃
    DWORD   th32ModuleID;           // 已废弃
    DWORD   cntThreads;				// 此进程开启的线程总数
    DWORD   th32ParentProcessID;    // 父进程ID
    LONG    pcPriClassBase;         // 线程优先权
    DWORD   dwFlags;				// 已废弃
    CHAR    szExeFile[MAX_PATH];    // 进程名
} PROCESSENTRY32;

  首次遍历进程

BOOL WINAPI Process32First(
    HANDLE hSnapshot,			// 进程快照句柄
    LPPROCESSENTRY32 lppe		// 进程信息结构体
);

  遍历下一个进程

BOOL WINAPI Process32NextW(
    HANDLE hSnapshot,			// 进程快照句柄
    LPPROCESSENTRY32W lppe		// 进程信息结构体
);

  示例代码

#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>

int main()
{
    // 创建进程快照
    HANDLE hProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    // 句柄无效
    if (hProcess == INVALID_HANDLE_VALUE)
    {
        return 0;
    }

    // 快照信息结构体
    PROCESSENTRY32 pe;
    ZeroMemory(&pe, sizeof(pe));
    pe.dwSize = sizeof(pe);

    // 首次遍历进程
    BOOL flag = Process32First(hProcess, &pe);
    // 循环遍历进程
    while (flag)
    {
        printf("%s %d\n",pe.szExeFile,pe.th32ProcessID);
        flag = Process32Next(hProcess, &pe);
    }

}

其他API

获取当前进程ID(PID):GetCurrentProcessId()
获取当前进程句柄:GetCurrentProcess()
获取命令行:GetCommandLine()
获取启动信息:GetStartupInfo()

线程

什么是线程

  线程是程序执行时的最小单位。是附属在进程上的执行实体,是代码的执行流程。一个进程可以有多个线程,但至少都有一个主线程。没有线程的进程是无法执行的。
  有几个线程就表示着有几个代码在执行,但是它们并不一定是同时执行,例如单核的CPU情况下是不存在多线程的,线程的执行是有时间顺序的,但是CPU切换的非常快,所以给我们的感觉和多核CPU没什么区别。

创建线程

  采用CreateThread()创建线程,语法格式如下

HANDLE WINAPI CreateThread(
    LPSECURITY_ATTRIBUTES lpThreadAttributes,	// 安全属性 通常为NULL表示使用默认设置
    SIZE_T dwStackSize,							// 线程栈空间大小 传入0表示使用默认大小(1MB)
    LPTHREAD_START_ROUTINE lpStartAddress,		// 表示新线程所执行的线程函数地址
    LPVOID lpParameter,							// 线程函数的参数
    DWORD dwCreationFlags,						// 控制线程的创建 0 创建完毕立即调度 CREATE_SUSPENDED 创建后挂起
    LPDWORD lpThreadId							// 返回线程的ID号 NULL表示不需要返回该线程ID号
);												// 返回值:线程句柄 创建失败返回NULL

  线程执行函数的语法规则

DWORD WINAPI MyThreadFunction( LPVOID lpParam ); //LPVOID = void *

  创建一个线程,执行for循环打印数字

#include <stdio.h>
#include <Windows.h>

DWORD WINAPI Demo(LPVOID parameter)
{
    for (int i = 0; i < 100; i++)
    {
        printf("%d\n", i);
        Sleep(1000);
    }
}

int main()
{
    CreateThread(NULL, 0, Demo, NULL, 0, NULL);
    system("pause");	// 防止主线程退出,导致程序立即结束,试试注释这行后运行
    return 0;
}

  输出结果为

0
请按任意键继续. . . 1
2
3
4
...... 更多的数字

线程传参

  给线程函数传递参数也是非常简单的,创建线程函数的第4个参数就是线程函数传参。这里演示传递一个参数n,让n在线程中递增

#include <stdio.h>
#include <Windows.h>

DWORD WINAPI Demo(LPVOID parameter)
{
    int *n = (int *)parameter;
    for (int i = 0; i < 100; i++)
    {
        printf("%d\n", *n);
        (*n)++;
        Sleep(1000);
    }
}

int main()
{
    int n = 0;
    CreateThread(NULL, 0, Demo, &n, 0, NULL);
    system("pause");
    return 0;
}

  输出结果为

0
请按任意键继续. . . 1
2
3
4
...... 更多的数字

挂起线程

  挂起线程也叫暂停线程,当暂停后该线程不会占用cpu,语法格式如下,只需要传入一个线程句柄即可

DWORD WINAPI SuspendThread(HANDLE hThread);

恢复线程

  恢复线程就是让暂停(挂起)的线程继续运行,语法格式如下,只需要传入一个线程句柄即可

DWORD WINAPI ResumeThread(HANDLE hThread);

终止线程

方式一:ExitThread(DWORD dwExitCode); 在线程内部使用,参数为退出状态码。
方式二:线程函数 执行return或者break
方式三:TerminateThread(HANDLE hThread,DWORD dwExitCode); 分别传入线程句柄和状态码

等待线程结束

  WaitForSingleObject用于等待一个内核对象状态发生已通知状态为止,。通常TerminateThread和此函数搭配使用

DWORD WINAPI WaitForSingleObject(
    HANDLE hHandle,
    DWORD dwMilliseconds	// 等待超时时间 传入INFINITE 表示一直等待
);

综合代码

#include <stdio.h>
#include <Windows.h>
#include <conio.h>

// 是否终止线程
// TRUE 终止 FALSE 不终止
BOOL flag = FALSE;

DWORD WINAPI Demo(LPVOID parameter)
{
    printf("已启动线程\n");

    int *n = (int *)parameter;
    for (int i = 0; i < 100; i++)
    {
        // 终止线程
        if (flag == TRUE)
        {
            // 也可以直接写break或return 一样的效果
            ExitThread(0); //线程内部终止
        }

        printf("%d\n", *n);
        (*n)++;
        Sleep(1000);
    }

    printf("子线程已结束\n");
}

// 线程操作
void ControlThread(HANDLE hThread)
{
    // 主线程死循环
    while (1)
    {
        int key = _getch();

        // 按1 挂起线程
        if (key == '1')
        {
            SuspendThread(hThread);
            printf("已挂起线程\n");
        }
        // 按2 恢复线程
        else if (key == '2')
        {
            ResumeThread(hThread);
            printf("已恢复线程\n");
        }
        // 按3|4 终止线程
        else if (key == '3' || key == '4')
        {
            if (key == '3')
            {
                flag = TRUE;
            }
            else
            {
                TerminateThread(hThread, 0);
                WaitForSingleObject(hThread, INFINITE);
            }

            printf("已终止主线程");
            break;
        }
    }
}

int main()
{
    int n = 0;
    HANDLE hThread = CreateThread(NULL, 0, Demo, &n, 0, NULL);
    // 传入线程句柄
    ControlThread(hThread);
    // 关闭句柄
    CloseHandle(hThread);

    return 0;
}

  运行时输入1 2 3,得到下面输出结果

已启动线程
0
1
2
已挂起线程
已恢复线程
3
4
5
6
已终止主线程

线程安全

  先看一段代码,多线程同时操作同一变量引发的问题

#include <Windows.h>
#include <stdio.h>
#define THREADCNT 2

int num = 0;

DWORD WINAPI threadTest(void* lp)
{
	for (int i = 0; i < 100000; i++)
	{
		num++;
	}
	return 0;
}

int main()
{
	HANDLE hThread[2];
	for (int i = 0; i < THREADCNT; i++)
	{
		hThread[i] = CreateThread(NULL, 0, threadTest, NULL, 0, NULL);		
	}
	WaitForMultipleObjects(THREADCNT, hThread, TRUE, INFINITE);
	printf("%d", num);
	return 0;
}

  线程对变量执行+100000的操作,两个线程执行完毕,num按理说应该=200000。然而结果我这里的结果为num=112968。注意每次运行的结果都是不一样的。

  从上述结果来看,说明了多线程访问同一个全局变量会使全局变量的值无法预测。这样会存在安全问题,如果仅仅是读的话是没有问题的。

临界区

  想要解决线程安全问题,可以使用临界区。临界区指的是一个访问共用资源的程序片段无法同时被多个线程访问的特性。

  临界资源是一次仅允许一个进程使用的共享资源。每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问。

实现

  下面是临界区api的使用

InitializeCriticalSection 	// 初始化临界区
DeleteCriticalSection		// 销毁临界区
EnterCriticalSection		// 进入临界区
LeaveCriticalSection		// 退出临界区

  代码非常简单,如下,即可完美解决线程安全问题

#include <Windows.h>
#include <stdio.h>
#define THREADCNT 2

CRITICAL_SECTION cs;

int num = 0;

DWORD WINAPI threadTest(void* lp)
{
	for (int i = 0; i < 100000; i++)
	{
		// 进入临界区
		EnterCriticalSection(&cs);
		num++;
		// 退出临界区
		LeaveCriticalSection(&cs);
	}
	return 0;
}

int main()
{
	// 初始化临界区
	InitializeCriticalSection(&cs);

	HANDLE hThread[2];
	for (int i = 0; i < THREADCNT; i++)
	{
		hThread[i] = CreateThread(NULL, 0, threadTest, NULL, 0, NULL);
	}

	WaitForMultipleObjects(THREADCNT, hThread, TRUE, INFINITE);
	printf("%d", num);
	// 销毁临界区
	DeleteCriticalSection(&cs);

	return 0;
}

误用

  要想使用临界区写出一个安全的代码,其实是比较困难的,下面是临界区的典型误用例子

int num = 0;

DWORD WINAPI thread1(void* lp)
{
	EnterCriticalSection(&cs);
	for (int i = 0; i < 100000; i++)
	{
		num++;
	}
	LeaveCriticalSection(&cs);
}

DWORD WINAPI thread2(void* lp)
{
	EnterCriticalSection(&cs);
	for (int i = 0; i < 100000; i++)
	{
		num++;
	}
	LeaveCriticalSection(&cs);
}

  这样的代码咋一看没有啥毛病,对,至少在结果看来还是num=200000。可仔细想想,当线程1在for之前进入了临界区,这个时候线程2执行准备进入临界区,因为线程1还没退出临界区,于是线程2就一直等待中直到线程1退出临界区。

  这样的写法,根本就没有体现出多线程,相反效率还低了起来。换言之它完全等于下面写法

for (int i = 0; i < 2; i++)
{
	for (int j = 0; j < 100000; j++)
	{
		num++;
	}
}

  正确的理解应该是,那个全局变量要被多线程访问,就在此全局变量前使用临界区即可。参考临界区实现代码。

死锁

  死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象。

  举个例子,线程A需要线程B的某一个资源才能继续执行,同时线程B需要线程A的某个资源才能继续执行。它们都在等待对方的资源,从而导致一直阻塞不能正常执行

  使用临界区造成死锁例子

#include <Windows.h>
#include <stdio.h>

CRITICAL_SECTION A;
CRITICAL_SECTION B;

DWORD WINAPI threadA(void* lp)
{
	printf("线程A进入临界区A\n");
	EnterCriticalSection(&A);
	{
		printf("线程A进入临界区B\n");
		EnterCriticalSection(&B);
		LeaveCriticalSection(&B);
		printf("线程A退出临界区B\n");
	}
	LeaveCriticalSection(&A);
	printf("线程A退出临界区A\n");
	return 0;
}

DWORD WINAPI threadB(void* lp)
{
	printf("线程B进入临界区B\n");
	EnterCriticalSection(&B);
	{
		printf("线程B进入临界区A\n");
		EnterCriticalSection(&A);
		LeaveCriticalSection(&A);
		printf("线程B退出临界区A\n");
	}
	LeaveCriticalSection(&B);
	printf("线程B退出临界区B\n");
	return 0;
}

// 死锁测试
void Deadlock()
{
	HANDLE hThreadA = CreateThread(NULL, 0, threadA, NULL, 0, NULL);
	HANDLE hThreadB = CreateThread(NULL, 0, threadB, NULL, 0, NULL);
	
	WaitForSingleObject(hThreadA, INFINITE);
	WaitForSingleObject(hThreadB, INFINITE);
	CloseHandle(hThreadA);
	CloseHandle(hThreadB);
}
int main()
{
	// 初始化临界区
	InitializeCriticalSection(&A);
	InitializeCriticalSection(&B);

	while (1)
	{
		Deadlock();
		printf("-------------------------------\n");
	}
	return 0;
}

  运行结果

-------------------------------
线程A进入临界区A
线程A进入临界区B
线程A退出临界区B
线程A退出临界区A
线程B进入临界区B
线程B进入临界区A
线程B退出临界区A
线程B退出临界区B
-------------------------------
线程A进入临界区A
线程B进入临界区B
线程B进入临界区A
线程A进入临界区B
产生死锁,无限等待

  希望好好理解下上面的示例,死锁可能很久都不会发生,但发生锁一辈子。

  想要解决上面的死锁状态,必须按照完全相同的顺序对资源进行访问。修改后

DWORD WINAPI threadA(void* lp)
{
	EnterCriticalSection(&A);
	{
		EnterCriticalSection(&B);
		LeaveCriticalSection(&B);
	}
	LeaveCriticalSection(&A);
}

DWORD WINAPI threadB(void* lp)
{
	EnterCriticalSection(&A);
	{
		EnterCriticalSection(&B);
		LeaveCriticalSection(&B);
	}
	LeaveCriticalSection(&A);
}

事件

  在讲事件之前,补充一些内容在此,线程内核对象总是在未通知状态中创建的。线程内核对象也可以处于已通知状态和未通知状态。

  未通知时,线程处于不可调度,即无法运行线程。当线程处于已通知状态,即变为可调度状态,线程很快恢复运行

  事件能够通知一个操作已经完成。有两种不同类型的事件,一种是手动重置事件,另一种是自动重置事件。当手动重置事件得到通知时,等待该事件的所有线程变为可调度。当自动重置事件得到通知时,等待该事件的线程中只有一个线程可变为可调度线程。

  使用CreateEvent创建事件

HANDLE CreateEvent(
	LPSECURITY_ATTRIBUTES lpEventAttributes,	// 指向SECURITY_ATTRIBUTES结构体指针
	BOOL                  bManualReset,			// TRUE:手动重置 FALSE:自动重置
	BOOL                  bInitialState,		// TRUE:已通知状态 FALSE:未通知状态
	LPCSTR                lpName				// 事件名
);												// 成功返回 句柄 失败返回 NULL

  一旦事件已经创建,就可以控制它的状态。当调用SetEvent可以将事件改为已通知状态。

BOOL SetEvent(HANDLE hEvent);					

  当调用ResetEvent函数时,可以将该事件改为未通知状态

BOOL ResetEvent(HANDLE hEvent);

手动

  下面是手动重置事件的例子

#include <stdio.h>
#include <Windows.h>

HANDLE hEvent;

DWORD WINAPI thread1(LPVOID lp)
{
	// 无限等待事件变为通知
	WaitForSingleObject(hEvent, INFINITE);
	printf("thread1 running\n");
}

DWORD WINAPI thread2(LPVOID lp)
{
	// 无限等待事件变为通知
	WaitForSingleObject(hEvent, INFINITE);
	printf("thread2 running\n");
}

int main()
{
	// 创建手动重置,默认未通知状态的事件
	hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

	CreateThread(NULL, 0, thread1, NULL, 0, NULL);
	CreateThread(NULL, 0, thread2, NULL, 0, NULL);

	printf("按任意键让线程运行\n");
	getchar();
	// 设置事件为已通知
	SetEvent(hEvent);
	
	CloseHandle(hEvent);

	return 0;
}

  上面代码,创建了一个手动重置的未通知状态的事件,上面两个线程都调用WaitForSingleObject,使得线程暂停运行。一旦主线程调用SetEvent,给事件发出通知信号,这时,系统就使所有线程进入可调度状态,它们都获得了CPU时间。

  运行结果

按任意键让线程运行

thread1 running
thread2 running

  如果将hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); 改为hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);。就创建了一个手动重置的通知状态的事件,即使不按任意键线程也会运行

  运行结果

按任意键让线程运行
thread1 running
thread2 running

  事件也可以让线程有序执行,示例代码

#include <stdio.h>
#include <Windows.h>

HANDLE hEvent1, hEvent2;

DWORD WINAPI thread1(LPVOID lp)
{
	for (int i = 0; i < 3; i++)
	{
		// 等待线程1变为通知状态
		WaitForSingleObject(hEvent1, INFINITE);
		printf("第%d次 thread1 running\n",i + 1);
		// 设置线程1为未通知
		ResetEvent(hEvent1);
		// 设置线程2为通知
		SetEvent(hEvent2);
	}
}

DWORD WINAPI thread2(LPVOID lp)
{
	for (int i = 0; i < 3; i++)
	{
		// 等待线程2变为通知状态
		WaitForSingleObject(hEvent2, INFINITE);
		printf("第%d次 thread2 running\n",i + 1);
		// 设置线程2为未通知
		ResetEvent(hEvent2);
		// 设置线程1为通知
		SetEvent(hEvent1);
	}
}

int main()
{

	hEvent1 = CreateEvent(NULL, TRUE, FALSE, NULL);
	hEvent2 = CreateEvent(NULL, TRUE, FALSE, NULL);

	CreateThread(NULL, 0, thread1, NULL, 0, NULL);
	CreateThread(NULL, 0, thread2, NULL, 0, NULL);

	// 线程1先运行
	SetEvent(hEvent1);
	// 线程2先运行 
	// SetEvent(hEvent2);
	system("pause");
	CloseHandle(hEvent1);
	CloseHandle(hEvent2);

	return 0;
}

  运行结果

第1次 thread1 running
第1次 thread2 running
第2次 thread1 running
第2次 thread2 running
第3次 thread1 running
第3次 thread2 running
请按任意键继续. . .

自动

  使用自动重置事件,系统只允许一个线程变成可调度状态,同时,也无法保证系统将那个线程变为可调度状态

  示例代码

#include <stdio.h>
#include <Windows.h>

HANDLE hEvent;

DWORD WINAPI Thread(LPVOID lp)
{
	WaitForSingleObject(hEvent, INFINITE);
	printf("thread1 running\n");
}

int main()
{
	// 创建一个自动重置、默认未通知状态的事件
	hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
	CreateThread(NULL, 0, Thread, NULL, 0, NULL);
	system("pause");

	return 0;
}

  运行后,无任何输出结果,将hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);,改为hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);

  输出结果为thread1 running

线程池

  线程池是聚集了一大堆的线程资源,等需要它们的时候,就可以使用它们,而不必向操作系统去申请。当申请线程资源的话,要从应用层转到内核层,这期间花费的时间是非常慢的,如果要经常使用线程,而采用动态申请就需要非常多的时间,这会大大降低程序的效率。因此,针对这种情况,在程序开始时,可以申请一大堆线程,需要的时候就从线程池里边拿,不需要的时候就让回到线程池。

异步方式

  第一种,不创建工作项的方式。使用TrySubmitThreadpoolCallback函数是将一个任务添加进线程池队列

  请注意,这种方式有可能失败,即线程不会启动,为了确保线程能够启动,看第二种方式。

BOOL TrySubmitThreadpoolCallback(
	PTP_SIMPLE_CALLBACK  pfns,		// 请求调用的回调函数
	PVOID                pv,		// 向回调函数传递的参数
	PTP_CALLBACK_ENVIRON pcbe		// 定制线程池
);									// 成功为TRUE  失败为FALSE

  跟进PTP_SIMPLE_CALLBACK,定义如下,必须按下面格式来定义函数

typedef VOID (NTAPI *PTP_SIMPLE_CALLBACK)(
	PTP_CALLBACK_INSTANCE Instance,
	PVOID                 Context
);

  示例代码

#include <stdio.h>
#include <Windows.h>

void NTAPI SimpleCallBack(PTP_CALLBACK_INSTANCE Instance, PVOID Context)
{
	printf("---------------------\n");
	printf("hello %s\n", (char*)Context);
	printf("Thread ID %d\n", GetCurrentThreadId());
}

int main()
{
	char* str = "world";
	if (!TrySubmitThreadpoolCallback(SimpleCallBack, (PVOID)str, NULL)) return 0;
	if (!TrySubmitThreadpoolCallback(SimpleCallBack, (PVOID)str, NULL)) return 0;
	// 延迟1s 确保两个线程都能够执行
	Sleep(1000);
	return 0;
}

  运行结果,可看到线程id是不同的,这就更加证明了这是一个线程池

---------------------
hello world
Thread ID 17344
---------------------
hello world
Thread ID 16944

  第二种方式,使用CreateThreadpoolWork创建工作项

PTP_WORK CreateThreadpoolWork(
	PTP_WORK_CALLBACK    pfnwk,		// 请求调用的回调函数
	PVOID                pv,		// 向回调函数传递的参数
	PTP_CALLBACK_ENVIRON pcbe		// 定制线程池
);									// 成功返回PTP_WORK 失败返回NULL

  PTP_WORK_CALLBACK定义

typedef VOID (NTAPI *PTP_WORK_CALLBACK)(
	PTP_CALLBACK_INSTANCE Instance,
	PVOID                 Context,
	PTP_WORK              Work
);

  当创建工作项成功后,可以使用SubmitThreadpoolWork提交请求

void SubmitThreadpoolWork(
	PTP_WORK pwk	// 指向PTP_WORK 由CreateThreadpoolWork函数返回此指针
);

  最后用CloseThreadpoolWork来释放工作项。

  如果想等待线程池中的线程全部都执行完毕,使用WaitForThreadpoolWorkCallbacks函数

void WaitForThreadpoolWorkCallbacks(
	PTP_WORK pwk,						// 指向PTP_WORK
	BOOL     fCancelPendingCallbacks	// TRUE:取消尚未运行的任务 FALSE:等待所有的任务完成
);

  下面示例会输出三遍hello world

#include <stdio.h>
#include <Windows.h>


VOID NTAPI WorkCallBack(PTP_CALLBACK_INSTANCE Instance,PVOID Context,PTP_WORK Work)
{
	printf("hello %s\n", (char*)Context);
}

int main()
{
	char* str = "world";

	// 创建一个线程池工作项
	PTP_WORK pwk = CreateThreadpoolWork(WorkCallBack, str, NULL);

	// 提交 3 次线程池请求,也就是运行 4 次线程池函数
	SubmitThreadpoolWork(pwk);
	SubmitThreadpoolWork(pwk);
	SubmitThreadpoolWork(pwk);

	// TRUE 取消尚未运行的任务 FALSE 等待所有任务完成
	WaitForThreadpoolWorkCallbacks(pwk, FALSE);

	// 销毁工作项 并不是销毁线程池
	CloseThreadpoolWork(pwk);
	return 0;
}

定时器

  CreateThreadpoolTimer,可以每隔一段时间调用一个函数

PTP_TIMER CreateThreadpoolTimer(
	PTP_TIMER_CALLBACK   pfnti,
	PVOID                pv,
	PTP_CALLBACK_ENVIRON pcbe
);

  PTP_TIMER_CALLBACK定义

typedef VOID (NTAPI *PTP_TIMER_CALLBACK)(
	PTP_CALLBACK_INSTANCE Instance,
	PVOID                 Context,
	PTP_TIMER             Timer
);

  之后,调用SetThreadpoolTimer设置定时器

void SetThreadpoolTimer(
	PTP_TIMER pti,				// CreateThreadpoolTimer函数返回的指针
	PFILETIME pftDueTime,		// 指向FILETIME结构的指针
	DWORD     msPeriod,			// 调用时间间隔单位毫秒 0表示只调用一次
	DWORD     msWindowLength	// 0即可
);

  等待计时器完成WaitForThreadpoolTimerCallbacks,实测不起作用

void WaitForThreadpoolTimerCallbacks(
	PTP_TIMER pti,
	BOOL      fCancelPendingCallbacks
);

  关闭定时器CloseThreadpoolTimer

  示例

#include <stdio.h>
#include <Windows.h>

VOID NTAPI TimerCallBack(PTP_CALLBACK_INSTANCE Instance,PVOID Context,PTP_TIMER Timer)
{
	printf("hello\n");
}

int main()
{
	PTP_TIMER pti = CreateThreadpoolTimer(TimerCallBack, NULL, NULL);

	ULARGE_INTEGER ulRelativeTimer;
	ulRelativeTimer.QuadPart = (LONGLONG)-(10000000);
	FILETIME ftRelativeTimer;
	ftRelativeTimer.dwHighDateTime = ulRelativeTimer.HighPart;
	ftRelativeTimer.dwLowDateTime = ulRelativeTimer.LowPart;
	SetThreadpoolTimer(pti, &ftRelativeTimer, 0, 0);

	Sleep(2000);
	// WaitForThreadpoolTimerCallbacks(pti,TRUE); // 不起作用
	CloseThreadpoolTimer(pti);
	return 0;
}

内核对象

  CreateThreadpoolWait创建线程池等待对象

PTP_WAIT CreateThreadpoolWait(
	PTP_WAIT_CALLBACK    pfnwa,
	PVOID                pv,
	PTP_CALLBACK_ENVIRON pcbe
);

  PTP_WAIT_CALLBACK定义

typedef VOID (NTAPI *PTP_WAIT_CALLBACK)(
	PTP_CALLBACK_INSTANCE Instance,
	PVOID                 Context,
	PTP_WAIT              Wait,
	TP_WAIT_RESULT        WaitResult
);

  SetThreadpoolWait将内核对象绑定到线程池

void SetThreadpoolWait(
	PTP_WAIT  pwa,			// CreateThreadpoolWait的返回值
	HANDLE    h,			// 句柄
	PFILETIME pftTimeout	// 等待时间 0 不等待 NULL 无限等待
);

  等待线程池中的线程操作完成使用WaitForThreadpoolWaitCallbacks,关闭使用CloseThreadpoolWait
  示例,吐槽一下我真的不知道WaitForThreadpoolWaitCallbacks在这里有什么用

#include <stdio.h>
#include <Windows.h>

VOID NTAPI WaitCallBack(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WAIT Wait, TP_WAIT_RESULT WaitResult)
{
	printf("hello");
}
int main()
{
	HANDLE hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
	PTP_WAIT pwa = CreateThreadpoolWait(WaitCallBack, NULL, NULL);
	SetThreadpoolWait(pwa, hEvent, NULL);
	Sleep(2000);
	CloseThreadpoolWait(pwa);
	return 0;
}

异步IO

  CreateThreadpoolIo创建线程池IO对象

PTP_IO CreateThreadpoolIo(
	HANDLE                fl,
	PTP_WIN32_IO_CALLBACK pfnio,
	PVOID                 pv,
	PTP_CALLBACK_ENVIRON  pcbe
);

  PTP_WIN32_IO_CALLBACK定义

typedef VOID (WINAPI *PTP_WIN32_IO_CALLBACK)(
    _Inout_     PTP_CALLBACK_INSTANCE Instance,
    _Inout_opt_ PVOID                 Context,
    _Inout_opt_ PVOID                 Overlapped,
    _In_        ULONG                 IoResult,
    _In_        ULONG_PTR             NumberOfBytesTransferred,
    _Inout_     PTP_IO                Io
);

  StartThreadpoolIo将线程池IO对象与线程池内部的完成端口关联

void StartThreadpoolIo( PTP_IO pio);

  示例

#include <stdio.h>
#include <Windows.h>

VOID WINAPI IoCallBack(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PVOID Overlapped, ULONG IoResult, ULONG_PTR NumberOfBytesTransferred, PTP_IO Io)
{
	printf("hello");
}

int main()
{
	HANDLE hFile = CreateFile("test.txt", GENERIC_WRITE, 0, 0, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, 0);
	PTP_IO pio = CreateThreadpoolIo(hFile, IoCallBack, NULL, NULL);
	StartThreadpoolIo(pio);

	char str[] = "hello";
	OVERLAPPED ol = { 0 };
	WriteFile(hFile, str, strlen(str), NULL, &ol);
	CloseHandle(hFile);
	WaitForThreadpoolIoCallbacks(pio, FALSE);
	CloseThreadpoolIo(pio);
	return 0;
}

内存

  物理内存分为两类。私有内存和共享内存,私有内存的意思是这块物理内存只能本进程使用。共享内存是多个进程一起用。

申请内存的两种方式

  私有内存:VirtualAlloc
  共享内存:CreateFileMapping

内存页面三种状态

  Free:进程不能访问这种页面,因为这个页面还没有被分配。任何属于这个页面的虚拟内存地址进行访问都将引用异常。

  Reserved:页面被保留以备将来使用,这些页面已被分配,但是没使用,物理地址空间中的内存不存在与其对应的物理内存分页。处于被保留的内存分页也不能被访问。

  Committed:内存已经被分配,并且已经被使用,具有与之对应的物理地址空间中的内存分页。

  VirtualAlloc可用于指定分配的内存是什么状态,如果当前内存的状态是Committed,则可以直接访问。

  VirtualAlloc能够将内存页面的状态从Free、Reserved改为Committed,也可以将Free->Reserved,Reserved->Committed。

私有内存

申请和释放

  申请函数 VirtualAlloc  申请其他进程内存使用VirtualAllocEx

LPVOID WINAPI VirtualAlloc(
    LPVOID 	lpAddress,			// 分配内存区域起始地址 一般为NULL
   	SIZE_T 	dwSize,				// 分配内存大小 为了内存对齐值为 0X1000 * n
    DWORD 	flAllocationType,	// 分配内存类型
    DWORD 	flProtect			// 内存保护属性
);								// 成功返回分配的页面区域的基址 失败返回NULL

  flAllocationType 参数

// 还有其他参数没写出
MEM_COMMIT		// 为指定地址空间提交物理内存
MEM_RESERVE		// 保留指定地址空间,不分配物理内存

  flProtect 参数

// 还有其他参数没写出
PAGE_NOACCESS			// 不可访问
PAGE_READONLY 			// 可读 
PAGE_READWRITE			// 可读写
PAGE_EXECUTE			// 可执行
PAGE_EXECUTE_READ		// 可执行 可读
PAGE_EXECUTE_READWRITE	// 可读 可写 可执行
PAGE_GUARD				// 设置为保护页 如果试图对该区域进行读写操作,会产生一个STATUS_GUARD_PAGE异常

  释放内存 VirtualFree  释放其他进程内存使用VirtualFreeEx

BOOL WINAPI VirtualFree(
    LPVOID lpAddress,	// 内存区域地址
    SIZE_T dwSize,		// 内存大小
    DWORD dwFreeType	// 释放类型 MEM_DECOMMIT(释放物理内存,保留线性地址) MEM_RELEASE(释放物理地址和线性地址,使用此值,内存大小必须为0)
);						// 成功返回非零 失败返回0

  简单使用例子

#include <stdio.h>
#include <Windows.h>

int main()
{
    LPVOID lpAddress = VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    // 成功申请内存
    if(lpAddress != NULL)
    {
        // 成功释放内存
        if(VirtualFree(lpAddress, 0, MEM_RELEASE))
        {
            printf("success free");
        }
    }
    return 0;
}
查询内存块信息

  获取内存块信息 VirtualQuery  获取其他进程内存块信息用VirtualQueryEx

SIZE_T VirtualQuery(
  LPCVOID                   lpAddress,	// 查询区域的地址
  PMEMORY_BASIC_INFORMATION lpBuffer,	// 接收返回信息的指针
  SIZE_T                    dwLength	// 缓冲区大小,上述结构体的大小
);										// 返回实际查询的字节数 失败返回0

  保存内存块信息结构体 MEMORY_BASIC_INFORMATION

typedef struct _MEMORY_BASIC_INFORMATION {
  PVOID  BaseAddress;			// 该页面的起始地址
  PVOID  AllocationBase;		// 配给该页面的首地址
  DWORD  AllocationProtect;		// 页面的保护属性
  WORD   PartitionId;
  SIZE_T RegionSize;			// 从BaseAddress开始,具有相同属性的页面的大小,
  DWORD  State;					// 页面的状态,有三种值:MEM_COMMIT、MEM_FREE和MEM_RESERVE,
  DWORD  Protect;				// 保护属性
  DWORD  Type;					// 内存块属性 MEM_IMAGE(映射类型) MEM_MAPPED(共享内存) MEM_PRIVATE(私有内存)
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

  使用例子

#include <stdio.h>
#include <Windows.h>

int main()
{
    // 内存块信息结构体
    MEMORY_BASIC_INFORMATION memoryInfo;

    LPVOID lpAddress = VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE);
    if (lpAddress != NULL)
    {
        // 获取内存块信息
        VirtualQuery(lpAddress, &memoryInfo, sizeof(memoryInfo));
		
        printf("lpAddress:%p\n",lpAddress);
        printf("BaseAddress:%X\n", memoryInfo.BaseAddress);
        printf("AllocationBase:%X\n", memoryInfo.AllocationBase);

        if (memoryInfo.State == MEM_COMMIT)
        {
            printf("State is MEM_COMMIT\n");
        }

        if (memoryInfo.Protect == PAGE_READWRITE)
        {
            printf("Protect is PAGE_READWRITE\n");
        }

        if (memoryInfo.Type == MEM_PRIVATE)
        {
            printf("Type is MEM_PRIVATE\n");
        }

        VirtualFree(lpAddress, 0, MEM_RELEASE);
    }

    return 0;
}

  运行结果

lpAddress:00030000
BaseAddress:30000
AllocationBase:30000
State is MEM_COMMIT
Protect is PAGE_READWRITE
Type is MEM_PRIVATE
更改保护属性

  改变保护属性VirtualProtect  改变其他进程保护属性使用VirtualProtectEx

BOOL WINAPI VirtualProtect(
  LPVOID lpAddress,			// 改变内存属性起始地址
  SIZE_T dwSize,			// 改变内存属性区域大小
  DWORD  flNewProtect,		// 新的内存属性类型
  PDWORD lpflOldProtect		// 旧的内存属性类型
);							// 成功返回非零 失败返回0

  一个规范的例子

#include <stdio.h>
#include <Windows.h>

int main()
{
    LPVOID lpAddress = VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_READWRITE);

    if (lpAddress != NULL)
    {
        // 保存旧的内存属性类型
        DWORD oldProtect = 0;

        if (VirtualProtect(lpAddress, 0x1000, PAGE_READONLY, &oldProtect))
        {
            if (oldProtect == PAGE_READWRITE)
            {
                printf("oldProtect is PAGE_READWRITE\n");
            }

            // 获取当前内存属性
            MEMORY_BASIC_INFORMATION memoryInfo;

            // 查询成功
            if (VirtualQuery(lpAddress, &memoryInfo, sizeof(memoryInfo)))
            {
                if (memoryInfo.Protect == PAGE_READONLY)
                {
                    printf("newProtect is PAGE_READONLY\n");
                }
            }
        }

        if (VirtualFree(lpAddress, 0, MEM_RELEASE))
        {
            printf("success free\n");
        }
    }
    return 0;
}

  输出结果

oldProtect is PAGE_READWRITE
newProtect is PAGE_READONLY
success free

读内存

  读取内存的函数为ReadProcessMemory

BOOL ReadProcessMemory(
	HANDLE  hProcess,				// 进程句柄 此句柄必须有PROCESS_VM_READ权限
    LPCVOID lpBaseAddress,			// 读取内存首地址
	LPVOID  lpBuffer,				// 指向缓冲区的指针
	SIZE_T  nSize,					// 读取大小
	SIZE_T  *lpNumberOfBytesRead	// 设为NULL即可
);									// 成功返回非0 失败返回0

  被读取内存的简单程序

#include <stdio.h>
#include <Windows.h>

int main()
{
    char message[] = "I am secure";
    printf("PID: %d\nMessage: %s\nAddr: %p", GetCurrentProcessId(), message, message);
    getchar();
    return 0;
}

  运行结果

PID: 14172
Message: I am secure
Addr: 0060FF04

  读取内存

#include <stdio.h>
#include <windows.h>

int main()
{
    // 进程pid
    DWORD pid = 14172;
    // 读取地址
    DWORD lpBaseAddress = 0x0060FF04;
    // 获取进程句柄
    HANDLE hProcess = OpenProcess(PROCESS_VM_READ, FALSE, pid);
    if (!hProcess)
    {
        printf("%d", GetLastError());
        return 0;
    }

    char lpBuffer[30];
    if(!ReadProcessMemory(hProcess, (LPCVOID)lpBaseAddress, (LPVOID)lpBuffer, 30, NULL))
    {
        printf("%d", GetLastError());
        return 0;
    }

    printf("%s", lpBuffer);
    CloseHandle(hProcess);

    return 0;
}

  运行结果

I am secure

写内存

  写内存和读内存的方式都大同小异,用到的函数为WriteProcessMemory

BOOL WriteProcessMemory(
    HANDLE  hProcess,					// 进程句柄 此句柄必须有PROCESS_VM_WRITE,PROCESS_VM_OPERATION权限
    LPVOID  lpBaseAddress,				// 写入内存首地址
    LPCVOID lpBuffer,					// 指向缓冲区的指针
    SIZE_T  nSize,						// 写入读取大小
    SIZE_T  *lpNumberOfBytesWritten		// 设为NULL即可
);										// 成功返回非0 失败返回0

  被写入内存的程序

#include <stdio.h>
#include <Windows.h>

int main()
{
    char message[] = "I am secure";
    printf("PID: %d\nMessage: %s\nAddr: %p", GetCurrentProcessId(), message, message);
    getchar();
    printf("Message:%s", message);
    return 0;
}

  运行结果

PID: 6644
Message: I am secure
Addr: 0060FF04
Message:pwnedsecure

  写内存

#include <stdio.h>
#include <windows.h>

int main()
{
    // 进程pid
    DWORD pid = 6644;
    // 读取地址
    DWORD lpBaseAddress = 0x0060FF04;
    // 获取进程句柄
    HANDLE hProcess = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid);
    if (!hProcess)
    {
        printf("error hProcess %d", GetLastError());
        return 0;
    }

    char lpBuffer[] = "pwned";
    if(!WriteProcessMemory(hProcess, (LPVOID)lpBaseAddress, (LPCVOID)lpBuffer, strlen(lpBuffer), NULL))
    {
        printf("error ReadProcessMemory %d", GetLastError());
        return 0;
    }

    printf("success!\n");
    CloseHandle(hProcess);

    return 0;
}

文件系统

  文件系统是操作系统用于管理磁盘上文件的方法和数据结构;也就是在磁盘上如何组织文件的方法。本章介绍卷、目录、文件相关操作的API

  卷就是我们本地磁盘(逻辑驱动器),看不懂没关系,继续看下面的内容就能理解了。

获取所有卷

  函数为GetLogicalDriveStrings

DWORD GetLogicalDriveStrings(
	DWORD nBufferLength,	// 缓冲区的大小
	LPTSTR lpBuffer			// 指向缓冲区的指针
);							// 返回实际需要缓冲区的大小 失败返回0 返回值大于nBufferLength 说明给定的缓冲区大小不够

  例子

#include <stdio.h>
#include <Windows.h>

int main()
{
    char drives[100] = {0};
    DWORD realSize = GetLogicalDriveStrings(100, drives);

    printf("realSize=%d\n",realSize);

    for (int i = 0; i < 100; i++)
    {
        if (drives[i] != '\0')
        {
            printf("%c",drives[i]);
        }
    }
    return 0;
}

  输出

realSize=16
C:\D:\E:\G:\
获取卷类型

  函数为GetDriveType

UINT GetDriveType(
	LPCSTR lpRootPathName	// 驱动器根目录 为NULL使用当前卷
);							// 返回驱动器类型

   驱动器类型如下

DRIVE_UNKNOWN     = 0; 		// 无法确定驱动器类型
DRIVE_NO_ROOT_DIR = 1; 		// 根路径无效
DRIVE_REMOVABLE   = 2; 		// 软盘
DRIVE_FIXED       = 3; 		// 本地硬盘
DRIVE_REMOTE      = 4; 		// 网络磁盘
DRIVE_CDROM       = 5; 		// CD-ROM
DRIVE_RAMDISK     = 6; 		// RAM 磁盘

  例子

#include <stdio.h>
#include <Windows.h>

int main()
{
    UINT type = GetDriveType("c:\\");
    printf("%d",type);
    return 0;
}

  输出

3
获取卷信息

  函数为GetVolumeInformation

BOOL GetVolumeInformationA(
	LPCSTR  	lpRootPathName,				// 卷根目录 为NULL使用当前卷
    LPSTR   	lpVolumeNameBuffer,			// 存放卷名
    DWORD   	nVolumeNameSize,			// 卷名长度
    LPDWORD 	lpVolumeSerialNumber,		// 卷序列号 不需要可以设置成NULL
    LPDWORD 	lpMaximumComponentLength,	// 最大文件文件名组件长度
    LPDWORD 	lpFileSystemFlags,			// 文件系统属性
    LPSTR   	lpFileSystemNameBuffer,		// 文件系统 NTFS/FAT
    DWORD   	nFileSystemNameSize			// 文件系统缓冲区长度
);											// 成功返回非0 失败返回0

  例子

#include <stdio.h>
#include <Windows.h>

int main()
{
    char szVolumeNameBuf[MAX_PATH] = {0};
    DWORD dwVolumeSerialNum;
    DWORD dwMaxComponentLength;
    DWORD dwSysFlags;
    char szFileSystemBuf[MAX_PATH] = {0};

    BOOL bGet = GetVolumeInformation(
                    "C:\\",
                    szVolumeNameBuf,
                    MAX_PATH,
                    &dwVolumeSerialNum,
                    &dwMaxComponentLength,
                    &dwSysFlags,
                    szFileSystemBuf,
                    MAX_PATH
                );
    if(bGet)
    {
        printf("%s",szFileSystemBuf);
    }

    return 0;
}

  输出

NTFS

目录

创建目录

  函数为CreateDirectory

BOOL CreateDirectory(
	LPCSTR                lpPathName,			// 要创建的目录的路径
	LPSECURITY_ATTRIBUTES lpSecurityAttributes	// 安全描述符
);												// 成功返回非0 失败返回0
删除目录

  函数为RemoveDirectory

BOOL RemoveDirectory(
	LPCSTR lpPathName		// 要删除的目录的路径
);							// 成功返回非0 失败返回0
移动目录

  函数为MoveFile

BOOL MoveFile(
	LPCTSTR lpExistingFileName,		// 目录名
	LPCTSTR lpNewFileName			// 新目录名
);									// 成功返回非0 失败返回0
获取程序当前目录

  函数为GetCurrentDirectory

DWORD GetCurrentDirectory(
	DWORD  nBufferLength,			// 字符串的缓冲区长度
	LPTSTR lpBuffer					// 接收缓冲区目录指针
);									// 成功返回写入缓冲区的字符数 失败返回0
// 若要确定所需的缓冲区大小 lpBuffer设置为 NULL 并将nBufferLength设置为0
设置程序当前目录

  函数为SetCurrentDirectory

BOOL SetCurrentDirectory(
	LPCTSTR lpPathName		// 新目录名
);							// 成功返回非0 失败返回0
综合代码
#include <stdio.h>
#include <Windows.h>

void RemoveDirectoryDemo()
{
    // 删除c:\text
    if (RemoveDirectory("c:\\test"))
    {
        printf("remove directory success\n");
    }
}

void CreateDirectoryDemo()
{
    // 在c盘下创建text文件夹
    if (CreateDirectory("c:\\test", NULL))
    {
        printf("create directory success\n");
    }
}

void MoveFileDemo()
{
    // 将c:\text 重命名成 c:\1
    if (MoveFile("c:\\test", "c:\\1"))
    {
        printf("move file success\n");
    }
}

void GetCurrentDirectoryDemo()
{
    // 获取实际大小
    DWORD size = GetCurrentDirectory(0, NULL);
    char *buffer = (char *)malloc(sizeof(char) * (size + 5));

    GetCurrentDirectory(size, buffer);
    printf("%s\n", buffer);
}

void SetCurrentDirectoryDemo()
{
    SetCurrentDirectory("c:\\");
}

int main()
{

    //CreateDirectoryDemo();
    //RemoveDirectoryDemo()
    //MoveFileDemo();

    GetCurrentDirectoryDemo();
    SetCurrentDirectoryDemo();
    GetCurrentDirectoryDemo();

    return 0;
}

文件

创建文件

  函数为CreateFile

HANDLE CreateFile(
    LPCSTR                lpFileName,				// 文件名
    DWORD                 dwDesiredAccess,			// 访问模式 GENERIC_READ GENERIC_WRITE
    DWORD                 dwShareMode,				// 共享模式
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,		// SD 安全属性
    DWORD                 dwCreationDisposition,	// 创建模式 
    DWORD                 dwFlagsAndAttributes,		// 文件属性
    HANDLE                hTemplateFile				// 设为NULL即可
);													// 成功返回文件句柄 失败返回INVALID_HANDLE_VALUE

  dwCreationDisposition 有以下参数

dwCreationDisposition	文件存在			文件不存在
CREATE_ALWAYS			 覆盖					新建
OPEN_ALWAYS				 打开					新建
CREATE_NEW				 ERROR_FILE_EXISTS   新建
OPEN_EXISTING			 打开				   	ERROR_FILE_NOT_FOUND
TRUNCATE_EXISTING		 清空文件内容			  ERROR_FILE_NOT_FOUND

  以可读可写方式,有就覆盖没有就新建的方式 创建文件

CreateFile("c:\\1.txt", GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
获取文件大小

  函数为GetFileSize

DWORD GetFileSize(
	HANDLE  hFile,			// 文件句柄
	LPDWORD lpFileSizeHigh	// 设为NULL即可
);							// 成功返回文件大小(单位字节) 失败返回INVALID_FILE_SIZE

  获取d:\1.txt文件大小

HANDLE hFile = CreateFile("d:\\1.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if (hFile != INVALID_HANDLE_VALUE)
{
    // 获取文件大小
    DWORD size = GetFileSize(hFile, NULL);
    printf("%d", size);
}
其他API
ReadFile	读文件
WriteFile	写文件
CopyFile	复制文件
DeleteFile	删除文件

进程通信

  进程通信就是指不同进程间进行数据共享和数据交换。

socket

  socket(套接字) 是操作系统提供给程序员操作「网络协议栈」的接口,通过socket 的接口,来控制协议工作,从而实现网络通信,达到跨主机通信。有tcp udp两种通信方法,这里使用tcp。

  凡是涉及到网络通信,都会有发送方和接收方,也就是我们平常说的服务器/客户端

  而编写winsock都需要头文件WinSock2.h,和链接ws2_32.lib

  大致流程
  发送方:socket -> connect -> send/recv -> close
  接收方:socket -> bind -> listen -> accept -> send/recv -> close

发送方

  1.初始化Winsock,用到WSAStartup函数

int WSAStartup(
	WORD      wVersionRequired,	 // 指定socket版本 现在已经是2.2版本了
	LPWSADATA lpWSAData			 // wsaData结构体指针
);								 // 成功返回0 失败返回非0

  最后在调用WSACleanup,进行收尾操作。

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")

int main()
{
    WSADATA wsa;
    int ret;
    // 初始化socket 使用2.2版本 成功返回0
    ret = WSAStartup(MAKEWORD(2, 2), &wsa);
    if(ret)
    {
        printf("WSAStartup fail\n");
    }

    // 清理
    WSACleanup();
}

  2.创建socket,使用socket函数

SOCKET WSAAPI socket(
	int af,			// 网络地址族 值为AF_INET 代表ipv4
	int type,		// 网络协议类型 SOCK_STREAM使用TCP SOCK_DGRAM使用UDP
	int protocol	// 网络地址族的特殊协议 IPPROTO_TCP使用TCP IPPROTO_UDP使用UDP 设为0自动使用对应的协议
);					// 成功返回SOCKET 失败返回INVALID_SOCKET

  SOCKET类型定义如下

typedef    UINT_PTR        SOCKET;
typedef    unsigned int    UINT_PTR;

  实际上就是一个unsigned int类型,它将被Socket环境管理和使用。套接字将被创建、设置、用来发送和接收数据,最后会被关闭。
  使用closesocket,来关闭一个套接字

int closesocket(
	SOCKET s	// SOCKET类型
);				// 成功返回0
// 创建Socket
SOCKET sClient = socket(AF_INET, SOCK_STREAM, 0);
if (sClient == INVALID_SOCKET)
{
    printf("socket fail\n");
    return 0;
}

// 关闭套接字
closesocket(sClient);

  3.指定socket发送和接收数据包的地址,使用sockaddr_in 结构体

typedef struct sockaddr_in {
  short          sin_family;	// 只能取值AF_INET
  USHORT         sin_port;		// IP地址端口
  IN_ADDR        sin_addr;		// 指向IN_ADDR结构体
  CHAR           sin_zero[8];	// 保留
} SOCKADDR_IN, *PSOCKADDR_IN;

  IN_ADDR结构体

typedef struct in_addr {
  union {
    	struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
      	struct { USHORT s_w1,s_w2; } S_un_w;
      	ULONG S_addr;
  } S_un;
} IN_ADDR, *PIN_ADDR, *LPIN_ADDR;

  使用connet 建立连接,是客户方连接服务方的函数

int WSAAPI connect(
	SOCKET         s,		// socket结构体
	const sockaddr *name,	// 指向sockaddr结构体的指针
	int            namelen	// sockaddr结构体大小
);							// 成功返回0 失败返回SOCKET_ERROR
// 指定发送端口
int port = 5899;
// 指定发送地址
char ip[] = "127.0.0.1";
// 设置发送地址
SOCKADDR_IN server;
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.S_un.S_addr = inet_addr(ip);
printf("connect to %s:%d\n", inet_ntoa(server.sin_addr), htons(server.sin_port));

// 建立连接
ret = connect(sClient, (SOCKADDR *)&server, sizeof(SOCKADDR));
if (ret == SOCKET_ERROR)
{
    printf("connect fail\n");
    return 0;
}

  4.发送/接收数据 ,发送数据用send函数

int WSAAPI send(
	SOCKET     s,		// socket结构体
	const char *buf,	// 发送数据指针
	int        len,		// 发送大小
	int        flags	// 设为0即可
);						// 成功返回实际发送大小 失败返回SOCKET_ERROR

  发送方完整代码

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")

int main()
{

    WSADATA wsa;
    int ret;
    // 初始化socket 使用2.2版本 成功返回0
    ret = WSAStartup(MAKEWORD(2, 2), &wsa);
    if (ret)
    {
        printf("WSAStartup fail\n");
    }

    // 创建Socket
    SOCKET sClient = socket(AF_INET, SOCK_STREAM, 0);
    if (sClient == INVALID_SOCKET)
    {
        printf("socket fail\n");
        return 0;
    }

    // 指定发送端口
    int port = 5899;
    // 指定发送地址
    char ip[] = "127.0.0.1";
    // 设置发送地址
    SOCKADDR_IN server;
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.S_un.S_addr = inet_addr(ip);
    printf("connect to %s:%d\n", inet_ntoa(server.sin_addr), htons(server.sin_port));

    // 建立连接
    ret = connect(sClient, (SOCKADDR *)&server, sizeof(SOCKADDR));
    if (ret == SOCKET_ERROR)
    {
        printf("connect fail\n");
        return 0;
    }

    // 发送数据
    char buffer[] = "hello";
    ret = send(sClient, buffer, sizeof(buffer), 0);
    if (ret == SOCKET_ERROR || ret == 0)
    {
        printf("send fail\n");
        return 0;
    }

    printf("sending:%s\n", buffer);
    printf("sending:%d bytes", ret);

    // 关闭套接字
    closesocket(sClient);
    // 清理
    WSACleanup();
    
}
接收方

  前面的步骤和发送方都一样,初始化socket,创建socket。之后就需要绑定和监听端口,使用bind绑定端口

int bind(
	SOCKET         s,		// socket
	const sockaddr *addr,	// 指向sockaddr结构体的指针
	int            namelen 	// sockaddr结构体大小
);							// 成功返回0 失败返回SOCKET_ERROR

  绑定完成后,使用listen监听端口

int WSAAPI listen(
	SOCKET s,			// socket
	int    backlog		// 挂起的连接队列的最大长度
);						// 成功返回0 失败返回SOCKET_ERROR

  使用accept 建立连接,是服务方同意客户方连接的函数

SOCKET WSAAPI accept(
	SOCKET   s,			// socket结构体
	sockaddr *addr,		// 指向sockaddr结构体的指针
	int      *addrlen	// sockaddr结构体大小
);						// 成功返回socket 失败返回INVALID_SOCKET

  成功之后,使用recv接收数据

int recv(
	SOCKET s,		// socket结构体
	char   *buf,	// 接收数据指针
	int    len,		// 接收大小
	int    flags	// 设为0即可
);					// 成功返回实际接收大小 失败返回SOCKET_ERROR

  接收方完整代码

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")
int main()
{

    WSADATA wsa;
    int ret;
    // 初始化socket
    ret = WSAStartup(MAKEWORD(2, 2), &wsa);
    if (ret)
    {
        printf("WSAStartup fail\n");
        return 0;
    }

    // 创建socket
    SOCKET sListen = socket(AF_INET, SOCK_STREAM, 0);
    if (sListen == INVALID_SOCKET)
    {
        printf("socket fail\n");
        return 0;
    }

    // 监听端口
    int port = 5899;
    // 接收数据
    SOCKADDR_IN local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    // 接收任意ip数据
    local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    // 绑定端口
    ret = bind(sListen, (SOCKADDR*)&local, sizeof(SOCKADDR));
    if (ret == SOCKET_ERROR)
    {
        printf("bind fail\n");
        return 0;
    }

    // 监听端口
    ret = listen(sListen, 5);
    if (ret == SOCKET_ERROR)
    {
        printf("listen fail\n");
        return 0;
    }

    printf("Listening %d\n", port);

    // 建立连接
    SOCKADDR_IN client;
    int AddrSize = sizeof(SOCKADDR);
    SOCKET sClient = accept(sListen, (SOCKADDR*)&client, &AddrSize);
    if (sClient == INVALID_SOCKET)
    {
        printf("accept fail\n");
        return 0;
    }

    printf("connect from %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));

    // 接收数据
    char buffer[1024];
    ZeroMemory(buffer, sizeof(buffer));

    ret = recv(sClient, buffer, 1024, 0);
    if (ret == 0 || ret == SOCKET_ERROR)
    {
        printf("recv fail\n");
        return 0;
    }

    printf("recv:%s\n", buffer);
    printf("recv:%d bytes", ret);

    // 收尾
    closesocket(sClient);
    closesocket(sListen);
    WSACleanup();
}

  运行顺序是:先启动接收方,再启动发送方
  发送方结果

connect to 127.0.0.1:5899
sending:hello
sending:6 bytes

  接收方结果

Listening 5899
connect from 127.0.0.1:8893
recv:hello
recv:6 bytes

命名管道

  命名管道(Named Pipes),按照字面意思理解就是有名字的管道,它可在同一台计算机的不同进程之间或在跨越一个网络的不同计算机的不同进程之间,支持可靠的、单向或双向的数据通信。

  命名管道是由服务器端的进程建立的,管道的命名必须遵循特定的命名方法。 本地访问:\\.\pipe\管道名,远程访问:\\ServerName\pipe\管道名。客户端要想连接服务端,必须知道服务端的管道名称。

服务端

  1.使用CreateNamedPipe来创建一个命名管道

HANDLE CreateNamedPipe(
	LPCSTR                lpName,				// 创建的管道名称
	DWORD                 dwOpenMode,			// 管道打开方式
	DWORD                 dwPipeMode,			// 管道模式
	DWORD                 nMaxInstances,		// 管道最大连接数 必须大于1小于255(PIPE_UNLIMITED_INSTANCES)
	DWORD                 nOutBufferSize,		// 管道输出缓冲区 0 使用默认大小
	DWORD                 nInBufferSize,		// 管道输入缓冲区 0 使用默认大小
	DWORD                 nDefaultTimeOut,		// 管道连接超时时间 0 默认超时50毫秒
	LPSECURITY_ATTRIBUTES lpSecurityAttributes  // 指向SECURITY_ATTRIBUTES指针
);												// 成功返回服务端管道句柄 失败返回INVALID_HANDLE_VALUE

  dwOpenMode 常用的有以下值,更多查阅msdn

PIPE_ACCESS_DUPLEX	 服务器和客户端都可以从管道读写数据
PIPE_ACCESS_INBOUND  客户端只能写 服务端只能读
PIPE_ACCESS_OUTBOUND 客户端只能读 服务端只能写

  dwPipeMode 管道模式,有以下常用值

写入类型二选一
PIPE_TYPE_BYTE			数据作为字节流写入管道
PIPE_TYPE_MESSAGE   	数据作为消息流写入管道
读取类型二选一
PIPE_READMODE_BYTE  	数据作为字节流读入管道
PIPE_READMODE_MESSAGE 	数据作为消息流读入管道
等待类型二选一
PIPE_WAIT				阻塞模式
PIPE_NOWAIT 			非阻塞模式
// 1.创建命名管道
HANDLE hPipe = CreateNamedPipe(
	"\\\\.\\pipe\\local",
	PIPE_ACCESS_DUPLEX,
	PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
	PIPE_UNLIMITED_INSTANCES,
	0, 0, 0, NULL
);

if (!hPipe)
{
	printf("CreateNamedPipe fail\n");
	return 0;
}

  2.使用ConnectNamedPipe,等待客户端连接命名管道

BOOL ConnectNamedPipe(
	HANDLE       hNamedPipe,	// 服务端管道句柄
	LPOVERLAPPED lpOverlapped	// 指向 OVERLAPPED 结构的指针 为NULL即可
);								// 成功非0 失败为0

  3.使用ReadFile来接收数据,或者使用WriteFile来发送数据。

BOOL ReadFile(
	HANDLE       hFile,						// 句柄
	LPVOID       lpBuffer,					// 缓冲区指针
	DWORD        nNumberOfBytesToRead,		// 读取大小
	LPDWORD      lpNumberOfBytesRead,		// 实际读取大小指针
	LPOVERLAPPED lpOverlapped				// 设为NULL
);											// 成功非0 失败为0

BOOL WriteFile(
	HANDLE       hFile,						// 句柄
	LPCVOID      lpBuffer,					// 缓冲区指针
	DWORD        nNumberOfBytesToWrite,		// 接收大小
	LPDWORD      lpNumberOfBytesWritten,	// 实际接收大小
	LPOVERLAPPED lpOverlapped				// 设为NULL
);											// 成功非0 失败为0

  4.最后使用DisconnectNamedPipe来断开与客户端的连接,必须由CreateNamedPipe创建。

  服务端 示例代码

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>


int main()
{
	printf("waiting to connect\n");

	// 1.创建命名管道
	HANDLE hPipe = CreateNamedPipe(
		"\\\\.\\pipe\\local",
		PIPE_ACCESS_DUPLEX,
		PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
		PIPE_UNLIMITED_INSTANCES,
		0, 0, 0, NULL
	);

	if (!hPipe)
	{
		printf("CreateNamedPipe fail\n");
		return 0;
	}

	// 2.等待客户端连接
	if (!ConnectNamedPipe(hPipe, NULL))
	{
		printf("ConnectNamedPipe fail\n");
		return 0;
	}

	// 3.接收客户端数据
	char buffer[256] = { 0 };
	ZeroMemory(buffer, sizeof(buffer));

	if (!ReadFile(hPipe, buffer, sizeof(buffer), 0, NULL))
	{
		printf("ReadFile fail\n");
		return 0;
	}

	printf("recvData is %s", buffer);

	// 4.向客户端发送数据
	ZeroMemory(buffer, sizeof(buffer));
	strcpy(buffer, "hello Client");

	if (!WriteFile(hPipe,buffer,sizeof(buffer),0,NULL))
	{
		printf("WriteFile fail\n");
		return 0;
	}

	// 断开连接
	DisconnectNamedPipe(hPipe);
	CloseHandle(hPipe);
	return 0;
}
客户端

  1.客户端使用WaitNamedPipe来等待管道的出现

BOOL WaitNamedPipe(
	LPCSTR lpNamedPipeName, // 命名管道名称
    DWORD  nTimeOut			// 超时时间 单位毫秒
);							// 成功返回非0 失败返回0

  nTimeOut 有两种值

NMPWAIT_USE_DEFAULT_WAIT	服务端在CreateNamedPipe函数中指定的默认值
NMPWAIT_WAIT_FOREVER 		一直等待一个命名管道
// 1.等待管道的出现
if (!WaitNamedPipe("\\\\.\\pipe\\local",NMPWAIT_WAIT_FOREVER))
{
    printf("WaitNamedPipe fail\n");
    return 0;
}

  2.调用CreateFile来打开命名管道的一个句柄,然后在调用WriteFile发数据,或者ReadFile接收数据
  客户端 代码示例

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    // 1.等待管道出现
    if (!WaitNamedPipe("\\\\.\\pipe\\local", NMPWAIT_WAIT_FOREVER))
    {
        printf("WaitNamedPipe fail\n");
        return 0;
    }

    printf("connect namePipe success\n");

    // 2.打开命名管道句柄
    HANDLE hPipe = CreateFile("\\\\.\\pipe\\local", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (!hPipe)
    {
        printf("CreateFile fail\n");
        return 0;
    }

    // 3.向服务端发送数据
    char buffer[256] = "hello Server";

    if (!WriteFile(hPipe, buffer, sizeof(buffer), 0, NULL))
    {
        printf("WriteFile fail\n");
        return 0;
    }


    // 4.从服务端接收数据
    ZeroMemory(buffer, sizeof(buffer));
    if (!ReadFile(hPipe, buffer, sizeof(buffer), 0, NULL))
    {
        printf("ReadFile fail\n");
        return 0;
    }

    printf("recvData is %s\n", buffer);
    CloseHandle(hPipe);
	return 0;
}

  运行结果

服务端
waiting to connect
recvData is hello Server

客户端
connect namePipe success
recvData is hello Client

注册表

  注册表是Windows中的一个重要的数据库,用于存储系统和应用程序的设置信息。

  注册表由主键、子键、键值构造,主键就是根,有以下主键

  HKEY_CLASSES_ROOTHKEY_CURRENT_CONFIGHKEY_CURRENT_USERHKEY_LOCAL_MACHINEHKEY_USERS

  而子键就是主键下的键,类似于文件夹的结构,例如HKEY_LOCAL_MACHINE\SOFTWARESOFTWARE就是子键。

  键值是子键下的具体内容,由名称、类型、数据组成

子键

打开

  打开注册表有两个api:RegOpenKeyRegOpenKeyEx。微软推荐使用后缀带Ex的api,因为RegOpenKey是以前16位机用的,Ex版本更灵活和强大。

LSTATUS RegOpenKeyEx(
	HKEY   hKey,		// 注册表句柄 或 主键
	LPCSTR lpSubKey,	// 注册表子键
	DWORD  ulOptions,	// 保留 必须是0
	REGSAM samDesired,	// 访问权限
	PHKEY  phkResult	// 返回打开的注册表句柄
);						// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  samDesired访问权限,这里列出常用的值,

KEY_ALL_ACCESS				// 获取所有权限
KEY_CREATE_SUB_KEY			// 创建子键
KEY_ENUMERATE_SUB_KEYS		// 枚举子键
KEY_EXECUTE					// 允许读操作
KEY_QUERY_VALUE				// 查询子键
KEY_READ					// STANDARD_RIGHTS_READ、KEY_QUERY_VALUE、KEY_ENUMERATE_SUB_KEYS、KEY_NOTIFY值的组合
KEY_SET_VALUE				// 创建、删除或设置子键
KEY_WRITE					// STANDARD_RIGHTS_WRITE、KEY_SET_VALUE、KEY_CREATE_SUB_KEY值的组合
KEY_WOW64_32KEY 			// 访问32位注册表
KEY_WOW64_64KEY				// 访问64位注册表

  关于KEY_WOW64_32KEYKEY_WOW64_64KEY,我建议你单独去了解了解。有些时候创建成功或者打开失败,都有可能和这个有关。

  关闭注册表句柄使用RegCloseKey函数

  例子,注意键名不区分大小写

HKEY hKey;
// 键名不区分大小写
// 打开HKEY_LOCAL_MACHINE\SOFTWARE
long ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SoFTwARE", 0, KEY_ALL_ACCESS, &hKey);
if (ret == ERROR_SUCCESS)
{
    printf("open key success\n");
    RegCloseKey(hKey);
}
创建

  RegCreateKeyEx,创建指定的注册表项。如果键已经存在,函数将打开它

LSTATUS RegCreateKeyEx(
	HKEY                        hKey,				// 注册表句柄 或 主键
	LPCSTR                      lpSubKey,			// 创建子键的名称
	DWORD                       Reserved,			// 保留 必须为0
	LPSTR                       lpClass,			// 忽略 填NULL
	DWORD                       dwOptions,			// 填0 具体参考msdn
	REGSAM                      samDesired,			// 访问权限
	LPSECURITY_ATTRIBUTES lpSecurityAttributes,		// 指向SECURITY_ATTRIBUTES结构的指针
	PHKEY                       phkResult,			// 返回打开或创建注册表句柄
	LPDWORD                     lpdwDisposition		// 接收处理后的结果 可以置NULL
);													// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  lpdwDisposition 有以下值

REG_CREATED_NEW_KEY 		// 不存在子键则创建
REG_OPENED_EXISTING_KEY		// 子键已经存在则打开

  这里演示两种创建子键的方法,一种是基于已经打开的注册表句柄创建,一种是直接创建

#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>

// 基于打开的注册表句柄创建
void method1()
{
    HKEY hKey;

    long ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"SOftWAre", 0, KEY_ALL_ACCESS, &hKey);
    if (ret != ERROR_SUCCESS)
        return 0;

    printf("open key success\n");

    DWORD result = 0;
    
    // 可一次性创建多级子键
    // 创建HKEY_LOCAL_MACHINE\SOFTWARE\aaa\b\c\d
    ret = RegCreateKeyEx(hKey, L"aaa\\b\\c\\d", 0, NULL, 0, KEY_CREATE_SUB_KEY, NULL, &hKey, &result);
    if (ret != ERROR_SUCCESS)
        return 0;

    // 成功创建子键
    if (result == REG_CREATED_NEW_KEY)
    {
        printf("create new key\n");
    }

    // 子键已经存在
    if (result == REG_OPENED_EXISTING_KEY)
    {
        printf("key is existing\n");
    }

    RegCloseKey(hKey);
}

// 直接创建子键
void method2()
{
    HKEY hKey;
    // 创建HKEY_LOCAL_MACHINE\SOFTWARE\aaa\b\c\d
    long ret = RegCreateKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\aaa\\b\\c\\d", 0, NULL, 0, KEY_CREATE_SUB_KEY, NULL, &hKey, NULL);
    if (ret == ERROR_SUCCESS)
    {
        printf("RegCreateKeyEx success\n");;
        RegCloseKey(hKey);
    }
}

int main() 
{
    // 直接创建子键
    method2();
    // 基于打开的注册表句柄创建
    method1();
    return 0;
}

  运行结果

RegCreateKeyEx success
open key success
key is existing
删除

  RegDeleteKeyEx,只能删除一个非空子键

LSTATUS RegDeleteKeyEx(
	HKEY   hKey,			// 注册表句柄 或 主键
	LPCSTR lpSubKey,		// 要删除的子键
	REGSAM samDesired,		// 访问掩码
	DWORD  Reserved			// 保留置0
);							// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  samDesired,有以下两种值

KEY_WOW64_32KEY		从 32 位注册表中删除该项
KEY_WOW64_64KEY		从 64 位注册表中删除该项

  示例

// 删除HKEY_LOCAL_MACHINE\SOFTWARE\aaa\b\c\d
long ret = RegDeleteKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\aaa\\b\\c\\d", KEY_WOW64_64KEY, 0);
if (ret == ERROR_SUCCESS)
    printf("delete key success\n");

  如果要递归删除,用RegDeleteTree
  示例

// 删除HKEY_LOCAL_MACHINE\SOFTWARE\aaa 以及aaa下的所有子键
RegDeleteTree(HKEY_LOCAL_MACHINE, L"SOFTWARE\\aaa");

键值

查询

  获取键值中的某个值的数据,可以使用RegQueryValueEx

LSTATUS RegQueryValueEx(
	HKEY    hKey,			// 注册表句柄 或 主键
	LPCSTR  lpValueName,	// 键值名称
	LPDWORD lpReserved,		// 保留置NULL
	LPDWORD lpType,			// 键值类型 可置NULL
	LPBYTE  lpData,			// 接收数据缓冲区指针 可置NULL
	LPDWORD lpcbData		// 缓冲区指针大小
);							// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  当lpData为NULL时,lpcbData返回实际获取到的缓冲区大小
  示例

#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>

int main() 
{
    HKEY hKey;
    TCHAR subKey[] = TEXT("HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0");
    long ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, subKey, 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &hKey);
    if (ret != ERROR_SUCCESS)
    {
        printf("RegOpenKeyEx fail\n");
        return 0;
    }
       
    DWORD size = 0;
    DWORD type = 0;
    // 第一次获取缓冲区大小
    ret = RegQueryValueEx(hKey, L"ProcessorNameString", NULL, &type, NULL, &size);
    if (ret != ERROR_SUCCESS)
        return 0;
    
    if (type == REG_SZ)
        printf("type is string\n");

    // 动态分配空间
    TCHAR* data = (TCHAR*)malloc(sizeof(TCHAR) * size + 5);

    ret = RegQueryValueEx(hKey, L"ProcessorNameString", NULL, NULL, data, &size);
    if (ret != ERROR_SUCCESS)
        return 0;

    wprintf(L"cpu is %s\n", data);
    free(data);
    RegCloseKey(hKey);
    return 0;
}

  运行结果

type is string
cpu is AMD Ryzen 5 2600 Six-Core Processor
增加&修改

  RegSetValueEx,可以修改键值的值,如果此键值不存在,则创建此键值

LSTATUS RegSetValueEx(
	HKEY       hKey,			// 注册表句柄
	LPCSTR     lpValueName,		// 键值名称
	DWORD      Reserved,		// 保留必须是0
	DWORD      dwType,			// 键值类型
	const BYTE *lpData,			// 写入的缓冲区指针
	DWORD      cbData			// 缓冲区大小
);								// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  经测试,使用RegSetValueExA,写入的数据才不会乱码
  示例

#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>

int main() 
{
    // 创建HKEY_LOCAL_MACHINE\SOFTWARE\aaa 子键
    HKEY hKey;
    long ret = RegCreateKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\aaa", 0, NULL, 0, KEY_ALL_ACCESS | KEY_WOW64_64KEY, NULL, &hKey, NULL);
    if (ret != ERROR_SUCCESS)
    {
        printf("RegCreateKeyEx fail\n");
        return 0;
    }

    // 创建test键值 内容为this is demo 这是个示例
    BYTE data[] = "this is demo 这是个示例";
    ret = RegSetValueExA(hKey, "test", 0, REG_SZ, (BYTE*)data, sizeof(data));
    if (ret == ERROR_SUCCESS)
        printf("sucess\n");

    RegCloseKey(hKey);
    return 0;
}
删除

  删除键值非常简单,只需要调用RegDeleteValue
  示例

// 创建HKEY_LOCAL_MACHINE\SOFTWARE\aaa 子键
HKEY hKey;
RegCreateKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\aaa", 0, NULL, 0, KEY_ALL_ACCESS | KEY_WOW64_64KEY, NULL, &hKey, NULL);

// 创建test键值
BYTE data[] = "this is demo 这是个示例";
RegSetValueExA(hKey, "test", 0, REG_SZ, (BYTE*)data, sizeof(data));

// 删除test键值
RegDeleteValue(hKey, L"test");
RegCloseKey(hKey);
枚举

  获取子键下的所有数据需要两个api,第一个RegQueryInfoKey,获取注册表项的相关信息

LSTATUS RegQueryInfoKey(
	HKEY      hKey,						// 注册表句柄
	LPSTR     lpClass,					// 可为NULL
 	LPDWORD   lpcchClass,				// 可为NULL
	LPDWORD   lpReserved,				// 必须为NULL
	LPDWORD   lpcSubKeys,				// 子键数量 可为NULL
	LPDWORD   lpcbMaxSubKeyLen,			// 可为NULL
	LPDWORD   lpcbMaxClassLen,			// 可为NULL
	LPDWORD   lpcValues,				// 键值数量 可为NULL
	LPDWORD   lpcbMaxValueNameLen,		// 键名最大大小 可为NULL
	LPDWORD   lpcbMaxValueLen,			// 键值最大大小 可为NULL
	LPDWORD   lpcbSecurityDescriptor,	// 可为NULL
	PFILETIME lpftLastWriteTime			// 可为NULL
);										// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  第二个RegEnumValue,枚举键值信息

LSTATUS RegEnumValueA(
	HKEY    hKey,				// 注册表句柄
	DWORD   dwIndex,			// 索引
	LPSTR   lpValueName,		// 键名缓冲区指针
	LPDWORD lpcchValueName,		// 键名缓冲区大小
	LPDWORD lpReserved,			// 保留 必须为NULL
	LPDWORD lpType,				// 键值类型 可为NULL
	LPBYTE  lpData,				// 键值缓冲区指针 可为NULL
	LPDWORD lpcbData			// 键值缓冲区大小 可为NULL
);								// 成功返回 ERROR_SUCCESS 失败返回 非0错误码

  示例

#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>

int main() 
{
    HKEY hKey;
    TCHAR subKey[] = TEXT("HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0");
    RegOpenKeyEx(HKEY_LOCAL_MACHINE, subKey, 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &hKey);

    DWORD cnt, maxNameLen, maxValueLen;
    // 查询键值信息
    RegQueryInfoKey(hKey, NULL, NULL, NULL, NULL, NULL, NULL, &cnt, &maxNameLen, &maxValueLen, NULL, NULL);
    printf("一共有%d个键\n最大键名:%d个字符\n最大键值:%d个字符\n", cnt, maxNameLen, maxValueLen);

    TCHAR* name = (TCHAR*)malloc(sizeof(TCHAR) * maxNameLen + 5);
    printf("\nHKEY_LOCAL_MACHINE\\HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0 有以下键名\n\n");

    // 循环遍历
    for (DWORD i = 0; i < cnt; i++)
    {
        ZeroMemory(name, maxNameLen);   // 内存置0
        // 必须+1 多个'\0'的空间
        DWORD nameSize = maxNameLen + 1;
        RegEnumValue(hKey, i, name, &nameSize, NULL, NULL, NULL, NULL);
        wprintf(L"%s\n", name);
    }
    
    free(name);
    RegCloseKey(hKey);

    return 0;
}

  运行结果

一共有11个键
最大键名:24个字符
最大键值:96个字符

HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0 有以下键名

Component Information
Identifier
Configuration Data
ProcessorNameString
VendorIdentifier
FeatureSet
~MHz
Update Revision
Update Status
Previous Update Revision
Platform Specific Field1

系统服务

  服务程序是windows上重要的一类程序,它们虽然不与用户进行界面交互,但是它们对于系统有着重要的意义。windows上为了管理服务程序提供了一个特别的程序:服务控制管理程序,系统上关于服务控制管理的API基本上都与这个程序打交道。下面通过对服务程序的操作来说明这些API函数

打开

  必须先建立一个到服务控制管理器的连接,即打开一个数据库。使用OpenSCManager

SC_HANDLE OpenSCManagerW(
	LPCWSTR lpMachineName,	// 主机名 NULL表示本机
	LPCWSTR lpDatabaseName,	// 数据库名 设置
	DWORD   dwDesiredAccess	// 访问权限 
);							// 成功返回 操作数据库句柄 失败返回 NULL

  dwDesiredAccess,常用值,更多值查询MSDN

SC_MANAGER_ALL_ACCESS			拥有所有权限
SC_MANAGER_CREATE_SERVICE		创建服务权限
SC_MANAGER_ENUMERATE_SERVICE	枚举服务权限
SERVICE_QUERY_CONFIG 			查询服务配置权限
SERVICE_START					启动服务权限
SERVICE_STOP					停止服务权限

  关闭服务句柄,使用CloseServiceHandle
  示例

#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>

int main()
{
	SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
	if (!hSCM) return;

	printf("OpenSCManager sucess");
	CloseServiceHandle(hSCM);
    
	return 0;
}

枚举

  枚举系统服务可以使用EnumServicesStatus,或者EnumServicesStatusEx

BOOL EnumServicesStatus(
	SC_HANDLE              hSCManager,			// 数据库句柄
	DWORD                  dwServiceType,		// 服务类型 SERVICE_WIN32(win32类型服务)
	DWORD                  dwServiceState,		// 服务状态
	LPENUM_SERVICE_STATUSW lpServices,			// 指向ENUM_SERVICE_STATUS结构体指针
	DWORD                  cbBufSize,			// 结构体缓冲区大小
	LPDWORD                pcbBytesNeeded,		// 实际需要缓冲区大小
	LPDWORD                lpServicesReturned,	// 服务个数
	LPDWORD                lpResumeHandle		// 额外的句柄
);

  dwServiceType,服务类型,主要有两种

SERVICE_DRIVER	驱动类型服务
SERVICE_WIN32	win32类型服务

  dwServiceState,服务状态,有以下值

SERVICE_ACTIVE		已启动的服务
SERVICE_INACTIVE	未启动的服务
SERVICE_STATE_ALL	所有服务

  ENUM_SERVICE_STATUS结构体定义

typedef struct _ENUM_SERVICE_STATUSA {
  LPSTR          lpServiceName;		// 服务名称
  LPSTR          lpDisplayName;		// 显示名称
  SERVICE_STATUS ServiceStatus;		// 指向SERVICE_STATUS结构体 下文讲解
} ENUM_SERVICE_STATUSA, *LPENUM_SERVICE_STATUSA;

  下图说明了服务名称和显示名称的关系,服务名称可能不等于显示名称。
  
  因为我们并不知道有多少个服务,也就无法申请一个合适的结构体大小。所以应该先调用EnumServicesStatus,来获取缓冲区的大小,注意首次调用EnumServicesStatuslpResumeHandle 必须为0
  示例代码

// 打开数据库
SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;

DWORD needSize = 0, serviceCnt = 0;
	
// 第一次调用获取缓冲区空间
BOOL ret = EnumServicesStatus(hSCM, SERVICE_WIN32, SERVICE_STATE_ALL, NULL, 0, &needSize, &serviceCnt, 0);
// 第一次调用返回0
if (ret) return;

LPENUM_SERVICE_STATUS servStatus;
// 动态分配空间
servStatus = (LPENUM_SERVICE_STATUS)malloc(sizeof(LPENUM_SERVICE_STATUS) * (needSize + 1));
if (!servStatus) return;

ret = EnumServicesStatus(hSCM, SERVICE_WIN32, SERVICE_STATE_ALL, servStatus, needSize, &needSize, &serviceCnt, NULL);
if (!ret) return;

// 循环显示服务信息
for (DWORD i = 0; i < serviceCnt; i++)
{
	printf("%s | %s\n", servStatus[i].lpDisplayName, servStatus[i].lpServiceName);
}

CloseServiceHandle(hSCM);
free(servStatus);

  运行结果

Acunetix | Acunetix
Acunetix Database | Acunetix Database
AllJoyn Router Service | AJRouter
Application Layer Gateway Service | ALG
Alibaba PC Safe Service | AlibabaProtect
... ...

查询

  查询服务信息之前,我们需要使用OpenService获取服务的句柄

SC_HANDLE OpenService(
	SC_HANDLE hSCManager,		// 数据库句柄
	LPCSTR    lpServiceName,	// 服务名称
	DWORD     dwDesiredAccess	// 访问权限
);								// 成功返回 服务句柄 失败返回 NULL

  常用访问权限,更多查阅msdn

SERVICE_ALL_ACCESS 		所有权限
SERVICE_QUERY_CONFIG 	查询配置权限
SERVICE_QUERY_STATUS 	查询状态权限
SERVICE_START 			启动服务权限
SERVICE_STOP 			停止服务权限
SERVICE_CHANGE_CONFIG 	改变配置权限
服务配置

  当成功拿到服务句柄后,可使用QueryServiceConfig查询服务配置信息,如果想获取更多的信息可使用QueryServiceConfig2

BOOL QueryServiceConfig(
	SC_HANDLE               hService,			// 服务句柄
	LPQUERY_SERVICE_CONFIGA lpServiceConfig,	// 指向QUERY_SERVICE_CONFIG结构体
	DWORD                   cbBufSize,			// 结构体缓冲区大小
	LPDWORD                 pcbBytesNeeded		// 实际需要缓冲区大小
);												// 成功返回 非0 失败返回 0

  QUERY_SERVICE_CONFIG结构体定义

typedef struct _QUERY_SERVICE_CONFIG {
  DWORD dwServiceType;		// 服务类型
  DWORD dwStartType;		// 启动类型
  DWORD dwErrorControl;		
  LPSTR lpBinaryPathName;	// 可执行文件路径
  LPSTR lpLoadOrderGroup;	
  DWORD dwTagId;
  LPSTR lpDependencies;		// 依赖项
  LPSTR lpServiceStartName;
  LPSTR lpDisplayName;		// 显示名称
} QUERY_SERVICE_CONFIG, *LPQUERY_SERVICE_CONFIG;

  示例

// 这里是服务名称 不是显示名称
LPCSTR servName = "NaturalAuthentication";
	
// 打开数据库
SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;

// 获取服务句柄
SC_HANDLE hSvc = OpenService(hSCM, servName, SERVICE_ALL_ACCESS);
if (!hSvc) return;

DWORD needSize = 0;
// 获取缓冲区大小
BOOL ret = QueryServiceConfig(hSvc, NULL, 0, &needSize);
if (ret)	return;

LPQUERY_SERVICE_CONFIG pSrvConfig = NULL;
pSrvConfig = (LPQUERY_SERVICE_CONFIG)malloc(sizeof(LPQUERY_SERVICE_CONFIG) * needSize);
if (!pSrvConfig) return;

ret = QueryServiceConfig(hSvc, pSrvConfig, needSize, &needSize);
if (!ret) return;

printf("可执行文件路径: %s\n", pSrvConfig->lpBinaryPathName);
	
printf("启动类型: ");
DWORD startType = pSrvConfig->dwStartType;
if (startType == SERVICE_AUTO_START)
{
	printf("自动");
}
else if (startType == SERVICE_DEMAND_START)
{
	printf("手动");
}
else if (startType == SERVICE_DISABLED)
{
	printf("禁用");
}

printf("\n显示名称: %s", pSrvConfig->lpDisplayName);

free(pSrvConfig);
CloseServiceHandle(hSCM);
CloseServiceHandle(hSvc);

  运行结果

可执行文件路径: C:\windows\system32\svchost.exe -k netsvcs -p
启动类型: 手动
显示名称: 自然身份验证
服务状态

  使用QueryServiceStatus,或者QueryServiceStatusEx获取服务状态

BOOL QueryServiceStatus(
	SC_HANDLE        hService,			// 服务句柄
	LPSERVICE_STATUS lpServiceStatus	// 指向SERVICE_STATUS结构体
);										// 成功返回 非0 失败返回 0

  SERVICE_STATUS 结构体定义

typedef struct _SERVICE_STATUS {
  DWORD dwServiceType;				// 服务类型
  DWORD dwCurrentState;				// 当前服务状态
  DWORD dwControlsAccepted;			// 允许的操作
  DWORD dwWin32ExitCode;
  DWORD dwServiceSpecificExitCode;
  DWORD dwCheckPoint;
  DWORD dwWaitHint;
} SERVICE_STATUS, *LPSERVICE_STATUS;

  dwCurrentState,服务状态常用值

SERVICE_STOPPED			已停止
SERVICE_RUNNING 		正在运行
SERVICE_PAUSED			已暂停

  示例代码

LPCSTR servName = "tzautoupdate";
SERVICE_STATUS sTs = { 0 };

SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;

SC_HANDLE hSvc = OpenService(hSCM, servName, SERVICE_QUERY_STATUS);
if (!hSvc) return;

BOOL ret = QueryServiceStatus(hSvc, &sTs);
if (!ret) return;

DWORD status = sTs.dwCurrentState;
if (status == SERVICE_RUNNING)
{
	printf("正在运行");
}
else if (status == SERVICE_STOPPED)
{
	printf("已停止");
}

修改

服务配置

  修改服务配置用到ChangeServiceConfig函数,要修改更多配置参数请用ChangeServiceConfig2

BOOL ChangeServiceConfig(
	SC_HANDLE hService,				// 服务句柄
	DWORD     dwServiceType,		// 服务类型
	DWORD     dwStartType,			// 启动类型
	DWORD     dwErrorControl,
	LPCWSTR   lpBinaryPathName,		// 可执行文件路径
	LPCWSTR   lpLoadOrderGroup,
	LPDWORD   lpdwTagId,
	LPCWSTR   lpDependencies,		// 依赖项
	LPCWSTR   lpServiceStartName,
	LPCWSTR   lpPassword,
	LPCWSTR   lpDisplayName			// 显示名称
);									// 成功返回 非0 失败返回 0

  函数中传递的都是服务的新信息,如果希望改变则填入相应的值,如果不想改变则对于DWORD类型的成员来说填入SERVICE_NO_CHANGE,对于指针类型的只需要填入NULL即可。
  示例代码

// 改变启动类型为手动
DWORD newType = SERVICE_DEMAND_START;
LPCSTR servName = "AntiCheatExpert Service";

SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;

SC_HANDLE hSvc = OpenService(hSCM, servName, SERVICE_ALL_ACCESS);
if (!hSvc) return;

BOOL ret = ChangeServiceConfig(hSvc, SERVICE_NO_CHANGE, newType, SERVICE_NO_CHANGE, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
if (ret)
	printf("sucess");
else
	printf("fail");

CloseServiceHandle(hSvc);
CloseServiceHandle(hSCM);
服务状态

  启动服务StartService

BOOL StartService(
	SC_HANDLE hService,				// 服务句柄
	DWORD     dwNumServiceArgs,		// 启动参数的个数
	LPCSTR    *lpServiceArgVectors	// 参数列表指针
);

  这个函数类型与main函数外部传参,传递命令行参数给程序,以便实现程序与用户的交互。当第二个参数为0时,第三个参数为NULL

  要修改成其他状态,要用ControlService

BOOL ControlService(
	SC_HANDLE        hService,			// 服务句柄
	DWORD            dwControl,			// 新状态类型
	LPSERVICE_STATUS lpServiceStatus	// 原始状态
);

  示例

LPCSTR servName = "NaturalAuthentication";
// 启动服务
DWORD newStatus = SERVICE_RUNNING;
SERVICE_STATUS sTs = { 0 };

SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;

SC_HANDLE hSvc = OpenService(hSCM, servName, SERVICE_ALL_ACCESS);
if (!hSvc) return;
	
BOOL ret = QueryServiceStatus(hSvc, &sTs);
if (!ret) return;

	
ret = FALSE;
// 相同状态
if (sTs.dwCurrentState == newStatus)
{
	ret = TRUE;
}
// 运行服务
else if (sTs.dwCurrentState == SERVICE_STOPPED && newStatus == SERVICE_RUNNING)
{	
	ret = StartService(hSvc, 0, NULL); 	 // 启动服务
}
// 停止服务
else if ((sTs.dwCurrentState == SERVICE_RUNNING || sTs.dwCurrentState == SERVICE_PAUSED) && newStatus == SERVICE_STOPPED)
{
	ret = ControlService(hSvc, SERVICE_CONTROL_STOP, &sTs);
}
// 暂停服务
else if (sTs.dwCurrentState == SERVICE_RUNNING && newStatus == SERVICE_PAUSED)
{
	ret = ControlService(hSvc, SERVICE_CONTROL_PAUSE, &sTs);
}

if (!ret)
	printf("fail");
else
	printf("sucess");

CloseServiceHandle(hSCM);
CloseServiceHandle(hSvc);

创建

  创建服务使用CreateService函数

SC_HANDLE CreateService(
	SC_HANDLE hSCManager,			// 数据库句柄
	LPCWSTR   lpServiceName,		// 服务名
	LPCWSTR   lpDisplayName,		// 显示名
	DWORD     dwDesiredAccess,		// 权限
	DWORD     dwServiceType,		// 服务类型 
	DWORD     dwStartType,			// 启动类型
	DWORD     dwErrorControl,		// 错误操作
	LPCWSTR   lpBinaryPathName,		// 可执行文件路径
	LPCWSTR   lpLoadOrderGroup,		
	LPDWORD   lpdwTagId,
	LPCWSTR   lpDependencies,
	LPCWSTR   lpServiceStartName,
	LPCWSTR   lpPassword
);									// 成功返回 服务句柄 失败返回 NULL

  后五项基本都用不到,填NULL即可。服务类型,一般填写SERVICE_WIN32_OWN_PROCESS,表示服务类型是win32类型拥有独立进程的服务。

  默认创建服务是没有服务描述信息的,如果要添加要使用ChangeServiceConfig2,这里就不具体讲解了。

  示例

SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;
LPCSTR path = "D:\\1.exe";
SC_HANDLE hService = CreateService(hSCM, "test", "test", SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, path, NULL, NULL, NULL, NULL, NULL);
if (!hService) return;

// 修改服务描述信息
SERVICE_DESCRIPTION servDesc = { 0 };
servDesc.lpDescription = "this is desc";
ChangeServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION, &servDesc);

printf("sucess\n");
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);

删除

  删除服务使用的函数是DeleteService,注意的是这个函数只对已停止的服务起作用,所以在删除之前需要将服务停止

BOOL DeleteService(
	SC_HANDLE hService	// 服务句柄
);						// 成功返回 非0 失败返回 0

  示例

SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCM) return;

SC_HANDLE hServ = OpenService(hSCM, "test", SERVICE_ALL_ACCESS);
if (!hServ) return;

SERVICE_STATUS sTs = { 0 };
// 尝试停止服务
BOOL ret = ControlService(hServ, SERVICE_CONTROL_STOP, &sTs);
if (!ret) return;

// 删除服务
if (!DeleteService(hServ)) return;

printf("ok");
CloseServiceHandle(hSCM);
CloseServiceHandle(hServ);

动态链接库

  动态链接库不能直接运行,不能接收消息.它们是一些独立的文件,其中包含能被可执行程序或其它DLL调用来完成某项工作的函数。只有在其它模块调用动态链接库中的函数时,它才发挥作用。

  微软任何一个版本的Windows操作系统,动态链接库(DLL)都是其核心和基础。

创建链接库

  链接库的创建有两种方法,这里先讲第一种。vs中创建一个项目。分别添加两个文件进项目
  DllTest.h

#pragma once

__declspec(dllexport) void SayHello();

  DllTest.c

#include <stdio.h>
#include "DllTest.h"

void SayHello()
{
	printf("hello dll");
}

  项目结构
  
  来到项目属性下的常规下的配置类型,选择动态库(.dll)
  
  之后再生成解决方案,将会成功生成dll
  再新建一个项目,将生成的DllTest.dllDllTest.libDllTest.h放到新项目目录下
  

无参调用

方法一

  将DllTest.h添加进头文件
  
  demo.c

#include <stdio.h>
#include "DllTest.h"

#pragma comment(lib,"DllTest.lib")

int main()
{
	SayHello();
	return 0;
}

  运行结果

hello dll
方法二

  不需要将DllTest.h添加进头文件
  demo.c

#include <stdio.h>
#include <Windows.h>

int main()
{
	HMODULE hModule = LoadLibrary("DllTest.dll");
	if (!hModule)
	{
		printf("load dll fail\n");
		return 0;
	}
	FARPROC fn = GetProcAddress(hModule,"SayHello");
	fn();
	FreeLibrary(hModule);
	return 0;
}

  运行结果

hello dll

  可有些时候,会调用GetModuleHandle来加载dll。GetModuleHandleLoadLibrary的区别是,GetModuleHandle是返回一个已经映射进调用进程地址空间的模块的句柄,并不增加引用计数。LoadLibrary是把一个模块映射进调用进程的地址空间,需要时增加引用计数。

  通俗点说如果dll已经加载进内存了,应该使用GetModuleHandle。反之使用LoadLibrary
  demo.c,示例

#include <stdio.h>
#include <Windows.h>

int main()
{
	HMODULE hModule = GetModuleHandle("DllTest.dll");
	// dll没加载进内存
	if (hModule == NULL)
	{
		printf("DllTest.dll not load in memory\n");
		// 将dll加载进内存
		hModule = LoadLibrary("DllTest.dll");
		if (!hModule)
		{
			printf("load dll fail\n");
			return 0;
		}
	}
	
	FARPROC fn = GetProcAddress(hModule,"SayHello");
	fn();
	FreeLibrary(hModule);
	return 0;
}

有参调用

  DllTest.h

#pragma once

__declspec(dllexport) int Add(int a,int b);

  DllTest.c

#include <stdio.h>
#include "DllTest.h"

int Add(int a, int b)
{
	return a + b;
}

  将生成的文件老规矩复制到新项目目录下

方法一

  将DllTest.h添加进头文件后
  demo.c

#include <stdio.h>
#include "DllTest.h"

#pragma comment(lib,"DllTest.lib")

int main()
{
	printf("%d", Add(1, 2));
	return 0;
}

  运行结果

3
方法二

  demo.c,如果有参函数无返回值,请使用无参调用方法二

#include <stdio.h>
#include <Windows.h>

typedef int (*Add)(int a, int b);

int main()
{
	HMODULE hModule = LoadLibrary("DllTest.dll");
	Add add = (Add)GetProcAddress(hModule,"Add");
	printf("%d", add(1, 2));
	FreeLibrary(hModule);
	return 0;
}

  运行结果

3

DllMain

  每一个DLL都会有一个入口函数,它是DLLMain,和在c开发当中的main函数是一样的都作为程序dll的入口段,系统在特定环境下会调用DLLMain。需要注意的是并不是所有的dll都必须要有DllMain,参考前面创建dll的例子,

  Dll被调用的四种状态

DLL_PROCESS_ATTACH 被进程装载时
DLL_THREAD_ATTACH  被线程装载时
DLL_THREAD_DETACH  被线程卸载时
DLL_PROCESS_DETACH 被进程卸载时

  这是第二种创建dll的方法,只需要新建项目的时候选择Dll即可
  
  改动后的dllmain.cpp

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <stdio.h>

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        printf("DLL_PROCESS_ATTACH\n");
        break;
    case DLL_THREAD_ATTACH:
        printf("DLL_THREAD_ATTACH\n");
        break;
    case DLL_THREAD_DETACH:
        printf("DLL_THREAD_DETACH\n");
        break;
    case DLL_PROCESS_DETACH:
        printf("DLL_PROCESS_DETACH\n");
        break;
    }
    return TRUE;
}

  生成解决方案后,只会生成dll,不会生成lib文件。因为没有导出函数,将dll拷贝过去后,分别说明这4个case什么情况下会进入
  demo.c

#include <stdio.h>
#include <Windows.h>


int main()
{
	HMODULE hModule = LoadLibrary("DllTest.dll");
	FreeLibrary(hModule);
	return 0;
}

  运行结果

DLL_PROCESS_ATTACH
DLL_PROCESS_DETACH

  再看一种情况,子线程加载dll,主线程卸载dll

#include <stdio.h>
#include <Windows.h>

HMODULE hModule;

DWORD WINAPI Thread(LPVOID lp)
{
	hModule = LoadLibrary("DllTest.dll");
}

int main()
{
	HANDLE hThread = CreateThread(NULL, 0, Thread, NULL, 0, NULL);
	WaitForSingleObject(hThread, INFINITE);
	FreeLibrary(hModule);
	CloseHandle(hThread);
	return 0;
}

  运行结果

DLL_PROCESS_ATTACH
DLL_THREAD_DETACH
DLL_PROCESS_DETACH

  子线程卸载dll

#include <stdio.h>
#include <Windows.h>

HMODULE hModule;

DWORD WINAPI Thread(LPVOID lp)
{
	FreeLibrary(hModule);
}

int main()
{
	hModule = LoadLibrary("DllTest.dll");
	HANDLE hThread = CreateThread(NULL, 0, Thread, NULL, 0, NULL);
	WaitForSingleObject(hThread, INFINITE);
	CloseHandle(hThread);
	return 0;
}

  运行结果

DLL_PROCESS_ATTACH
DLL_THREAD_ATTACH
DLL_PROCESS_DETACH
查看评论 -
评论