欢迎来到每日并非每日一个嘻家家小寄巧第五期——使用模板获取对象的私有成员,为什么说是第五期呢,因为之前的几期都在xa大佬的博客上,大家可以前往xa大佬的博客进行学习:
- 第一期:关于RVO和std::move
- 第二期:C++类模板中声明友元函数
- 第三期:获取 C++ 结构体字段数量
- 第四期:在 C++ 里实现 Go 的接口 / Rust 的 Trait
言归正传,这个需求来自于有人问我mockcpp能不能mock对象的私有成员,我说那当然没问题了,mockcpp从原理上讲和gmock等库完全不同,它采用的是(游戏外挂经常使用的Hook)胡克技术,即在运行时修改函数开头的几个指令,将其改为跳转至mock函数,从而实现非侵入式的mock,和gmock相比可谓是好用到家了,而且更重要的是只需要函数地址就能mock,无论是成员函数、虚函数,乃至c的函数都能使用。
综上所述,只要我们能获取到对象私有成员函数的地址,不就能进行mock了吗,那么问题就来了,众所周知,在对象外部是无法访问对象内部的私有成员的,那么如何在不修改原类的代码的情况下获取到私有成员函数的指针呢?(如果能改原来代码的话用个getter暴露出来就好了)
一、打CTF打的
基于我在工作中的嵌入式开发经验,我第一反应是反汇编获取函数地址,然后在运行测试的时候通过某种方式将函数地址传进去,但是在学校打CTF的经验立刻否定了这种想法,这在单片机上是可行的,但在Linux等系统中有基地址偏移和地址空间分布随机化(ASLR)等安全手段,导致这种办法几乎不可行。
二、模板黑科技
然后,我突然灵光一闪,想起几年前xa大佬在从Mlivus实习回来后给我讲过的一个模板黑科技,那就是通过模板获取对象中的私有成员,没想到几年后在这用上了,可惜我当时只觉得这是屠龙之术,没有认真听讲,记不住具体的原理和细节了,好在一通google之下我还真搜到了一篇相关的文章:access private var in c++
如果懒得看原文(建议还是看一下原文,有作者写的和其他方式的对比以及思考过程),那我简单总结一下,就是模板显式实例化会忽略成员访问说明符,不会执行访问控制检查。利用这个特性就可以实现在对象外面访问对象里面的私有成员,示例代码如下:
class Bank { int money = 999'999'999; public: int getMoney() { return money; } }; template<typename Bank, typename Money, Money Bank::* p> class Thief { public: friend Money& steal(Bank& bank) { return bank.*p; } }; // 显式实例化类模板 Thief,编译器生成模板类 Thief<Bank, int, &Bank::money> // https://cppinsights.io/s/0f7828b7 template class Thief<Bank, int, &Bank::money>; int& steal(Bank&); int main() { Bank bank; cout << bank.getMoney() << endl; // 999999999 steal(bank) -= 123456789; cout << bank.getMoney() << endl; // 876543210 }
可是这篇文章里只有对私有成员变量的访问,如果想获取私有成员函数的指针应该怎么做呢,我想既然成员变量能获取到,那理论上成员函数指针也能获取到,毕竟都是成员,没理由不行,于是我们中午利用午饭时间拉了半个点的会,居然成功举一反三,真被我们给搞出来了,这个过程中我学习了成员函数的指针类型应该怎么写,也对模板有了更深的理解,下面是我们的研究成果(刚开始因为自己犯蠢,走了不少弯路):
// 这回我们以银行卡为例, wls的卡里没钱了, 现在想偷偷把钱改成999999999, 可是setMoney函数只能被ATM机访问, wls该怎么办呢 class DepositCard { int money = 0; void setMoney(int money) { this->money = money; } public: int getMoney() { return money; } }; template<typename Card, typename setMoney, setMoney Card::* f> class Thief { public: // 第一个坑, 返回的不是引用而是函数指针, 所以返回值类型应为setMoney Card::* // 如果你想的话可以用bind把card绑定进去, 这样就会返回std::function<setMoney>, 可以直接调用, 不需要card了 friend setMoney Card::* steal(Card& card) { // 第二个坑, 我脑子抽抽了, 定位20分钟才发现, 我们要的是类的成员函数指针, 不是对象的成员变量 // 所以不需要绑定到对象, 这也是和获取对象成员变量之间的最大差异, 直接返回f即可 // return card.*f; return f; } }; template class Thief<DepositCard, void (int), &DepositCard::setMoney>; using SetMoneyFunc = void (DepositCard::*)(int); // *是函数指针, DepositCard::表示是DepositCard对象的成员函数, 因此这里的*不可省略 SetMoneyFunc steal(DepositCard&); int main() { DepositCard card; cout << card.getMoney() << endl; // 0 (card.*steal(card))(999'999'999); cout << card.getMoney() << endl; // 999999999 }
好的,本期嘻家家小寄巧就学习到这里了,同时恭喜wls也成为亿万富翁了(撒花),最后再给大家推荐两个学习嘻家家的神器,是两个在线编译器,一个可以用来看汇编(嵌入式必备),另一个可以看模板展开后的代码(学习模板元必备),我们下期再见!
PS:你说这些玩意都谁想出来的呢(咋嫩无聊)