干Boss
通过前辈留下的印记,我们发现Boss主要有三个技能
- initMMAP
- setData
- getData
好,我们打Boss前主要就是,见招拆招,只要能躲过这三个技能,我们就可以无伤通关,下面开始拆招
拆招
同样我们通过前辈的了解,发现boss mmap有六个参数
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
- addr 代表映射的虚拟内存起始地址;
- length 代表该映射长度;
- prot 描述了这块新的内存区域的访问权限;
- flags 描述了该映射的类型;
- fd 代表文件描述符;
- offset 代表文件内的偏移值;
关键在于对length、prot、flags、 fd四个参数的调整,来让boss变化形态。boss的核心设计如下
m_ptr = (char *) ::mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
- m_ptr 将boss指针交给它,后面通过它来打boss
- m_size 设计boss怒气值,怒气值满了就死掉,不是血条哈,因为这个boss免疫任何伤害,但就怕被人给它东西,一给东西就涨怒气值,自己气自己,你说气人不
- PROT_READ | PROT_WRITE,设计可读可写
- MAP_SHARED,设计共享
- m_fd,boss真身的位置,这个boss比较狡猾,喜欢保护自己,如果突然断电了,这boss能跑掉,你只能下次宰它了
这样一来,我们的boss对象就有着落了,为了打他方便,我们把所有变量都存起来,然后设计一些工具方法来揍他
class MMIPC { int m_fd = -1; //文件句柄,boss的真身 string m_path; //文件的路径,用于初始化文件句柄,boss真身的位置 char *m_ptr; //mmap内存的指针,boss的替身,对它的伤害最终会作用到真身上 size_t m_size; //mmap内存映射大小,boss的怒气值,越打越大,满了会自杀,估计是被气死了 size_t default_mmap_size; //系统规定的默认缓存页大小,怒气值的大小限制就靠它了 size_t m_position = 0; //当前mmap内存数据记录的位置,当前怒气值位置 public: ~MMIPC() { doCleanMemoryCache(true); } //对象销毁时自动调用 void doCleanMemoryCache(bool forceClean); //boss死的时候要清理战场 bool open();//链接Boss真身 void close();//关闭真身的链接,可以打扫战场了 bool truncate(size_t size);//刷新怒气值 bool mmap();//构建怒气值槽 void reloadMmap(const string &path);//多个人来打boss时,重新链接boss void setData(const string &key, const string &value);//打他 string getData(const string &key, const string &value);//同步怒气值给别的人 bool isFileValid() { return m_fd >= 0; }//检查这个boss能不能被打 };
通过如上,我们是不是终于摸透了Boss,那就打吧开始,还墨迹啥。
开始
由于initMMAP无法限制游戏角色调几次,所以需要设计一个多次调用的兼容逻辑,防止重新加载出现异常,于是在initMMAP中,调用如下函数
void MMIPC::reloadMmap(const string &path) { m_path = path; // 重复加载时,文件句柄已经有效,所以先清理下缓存 if (isFileValid()) { doCleanMemoryCache(false); } // 获取系统默认缓存页大小,也就是boss的怒气槽大小 default_mmap_size = getPageSize(); // 如果打开文件失败就直接打印错误日志 if (!open()) { ALOGD("fail to open [%s], %d(%s)", m_path.c_str(), errno, strerror(errno)); } else { // 如果打开成功,这里通过m_fd获取实际文件大小,重新给m_size赋值 getFileSize(m_fd, m_size); // 如果文件大小小于一个缓存页大小,且不等0 if (m_size < default_mmap_size || (m_size % default_mmap_size != 0)) { // 获取比m_size 小 m_size*default_mmap_size分之一的值,这个大小说明:在怒气槽即将满时,给boss留点空间,好逃跑不是 size_t roundSize = (m_size / default_mmap_size + 1) * default_mmap_size; // 看下方函数解释 truncate(roundSize); } else { // 如果 m_size 大于一个缓存页大小,重新创建一个boss文件真身,继续干他 auto ret = mmap(); if (!ret) { // 如果创建替身失败了,boss就怂了 doCleanMemoryCache(true); } } } } // 此函数的目的是为了改变文件大小,也就是boss怒气槽的大小 bool MMIPC::truncate(size_t size) { // 文件句柄无效,还打个p,直接收拾装备回家吧,boss跑了 if (!isFileValid()) { return false; } // 如果怒气槽大小和当前大小一样,直接返回,不需要更新 if (size == m_size) { return true; } // 如果传进来的大小和旧的不一样,记录下当前怒气槽大小 auto oldSize = m_size; // 直接改变当前怒气槽大小 m_size = size; // 按照m_size开始改变文件m_fd怒气槽的大小 if (::ftruncate(m_fd, static_cast<off_t>(m_size)) != 0) { ALOGE("truncate failed [%s] to size %zu, %s", m_path.c_str(), m_size, strerror(errno)); m_size = oldSize; return false; } // 改变成功后,判断下旧的是否比现在的大 if (oldSize > m_size) { // 如果比现在的大,改变文件的读写位置,并在合适的位置进行填充0 if (!zeroFillFile(m_fd, oldSize, m_size - oldSize)) { ALOGE("zeroFile fail [%s] to size %zu, %s", m_path.c_str(), m_size, strerror(errno)); m_size = oldSize; return false; } } // 如果发现boss有替身,解除替身的引用,把替身受到的伤害,写入文件里 if (m_ptr) { if (munmap(m_ptr, oldSize) != 0) { ALOGE("munmap fail [%s], %s", m_path.c_str(), strerror(errno)); } } // 继续创建新的替身,又可以愉快的揍他了 auto ret = mmap(); if (!ret) { // 如果创建失败,则释放掉 doCleanMemoryCache(true); } return ret; }
boss替身在这里
bool MMIPC::mmap() { m_ptr = (char *) ::mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0); if (m_ptr == MAP_FAILED) { ALOGD("mmap failed"); m_ptr = nullptr; return false; } ALOGD("mmap success"); return true; }
boss真身在这里
bool MMIPC::open() { //判断文件是否已经打开过 if (isFileValid()) { return true; } //O_RDWR 可读可写,O_CREAT 如果没有则创建 m_fd = ::open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU); if (!isFileValid()) { //fail to open return false; } //输出文件句柄的值,可以判断m_fd的值是否==-1,来判断是否获取失败 ALOGD("open m_fd[%d], %s", m_fd, m_path.c_str()); return true; } void MMIPC::close() { if (isFileValid()) { // 关闭 if (::close(m_fd) == 0) { m_fd = -1; } else { //fail to close } } }
- open 通过游戏角色给的路径m_path,我们定位到boss的真身位置,并获取boss文件句柄
- close 关闭boss文件句柄
真实伤害
void MMIPC::setData(const string &key, const string &value) { // 组合伤害,憋大招 string content = key + ":" + value; if (m_position != 0) { content = "," + content; } ALOGD("setData content=%s", content.c_str()); size_t numberOfBytes = content.length(); // 如果伤害溢出,则抛异常给上面 if (m_position + numberOfBytes > m_size) { auto msg = "m_position: " + to_string(m_position) + ", numberOfBytes: " + to_string(numberOfBytes) + ", m_size: " + to_string(m_size); throw out_of_range(msg); } // 对替身施加伤害,每次都在上次的m_position之后,累加伤害值 memcpy(m_ptr + m_position, (void *) content.c_str(), numberOfBytes); // 刷新上次的m_position m_position += numberOfBytes; ALOGD("setData success m_ptr=%s", m_ptr); } // 其他跨进程的角色也可以同步到伤害的值 string MMIPC::getData(const string &key, const string &value) { return m_ptr; }
验证
- App被初始化两次,多进程初始化后都能见到Boss
- MainActivty,揍了两下
- OtherProcessActivity,获取MainActivty在主进程中打的伤害,完美跨进程通信
这里面看到两个有价值的信息
- m_fd = 78 两个进程是同样的虚拟地址,有个疑问,它们是一个虚拟地址么?(答案:不是的,有问题需要自己去探索,去搜吧)
- 缓存页的大小都是 4096,也就是4kb
复盘
打完boss我们做个小复盘,以便后面打的更快更爽。
做的好的
- 首先值得肯定是完成了,很多时候完成是第一步,设计过渡应避免
- 层级划分清晰,各个模块职责分明,功能实现干净利索不拖泥大水
做的不好的
- Boss真身在Sdcard,被别人上大号,连接上后,一刀抢走怎么办
- 伤害值,传输过程被别人拦截怎么办,本来你砍了1万点伤害,被别人改成1点
- 如果跨线程、跨进程都来了伤害,你怎么保证这些伤害都能正常到Boss身上
如何改进
- 有个办法就是将 真身放到data/data/包名目录下,有人说你为啥不用 mmap参数中 MAP_ANONYMOUS 属性,因为它只能父子进程通信,应用的子进程其实是zygote 的子进程,非你的,我把你当兄弟,你把我当儿子,但data/data下也有风险,如果root手机,你的通信数据就有可能被人篡改
- 如何防止被篡改呢?建议pb+加密,pb保证传输快,效率高,加密保证不被篡改
- 如果跨线程可以通过线程锁来实现,如果跨进程怎么办,我看binder实现中用到了MAP_PRIVATE,这个参数有个好处就是,当你往映射内存里写的时候,内核会先copy一份出来再写,这边避免其他进程读的时候不能读,这样一来是不是就好了呢,当然需要验证的(已验证,MAP_PRIVATE方式,拷贝再写入的值,并不会被刷的文件中,也就无法实现共享),多进程的写入,需要写测试用例验证。
以上改进,Doing .......
游戏副本在哪?
说了这么多,游戏副本在哪?欢迎跟我一起来打Boss