7z 自解压功能,实际是将三个文件连接在一起,第一个文件是 7z 的自解压模块(实际是一个通用的包含了界面界面的应用程序)+ config.txt(配置文件)+ 实际要解压的 7z 压缩包文件。三个文件通过 Windows 的 copy 命令拼接在一起,你也可以自己实现代码,将三个文件拼接在一起,因为第一个文件的首地址 PE 结构不变,所以当程序运行时相当于运行了 7z 的自解压模块。他们的组成如下图: 图中可以看的出来,使用 copy /b 将三个文件连接在了一起,我们需要在自解压的模块程序 7z_sfx.exe 中实现读取查找 config.txt 文件的位置和内容,从而也就可以得到自解压文件的起始位置。这样在解压文件的时候将包装在我们程序中的自解压程序起始地址传递进去就可以了。

前提条件

首先要在代码中找到被追加进自己程序的 config.txt 文件内容,config.txt 必须要有一个标识来记录文件的开头和结束,这样我们才知道这个文件中间的内容,参考 7z 自解压模块的代码,其在 config.txt 头部和尾部分别设计了两个标识,如下所示:

1
2
3
4
5
;!@Install@!UTF-8!
Title="SOFTWARE v1.0.0.0"
BeginPrompt="Do you want to install SOFTWARE v1.0.0.0?"
RunProgram="setup.exe"
;!@InstallEnd@!

在程序中只要将程序一块一块的读取到内存,对比每一个字节如果存在 ;!@Install@!UTF-8! 就是 config 文件的开头,存在 ;!@InstallEnd@! 就是 config 文件的结尾。这样中间的内容也就确定了,文件结尾的位置就是 7z 压缩包文件的开头。

实现代码

代码实现起来要考虑的内容还是比较多的,我参考了 7z 的代码从头实现了一遍,对每一个变量都做了作用注释,因为 7z 官方的代码一个注释都没有,看起来很难懂,索性就参考他的思路一点一点重写了一遍。调用 FindSignature 方法就可以查找到 config.txt 中的内容了,用 strOutput 参数将内容传出。 程序编译完成后,使用 copy /b 程序名 + 带有标记的 config.txt 就可以测试出效果,自己再加上解压的代码你就可以实现一个属于自己的自解压模块了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <iostream>
#include <fstream>
#include <windows.h>

static char kBeginSignature[] = { ';','!','@','I','n','s','t','a','l','l','@','!','U','T','F','-','8','!', 0 };
static char kEndSignature[] = { ';','!','@','I','n','s','t','a','l','l','E','n','d','@','!', 0 };

bool FindSignature(const std::string& strBeginSignature, const std::string& strEndSignature, std::string& strOutput)
{
const size_t nFixedBufferSize = (1 << 12);
char szApplication[MAX_PATH] = { 0 };
GetModuleFileNameA(NULL, szApplication, MAX_PATH);

FILE* hFile = NULL;
fopen_s(&hFile, szApplication, "rb");

// 标记是否找到头部
bool bFoundBegin = false;

// 记录需要跳过多少个字节(上一次读取长度不足的内容会被填充到当前 buffer 中)
size_t nBytesPrev = 0;

BYTE szBuffer[nFixedBufferSize] = { 0 };

for (;;)
{
size_t nReadSize = nFixedBufferSize - nBytesPrev;
size_t nProcessedSize = fread(szBuffer + nBytesPrev, 1, nReadSize, hFile);
if (nProcessedSize == 0)
return false;

// 上一次读取剩余的字节 + 本次读取到的字节总数
size_t nTotalSize = nBytesPrev + nProcessedSize;

// 标记读取出来的内存块中已经对比的数据位置
size_t nPos = 0;

for (;;)
{
if (!bFoundBegin)
{
// 剩余长度不足头部内容,直接跳出
if (nPos > nTotalSize - strBeginSignature.size())
break;

// 标记已经找到头部,找到头部后将读取指针移动到头部关键字末尾
if (memcmp(szBuffer + nPos, strBeginSignature.c_str(), strBeginSignature.size()) == 0)
{
bFoundBegin = true;
nPos += strBeginSignature.size();
}
else
{
nPos++;
}
}
else
{
// 剩余长度不足尾部内容,直接跳出
if (nPos > nTotalSize - strEndSignature.size())
break;

// 如果找到末尾则直接返回
if (memcmp(szBuffer + nPos, strEndSignature.c_str(), strEndSignature.size()) == 0)
return true;

// 将不是末尾标记的数据追加给传出参数
BYTE pByte = szBuffer[nPos];

// 程序中常量字符串末尾是 0,但文件中不是 0,如果读到 0 证明是程序中的常量,而不是文件中的
if (pByte == 0)
{
bFoundBegin = false;
break;
}
strOutput += pByte;

// 向后步进
nPos++;
}
}

// 记录下次需要跳过的字节数量
nBytesPrev = nTotalSize - nPos;

// 将不足以对比的剩余内容拷贝到 buffer 的首位,下次读取的新数据衔接在该数据后面
memmove(szBuffer, szBuffer + nPos, nBytesPrev);
}

if (hFile)
{
fclose(hFile);
}

return false;
}

int main()
{
std::string strOutput;
if (FindSignature(kBeginSignature, kEndSignature, strOutput))
std::cout << strOutput.c_str() << std::endl;
else
std::cout << "Not found..." << std::endl;

}