Windows API教程(四) 进程编程

茵蒂克丝

博主很想详细的介绍一下进程神马的、内存神马的,但是真的整理起来发现要做到让有C基础的学生能很好理解,一看就懂还是感觉很有难度。也许是博主水平还不够吧。

作为90后程序员,博主还是不走寻常路吧,到了进程这里开始,我们反过来学一下,顺便补一补一些基础知识。

上一节文件编程的时候,有说过文件流的状态,当我们开始操作一个文件,这个文件的打开关闭都由程序控制,而其中的内容是可以动态增减的,这样的情况类似水流一般,我们把这样的文件状态称作文件流。

那么实际上,用户的输入、输出这一类的“缓存”也就是这样的状态,通常是叫做输入流和输出流,除了这两个还有一个错误流。C语言中的标准库(stdio.h)中有定义这三个常用的流,也即stdin(标准输入流)、stdout(标准输出流)和stderr(标准错误流)。如果大家仔细深究 stdio.h 的头文件会发现,这些流的定义跟文件的定义实际上是同样的。

好了,回到终点,那么既然这些东西都已经是 “流” 的状态了,那么也就意味着我们不需要去打开什么文件,来生成什么文件流,直接可以用文件操作的函数来操作这些输入输出流。

例如:

scanf("%d", &i);
实际上就是 fscanf(stdin, "%d", &i); 的简写而已。

PS:这实际上应该算是C语言的基础知识,不能理解为什么大学都不教(博主观点)

CMD 工具集

那么,开始有讲过的,windows编程其实就是去查API,然后用API。这一节讲进程,我们就通过编写一个学习用的cmd工具集来一步步走向进程编程。

首先我们要从输入流来读取用户的输入,这一操作在标准库中可以使用 fgets 函数来实现,调用 windows API 的话我们就用 ReadFile 就可以了。

BOOL WINAPI ReadFile(
  _In_         HANDLE hFile,				// 读取的文件句柄
  _Out_        LPVOID lpBuffer,				// 保存读取缓冲字符数组
  _In_         DWORD nNumberOfBytesToRead,	// 缓冲数组的大小
  _Out_opt_    LPDWORD lpNumberOfBytesRead,	// 实际读出的大小
  _Inout_opt_  LPOVERLAPPED lpOverlapped 	// 异步IO文件结构体
);

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

void welcome();

int main()
{
	char Command_str[MAX_PATH];
	DWORD Command_len;
	HANDLE hConsoleInput;  

    // 获取输出流的句柄
    hConsoleInput = GetStdHandle(STD_INPUT_HANDLE);  

	// 输出欢迎信息
	welcome();

	while(1)
	{
		// 清空命令字符串
		memset(&Command_str, 0, MAX_PATH);
		// 输出提示符
		printf("nLscmd>;");
		// 读取输入流
		ReadFile(
			hConsoleInput,	// 文件句柄
			Command_str,	// 获取内容的缓冲字符数组
			MAX_PATH,		// 缓冲数组大小
			&Command_len,	// 实际读出的大小
			NULL);

		printf("接收到命令:[%s]", Command_str);
	}
}

void welcome()
{
	printf("Lellansin's CMD Tool [版本 0.0.1]n");
	printf("学习自制 (c) www.lellansin.com 欢迎交流n");
}

可以简单的看到,我们的输入都有获取到,并且输出的时候还连带我们输的回车(换行符)

定制两个简单的命令

#include <Windows.h>;
#include <stdio.h>;
#include <string.h>;	// for strcmp
#include <stdlib.h>;	// for exit

void welcome();
void command_switch(char *cmd_str);

int main()
{
	char Command_str[MAX_PATH];
	DWORD Command_len;
	HANDLE hConsoleInput;  

    // 获取输出流的句柄
    hConsoleInput = GetStdHandle(STD_INPUT_HANDLE);  

	// 输出欢迎信息
	welcome();

	while(1)
	{
		// 清空命令字符串
		memset(&Command_str, 0, MAX_PATH);
		// 输出提示符
		printf("nLscmd>;");
		// 读取输入流
		ReadFile(
			hConsoleInput,	// 文件句柄
			Command_str,	// 获取内容的缓冲字符数组
			MAX_PATH,		// 缓冲数组大小
			&Command_len,	// 实际读出的大小
			NULL);

		command_switch(Command_str);
	}
}

void command_switch(char *cmd_str)
{
	char cmd_tmp[MAX_PATH]={0};
	char *pstr = cmd_str, *ptmp = cmd_tmp;

	// 一直赋值到换行之前
	while(*pstr != '\r' && *pstr != '\n')
	{
		*ptmp++ = *pstr++;
	}
	// printf("收到命令:[%s]n", cmd_tmp);

	// 判断命令
	if( strcmp(cmd_tmp, "hi") == 0 )
	{
		printf("你好~");
	} else if ( strcmp( cmd_tmp, "exit" ) == 0 )
	{
		exit(0);
	}else
	{
		printf("Error: 命令未找到n");
	}
}

void welcome()
{
	printf("Lellansin's CMD Tool [版本 0.0.1]n");
	printf("学习自制 (c) www.lellansin.com 欢迎交流n");
}

创建进程

那么到现在为止,我们的cmd工具已经有了一个基本的雏形,接下来要做的就是调用我们原来写的命令。

有的同学可能会想到直接用 stdlib.h 里的 system 函数来解析命令,这个是可以的。但是这个函数是C标准库的,并不是windows系统的API,它的效率是非常的低的。而且,这里是在在讲的是 windows 编程所以读者请不要偷懒哦。

首先,我们来了解一下进程的概念。当一个程序运行起来的时候,操作系统一定要为这个程序(Program)创建一个进程(Process),以方便管理。有些同学也知道每个程序跑起来之后都被分配了一个进程ID,实际上在进程调度的时候,操作系统还会为我们的程序分配进程的一些列事务:资源、虚拟内存地址空间、系统调用接口、优先级、环境变量等等。

所以进程实际上可以理解成一个运行起来的程序,就如进程的英文原本的意思(Process)一样这是个运行过程。每个运行起来的程序都要被操作系统安排进程(过程)的来管理。

如果你能想象这一过程,那么就不难理解,想要运行一个程序必须要创建一个进程。所以,这里我们就需要为我们原本所写的程序创建一个进程,就可以调用了。

BOOL WINAPI CreateProcess(
  _In_opt_     LPCTSTR lpApplicationName,	// 启动程序路径
  _Inout_opt_  LPTSTR lpCommandLine,		// 启动程序的命令行代码
  _In_opt_     LPSECURITY_ATTRIBUTES lpProcessAttributes,	// 进程属性
  _In_opt_     LPSECURITY_ATTRIBUTES lpThreadAttributes,	// 线程属性
  _In_         BOOL bInheritHandles,		// 是否继承句柄
  _In_         DWORD dwCreationFlags,		// 标识和优先级
  _In_opt_     LPVOID lpEnvironment,		// 环境变量设置
  _In_opt_     LPCTSTR lpCurrentDirectory,	// 当前目录设置
  _In_         LPSTARTUPINFO lpStartupInfo,	// 启动信息设置
  _Out_        LPPROCESS_INFORMATION lpProcessInformation 	// 新进程的信息
);

MSDN 文档: CreateProcess function

返回值

如果函数执行成功,返回非零值。如果函数执行失败,返回零,可以使用GetLastError函数获得错误的附加信息。


#include <Windows.h>;
#include <stdio.h>;
#include <string.h>;	// for strcmp
#include <stdlib.h>;	// for exit

void welcome();
void command_switch(char *cmd_str);
BOOL CreateChildProcess(char *cmd_str);

int main()
{
	char Command_str[MAX_PATH];
	DWORD Command_len;
	HANDLE hConsoleInput;  

    // 获取输出流的句柄
    hConsoleInput = GetStdHandle(STD_INPUT_HANDLE);  

	// 输出欢迎信息
	welcome();

	while(1)
	{
		// 清空命令字符串
		ZeroMemory(&Command_str, MAX_PATH);
		// 输出提示符
		printf("nLscmd>;");
		// 读取输入流
		ReadFile(
			hConsoleInput,	// 文件句柄
			Command_str,	// 获取内容的缓冲字符数组
			MAX_PATH,		// 缓冲数组大小
			&Command_len,	// 实际读出的大小
			NULL);

		command_switch(Command_str);
	}
}

void command_switch(char *cmd_str)
{
	char *pstr = cmd_str;

	// 遍历到换行之前
	while(*pstr != '\r' && *pstr != '\n')
	{
		*pstr++;
	}
	// 覆盖换行
	*pstr = '\0';
	// printf("收到命令:[%s]n", cmd_str);

	// 判断命令
	if( strcmp(cmd_str, "hi") == 0 )
	{
		printf("你好~ 欢迎使用 Lellansin 的cmd工具n");
	} else if ( strcmp( cmd_str, "exit" ) == 0 )
	{
		exit(0);
	}else
	{
		// 创建子进程
		CreateChildProcess(cmd_str);
	}
}

BOOL CreateChildProcess(char *cmd_str)
{
	STARTUPINFO start_info;
	PROCESS_INFORMATION process_info;
	BOOL flag;

	// 将启动信息结构清零 ( 相当于 memset 0, 不过效率更高 )
	ZeroMemory( &start_info, sizeof(start_info) );
	// 设置结构大小,cb属性应为结构的大小
	start_info.cb = sizeof(start_info);
	// 将进程信息结构清零
	ZeroMemory( &process_info, sizeof(process_info) );

	flag = CreateProcess(
		NULL,			// 不传程序路径, 使用命令行
		cmd_str,		// 命令行命令
		NULL,			// 不继承进程句柄(默认)
		NULL,			// 不继承线程句柄(默认)
		FALSE,			// 不继承句柄(默认)
		0,				// 没有创建标志(默认)
		NULL,			// 使用默认环境变量
		NULL,			// 使用父进程的目录
		&start_info,    // STARTUPINFO 结构
		&process_info );// PROCESS_INFORMATION 保存相关信息

	if ( !flag )
	{
		// 创建失败
		printf( "Error: 命令未找到 (%d).n", GetLastError() );
		return 0;
	}

	// 等待子进程结束
	// 使用到了通过 PROCESS_INFORMATION 结构体获取子进程的句柄 hProcess
	WaitForSingleObject( process_info.hProcess, INFINITE );
	// 关闭进程句柄和线程句柄
	CloseHandle( process_info.hProcess );
	CloseHandle( process_info.hThread );

	return 1;
}

void welcome()
{
	printf("Lellansin's CMD Tool [版本 0.0.1]n");
	printf("学习自制 (c) www.lellansin.com 欢迎交流n");
}

查看进程

CreateToolhelp32Snapshot

获取当前的系统快照

HANDLE WINAPI CreateToolhelp32Snapshot(
  _In_  DWORD dwFlags,
  _In_  DWORD th32ProcessID
);

dwFlags [输入参数]
指明所需的系统快照。该参数可以是如下列表中的一个或多个值。

Value Meaning
TH32CS_INHERIT (0x80000000) 表明快照 (snapshot) 句柄是可以被继承 (inheritable) 。
TH32CS_SNAPALL 包含所有系统中的所有进程和线程,加上指定进程中堆和模块的信息( 通过th32ProcessID特别指明的进程id,如果为0则无)。相当于指定 TH32CS_SNAPHEAPLIST, TH32CS_SNAPMODULE, TH32CS_SNAPPROCESS, 和 TH32CS_SNAPTHREAD 通过或运算(‘|’) 联合使用
TH32CS_SNAPHEAPLIST (0x00000001) 快照中包含通过 th32ProcessID 指定进程的所有堆 (heaps) 信息。 想要列举堆的信息,请查询 Heap32ListFirst
TH32CS_SNAPMODULE (0x00000008) 快照中包含通过 th32ProcessID 指定进程的所有modules信息。 想要列举modules信息,请查询 Module32First 。 如果函数失败报错ERROR_BAD_LENGTH,请重试此函数直到成功。
TH32CS_SNAPMODULE32 (0x00000010) 快照中包含所有通过 th32ProcessID 指定进程的32位模块(modules)信息(通过64位调用也是返回32位)。 该flag可以与 TH32CS_SNAPMODULE 或者 TH32CS_SNAPALL 联合使用。如果函数失败报错 ERROR_BAD_LENGTH ,请重试此函数直到成功。
TH32CS_SNAPPROCESS (0x00000002) 快照中包含系统中的所有进程 (processes) 信息。 想要列举 processes 信息,请查询 Process32First
TH32CS_SNAPTHREAD (0x00000004) 快照中包含系统中的所有线程 (threads) 信息。想要列举 threads 信息, 请查询 Thread32First

确认是否属于某个指定的进程,可以在列举线程信息的时候,拿进程标识与 THREADENTRY32 结构体的 th32OwnerProcessID 成员相比较来判断 。

th32ProcessID [输入参数]
如果为0则获取所有进程的快照,如果不为零则获取该进程id的信息

返回值

成功则返回该快照的句柄

遍历进程信息

BOOL WINAPI Process32First(
  _In_     HANDLE hSnapshot,
  _Inout_  LPPROCESSENTRY32 lppe
);
BOOL WINAPI Process32Next(
  _In_   HANDLE hSnapshot,
  _Out_  LPPROCESSENTRY32 lppe
);

与文件目录类似, Process32First 用于获取第一个进程的信息, Process32Next 用于获取下一个进程的信息。参数一 hSnapshot 是指获取的进程快照,参数二 lppe 则是一个指向 PROCESSENTRY32 结构体的指针(LP + PROCESSENTRY32)。

PROCESSENTRY32 结构体

系统进程快照中某一个进程信息的具体结构

typedef struct tagPROCESSENTRY32
{
    DWORD   dwSize;
    DWORD   cntUsage;
    DWORD   th32ProcessID;          // this process
    ULONG_PTR th32DefaultHeapID;
    DWORD   th32ModuleID;           // associated exe
    DWORD   cntThreads;
    DWORD   th32ParentProcessID;    // this process's parent process
    LONG    pcPriClassBase;         // Base priority of process's threads
    DWORD   dwFlags;
    CHAR    szExeFile[MAX_PATH];    // Path
} PROCESSENTRY32;

MSDN 文档 :
CreateToolhelp32Snapshot
Process32First
Process32Next
PROCESSENTRY32 结构体

ps 查看进程列表

再来新建一个项目名字叫做 ps ,记得要是空项目随后添加如下代码


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

#include <TlHelp32.h>
/*
TlHelp32.h for
	PROCESSENTRY32
	CreateToolhelp32Snapshot()
	Process32First()
	Process32Next()
*/

int main(int argc, char const *argv[])
{
	HANDLE hSnapshot;
	HANDLE hProcess;
	PROCESSENTRY32 pe32;
	// 获取进程快照
	hSnapshot = CreateToolhelp32Snapshot( TH32CS_SNAPPROCESS, 0 );
	if( hSnapshot == INVALID_HANDLE_VALUE )
	{
		printf( "CreateToolhelp32Snapshot (of processes) 失败" );
		return ;
	}
	// 设置输入参数,结构的大小
	pe32.dwSize = sizeof( PROCESSENTRY32 );

	// 开始列举进程信息
	if( !Process32First( hSnapshot, &pe32 ) )
	{
		printf( "Process32First() 失败" );
		CloseHandle( hSnapshot ); // 关闭句柄
		return ;
	}

	printf("进程IDt父进程t线程数t优先级t进程名"); // 基本优先级
	do {
		// 打印进程相关信息
		printf( "n%u", pe32.th32ProcessID );	// 进程id
		printf( "t%u", pe32.th32ParentProcessID );	// 父进程id
		printf( "t%d", pe32.cntThreads );		// 线程数
		printf( "t%d", pe32.pcPriClassBase );	// 基本优先级
		printf( "t%s", pe32.szExeFile );		// 进程名

	} while( Process32Next( hSnapshot, &pe32 ) );

	CloseHandle( hSnapshot );	//关闭句柄

	return ;
}

因为是在命令行的环境下运行,所以直接F7生成exe即可,不需要直接执行。如果是拿lscmd来做实验的话,程序写好了还要找到exe复制到path目录下,感觉有点麻烦,为了方便大家也可以直接将程序的生成目录,改成博主开始使用的 F:mytools目录 (Visual Studio 如何设置生成目录)

测试数据:

Lellansin's CMD Tool [版本 0.0.1]
学习自制 (c) www.lellansin.com 欢迎交流

Lscmd>ps
进程ID  父进程  线程数  优先级  进程名
0       0       2       0       [System Process]
4       0       106     8       System
292     4       2       11      smss.exe
392     380     49      8       avgrsa.exe
432     392     10      8       avgcsrva.exe
640     632     10      13      csrss.exe
704     632     3       13      wininit.exe
720     696     13      13      csrss.exe
764     696     3       13      winlogon.exe
812     704     10      9       services.exe
824     704     7       9       lsass.exe
832     704     11      8       lsm.exe
940     812     10      8       svchost.exe
1020    812     9       8       svchost.exe
724     812     19      8       svchost.exe
888     812     22      8       svchost.exe
1732    812     33      8       avgwdsvc.exe
1816    812     4       8       sqlwriter.exe
1852    812     6       8       svchost.exe
1924    812     4       8       vmware-usbarbitrator64.exe
1976    812     6       8       vmnat.exe
...... 省略N条 ......
7008    1676    3       8       notepad++.exe
6204    1500    11      6       chrome.exe
6600    1500    11      6       chrome.exe
6628    1500    11      6       chrome.exe
6920    1500    11      6       chrome.exe
7716    3800    10      8       MSBuild.exe
5980    724     5       8       audiodg.exe
4132    1500    11      8       chrome.exe
5048    1500    11      8       chrome.exe
4976    2052    6       8       vcpkgsrv.exe
8072    7196    5       8       mspdbsrv.exe
4724    3800    8       8       vcpkgsrv.exe
3844    720     5       8       conhost.exe
7124    3400    1       8       ps.exe
Lscmd>

终止进程

OpenProcess 获取进程句柄

HANDLE WINAPI OpenProcess(
  _In_  DWORD dwDesiredAccess,
  _In_  BOOL bInheritHandle,
  _In_  DWORD dwProcessId
);

参数

dwDesiredAccess [输入参数]

进程句柄对该进程的访问权限。详见进程访问权限

bInheritHandle [输入参数]

句柄是否继承(填写 TRUE 或者 FALSE),如果继承(TRUE)那么如果该进程创建子进程的时候这个句柄也会被继承到子进程。

dwProcessId [输入参数]

将要打开(open)的进程ID。

如果指定进程是系统进程 (0x00000000),该函数会失败并且最后的错误会是ERROR_INVALID_PARAMETER。如果指定进程是系统空闲进程(原文:Idle process 博主备注:一种内存管理进程,准确的来讲名字叫做 System Idle Process)或者某个子系统进程(原文:CSRSS processes 博主备注:准确的说是 Client Server Runtime Process 任务管理器里可以看到它 csrss.exe),该函数会失败并且最后的错误会是ERROR_ACCESS_DENIED 因为其访问限制会阻止用户级别(user-level)的代码获取其句柄。

返回值

如果函数成功,返回值为指定进程的句柄。
如果函数失败,返回值为NULL。可以使用 GetLastError 函数获得错误的附加信息。

MSDN: OpenProcess

TerminateProcess

终止(Terminate) + 进程(Process) = 终止进程(TerminateProcess)

BOOL WINAPI TerminateProcess(
  _In_  HANDLE hProcess,
  _In_  UINT uExitCode
);

参数一 hProcess [输入参数]

待终止的进程句柄。该句柄必须拥有 PROCESS_TERMINATE (进程终止) 的权限。更多信息,请查看进程的安全与访问权限

参数二 uExitCode [输入参数]

设置通过使用该方法退出的进程与线程的退出码?(exit code 嘛,博主觉得返回值更好理解一些)。 使用GetExitCodeProcess函数可以可以获取到进程退出时返回的值。使用GetExitCodeThread函数可以获取到线程退出时返回的值。

返回值
函数执行成功,返回值为非零。
函数执行失败,返回值为零。更多信息可以通过GetLastError函数获取。

kill 终止进程

新建一个空项目名为 kill ,随后代码如下


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

void help();

int main(int argc, char const *argv[])
{
	int ProcessID;
	HANDLE hProcess;

	// 如果只有一个参数
	if(argc == 1)
	{
		help();
		return 0;
	}

	// 如果有两个参数
	if(argc == 2)
	{
		// 获取进程id
		ProcessID = atoi(argv[1]);
		// 获取进程句柄
		hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, (DWORD)ProcessID );
		// 终止进程
		TerminateProcess(hProcess, 0);
	}
}

void help()
{
	printf("终止进程n");
	printf("kill <进程id>n");
}

使用演示:

打开两个lscmd,第一个执行

Lellansin's CMD Tool [版本 0.0.1]
学习自制 (c) www.lellansin.com 欢迎交流

Lscmd>cmd.exe
Microsoft Windows [版本 6.1.7601]
版权所有 (c) 2009 Microsoft Corporation。保留所有权利。

C:UsersLellansin>

执行cmd.exe之后发现程序切换到了cmd之下,这个时候我们在打开第二个 lscmd ,(注:如果你的 lscmd 放在path路径之下的话,博主的也就是F:mytools,可以直接 win+r 调出运行,输入 lscmd 回车即可调用出来)

在我们的第二个CMD工具集中使用ps查看进程

Lellansin's CMD Tool [版本 0.0.1]
学习自制 (c) www.lellansin.com 欢迎交流

Lscmd>ps
进程ID  父进程  线程数  优先级  进程名
... 其余省略 ...
1676    1956    36      8       explorer.exe
7904    1676    1       8       lscmd.exe
5844    7904    1       8       cmd.exe
4180    1676    1       8       lscmd.exe
5568    4180    1       8       ps.exe

#通过观察进程列表可以发现我们使用lscmd打开的cmd.exe
#其进程id是5844,那么我们用新鲜的kill.exe来试试它

Lscmd>kill 5844

#运行之后发现第一个lscmd中的cmd.exe退出来了,牛刀小试、程序ok

更多改进

1.首先工具集本身还缺乏一些过多的指令,比如我们常用的cd(切换目录)命令,我们可以继续自定义一些命令

2.实际上关于一个进程我们还可以再获取更多的信息,也即我们的ps命令可以修改一下多加一个参数。形如: ps 1676 这样调用时则列举出进程id为1676的搜有信息,这个信息可以有很多,包括其进程具体的:

  • 线程信息(Thread32FirstThread32Next
  • 模块信息(Module32FirstMoudle32Next
  • 堆信息(Heap32ListFirstHeap32Next
  • 内存使用情况(GetProcessMemoryInfo

3.kernel32.dll是windows的核心DLL,很多内核级别的API都需要从其中导出,其实上述的例子中关于查看进程信息的大部分介绍的都是该DLL导出的函数(如果上一个问题又解决的,仔细查看模块信息会可以找到到程序调用掉用的每一个DLL),不过不是在Windows API中而是在Tool help API中,需要引用的是 Tlhelp32.h (不是Windows.h)

关于进程一些信息操作,除了kernel32.dll中的Tool help API还有一个 PS API (从Psapi.dll中导出头文件为 psapi.h ),大家有兴趣的可以搜搜看,然后尝试用其中的函数来改写。

4.kill 命令需要我们通过一个进程的id来杀死它,有的时候找一个进程的id有些麻烦,不过找名字却很容易所以你可以考虑改写这个程序,让它可以通过名称来(etProcessIdByName函数)终止一个进程。

5.设置与获取环境变量( GetEnvironmentStrings,GetEnvironmentVariableSetEnvironmentVariable等)属于支线部分,大家可以研究使用这个API来跳过设置PATH系统环境变量,直接弄一个程序内部的环境变量就可以了,这样程序的可移植性更高。

小结

任何一个程序,想要运行那么必须为这个进程分配空间并且分配一个唯一的进程标识(进程ID),在上面的例子中我们有看到这样的额一串数据:

进程ID  父进程  线程数  优先级  进程名
1676    1956    36      8       explorer.exe
7904    1676    1       8       lscmd.exe
5844    7904    1       8       cmd.exe
4180    1676    1       8       lscmd.exe
5568    4180    1       8       ps.exe

在其中一个lscmd中打开cmd,我们很直观的就能看到cmd.exe的父进程就是该lscmd.exe。
仔细看我们可以发现,我打开了两个lscmd程序,而这个两个进程的父进程则是explorer.exe(1676)即我们的桌面,不论是通过win+r调用还是双击打开,实际上都需要explorer为其新建一个进程来运行我们打开的程序。

概念上理解之后,剩下的就是熟悉API了,各位可以参照上面的 “更多改进” 来编写一些程序提高API的熟练度

相关函数

大部分与进程或线程有关的函数可以在MSND的 Process and Thread Functions 中被找到。小部分,例如 PS API 中的一些函数就找不到了。

文章索引

上一讲:windows 编程之路(三) 文件系统
下一讲:windows 编程之路(五) 线程编程

7 thoughts on “Windows API教程(四) 进程编程

  1. “严重性 代码 说明 项目 文件 行 Suppression State
    错误 LNK1168 无法打开 G:\CMD工具集\Debug\CMD工具集.exe 进行写入 CMD工具集 G:\CMD工具集\CMD工具集\LINK 1 ”
    我编译第二个的时候出现这个,不知道为什么。

留下评论