在C++中创建自定义的EXE文件
我正在开发一个简单的游戏引擎,只是为了体验一下。不过,我已经意识到,我不知道如何将用户的自定义游戏导出为自己的独立可执行文件。例如,这不是我实际的游戏引擎,它只是提供了一个简单的讨论参考,假设我们有以下非常简单的代码:在C++中创建自定义的EXE文件,c++,C++,我正在开发一个简单的游戏引擎,只是为了体验一下。不过,我已经意识到,我不知道如何将用户的自定义游戏导出为自己的独立可执行文件。例如,这不是我实际的游戏引擎,它只是提供了一个简单的讨论参考,假设我们有以下非常简单的代码: #include "stdafx.h" #include <iostream> #include <string> using namespace std; void RunGame(string question, string answer) {
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
void RunGame(string question, string answer)
{
string submission;
cout << question << endl;
getline(cin, submission);
if (submission == answer)
cout << "Correct!";
else
cout << "Wrong!";
}
int main()
{
string question;
string answer;
cout << "Enter Question:" << endl;
getline(cin, question);
cout << "Enter Answer:" << endl;
getline(cin, answer);
RunGame(question, answer);
}
在本例中,用户可以创建自己的定制琐事,然后在调用RunGame后立即对其进行测试。现在,我想能够保存他们的游戏与琐事信息,他们提供了自己的。基本上,它将执行从调用RunGame开始。我该怎么做呢
明确地说,这不是一个关于制作游戏最简单/最快的方法的问题。它正在寻找如何从代码中构建独立的可执行文件。您不想这样做。更好的方法是以一些自定义格式保存琐事,例如.txt、.dat、 然后游戏只处理这些数据 因此,首先考虑一下.txt内部的格式,例如 让我们假设首先有一个数字,指示这是哪个条目。其次是问题,然后是答案。这一点,你必须自己决定 示例trivia-data.txt
这比你的方法要简单得多,而且这也使得其他人(而不仅仅是你)可以进行测验。你不想这样做。更好的方法是以一些自定义格式保存琐事,例如.txt、.dat、 然后游戏只处理这些数据 因此,首先考虑一下.txt内部的格式,例如 让我们假设首先有一个数字,指示这是哪个条目。其次是问题,然后是答案。这一点,你必须自己决定 示例trivia-data.txt 这比你的方法要简单得多,这也使得其他人(而不仅仅是你)可以创建测验。如果你真的想在.exe本身中存储数据: 一个可执行文件有一个头文件,它定义了它的大小、边界和其他对操作系统有用的东西,因此,从本质上说,操作系统知道代码和数据段的开始和结束位置,当要求运行时,它最终使用此信息将.exe加载到内存中 由于操作系统除了知道.exe的文件大小外,还知道可执行文件的实际结束位置,这也意味着在.exe的计算结束方式头之后粘贴的任何数据都不会对二进制文件产生负面影响。它仍将加载并执行得很好 可以滥用此属性在可执行文件结束后连接数据 我将把这个测试留给您,使用Windows捆绑的写字板应用程序作为一些其他数据的“主机”: 转到C:\Windows并将write.exe写字板复制到另一个文件夹,这样我们就可以在不损坏任何东西的情况下进行实验 将另一个文件带到该文件夹,任何文件都可以。在我的示例中,数据文件将是名为myfancyfile.PDF的PDF 现在,打开命令提示符并使用“复制”命令将两个文件缝合在一起,确保先使用.exe:
copy /B write.exe+myfancyfile.pdf mynewprogram.exe
copy的/B标志表示二进制复制,因此基本上两个文件都是粘贴在一起的,没有任何文本或数据转换。
尝试运行mynewprogram.exe。意识到它运行得很好:-
使用数据自修改.exe不仅可行,而且不会对功能产生负面影响。尽管如此,它仍然是一种保存数据的丑陋方式
享受编写解决方案的乐趣。如果您确实希望在.exe本身中存储数据:
一个可执行文件有一个头文件,它定义了它的大小、边界和其他对操作系统有用的东西,因此,从本质上说,操作系统知道代码和数据段的开始和结束位置,当要求运行时,它最终使用此信息将.exe加载到内存中
由于操作系统除了知道.exe的文件大小外,还知道可执行文件的实际结束位置,这也意味着在.exe的计算结束方式头之后粘贴的任何数据都不会对二进制文件产生负面影响。它仍将加载并执行得很好
可以滥用此属性在可执行文件结束后连接数据
我将把这个测试留给您,使用Windows捆绑的写字板应用程序作为一些其他数据的“主机”:
转到C:\Windows并将write.exe写字板复制到另一个文件夹,这样我们就可以在不损坏任何东西的情况下进行实验
将另一个文件带到该文件夹,任何文件都可以。在我的示例中,数据文件将是名为myfancyfile.PDF的PDF
现在,打开命令提示符并使用“复制”命令将两个文件缝合在一起,确保先使用.exe:
copy /B write.exe+myfancyfile.pdf mynewprogram.exe
copy的/B标志表示二进制复制,因此基本上两个文件都是粘贴在一起的,没有任何文本或数据转换。
尝试运行mynewprogram.exe。意识到它运行得很好:-
自我修饰
带有数据的ur.exe不仅可行,而且不会对功能产生负面影响。尽管如此,它仍然是一种保存数据的丑陋方式
享受编写解决方案的乐趣。构建可执行文件绝非易事。您首先需要遵守目标操作系统的ABI,以便它能够找到您的程序的入口点。下一步将决定您的程序将如何访问系统资源:您可能希望您的可执行文件实现动态链接,以便它能够访问共享库,并且您需要加载您将需要的各种.dll或.so文件。您需要为此编写的所有说明因操作系统而异,您可能需要引入逻辑来检测确切的平台并做出明智的决策,并且您需要针对32位和64位进行更改 在这一点上,您就可以开始为您的游戏发出机器指令了 这里一个合理的替代方案是Unity提供一个引擎的空白可执行文件。您的引擎本身将是一个共享库。DLL或.SO和空白可执行文件只不过是一个包装器,它加载共享库并用指针指向它的数据段中的某个函数。 生成用户的可执行文件将包括加载适当的空白文件,对其进行特定于平台的修改,以告诉它您打算向其提供的数据部分的大小,并以适当的格式写入数据。或者,您可以简单地创建一个空白,其中包含原始结构的嵌入副本,您可以将值写入其中,就像在内存中填充结构一样:
struct GameDefinition {
constexpr size_t AuthorNameLen = 80;
char author_[AutherNameLen+1];
constexpr size_t PublisherNameLen = 80;
char publisher_[PublisherNameLen+1];
constexpr size_t GameNameLen = 80;
char name_[GameNameLen+1];
constexpr size_t QuestionLen = 80;
constexpr size_t AnswerLen = 80;
char question_[QuestionLen+1];
char answer_[AnswerLen+1];
};
static GameDefinition gameDef;
#include "engine_library.h" // for run_engine
int main() {
run_engine(&gameDef);
}
您需要重新编译此文件,并将其作为可执行文件发送给引擎的共享库存根,然后查找可执行文件格式的特定于平台的详细信息,找到gameDef在其中的位置。你把空白读入内存中,然后用GAMDEF的定义写出来,替换为一个基于用户输入的定义。
但许多引擎所做的只是简单地发送或要求用户安装编译器Unity依赖于C。因此,它们不必调整可执行文件并完成所有这些疯狂的特定于平台的工作,只需输出一个C/C++程序并编译即可
// game-generator
bool make_game(std::string filename, std::string q, std::string a) {
std::ostream cpp(filename + ".cpp");
if (!cpp.is_open()) {
std::cerr << "open failed\n";
return false;
}
cpp << "#include <engine.h>\n";
cpp << "Gamedef gd(\"" << gameName << "\", \"" << authorName << \");\n";
cpp << "int main() {\n";
cpp << " gd.q = \"" << q << \"\n";
cpp << " gd.a = \"" << a << \"\n";
cpp << " RunGame(gd);\n";
cpp << "}\n";
cpp.close();
if (!invoke_compiler(filename, ".cpp")) {
std::cerr << "compile failed\n";
return false;
}
if (!invoke_linker(filename)) {
std::cerr << "link failed\n";
return false;
}
}
然后
g++ -Wall -O3 -o ${filename} ${filename}.o -lengine_library
将其链接到引擎库。构建可执行文件并非易事。您首先需要遵守目标操作系统的ABI,以便它能够找到您的程序的入口点。下一步将决定您的程序将如何访问系统资源:您可能希望您的可执行文件实现动态链接,以便它能够访问共享库,并且您需要加载您将需要的各种.dll或.so文件。您需要为此编写的所有说明因操作系统而异,您可能需要引入逻辑来检测确切的平台并做出明智的决策,并且您需要针对32位和64位进行更改 在这一点上,您就可以开始为您的游戏发出机器指令了 这里一个合理的替代方案是Unity提供一个引擎的空白可执行文件。您的引擎本身将是一个共享库。DLL或.SO和空白可执行文件只不过是一个包装器,它加载共享库并用指针指向它的数据段中的某个函数。 生成用户的可执行文件将包括加载适当的空白文件,对其进行特定于平台的修改,以告诉它您打算向其提供的数据部分的大小,并以适当的格式写入数据。或者,您可以简单地创建一个空白,其中包含原始结构的嵌入副本,您可以将值写入其中,就像在内存中填充结构一样:
struct GameDefinition {
constexpr size_t AuthorNameLen = 80;
char author_[AutherNameLen+1];
constexpr size_t PublisherNameLen = 80;
char publisher_[PublisherNameLen+1];
constexpr size_t GameNameLen = 80;
char name_[GameNameLen+1];
constexpr size_t QuestionLen = 80;
constexpr size_t AnswerLen = 80;
char question_[QuestionLen+1];
char answer_[AnswerLen+1];
};
static GameDefinition gameDef;
#include "engine_library.h" // for run_engine
int main() {
run_engine(&gameDef);
}
您需要重新编译此文件,并将其作为可执行文件发送给引擎的共享库存根,然后查找可执行文件格式的特定于平台的详细信息,找到gameDef在其中的位置。你把空白读入内存中,然后用GAMDEF的定义写出来,替换为一个基于用户输入的定义。
但许多引擎所做的只是简单地发送或要求用户安装编译器Unity依赖于C。因此,它们不必调整可执行文件并完成所有这些疯狂的特定于平台的工作,只需输出一个C/C++程序并编译即可
// game-generator
bool make_game(std::string filename, std::string q, std::string a) {
std::ostream cpp(filename + ".cpp");
if (!cpp.is_open()) {
std::cerr << "open failed\n";
return false;
}
cpp << "#include <engine.h>\n";
cpp << "Gamedef gd(\"" << gameName << "\", \"" << authorName << \");\n";
cpp << "int main() {\n";
cpp << " gd.q = \"" << q << \"\n";
cpp << " gd.a = \"" << a << \"\n";
cpp << " RunGame(gd);\n";
cpp << "}\n";
cpp.close();
if (!invoke_compiler(filename, ".cpp")) {
std::cerr << "compile failed\n";
return false;
}
if (!invoke_linker(filename)) {
std::cerr << "link failed\n";
return false;
}
}
然后
g++ -Wall -O3 -o ${filename} ${filename}.o -lengine_library
将其链接到引擎的库。< /p>您是否尝试将客户的数据放入数据文件中而不是可执行文件中?您必须采用主要方法:1将数据写入C或C++数据报表,并将其编译成主程序。2研究可执行文件的格式,找出如何将数据添加到可执行文件中。第2项可能会变得困难,特别是如果您调整了函数或静态数据的任何地址。例如,如果可执行文件要求数据表为20,而您将其扩展为40,则需要调整偏移量
平台有一个unexec函数,可以根据程序的当前状态创建可执行文件,但Windows没有类似的功能。如果要生成可执行文件,需要编译器堆栈。创建自己的语言并使用LLVM非常容易。@ArcaneLight这些引擎都没有将游戏导出到可执行文件,它们生成的代码可以编译或自定义stock player可执行文件,并将数据放入数据文件中。对于Windows生成目标,将生成一个可执行文件.exe,除了一个包含你的应用程序的所有资源的数据文件夹,你还试着将客户的数据放到一个数据文件中而不是在可执行文件中?你必须要有主要的方法:1把数据写成C或C++数据语句,并把它编译成一个主程序。2研究可执行文件的格式,找出如何将数据添加到可执行文件中。第2项可能会变得困难,特别是如果您调整了函数或静态数据的任何地址。例如,如果可执行文件要求数据表为20,而您将其扩展为40,则需要调整偏移量。某些类似UNIX的平台具有unexec函数,可根据程序的当前状态创建可执行文件,但Windows没有类似的功能。如果要生成可执行文件,您需要一个编译器堆栈。创建自己的语言并使用LLVM非常容易。@ArcaneLight这些引擎都没有将游戏导出到可执行文件,它们生成的代码可以编译或自定义stock player可执行文件,并将数据放入数据文件中。对于Windows生成目标,将生成一个可执行文件.exe,以及一个数据文件夹,其中包含应用程序的所有资源。我试图说服OP使用此路由,但OP坚持使用可执行文件:-@是的,我刚看到。哦,好吧。我试着说服OP使用这条路线,但OP坚持使用可执行文件-@是的,我刚看到。Oh well.OP还可以修改全局字符串/数组,如果他在.exe中添加某种标记来查找它们。程序如何知道所显示的数据在哪里?@HolyBlackCat谢谢,根据上面kfsone的评论,我想我已经找到了答案。@ThomasMatthews这里有一个示例,说明如何让程序知道在其.exe | | 1中查找/更新数据,让程序有逻辑在自己的文件中查找模式,如标记,例如:!数据数据//2编译它。//3将其与数据标记一起附加,您的程序现在已准备就绪并可交付//4无论何时运行,它都会找到数据标记,无论数据是否在ir中。使用您自己的编码方法更新/删除该数据。OP还可以修改全局字符串/数组,如果他在.exe中添加某种标记来查找这些字符串/数组。程序如何知道所显示的数据在哪里?@HolyBlackCat谢谢,根据上面kfsone的评论,我想我已经找到了答案。@ThomasMatthews这里有一个示例,说明如何让程序知道在其.exe | | 1中查找/更新数据,让程序有逻辑在自己的文件中查找模式,如标记,例如:!数据数据//2编译它。//3将其与数据标记一起附加,您的程序现在已准备就绪并可交付//4无论何时运行,它都会找到数据标记,无论数据是否在ir中。使用您自己的编码方法来更新/删除该数据。不确定在我的手机上这样做有多聪明,希望它清晰易读。注意:Visual studio的编译器等是独立的应用程序,没有内置到Visual ide中。invoke_编译器和invoke_链接器是您必须编写的函数,它们可以简单到std::string args=g++-Wall-o。。。;systemargs.c_str;对于linux和与Windows等效的ifdfef WIN32部分,但您可能希望查看对子进程的细粒度控制。不确定在我的手机上这样做有多聪明,希望它清晰易读。注意:Visual studio的编译器等是独立的应用程序,未内置到Visual ide中。invoke_编译器和invoke_链接器是您必须编写的函数,它们可以简单到std::string args=g++-Wall-o。。。;systemargs.c_str;对于linux和一个ifdfef WIN32部分,它与Windows具有同等作用,但您可能希望了解对子进程的更细粒度控制。