知识大全 “扫雷”游戏的幕后
Posted 知
篇首语:知识的价值不在于占有,而在于使用。本文由小常识网(cha138.com)小编为大家整理,主要介绍了知识大全 “扫雷”游戏的幕后相关的知识,希望对你有一定的参考价值。
介绍
曾想了解 扫雷 游戏在幕后所发生的一切吗?嗯 我想过 还由此决定对其进行了研究 本文是我的研究结果 现公之于众
主要概念
使用 P/Invoke 调用 Win API
直接读取另一个进程的内存
注 本文的第一部分包括一些汇编代码 如果你不是很明白 无关要紧 这不是本文的目的 你尽可以跳过不管 然而 如果你想问我有关这些代码的问题 非常欢迎你写信给我
注 本程序是在Windows XP下测试的 所以如果它不能运行在其它的系统下 请注明该系统的信息 好让我们大家都知道
注 之更新: 本代码现在经过修改后也能在Windows 下运行 谢谢Ryan Schreiber找到了Win K下的内存地址
第一步 – 探索 winmine exe
如果你不是一个汇编迷 可以跳到这一步的最后 只看结论
为了更好地了解 扫雷 幕后所发生的一切 我以一个调试器打开此文件作为开端 我个人最喜欢的调试器是Olly Debugger v 这是一个非常简单且直观的调试器 总之 我在调试器中打开winmine exe 并查看该文件 我发现在Import区(列出在程序中用到的所有dll函数的区域)有下面一行
B D C DD msvcrt rand
这就意味着 扫雷 用到了VC运行库的随机函数 因此我认为这对我可能有帮助 我搜索了该文件 看看到底在哪里调用了rand()函数 不过只在一个地方找到了这个函数
FF B CALL DWORD PTR DS:[<&msvcrt rand>]
接着我在这一行单步调用插入了一个断点并运行程序 我发现每当点击笑脸图标时 一个新的布雷图就生成了 布雷图按以下步骤创建
首先 给布雷图分配一块内存区 并把所有的内存字节都设置成 x F 说明在该单元(cell)中没有地雷
其次 按地雷数遍历每一个地雷
随机化 x 位置 (取值在 至宽度之间) 随机化 y 位置 (取值在 至高度之间) 设置内存块中被选中的单元的值为 x F 这意味着在该单元中有一个地雷
下面是原码 我已加入了一些注释 并加粗了重点部分
A MOV DWORD PTR DS:[ ] EAX ; [ x ] = 宽度(即横向格数)
AC MOV DWORD PTR DS:[ ] ECX ; [ x ] = 高度(即纵向格数)
B CALL winmine ED ; 生成空的内存块并进行清除
B MOV EAX DWORD PTR DS:[ A ]
BC MOV DWORD PTR DS:[ ] EDI
C MOV DWORD PTR DS:[ ] EAX ; [ x ] = 地雷的个数
; 以地雷个数进行循环
C PUSH DWORD PTR DS:[ ] ; 把最大宽度(max width)压入栈
CD CALL winmine ; Mine_Width = 随机化 x 位置 ( 至 max width ) (即在 和max width 之间随机选一个值)
D PUSH DWORD PTR DS:[ ] ; 把最大高度压入栈
D MOV ESI EAX
DA INC ESI ; Mine_Width = Mine_Width +
DB CALL winmine ; Mine_Height =随机化 y 位置
; ( 至 max height )
E INC EAX ; Mine_Height = Mine_Height +
E MOV ECX EAX ;计算单元在内存块(布雷图)中的地址
E SHL ECX ; 按这样计算
; 单元内存地址 = x + * height + width
E TEST BYTE PTR DS:[ECX+ESI+ ] ; [单元内存地址] ==是否已是地雷?
EE JNZ SHORT winmine C ; 如果已是地雷 则重新迭代
F SHL EAX ; 否则 设置此单元为地雷
F LEA EAX DWORD PTR DS:[EAX+ESI+ ]
FA OR BYTE PTR DS:[EAX]
FD DEC DWORD PTR DS:[ ]
JNZ SHORT winmine C ; 进行下一次迭代
正如你从代码所看到的 我发现了 个要点
读内存地址[ x ]得出布雷图的宽度
读内存地址[ x ]得出布雷图的高度
读内存地址[ x ]得出布雷图中地雷的个数
给出x y 它们代表布雷图中的一个单元 位于x列 y行 地址 [ x + * y + x] 给出了该单元的值 这样我们就进入了下一步
第 步– 设计一个解决方案
你可能在想 我将会谈到了哪一种解决方案呢?显然 在发现了所有的地雷信息均可为我所用后 我所要做的就是从内存中读取数据 我决定编写读取这些信息的一个小程序 并给予说明 它能自己绘出布雷图 显示出每一个被发现的地雷
那么 怎么设计呢?我所做的就是把地址装到一个指针中(是的 它在C#中还存在) 并读出其所指的数据 这样行吗?嗯 并不完全如些 因为场合不同 存储这些数据的内存并不在我的应用程序之中 要知道 每一个进程都拥有自己的地址空间 所以它就不会 意外地 访问属于别的程序的内存 因此 为了能读出这此数据 就必须找到一种方法 用来读取另一个进程的内存 在本例中 这个进程就是 扫雷 进程
我决定写一个小小的类库 它将接收一个进程 并提供读取该进程内存地址的功能 之所以这样做 是因为我还要在很多程序中用到它 没有必要反反复复地编写这些代码 这样 你就可以得到这个类 并在应用程序中使用它 且是免费的 例如 如果你编写一个调试器 这个类对你会有所帮助 据我所知 所有的调试器都具有读取被调试程序内存的能力
那么 我们怎么才能读取别的进程的内存呢?答案在于一个叫做ReadProcessMemory的API 这个API实际上可以让你读取进程内存中的一个指定地址 但在进行此操作之前 必须以特定的模式打开进程 而在完成操作之后 就必须关闭句柄以避免资源泄漏 我们利用OpenProcess 和 CloseHandle这几个API的帮助说明 完成了相应的操作
为了在C#中使用API 必须使用P/Invoke 这意味着在使用API之前需要先对其进行声明 一般情况下都很简单 但要是让你以 NET的方式实现的话 有时就不那么容易了 我在MSDN中找到了这些API声明
HANDLE OpenProcess(
DWORD dwDesiredAccess // 访问标志
BOOL bInheritHandle // 句柄继承选项
DWORD dwProcessId // 进程ID
);
BOOL ReadProcessMemory(
HANDLE hProcess // 进程句柄
LPCVOID lpBaseAddress // 内存区基址
LPVOID lpBuffer // 数据缓冲
SIZE_T nSize // 要读的字节数
SIZE_T * lpNumberOfBytesRead // 已读字节数
);
BOOL CloseHandle(
HANDLE hObject // 进程句柄
);
这些声明转换为如下的C#声明
[DllImport( kernel dll )]
public static extern IntPtr OpenProcess(
UInt dwDesiredAccess
Int bInheritHandle
UInt dwProcessId
);
[DllImport( kernel dll )]
public static extern Int ReadProcessMemory(
IntPtr hProcess
IntPtr lpBaseAddress
[In Out] byte[] buffer
UInt size
out IntPtr lpNumberOfBytesRead
);
[DllImport( kernel dll )] public static extern Int CloseHandle(
IntPtr hObject
);
如果你想知道在c++和c#之间有关类型转换的更多信息 我建议你从站点搜索此话题 Marshaling Data with Platform Invoke 基本上 如果你把逻辑上是正确的程序搁在那儿 它便能运行 但有时还需要一点点的调整
在声明了这些函数之后 我要做的是用一个简单的类把它们包装起来 并使用这个类 我把声明放在一个叫做ProcessMemoryReaderApi的类中 这样做更有条有理 主要的实用类称为ProcessMemoryReade 这个类有一个ReadProcess属性 它源于System Diagnostics Process类型 用于存放你要读取其内存的进程 类中有一个方法 用来以读模式打开进程
public void OpenProcess()
m_hProcess = ProcessMemoryReaderApi OpenProcess(
ProcessMemoryReaderApi PROCESS_VM_READ
(uint)m_ReadProcess Id);
PROCESS_VM_READ 常量告诉系统以读模式打开进程 而m_ReadProcess Id 声明了我要打开的是什么进程
在该类中最重要的是一个方法 它从进程中读取内存
public byte[] ReadProcessMemory(IntPtr MemoryAddress uint bytesToRead
out int bytesReaded)
byte[] buffer = new byte[bytesToRead];
IntPtr ptrBytesReaded;
ProcessMemoryReaderApi ReadProcessMemory(m_hProcess MemoryAddress buffer
bytesToRead out ptrBytesReaded);
bytesReaded = ptrBytesReaded ToInt ();
return buffer;
这个函数以所请求的大小声明一个字节数组 并使用API读取内存 就这么简单!
最后 下面这个方法关闭了进程
public void CloseHandle()
int iRetValue;
iRetValue = ProcessMemoryReaderApi CloseHandle(m_hProcess);
if (iRetValue == )
throw new Exception( CloseHandle failed );
第三步 – 使用类
现在轮到了有趣的部分 使用这个类就是为了读取 扫雷 的内存并揭开布雷图 要使用类 需要先对其进行初始化
ProcessMemoryReaderLib ProcessMemoryReader pReader
= new ProcessMemoryReaderLib ProcessMemoryReader();
接着 必须设置你想要读取其内存的进程 以下是如何获得 扫雷 进程的例子 这个进程一旦被装入 就被设置为ReadProcess属性
System Diagnostics Process[] myProcesses
= System Diagnostics Process GetProcessesByName( winmine );
pReader ReadProcess = myProcesses[ ];
我们现在需要做的是 打开进程 读取内存 并在完成后关闭它 下面还是有关操作的例子 它读取代表布雷图宽度的地址
pReader OpenProcess();
int iWidth;
byte[] memory;
memory = pReader ReadProcessMemory((IntPtr) x out bytesReaded);
iWidth = memory[ ];
pReader CloseHandle();
简单吧!
在结论部分 我列出了显示布雷图的完整代码 别忘了 我要访问的所有内存位置就是在本文第一部分中所找到位置
// 布雷图的资料管理器
System Resources ResourceManager resources = new System Resources ResourceManager(typeof(Form ));
ProcessMemoryReaderLib ProcessMemoryReader pReader
= new ProcessMemoryReaderLib ProcessMemoryReader();
System Diagnostics Process[] myProcesses
= System Diagnostics Process GetProcessesByName( winmine );
// 获得 扫雷 进程的第一个实列
if (myProcesses Length == )
MessageBox Show( No MineSweeper process found! );
return;
pReader ReadProcess = myProcesses[ ];
// 以读内存模式打开进程
pReader OpenProcess();
int bytesReaded;
int iWidth iHeight iMines;
int iIsMine;
int iCellAddress;
byte[] memory;
memory = pReader ReadProcessMemory((IntPtr) x out bytesReaded);
iWidth = memory[ ];
txtWidth Text = iWidth ToString();
memory = pReader ReadProcessMemory((IntPtr) x out bytesReaded);
iHeight = memory[ ];
txtHeight Text = iHeight ToString();
memory = pReader ReadProcessMemory((IntPtr) x out bytesReaded);
iMines = memory[ ];
txtMines Text = iMines ToString();
// 删除以前的按钮数组
this Controls Clear();
this Controls AddRange(MainControls);
// 创建一个按钮数组 用于画出布雷图的每一格
ButtonArray = new System Windows Forms Button[iWidth iHeight];
int x y;
for (y= ; y<iHeight ; y++)
for (x= ; x<iWidth ; x++)
ButtonArray[x y] = new System Windows Forms Button();
ButtonArray[x y] Location = new System Drawing Point( + x* + y* );
ButtonArray[x y] Name = ;
ButtonArray[x y] Size = new System Drawing Size( );
iCellAddress = ( x ) + ( * (y+ )) + (x+ );
memory = pReader ReadProcessMemory((IntPtr)iCellAddress out bytesReaded);
iIsMine = memory[ ];
if (iIsMine == x f)//如果有雷 则画出地雷位图
ButtonArray[x y] Image = ((System Drawing Bitmap)
(resources GetObject( button Image )));
this Controls Add(ButtonArray[x y]);
// 关闭进程句柄
pReader CloseHandle();
cha138/Article/program/net/201311/13469相关参考
<HTML><HEAD><TITLE>Javascript地雷</TITLE> <style>inputno backgroundcolor:eeeeee; border:n
装有扫雷器的坦克。用于在地雷场中为坦克开辟道路。扫雷器主要有机械扫雷器和爆破扫雷器两类。机械扫雷器又分滚压式、挖掘式和打击式三种。前两种开辟车辙式通路,每侧扫雷宽度
使用扫雷、猎雷、破雷设备搜索和排除水雷的舰艇。包括扫雷舰艇、猎雷舰和破雷舰。扫雷舰艇有:舰队扫雷艇、基地扫雷舰、港海扫雷舰、扫雷母舰等,主要用于在基地、港口附近及近
对于体内的多数脏器,不管我们是否学医出身的,由于经常使用诸如肝胆相照、脑满肠肥、撕心裂肺、胃口不小、脾气大、肾气不足等词汇,或多或少知道些它们的功能或形态。惟有胰腺这个脏器,是大家不太熟悉的。这是由于
法国专门制造水下工作机器人的因泰絮德公司,1989年4月在塞纳滨海省介绍了它的第一个用于扫雷的陆地机器人。这个命名为机动操作平台250型的机器人,是一个远距离操纵的
Java扫雷算法 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧! 建立一个雷区可以用一个一个的JB
扫雷程序(Java语言) 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧! importjavaaw
Java实现扫雷(2) 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧! */ //清楚原来的方形
恒大真正的幕后老板是谁?是许家印。许家印才是真正的草根英雄,恒大地产的成功是许家印一步一个脚印走出来的,比起前段时间传的沸沸扬扬的巨力集团的公子哥杨子,许家印的成功更具借鉴性。他的成功更多地归功于他极
趣味编程:自创C#扫雷代码 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧! &