文章目录
- [C语言 完整实现](#C语言 完整实现)
- C++重构版本
-
- 重构后的完整代码(C++20)
- 重构说明文档
-
- 主要改进点
-
- [1. **现代 C++ 特性使用**](#1. 现代 C++ 特性使用)
- [2. **错误处理改进**](#2. 错误处理改进)
- [3. **RAII 包装器**](#3. RAII 包装器)
- [4. **结构化数据**](#4. 结构化数据)
- [5. **使用 ranges 和 views**](#5. 使用 ranges 和 views)
- [6. **RAII 资源管理总结对比**](#6. RAII 资源管理总结对比)
- [7. **类型安全和编译时检查**](#7. 类型安全和编译时检查)
步骤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 的现代特性,提供了更好的类型安全、错误处理和代码可维护性,同时保持了原有的功能完整性。