PE文件二进制对比

文章目录

步骤1:基础框架和PE文件验证

代码实现

cpp 复制代码
#include "windows_header.h"
#include "ConsoleColor.hpp"
#include "c_ConsoleColor.hpp"

BOOL IsPeFile(CONST CHAR* file_path)
{
	FILE* pfile = fopen(file_path, "rb");
	if (!pfile) return FALSE;

	WORD dosSignature = 0;
	if (fread(&dosSignature, sizeof(WORD), 1, pfile) != 1)
	{
		fclose(pfile);
		return FALSE;
	}
	if (dosSignature != IMAGE_DOS_SIGNATURE)
	{
		fclose(pfile);
		return FALSE;
	}
	fclose(pfile);
	return TRUE;
}

ULONGLONG GetFileSizeByPtr(FILE* file)
{
	ULONGLONG size = 0;
	fseek(file, 0, SEEK_END);
	size = ftell(file);
	fseek(file, 0, SEEK_SET);
	return size;
}

int main()
{
	CHAR file_path1[MAX_PATH] = { 0 };
	CHAR file_path2[MAX_PATH] = { 0 };
	
	SetConsoleColor(COLOR_RED);
	printf("========================================\r\n");
	printf("      PE文件二进制对比工具 v1.0\r\n");
	printf("========================================\r\n");
	SetConsoleColor(COLOR_WHITE);
	
	printf("\r\n请输入第一个PE文件的完整路径:");
	if (fgets(file_path1, MAX_PATH, stdin) != NULL)
	{
		file_path1[strcspn(file_path1, "\n")] = 0;
	}
	
	printf("请输入第二个PE文件的完整路径:");
	if (fgets(file_path2, MAX_PATH, stdin) != NULL)
	{
		file_path2[strcspn(file_path2, "\n")] = 0;
	}
	
	printf("\r\n");
	
	// 验证PE文件
	if (!IsPeFile(file_path1) || !IsPeFile(file_path2))
	{
		SetConsoleColor(COLOR_RED);
		printf("PE文件格式错误!\r\n");
		SetConsoleColor(COLOR_WHITE);
		return 1;
	}
	
	SetConsoleColor(COLOR_GREEN);
	printf("PE文件验证通过!\r\n");
	SetConsoleColor(COLOR_WHITE);
	
	printf("\r\n按回车键退出...");
	getchar();
	
	return 0;
}

步骤1代码分析

为什么这样处理?

1. IsPeFile函数的设计

cpp 复制代码
// 为什么返回BOOL而不是bool?
// 答:因为使用了Windows头文件,BOOL是Windows标准类型
// TRUE/FALSE 与其他Windows API保持一致

// 为什么要分三步处理?
if (!pfile) return FALSE;           // 步骤1:检查文件打开
if (fread(...) != 1) return FALSE; // 步骤2:检查读取成功
if (dosSignature != ...) return FALSE; // 步骤3:验证签名

// 这种设计的好处:
// - 每个错误分支都正确关闭已打开的资源
// - 提前返回避免深层嵌套
// - 职责单一,只做PE验证

2. GetFileSizeByPtr的实现原理

cpp 复制代码
ULONGLONG GetFileSizeByPtr(FILE* file)
{
	ULONGLONG size = 0;
	fseek(file, 0, SEEK_END);  // 移动到文件末尾
	size = ftell(file);         // 获取当前位置(即文件大小)
	fseek(file, 0, SEEK_SET);   // 恢复到文件开头
	return size;
}

// 为什么不直接用系统API?
// 答:保持跨平台兼容性,FILE*是C标准库

// 为什么保存位置后再恢复?
// 答:不影响调用者的文件指针位置,这是良好的函数行为

// 潜在问题:ftell返回long,最大2GB
// 但代码用ULONGLONG接收,转换是安全的
// 超过2GB时ftell返回-1,需要额外处理

3. 路径输入处理

cpp 复制代码
if (fgets(file_path1, MAX_PATH, stdin) != NULL)
{
	file_path1[strcspn(file_path1, "\n")] = 0;
}

// 为什么用fgets而不是gets?
// 答:fgets有缓冲区大小限制,防止溢出

// 为什么用strcspn去除换行符?
// 答:因为fgets会保留换行符,需要手动去除
// strcspn找到第一个换行符位置,替换为'\0'

// 如果用户输入超过MAX_PATH会发生什么?
// 答:fgets会截断,只读取MAX_PATH-1个字符
// 可能导致路径不完整,但没有缓冲区溢出风险

4. 控制台颜色设置模式

cpp 复制代码
SetConsoleColor(COLOR_RED);      // 设置颜色
printf("错误信息\r\n");          // 输出彩色内容
SetConsoleColor(COLOR_WHITE);    // 恢复颜色

// 为什么要恢复颜色?
// 答:避免影响后续输出,保持界面一致性

// 为什么使用\r\n而不是\n?
// 答:Windows控制台需要回车换行,确保格式正确

// 这种配对使用的设计模式:
// - 类似于资源获取即初始化(RAII)
// - 确保颜色状态不会"泄漏"到其他代码

5. PE文件验证的必要性

cpp 复制代码
// 为什么只验证DOS签名而不验证完整的PE结构?
// 答:步骤1只建立基础框架,完整验证在后续步骤

// 为什么不验证就继续?
// 答:提前过滤非PE文件,避免无意义的对比

// 验证失败返回1的意义
return 1;  // 非0表示程序异常退出
// 便于批处理脚本判断执行结果

步骤2:添加文件对比核心逻辑

代码实现

cpp 复制代码
#include "windows_header.h"
#include "ConsoleColor.hpp"
#include "c_ConsoleColor.hpp"

BOOL IsPeFile(CONST CHAR* file_path)
{
	FILE* pfile = fopen(file_path, "rb");
	if (!pfile) return FALSE;

	WORD dosSignature = 0;
	if (fread(&dosSignature, sizeof(WORD), 1, pfile) != 1)
	{
		fclose(pfile);
		return FALSE;
	}
	if (dosSignature != IMAGE_DOS_SIGNATURE)
	{
		fclose(pfile);
		return FALSE;
	}
	fclose(pfile);
	return TRUE;
}

ULONGLONG GetFileSizeByPtr(FILE* file)
{
	ULONGLONG size = 0;
	fseek(file, 0, SEEK_END);
	size = ftell(file);
	fseek(file, 0, SEEK_SET);
	return size;
}

bool CompareFileByBin(CONST CHAR* file_path1, CONST CHAR* file_path2)
{
	FILE* pfile1 = NULL;
	FILE* pfile2 = NULL;
	DWORD dwFileSize1 = 0;
	DWORD dwFileSize2 = 0;
	PUCHAR szBuffer1 = NULL;
	PUCHAR szBuffer2 = NULL;
	DWORD dwReadSize1 = 0;
	DWORD dwReadSize2 = 0;
	bool bResult = true;
	DWORD dwDiffCount = 0;
	DWORD dwCheckSize = 0;
	
	// 检查是否为PE文件
	if (!IsPeFile(file_path1) || !IsPeFile(file_path2))
	{
		SetConsoleColor(COLOR_RED);
		printf("PE文件格式错误!\r\n");
		SetConsoleColor(COLOR_WHITE);
		return false;
	}
	
	// 打开文件
	pfile1 = fopen(file_path1, "rb");
	if (!pfile1)
	{
		SetConsoleColor(COLOR_RED);
		printf("无法打开文件: %s\r\n", file_path1);
		SetConsoleColor(COLOR_WHITE);
		return false;
	}
	
	pfile2 = fopen(file_path2, "rb");
	if (!pfile2)
	{
		SetConsoleColor(COLOR_RED);
		printf("无法打开文件: %s\r\n", file_path2);
		SetConsoleColor(COLOR_WHITE);
		fclose(pfile1);
		return false;
	}
	
	// 获取文件大小
	dwFileSize1 = GetFileSizeByPtr(pfile1);
	dwFileSize2 = GetFileSizeByPtr(pfile2);
	
	SetConsoleColor(COLOR_GREEN);
	printf("\r\n文件1: %s (大小: %d bytes)\r\n", file_path1, dwFileSize1);
	printf("文件2: %s (大小: %d bytes)\r\n", file_path2, dwFileSize2);
	SetConsoleColor(COLOR_WHITE);
	
	// 分配缓冲区
	szBuffer1 = (PUCHAR)malloc(4096);
	szBuffer2 = (PUCHAR)malloc(4096);
	
	if (!szBuffer1 || !szBuffer2)
	{
		SetConsoleColor(COLOR_RED);
		printf("内存分配失败!\r\n");
		SetConsoleColor(COLOR_WHITE);
		bResult = false;
		goto CLEANUP;
	}
	
	// 开始对比
	printf("\r\n开始对比文件...\r\n");
	printf("----------------------------------------\r\n");
	
	dwCheckSize = (dwFileSize1 < dwFileSize2) ? dwFileSize1 : dwFileSize2;
	
	for (DWORD offset = 0; offset < dwCheckSize; offset += 4096)
	{
		DWORD bytesToRead = 4096;
		if (offset + bytesToRead > dwCheckSize)
			bytesToRead = dwCheckSize - offset;
		
		// 读取文件块
		fseek(pfile1, offset, SEEK_SET);
		fseek(pfile2, offset, SEEK_SET);
		
		dwReadSize1 = fread(szBuffer1, 1, bytesToRead, pfile1);
		dwReadSize2 = fread(szBuffer2, 1, bytesToRead, pfile2);
		
		if (dwReadSize1 != dwReadSize2 || dwReadSize1 != bytesToRead)
		{
			SetConsoleColor(COLOR_RED);
			printf("读取文件错误于偏移 0x%08X\r\n", offset);
			SetConsoleColor(COLOR_WHITE);
			bResult = false;
			goto CLEANUP;
		}
		
		// 对比数据块
		for (DWORD i = 0; i < bytesToRead; i++)
		{
			if (szBuffer1[i] != szBuffer2[i])
			{
				if (dwDiffCount < 100) // 最多显示100处差异
				{
					SetConsoleColor(COLOR_YELLOW);
					printf("差异位置: [0x%08X] \t文件1: 0x%02X \t文件2: 0x%02X\r\n", 
						offset + i, szBuffer1[i], szBuffer2[i]);
				}
				dwDiffCount++;
				bResult = false;
			}
		}
	}
	
	// 检查文件大小差异
	if (dwFileSize1 != dwFileSize2)
	{
		SetConsoleColor(COLOR_YELLOW);
		printf("\r\n文件大小不同!\r\n");
		printf("文件1大小: %d bytes\r\n", dwFileSize1);
		printf("文件2大小: %d bytes\r\n", dwFileSize2);
		bResult = false;
	}
	
CLEANUP:
	// 清理资源
	if (szBuffer1) free(szBuffer1);
	if (szBuffer2) free(szBuffer2);
	if (pfile1) fclose(pfile1);
	if (pfile2) fclose(pfile2);
	
	// 输出最终结果
	printf("----------------------------------------\r\n");
	if (bResult)
	{
		SetConsoleColor(COLOR_GREEN);
		printf("✓ 文件完全相同!\r\n");
	}
	else
	{
		SetConsoleColor(COLOR_RED);
		printf("✗ 文件存在差异!共发现 %d 处不同\r\n", dwDiffCount);
	}
	SetConsoleColor(COLOR_WHITE);
	
	return bResult;
}

int main()
{
	CHAR file_path1[MAX_PATH] = { 0 };
	CHAR file_path2[MAX_PATH] = { 0 };
	
	SetConsoleColor(COLOR_RED);
	printf("========================================\r\n");
	printf("      PE文件二进制对比工具 v1.0\r\n");
	printf("========================================\r\n");
	SetConsoleColor(COLOR_WHITE);
	
	printf("\r\n请输入第一个PE文件的完整路径:");
	if (fgets(file_path1, MAX_PATH, stdin) != NULL)
	{
		file_path1[strcspn(file_path1, "\n")] = 0;
	}
	
	printf("请输入第二个PE文件的完整路径:");
	if (fgets(file_path2, MAX_PATH, stdin) != NULL)
	{
		file_path2[strcspn(file_path2, "\n")] = 0;
	}
	
	printf("\r\n");
	CompareFileByBin(file_path1, file_path2);
	
	printf("\r\n按回车键退出...");
	getchar();
	
	return 0;
}

步骤2代码分析

为什么这样处理?

1. 缓冲区大小的选择

cpp 复制代码
szBuffer1 = (PUCHAR)malloc(4096);  // 4KB缓冲区

// 为什么选择4096字节?
// 答:4KB是大多数文件系统的簇大小,也是内存页大小
// - 太小(如512):系统调用频繁,性能差
// - 太大(如1MB):浪费内存,大文件优势不明显
// - 4KB是平衡点,适合大多数场景

// 为什么使用动态分配而不是栈数组?
// 答:4KB的栈数组也可以,但动态分配更灵活
// 如果后续需要调整大小,只需修改一处

// 内存分配失败的处理
if (!szBuffer1 || !szBuffer2)
{
	bResult = false;
	goto CLEANUP;
}
// 注意:C语言malloc失败返回NULL,需要检查

2. 分块读取策略

cpp 复制代码
for (DWORD offset = 0; offset < dwCheckSize; offset += 4096)
{
	DWORD bytesToRead = 4096;
	if (offset + bytesToRead > dwCheckSize)
		bytesToRead = dwCheckSize - offset;
	
	fseek(pfile1, offset, SEEK_SET);
	fseek(pfile2, offset, SEEK_SET);
	
	dwReadSize1 = fread(szBuffer1, 1, bytesToRead, pfile1);
	dwReadSize2 = fread(szBuffer2, 1, bytesToRead, pfile2);
}

// 为什么使用fseek移动到每个块?
// 答:因为文件是按顺序读取的,每次定位到正确位置
// 虽然连续读取可以不移动,但显式定位更安全

// 为什么检查读取大小?
if (dwReadSize1 != dwReadSize2 || dwReadSize1 != bytesToRead)
// 确保两个文件都读取了相同的、预期的字节数

3. 差异检测和显示

cpp 复制代码
if (dwDiffCount < 100)  // 最多显示100处差异
{
	SetConsoleColor(COLOR_YELLOW);
	printf("差异位置: [0x%08X] \t文件1: 0x%02X \t文件2: 0x%02X\r\n", 
		offset + i, szBuffer1[i], szBuffer2[i]);
}

// 为什么最多显示100处?
// 答:避免输出过多导致:
// - 控制台缓冲区溢出
// - 用户无法查看所有信息
// - 性能下降(大量printf调用)

// 为什么在循环内dwDiffCount < 100?
// 答:一旦超过100,只计数不输出,提高效率

// 输出格式设计
// [0x00001234]    文件1: 0xAB    文件2: 0xCD
// - 16进制地址便于调试
// - 制表符对齐结构清晰

4. 资源清理的goto模式

cpp 复制代码
CLEANUP:
	if (szBuffer1) free(szBuffer1);
	if (szBuffer2) free(szBuffer2);
	if (pfile1) fclose(pfile1);
	if (pfile2) fclose(pfile2);

// 为什么使用goto而不是多处重复代码?
// 答:集中式清理保证:
// - 不会遗漏资源释放
// - 代码维护简单(只需修改一处)
// - 所有错误路径统一处理

// 为什么每个资源释放前检查NULL?
// 答:free(NULL)安全,fclose(NULL)未定义行为
// 所以只对fclose检查NULL

// 清理顺序:
// 先释放内存(可能依赖文件?不依赖)
// 再关闭文件(顺序无严格要求)

5. 文件大小差异处理

cpp 复制代码
dwCheckSize = (dwFileSize1 < dwFileSize2) ? dwFileSize1 : dwFileSize2;
// 只对比较小的文件大小,避免越界

// 对比完成后再检查大小差异
if (dwFileSize1 != dwFileSize2)
{
	printf("文件大小不同!\r\n");
	bResult = false;
}

// 为什么不立即返回?
// 答:即使大小不同,也要对比相同部分
// 让用户知道具体哪些字节不同

步骤3:完整集成和最终优化

代码实现

cpp 复制代码
#include "windows_header.h"
#include "ConsoleColor.hpp"
#include "c_ConsoleColor.hpp"

BOOL IsPeFile(CONST CHAR* file_path)
{
	FILE* pfile = fopen(file_path, "rb");
	if (!pfile) return FALSE;

	WORD dosSignature = 0;
	if (fread(&dosSignature, sizeof(WORD), 1, pfile) != 1)
	{
		fclose(pfile);
		return FALSE;
	}
	if (dosSignature != IMAGE_DOS_SIGNATURE)
	{
		fclose(pfile);
		return FALSE;
	}
	fclose(pfile);
	return TRUE;
}

ULONGLONG GetFileSizeByPtr(FILE* file)
{
	ULONGLONG size = 0;
	fseek(file, 0, SEEK_END);
	size = ftell(file);
	fseek(file, 0, SEEK_SET);
	return size;
}

bool CompareFileByBin(CONST CHAR* file_path1, CONST CHAR* file_path2)
{
	FILE* pfile1 = NULL;
	FILE* pfile2 = NULL;
	DWORD dwFileSize1 = 0;
	DWORD dwFileSize2 = 0;
	PUCHAR szBuffer1 = NULL;
	PUCHAR szBuffer2 = NULL;
	DWORD dwReadSize1 = 0;
	DWORD dwReadSize2 = 0;
	bool bResult = true;
	DWORD dwDiffCount = 0;
	DWORD dwCheckSize = 0;
	
	// 检查是否为PE文件
	if (!IsPeFile(file_path1) || !IsPeFile(file_path2))
	{
		SetConsoleColor(COLOR_RED);
		printf("PE文件格式错误!\r\n");
		SetConsoleColor(COLOR_WHITE);
		return false;
	}
	
	// 打开文件
	pfile1 = fopen(file_path1, "rb");
	if (!pfile1)
	{
		SetConsoleColor(COLOR_RED);
		printf("无法打开文件: %s\r\n", file_path1);
		SetConsoleColor(COLOR_WHITE);
		return false;
	}
	
	pfile2 = fopen(file_path2, "rb");
	if (!pfile2)
	{
		SetConsoleColor(COLOR_RED);
		printf("无法打开文件: %s\r\n", file_path2);
		SetConsoleColor(COLOR_WHITE);
		fclose(pfile1);
		return false;
	}
	
	// 获取文件大小
	dwFileSize1 = GetFileSizeByPtr(pfile1);
	dwFileSize2 = GetFileSizeByPtr(pfile2);
	
	SetConsoleColor(COLOR_GREEN);
	printf("\r\n文件1: %s (大小: %d bytes)\r\n", file_path1, dwFileSize1);
	printf("文件2: %s (大小: %d bytes)\r\n", file_path2, dwFileSize2);
	SetConsoleColor(COLOR_WHITE);
	
	// 分配缓冲区
	szBuffer1 = (PUCHAR)malloc(4096);
	szBuffer2 = (PUCHAR)malloc(4096);
	
	if (!szBuffer1 || !szBuffer2)
	{
		SetConsoleColor(COLOR_RED);
		printf("内存分配失败!\r\n");
		SetConsoleColor(COLOR_WHITE);
		bResult = false;
		goto CLEANUP;
	}
	
	// 开始对比
	printf("\r\n开始对比文件...\r\n");
	printf("----------------------------------------\r\n");
	
	dwCheckSize = (dwFileSize1 < dwFileSize2) ? dwFileSize1 : dwFileSize2;
	
	for (DWORD offset = 0; offset < dwCheckSize; offset += 4096)
	{
		DWORD bytesToRead = 4096;
		if (offset + bytesToRead > dwCheckSize)
			bytesToRead = dwCheckSize - offset;
		
		// 读取文件块
		fseek(pfile1, offset, SEEK_SET);
		fseek(pfile2, offset, SEEK_SET);
		
		dwReadSize1 = fread(szBuffer1, 1, bytesToRead, pfile1);
		dwReadSize2 = fread(szBuffer2, 1, bytesToRead, pfile2);
		
		if (dwReadSize1 != dwReadSize2 || dwReadSize1 != bytesToRead)
		{
			SetConsoleColor(COLOR_RED);
			printf("读取文件错误于偏移 0x%08X\r\n", offset);
			SetConsoleColor(COLOR_WHITE);
			bResult = false;
			goto CLEANUP;
		}
		
		// 对比数据块
		for (DWORD i = 0; i < bytesToRead; i++)
		{
			if (szBuffer1[i] != szBuffer2[i])
			{
				if (dwDiffCount < 100) // 最多显示100处差异
				{
					SetConsoleColor(COLOR_YELLOW);
					printf("差异位置: [0x%08X] \t文件1: 0x%02X \t文件2: 0x%02X\r\n", 
						offset + i, szBuffer1[i], szBuffer2[i]);
				}
				dwDiffCount++;
				bResult = false;
			}
		}
	}
	
	// 检查文件大小差异
	if (dwFileSize1 != dwFileSize2)
	{
		SetConsoleColor(COLOR_YELLOW);
		printf("\r\n文件大小不同!\r\n");
		printf("文件1大小: %d bytes\r\n", dwFileSize1);
		printf("文件2大小: %d bytes\r\n", dwFileSize2);
		bResult = false;
	}
	
CLEANUP:
	// 清理资源
	if (szBuffer1) free(szBuffer1);
	if (szBuffer2) free(szBuffer2);
	if (pfile1) fclose(pfile1);
	if (pfile2) fclose(pfile2);
	
	// 输出最终结果
	printf("----------------------------------------\r\n");
	if (bResult)
	{
		SetConsoleColor(COLOR_GREEN);
		printf("✓ 文件完全相同!\r\n");
	}
	else
	{
		SetConsoleColor(COLOR_RED);
		printf("✗ 文件存在差异!共发现 %d 处不同\r\n", dwDiffCount);
	}
	SetConsoleColor(COLOR_WHITE);
	
	return bResult;
}

int main()
{
	CHAR file_path1[MAX_PATH] = { 0 };
	CHAR file_path2[MAX_PATH] = { 0 };
	
	SetConsoleColor(COLOR_RED);
	printf("========================================\r\n");
	printf("      PE文件二进制对比工具 v1.0\r\n");
	printf("========================================\r\n");
	SetConsoleColor(COLOR_WHITE);
	
	printf("\r\n请输入第一个PE文件的完整路径:");
	if (fgets(file_path1, MAX_PATH, stdin) != NULL)
	{
		file_path1[strcspn(file_path1, "\n")] = 0;
	}
	
	printf("请输入第二个PE文件的完整路径:");
	if (fgets(file_path2, MAX_PATH, stdin) != NULL)
	{
		file_path2[strcspn(file_path2, "\n")] = 0;
	}
	
	printf("\r\n");
	CompareFileByBin(file_path1, file_path2);
	
	printf("\r\n按回车键退出...");
	getchar();
	
	return 0;
}

步骤3代码分析

为什么这是最终完整版本?

1. 完整的错误处理链

cpp 复制代码
// 错误检查的层次:
if (!IsPeFile())        // 层次1: 文件格式验证
if (!pfile1)            // 层次2: 文件打开验证
if (!szBuffer1)         // 层次3: 内存分配验证
if (dwReadSize1 != ...) // 层次4: 读取完整性验证
if (szBuffer1[i] != ...)// 层次5: 数据对比验证

// 这种多层验证确保:
// - 任何环节出错都不会导致崩溃
// - 用户能明确知道哪里出错
// - 资源能正确释放

2. 用户体验优化

cpp 复制代码
// 颜色编码设计:
COLOR_RED    // 错误、警告信息
COLOR_GREEN  // 成功、通过信息
COLOR_YELLOW // 差异、需要注意的信息
COLOR_WHITE  // 正常信息

// 格式对齐设计:
printf("========================================\r\n"); // 标题框
printf("文件1: %s (大小: %d bytes)\r\n");                // 信息展示
printf("差异位置: [0x%08X] \t文件1: 0x%02X\r\n");       // 差异展示

// 为什么要用这样的格式?
// - 视觉上清晰区分不同类型信息
// - 便于用户快速定位问题
// - 专业化的工具界面

3. 性能优化细节

cpp 复制代码
// 限制差异显示数量
if (dwDiffCount < 100)  // 避免输出过多

// 批量读取处理
for (offset; offset < size; offset += 4096)  // 减少I/O次数

// 提前计算对比大小
dwCheckSize = min(dwFileSize1, dwFileSize2);  // 避免重复比较

// 这些优化在实际大文件对比时效果显著:
// 1GB文件,4KB缓冲区,只需262144次I/O操作
// 如果每次1字节,需要10亿次I/O,完全不可用

4. 内存管理策略

cpp 复制代码
// 动态缓冲区的生命周期:
分配 -> 使用 -> 检查 -> 释放

szBuffer1 = (PUCHAR)malloc(4096);     // 1. 分配
if (!szBuffer1) goto CLEANUP;          // 2. 检查
// 使用缓冲区                           // 3. 使用
CLEANUP:                               // 4. 释放
if (szBuffer1) free(szBuffer1);

// 为什么使用goto统一释放?
// 答:确保任何错误路径都能正确释放
// 避免内存泄漏,这是C语言的标准模式

5. 函数职责划分

cpp 复制代码
IsPeFile()           // 职责:验证PE文件格式
GetFileSizeByPtr()   // 职责:获取文件大小
CompareFileByBin()   // 职责:对比文件内容
main()               // 职责:用户交互和流程控制

// 这种划分的好处:
// - 每个函数功能单一,易于测试
// - 可以独立修改某个功能
// - 代码复用性高

6. 跨平台兼容性考虑

cpp 复制代码
// 使用标准C库函数:
fopen/fread/fseek/ftell/fclose  // 跨平台文件操作
malloc/free                      // 跨平台内存管理
printf                           // 跨平台输出

// 使用Windows特定但通过头文件封装的功能:
SetConsoleColor()                // 通过ConsoleColor.hpp封装
IMAGE_DOS_SIGNATURE               // 通过windows_header.h提供

// 这种设计平衡了功能和移植性

7. 最终输出完整性

cpp 复制代码
// 对比结果有三种情况:
// 1. 完全相同:显示绿色✓标记
// 2. 存在差异:显示红色✗标记和差异数量
// 3. 发生错误:显示红色错误信息

// 每种情况都有清晰的状态码返回
return bResult;  // true表示相同,false表示不同或错误

// 便于其他程序调用时判断结果

这个最终版本是一个完整的、可用的PE文件二进制对比工具,包含了错误处理、用户界面、核心算法和资源管理,适合实际使用。

C语言 完整实现

cpp 复制代码
#include "windows_header.h"

// 使用示例 main.cpp
#include "ConsoleColor.hpp"
#include "c_ConsoleColor.hpp"

BOOL IsPeFile(CONST CHAR* file_path)
{
	FILE* pfile = fopen(file_path, "rb");
	if (!pfile) return FALSE;

	WORD dosSignature = 0;
	if (fread(&dosSignature, sizeof(WORD), 1, pfile) != 1)
	{
		fclose(pfile);
		return FALSE;
	}
	if (dosSignature != IMAGE_DOS_SIGNATURE)
	{
		fclose(pfile);
		return FALSE;
	}
	fclose(pfile);
	return TRUE;
}

ULONGLONG GetFileSizeByPtr(FILE* file)
{
	ULONGLONG size = 0;
	fseek(file, 0, SEEK_END);
	size = ftell(file);
	fseek(file, 0, SEEK_SET);
	return size;
}

bool CompareFileByBin(CONST CHAR* file_path1, CONST CHAR* file_path2)
{
	FILE* pfile1 = NULL;
	FILE* pfile2 = NULL;
	DWORD dwFileSize1 = 0;
	DWORD dwFileSize2 = 0;
	PUCHAR szBuffer1 = NULL;
	PUCHAR szBuffer2 = NULL;
	DWORD dwReadSize1 = 0;
	DWORD dwReadSize2 = 0;
	bool bResult = true;
	DWORD dwDiffCount = 0;
	DWORD dwCheckSize = 0;
	
	// 检查是否为PE文件
	if (!IsPeFile(file_path1) || !IsPeFile(file_path2))
	{
		SetConsoleColor(COLOR_RED);
		printf("PE文件格式错误!\r\n");
		SetConsoleColor(COLOR_WHITE);
		return false;
	}
	
	// 打开文件
	pfile1 = fopen(file_path1, "rb");
	if (!pfile1)
	{
		SetConsoleColor(COLOR_RED);
		printf("无法打开文件: %s\r\n", file_path1);
		SetConsoleColor(COLOR_WHITE);
		return false;
	}
	
	pfile2 = fopen(file_path2, "rb");
	if (!pfile2)
	{
		SetConsoleColor(COLOR_RED);
		printf("无法打开文件: %s\r\n", file_path2);
		SetConsoleColor(COLOR_WHITE);
		fclose(pfile1);
		return false;
	}
	
	// 获取文件大小
	dwFileSize1 = GetFileSizeByPtr(pfile1);
	dwFileSize2 = GetFileSizeByPtr(pfile2);
	
	SetConsoleColor(COLOR_GREEN);
	printf("\r\n文件1: %s (大小: %d bytes)\r\n", file_path1, dwFileSize1);
	printf("文件2: %s (大小: %d bytes)\r\n", file_path2, dwFileSize2);
	SetConsoleColor(COLOR_WHITE);
	
	// 分配缓冲区
	szBuffer1 = (PUCHAR)malloc(4096);
	szBuffer2 = (PUCHAR)malloc(4096);
	
	if (!szBuffer1 || !szBuffer2)
	{
		SetConsoleColor(COLOR_RED);
		printf("内存分配失败!\r\n");
		SetConsoleColor(COLOR_WHITE);
		bResult = false;
		goto CLEANUP;
	}
	
	// 开始对比
	printf("\r\n开始对比文件...\r\n");
	printf("----------------------------------------\r\n");
	
	dwCheckSize = (dwFileSize1 < dwFileSize2) ? dwFileSize1 : dwFileSize2;
	
	for (DWORD offset = 0; offset < dwCheckSize; offset += 4096)
	{
		DWORD bytesToRead = 4096;
		if (offset + bytesToRead > dwCheckSize)
			bytesToRead = dwCheckSize - offset;
		
		// 读取文件块
		fseek(pfile1, offset, SEEK_SET);
		fseek(pfile2, offset, SEEK_SET);
		
		dwReadSize1 = fread(szBuffer1, 1, bytesToRead, pfile1);
		dwReadSize2 = fread(szBuffer2, 1, bytesToRead, pfile2);
		
		if (dwReadSize1 != dwReadSize2 || dwReadSize1 != bytesToRead)
		{
			SetConsoleColor(COLOR_RED);
			printf("读取文件错误于偏移 0x%08X\r\n", offset);
			SetConsoleColor(COLOR_WHITE);
			bResult = false;
			goto CLEANUP;
		}
		
		// 对比数据块
		for (DWORD i = 0; i < bytesToRead; i++)
		{
			if (szBuffer1[i] != szBuffer2[i])
			{
				if (dwDiffCount < 100) // 最多显示100处差异
				{
					SetConsoleColor(COLOR_YELLOW);
					printf("差异位置: [0x%08X] \t文件1: 0x%02X \t文件2: 0x%02X\r\n", 
						offset + i, szBuffer1[i], szBuffer2[i]);
				}
				dwDiffCount++;
				bResult = false;
			}
		}
	}
	
	// 检查文件大小差异
	if (dwFileSize1 != dwFileSize2)
	{
		SetConsoleColor(COLOR_YELLOW);
		printf("\r\n文件大小不同!\r\n");
		printf("文件1大小: %d bytes\r\n", dwFileSize1);
		printf("文件2大小: %d bytes\r\n", dwFileSize2);
		bResult = false;
	}
	
CLEANUP:
	// 清理资源
	if (szBuffer1) free(szBuffer1);
	if (szBuffer2) free(szBuffer2);
	if (pfile1) fclose(pfile1);
	if (pfile2) fclose(pfile2);
	
	// 输出最终结果
	printf("----------------------------------------\r\n");
	if (bResult)
	{
		SetConsoleColor(COLOR_GREEN);
		printf("✓ 文件完全相同!\r\n");
	}
	else
	{
		SetConsoleColor(COLOR_RED);
		printf("✗ 文件存在差异!共发现 %d 处不同\r\n", dwDiffCount);
	}
	SetConsoleColor(COLOR_WHITE);
	
	return bResult;
}

int main()
{
	CHAR file_path1[MAX_PATH] = { 0 };
	CHAR file_path2[MAX_PATH] = { 0 };
	
	SetConsoleColor(COLOR_RED);
	printf("========================================\r\n");
	printf("      PE文件二进制对比工具 v1.0\r\n");
	printf("========================================\r\n");
	SetConsoleColor(COLOR_WHITE);
	
	printf("\r\n请输入第一个PE文件的完整路径:");
	if (fgets(file_path1, MAX_PATH, stdin) != NULL)
	{
		file_path1[strcspn(file_path1, "\n")] = 0;
	}
	
	printf("请输入第二个PE文件的完整路径:");
	if (fgets(file_path2, MAX_PATH, stdin) != NULL)
	{
		file_path2[strcspn(file_path2, "\n")] = 0;
	}
	
	printf("\r\n");
	CompareFileByBin(file_path1, file_path2);
	
	printf("\r\n按回车键退出...");
	getchar();
	
	return 0;
}

C++重构版本

我将使用现代 C++20 特性重构这个 PE 文件二进制对比工具。以下是重构后的代码:

重构后的完整代码(C++20)

cpp 复制代码
// PE_File_Comparator.hpp
#pragma once

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <memory>
#include <algorithm>
#include <span>
#include <format>
#include <chrono>
#include <concepts>
#include <expected>
#include <ranges>
#include <filesystem>

#ifdef _WIN32
    #include <windows.h>
    #define IMAGE_DOS_SIGNATURE 0x5A4D
#endif

namespace PE_Comparator {

    // 自定义异常类型
    class PeFileError : public std::runtime_error {
    public:
        explicit PeFileError(const std::string& message) : std::runtime_error(message) {}
    };

    // 文件信息结构体
    struct FileInfo {
        std::filesystem::path path;
        std::uintmax_t size;
        bool is_pe;
    };

    // 差异信息结构体
    struct DiffInfo {
        std::size_t offset;
        std::uint8_t value1;
        std::uint8_t value2;
        
        std::string toString() const {
            return std::format("差异位置: [0x{:08X}] \t文件1: 0x{:02X} \t文件2: 0x{:02X}", 
                             offset, value1, value2);
        }
    };

    // 对比结果结构体
    struct CompareResult {
        bool identical;
        std::size_t diff_count;
        std::vector<DiffInfo> diffs;  // 存储前N个差异
        std::optional<std::size_t> size_diff;
        
        std::string summary() const {
            if (identical) {
                return "✓ 文件完全相同!";
            }
            return std::format("✗ 文件存在差异!共发现 {} 处不同", diff_count);
        }
    };

    // 现代 C++ 风格的 PE 文件验证器类
    class PeValidator {
    private:
        static constexpr std::size_t BUFFER_SIZE = 4096;
        
    public:
        // 使用 RAII 和 std::ifstream
        static auto IsPeFile(const std::filesystem::path& file_path) -> std::expected<bool, std::string> {
            std::ifstream file(file_path, std::ios::binary);
            if (!file.is_open()) {
                return std::unexpected(std::format("无法打开文件: {}", file_path.string()));
            }
            
            std::uint16_t dos_signature = 0;
            if (!file.read(reinterpret_cast<char*>(&dos_signature), sizeof(dos_signature))) {
                return std::unexpected("读取DOS签名失败");
            }
            
            if (dos_signature != IMAGE_DOS_SIGNATURE) {
                return false;
            }
            
            return true;
        }
        
        // 使用 concepts 约束文件流类型
        template<typename StreamType>
        requires std::derived_from<StreamType, std::istream>
        static auto GetFileSize(StreamType& stream) -> std::uintmax_t {
            auto current_pos = stream.tellg();
            stream.seekg(0, std::ios::end);
            auto size = stream.tellg();
            stream.seekg(current_pos);
            return static_cast<std::uintmax_t>(size);
        }
    };

    // 缓冲区管理器 - 使用 RAII
    template<std::size_t BufferSize = 4096>
    class BufferManager {
    private:
        std::vector<std::uint8_t> buffer_;
        
    public:
        BufferManager() : buffer_(BufferSize, 0) {}
        
        auto data() -> std::uint8_t* { return buffer_.data(); }
        auto size() const -> std::size_t { return buffer_.size(); }
        auto span() -> std::span<std::uint8_t> { return {buffer_.data(), buffer_.size()}; }
    };

    // 主要对比器类
    class FileComparator {
    private:
        static constexpr std::size_t BUFFER_SIZE = 4096;
        static constexpr std::size_t MAX_DIFFS_DISPLAY = 100;
        
        // 使用 std::expected 进行错误处理
        static auto OpenFile(const std::filesystem::path& path) 
            -> std::expected<std::ifstream, std::string> {
            std::ifstream file(path, std::ios::binary);
            if (!file.is_open()) {
                return std::unexpected(std::format("无法打开文件: {}", path.string()));
            }
            return file;
        }
        
        static auto ReadChunk(std::ifstream& file, std::span<std::uint8_t> buffer, 
                              std::size_t offset) -> std::expected<std::size_t, std::string> {
            file.seekg(offset);
            if (!file) {
                return std::unexpected(std::format("定位文件失败于偏移: 0x{:08X}", offset));
            }
            
            file.read(reinterpret_cast<char*>(buffer.data()), buffer.size());
            auto bytes_read = file.gcount();
            
            if (file.fail() && !file.eof()) {
                return std::unexpected(std::format("读取文件失败于偏移: 0x{:08X}", offset));
            }
            
            return static_cast<std::size_t>(bytes_read);
        }
        
    public:
        static auto CompareFiles(const std::filesystem::path& file1, 
                                const std::filesystem::path& file2) -> std::expected<CompareResult, std::string> {
            // 1. 验证 PE 文件格式
            auto is_pe1 = PeValidator::IsPeFile(file1);
            if (!is_pe1) {
                return std::unexpected(is_pe1.error());
            }
            
            auto is_pe2 = PeValidator::IsPeFile(file2);
            if (!is_pe2) {
                return std::unexpected(is_pe2.error());
            }
            
            if (!(*is_pe1) || !(*is_pe2)) {
                return std::unexpected("文件不是有效的PE格式");
            }
            
            // 2. 打开文件
            auto file_stream1 = OpenFile(file1);
            if (!file_stream1) {
                return std::unexpected(file_stream1.error());
            }
            
            auto file_stream2 = OpenFile(file2);
            if (!file_stream2) {
                return std::unexpected(file_stream2.error());
            }
            
            // 3. 获取文件大小
            auto size1 = PeValidator::GetFileSize(file_stream1.value());
            auto size2 = PeValidator::GetFileSize(file_stream2.value());
            
            FileInfo info1{file1, size1, *is_pe1};
            FileInfo info2{file2, size2, *is_pe2};
            
            // 4. 准备对比
            auto compare_size = std::min(size1, size2);
            BufferManager<BUFFER_SIZE> buffer1;
            BufferManager<BUFFER_SIZE> buffer2;
            
            CompareResult result{true, 0, {}, {}};
            
            // 5. 使用 ranges 和 views 进行高效对比
            for (std::size_t offset = 0; offset < compare_size; offset += BUFFER_SIZE) {
                auto bytes_to_read = std::min<std::size_t>(BUFFER_SIZE, compare_size - offset);
                
                auto bytes1 = ReadChunk(file_stream1.value(), buffer1.span().first(bytes_to_read), offset);
                if (!bytes1) {
                    return std::unexpected(bytes1.error());
                }
                
                auto bytes2 = ReadChunk(file_stream2.value(), buffer2.span().first(bytes_to_read), offset);
                if (!bytes2) {
                    return std::unexpected(bytes2.error());
                }
                
                // 使用 std::ranges 进行对比
                auto view1 = std::span(buffer1.data(), *bytes1);
                auto view2 = std::span(buffer2.data(), *bytes2);
                
                // 使用 ranges 算法查找差异
                auto diff_positions = std::views::iota(0u, bytes_to_read)
                    | std::views::filter([&](auto i) { return view1[i] != view2[i]; });
                
                for (auto i : diff_positions) {
                    if (result.diffs.size() < MAX_DIFFS_DISPLAY) {
                        result.diffs.emplace_back(offset + i, view1[i], view2[i]);
                    }
                    result.diff_count++;
                    result.identical = false;
                }
            }
            
            // 6. 检查文件大小差异
            if (size1 != size2) {
                result.size_diff = size1 > size2 ? size1 - size2 : size2 - size1;
                result.identical = false;
            }
            
            return result;
        }
    };

    // 控制台颜色管理 - C++20 风格
    class ConsoleColorGuard {
    private:
        HANDLE console_handle_;
        WORD original_color_;
        
    public:
        explicit ConsoleColorGuard(WORD color) {
            console_handle_ = GetStdHandle(STD_OUTPUT_HANDLE);
            CONSOLE_SCREEN_BUFFER_INFO console_info;
            GetConsoleScreenBufferInfo(console_handle_, &console_info);
            original_color_ = console_info.wAttributes;
            SetConsoleTextAttribute(console_handle_, color);
        }
        
        ~ConsoleColorGuard() {
            SetConsoleTextAttribute(console_handle_, original_color_);
        }
        
        // 禁止拷贝
        ConsoleColorGuard(const ConsoleColorGuard&) = delete;
        ConsoleColorGuard& operator=(const ConsoleColorGuard&) = delete;
        
        // 允许移动
        ConsoleColorGuard(ConsoleColorGuard&& other) noexcept
            : console_handle_(other.console_handle_)
            , original_color_(other.original_color_) {
            other.console_handle_ = nullptr;
        }
    };

    // 格式化输出函数
    void PrintFileInfo(const FileInfo& info) {
        ConsoleColorGuard color(COLOR_GREEN);
        std::cout << std::format("文件: {} (大小: {} bytes)\n", 
                                info.path.string(), info.size);
    }
    
    void PrintResult(const CompareResult& result) {
        std::cout << "----------------------------------------\n";
        
        if (result.identical) {
            ConsoleColorGuard color(COLOR_GREEN);
            std::cout << result.summary() << '\n';
        } else {
            ConsoleColorGuard color(COLOR_RED);
            std::cout << result.summary() << '\n';
            
            if (result.size_diff.has_value()) {
                ConsoleColorGuard yellow(COLOR_YELLOW);
                std::cout << std::format("文件大小不同!相差 {} bytes\n", *result.size_diff);
            }
            
            // 显示前几个差异
            for (const auto& diff : result.diffs) {
                ConsoleColorGuard yellow(COLOR_YELLOW);
                std::cout << diff.toString() << '\n';
            }
        }
    }
}

// 主函数 - 使用现代 C++ 特性
int main() {
    using namespace PE_Comparator;
    
    try {
        // 设置控制台编码为 UTF-8
        SetConsoleOutputCP(CP_UTF8);
        
        {
            ConsoleColorGuard color(COLOR_RED);
            std::cout << "========================================\n";
            std::cout << "      PE文件二进制对比工具 v2.0\n";
            std::cout << "      (C++20 重构版)\n";
            std::cout << "========================================\n";
        }
        
        // 使用 std::string 和 std::getline
        std::string path1_str, path2_str;
        
        std::cout << "\n请输入第一个PE文件的完整路径:";
        std::getline(std::cin, path1_str);
        
        std::cout << "请输入第二个PE文件的完整路径:";
        std::getline(std::cin, path2_str);
        
        // 去除可能的引号
        auto clean_path = [](std::string& path) {
            if (path.size() >= 2 && path.front() == '"' && path.back() == '"') {
                path = path.substr(1, path.size() - 2);
            }
        };
        
        clean_path(path1_str);
        clean_path(path2_str);
        
        std::filesystem::path file_path1(path1_str);
        std::filesystem::path file_path2(path2_str);
        
        // 检查文件是否存在
        if (!std::filesystem::exists(file_path1)) {
            ConsoleColorGuard color(COLOR_RED);
            std::cout << std::format("文件不存在: {}\n", file_path1.string());
            return 1;
        }
        
        if (!std::filesystem::exists(file_path2)) {
            ConsoleColorGuard color(COLOR_RED);
            std::cout << std::format("文件不存在: {}\n", file_path2.string());
            return 1;
        }
        
        std::cout << '\n';
        
        // 执行对比
        auto result = FileComparator::CompareFiles(file_path1, file_path2);
        
        if (!result) {
            ConsoleColorGuard color(COLOR_RED);
            std::cout << std::format("错误: {}\n", result.error());
            return 1;
        }
        
        // 打印结果
        PrintResult(*result);
        
        std::cout << "\n按回车键退出...";
        std::cin.get();
        
        return result->identical ? 0 : 1;
        
    } catch (const std::exception& e) {
        ConsoleColorGuard color(COLOR_RED);
        std::cout << std::format("程序异常: {}\n", e.what());
        return 1;
    }
}

重构说明文档

主要改进点

1. 现代 C++ 特性使用

RAII (资源获取即初始化)

cpp 复制代码
// 旧代码
FILE* pfile = fopen(...);
if (!pfile) return false;
// ... 需要手动 fclose

// 新代码
std::ifstream file(path, std::ios::binary);
// 自动管理生命周期,析构时自动关闭

std::expected - 现代错误处理

cpp 复制代码
// 返回类型明确表达"可能失败"的语义
static auto IsPeFile(const std::filesystem::path& file_path) 
    -> std::expected<bool, std::string>

std::span - 安全的内存视图

cpp 复制代码
// 避免指针传递,提供边界检查
static auto ReadChunk(std::ifstream& file, std::span<std::uint8_t> buffer, 
                      std::size_t offset) -> std::expected<std::size_t, std::string>

std::format - 类型安全的格式化

cpp 复制代码
// 替代 printf,编译时检查格式字符串
std::format("差异位置: [0x{:08X}] \t文件1: 0x{:02X} \t文件2: 0x{:02X}", 
            offset, value1, value2)
2. 错误处理改进
cpp 复制代码
// 使用 std::expected 统一错误处理
auto is_pe1 = PeValidator::IsPeFile(file1);
if (!is_pe1) {
    return std::unexpected(is_pe1.error());  // 传播错误信息
}

if (!(*is_pe1)) {
    return std::unexpected("文件不是有效的PE格式");
}
3. RAII 包装器
cpp 复制代码
// 控制台颜色自动管理
class ConsoleColorGuard {
public:
    explicit ConsoleColorGuard(WORD color) {
        // 设置颜色
    }
    ~ConsoleColorGuard() {
        // 自动恢复原始颜色
    }
};
4. 结构化数据
cpp 复制代码
// 使用结构体组织相关数据
struct FileInfo {
    std::filesystem::path path;
    std::uintmax_t size;
    bool is_pe;
};

struct DiffInfo {
    std::size_t offset;
    std::uint8_t value1;
    std::uint8_t value2;
    
    std::string toString() const {
        return std::format(...);
    }
};

struct CompareResult {
    bool identical;
    std::size_t diff_count;
    std::vector<DiffInfo> diffs;
    std::optional<std::size_t> size_diff;
    
    std::string summary() const { ... }
};
5. 使用 ranges 和 views
cpp 复制代码
// 声明式编程,自动查找差异位置
auto diff_positions = std::views::iota(0u, bytes_to_read)
    | std::views::filter([&](auto i) { return view1[i] != view2[i]; });

for (auto i : diff_positions) {
    // 处理差异
}
6. RAII 资源管理总结对比
资源类型 旧代码管理方式 新代码管理方式
文件句柄 手动 fopen/fclose std::ifstream (自动关闭)
内存缓冲区 手动 malloc/free std::vector (自动释放)
控制台颜色 手动配对设置/恢复 ConsoleColorGuard (RAII)
字符串 C风格字符数组 std::string (自动管理)
路径 MAX_PATH 缓冲区 std::filesystem::path
7. 类型安全和编译时检查
cpp 复制代码
// 旧代码:类型转换不安全
PUCHAR szBuffer1 = (PUCHAR)malloc(4096);

// 新代码:类型明确,无强制转换
std::vector<std::uint8_t> buffer(BUFFER_SIZE, 0);

// 使用 concepts 约束模板参数
template<typename StreamType>
requires std::derived_from<StreamType, std::istream>
static auto GetFileSize(StreamType& stream) -> std::uintmax_t

这个重构版本充分利用了 C++20 的现代特性,提供了更好的类型安全、错误处理和代码可维护性,同时保持了原有的功能完整性。

相关推荐
西城微科方案开发3 小时前
华润微CS98P370D2L:高性价比低功耗8位MCU
单片机·嵌入式硬件
时光の尘4 小时前
【嵌入式大厂面经】·CAN总线常见考点(持续更新中···)
stm32·单片机·mcu·物联网·can·ack
小叮当⇔4 小时前
系统认为 “从网络 / AI 生成的文件” 不安全,禁止预览但允许手动打开
嵌入式硬件
国科安芯5 小时前
空间辐射环境下电机伺服系统的抗扰动控制:AS32S601 抗辐射 MCU 在航天机电执行机构中的多场景应用与可靠性评估
单片机·嵌入式硬件·mcu·cocos2d·risc-v
国科安芯6 小时前
AS32S601 抗辐射 MCU 在星载高速光通信链路的集成设计与性能验证
网络·单片机·嵌入式硬件·risc-v·安全性测试
行者将至X6 小时前
第一篇:RA-ECO-RA4M2 开发板开箱与开发环境体验评测
单片机·嵌入式硬件
染予6 小时前
将星历算法移植到stm32F427要面临的问题
stm32·单片机·嵌入式硬件
山木嵌入式7 小时前
零基础入门单片机:从核心组成到最小系统全解析
单片机·最小系统·单片机入门
笨笨饿7 小时前
80_聊聊SPI以及它们的变体
linux·c语言·网络·stm32·单片机·算法·个人开发