一种实现C++反射功能的想法(二)

在介绍我的思路前, 让我们准备下预备知识

C++是怎么实现类函数的绑定的. 我们知道类的非静态成员函数是存储在全局区, 并在内存中只保存一份副本. 我们调用非静态成员函数是通过类对象进行调用. 那么如果有两个不同的类类型有同样的成员函数, 那么编译器是怎么区别的呢? 其实编译器作了某些工作, 将类似void A::test(int) 的成员函数改成 void test(const A*, int)这样的函数

1 class A {
 2 private:
 3     int a_;
 4 
 5 public:
 6     A(int a):a_(a){}
 7     void run(int data);
 8 
 9 };
10 
11 void A::run(int data) {
12 
13     cout<<data<<endl;
14     cout<<a_<<endl;
15 }
16 
17 void test() {}
18 typedef void(A::*func)(int data); 
19 typedef void(*funcc)(const A*, int data);
20 
21 int main(int argc, char** argv) {
22 
23     func p = &A::run;
24     int* dp = (int*)(reinterpret_cast<void*>(p));
25     cout<<dp<<endl;
26 
27     A a(6666);
28         //A* ptr = NULL;
29     funcc f = (funcc)(dp);
30     f(&a, 100);
31         //f(ptr, 8888);   // 将输出8888, 但输出a_时肯定出错
32 
33         return 0;  
34 }

运行结果:

一种实现C++反射功能的想法(二)

在linux 下终端开启一个程序, 都是shell进程开启一个子进程, 而我们知道每个进程都拥有独立的地址空间. 也就是说, C++程序静态区存放的数据地址位置都是固定的, 或者说相对固定, 相对自己的地址空间是固定的. 所以只要我们能获取函数的入口地址, 并将地址转化成相应的函数类型指针, 传入正确的参数, 就能够实现函数反射调用了.

先申明一下, 我所写的东西并非是工业级的应用, 纯粹是自己的技术爱好, 技术也比较稚嫩, 所以缺点肯定是有的, 效率低, 局限性大, 有效性低, 实际应用没有. 我只是很享受在深入探究的过程中, 尝试去解决问题并开阔自己视野, 丰富自己经历的过程, 虽然不一定每次都能完美地解决问题, 甚至我悲观地认为并不存在完美的解决方案. 有不足或错误的地方大家都可以交流提出指正. 言归正传.

如何获取这些信息, 或者说元信息. 我是在看调试器原理时发现的, 在使用GDB, 或者vs调试时, 你都能看到变量的地址, 函数的地址, 而且每次都不会变化, 调试器是如何知道的呢?其实这些都是调试信息, 是编译器在编译时加入到程序中的. (这里引出第一个限制条件, 调试版本, 发行版本并不包含调试信息, 这个功能就不能实现了) , window下的pdb文件, linux下的elf文件包含这些调试信息.

我是在linux 下尝试的, 环境是ubuntu 14 和 gcc 4.8.4. 调试信息是以dwarf格式存放在elf文件中, 可以使用objdump工具查看, 编译时加入 -g 选项表示是调试版本. 下面是一个例子

1 class Data1 {
 2 
 3 public:
 4 
 5     void hello(){}
 6     void test();
 7 };
 8 
 9 void Data1::test() {
10 
11 }
12 
13 int main() { 
14 }
./data1:     file format elf64-x86-64

Contents of the .debug_info section:

  Compilation Unit @ offset 0x0:
   Length:        0xc3 (32-bit)
   Version:       4
   Abbrev Offset: 0x0
   Pointer Size:  8
 <0><b>: Abbrev Number: 1 (DW_TAG_compile_unit)
    <c>   DW_AT_producer    : (indirect string, offset: 0x0): GNU C++ 4.8.4 -mtune=generic -march=x86-64 -g -fstack-protector    
    <10>   DW_AT_language    : 4    (C++)
    <11>   DW_AT_name        : (indirect string, offset: 0x46): ./data1.cpp    
    <15>   DW_AT_comp_dir    : (indirect string, offset: 0x52): /home/lz/Workplace/debug/reflection/demo    
    <19>   DW_AT_low_pc      : 0x4004ee    
    <21>   DW_AT_high_pc     : 0x15    
    <29>   DW_AT_stmt_list   : 0x0    
 <1><2d>: Abbrev Number: 2 (DW_TAG_class_type)   // 表示是一个类类型
    <2e>   DW_AT_name        : (indirect string, offset: 0x40): Data1    
    <32>   DW_AT_byte_size   : 1    
    <33>   DW_AT_decl_file   : 1    
    <34>   DW_AT_decl_line   : 1    
    <35>   DW_AT_sibling     : <0x6a>    
 <2><39>: Abbrev Number: 3 (DW_TAG_subprogram)   // 表示是一个函数类型
    <3a>   DW_AT_external    : 1    
    <3a>   DW_AT_name        : (indirect string, offset: 0x7b): hello    
    <3e>   DW_AT_decl_file   : 1    
    <3f>   DW_AT_decl_line   : 5    
    <40>   DW_AT_linkage_name: (indirect string, offset: 0x97): _ZN5Data15helloEv    
    <44>   DW_AT_accessibility: 1    (public)
    <45>   DW_AT_declaration : 1    
    <45>   DW_AT_object_pointer: <0x4d>    
    <49>   DW_AT_sibling     : <0x53>    
 <3><4d>: Abbrev Number: 4 (DW_TAG_formal_parameter)
    <4e>   DW_AT_type        : <0x6a>    
    <52>   DW_AT_artificial  : 1    
 <3><52>: Abbrev Number: 0
 <2><53>: Abbrev Number: 5 (DW_TAG_subprogram)
    <54>   DW_AT_external    : 1    
    <54>   DW_AT_name        : (indirect string, offset: 0xae): test    
    <58>   DW_AT_decl_file   : 1    
    <59>   DW_AT_decl_line   : 6    
    <5a>   DW_AT_linkage_name: (indirect string, offset: 0x86): _ZN5Data14testEv    
    <5e>   DW_AT_accessibility: 1    (public)
    <5f>   DW_AT_declaration : 1    
    <5f>   DW_AT_object_pointer: <0x63>    
 <3><63>: Abbrev Number: 4 (DW_TAG_formal_parameter)
    <64>   DW_AT_type        : <0x6a>    
    <68>   DW_AT_artificial  : 1    
 <3><68>: Abbrev Number: 0
 <2><69>: Abbrev Number: 0
 <1><6a>: Abbrev Number: 6 (DW_TAG_pointer_type)
    <6b>   DW_AT_byte_size   : 8    
    <6c>   DW_AT_type        : <0x2d>    
 <1><70>: Abbrev Number: 7 (DW_TAG_subprogram)
    <71>   DW_AT_specification: <0x53>      // 指向声明的位置, 可以得到函数名, test
    <75>   DW_AT_decl_line   : 9    
    <76>   DW_AT_low_pc      : 0x4004ee    // 我们的目标所在, 函数地址
    <7e>   DW_AT_high_pc     : 0xa    
    <86>   DW_AT_frame_base  : 1 byte block: 9c     (DW_OP_call_frame_cfa)
    <88>   DW_AT_object_pointer: <0x90>    
    <8c>   DW_AT_GNU_all_call_sites: 1    
    <8c>   DW_AT_sibling     : <0x9d>    
 <2><90>: Abbrev Number: 8 (DW_TAG_formal_parameter)   // 紧跟着函数参数类型, 第一个参数为函数所属类型的指针
    <91>   DW_AT_name        : (indirect string, offset: 0x81): this    
    <95>   DW_AT_type        : <0x9d>    
    <99>   DW_AT_artificial  : 1    
    <99>   DW_AT_location    : 2 byte block: 91 68     (DW_OP_fbreg: -24)
 <2><9c>: Abbrev Number: 0
 <1><9d>: Abbrev Number: 9 (DW_TAG_const_type)
    <9e>   DW_AT_type        : <0x6a>    
 <1><a2>: Abbrev Number: 10 (DW_TAG_subprogram)
    <a3>   DW_AT_external    : 1    
    <a3>   DW_AT_name        : (indirect string, offset: 0xa9): main    
    <a7>   DW_AT_decl_file   : 1    
    <a8>   DW_AT_decl_line   : 13    
    <a9>   DW_AT_type        : <0xbf>    
    <ad>   DW_AT_low_pc      : 0x4004f8    
    <b5>   DW_AT_high_pc     : 0xb    
    <bd>   DW_AT_frame_base  : 1 byte block: 9c     (DW_OP_call_frame_cfa)
    <bf>   DW_AT_GNU_all_call_sites: 1    
 <1><bf>: Abbrev Number: 11 (DW_TAG_base_type)
    <c0>   DW_AT_byte_size   : 4    
    <c1>   DW_AT_encoding    : 5    (signed)
    <c2>   DW_AT_name        : int    
 <1><c6>: Abbrev Number: 0

可以看出里面包含了丰富的信息, 但也看出了局限所在, 函数必须在类外定义, 才能看到函数地址所在, 这是为何我也不知道. 还有函数类型有很多种, 全局函数, 静态函数, 模板函数, 继承函数, 虚函数, 各种函数的规则可能都不一样, 我也没能完全地从那些调试信息中分辨他们的区别, 看那些信息很痛苦, 所以我目前只实现了最简单的非静态成员函数.

通过命令行工具来访问DWARF信息这虽然有用但还不能完全令我们满意。作为程序员,我们希望知道应该如何写出实际的代码来解析DWARF格式并从中读取我们需要的信息。自然地, 你可以开始专研dwarf的规范格式, 尝试自己写个程序解析. 我会提示您这个比解析html还要复杂. 我们还是利用现成的开源库来解析吧, 况且解析这一块并不是我们的重点所在. 现成的开源库有libdwarf, 现在可以把你精力专注于libdwarf的使用文档上了, 安装libdwarf 还要依赖libelf库. 这里就不介绍了.

好了, 现在我们获取到了函数地址, 而且是字符串形式表示的地址. 我们可以在程序编译完成后解析一次, 将信息保存到一个文件中. 有了这些信息接下来该做什么, 如何将地址转换成相应的函数类型指针, 如何实例化一个类型指针并传入该函数, 前面提到的map, 第一个参数是类型名, 第二个是相应的类型产生器, 或者容器, 这些又该怎么实现. 在下一篇中我将继续介绍讨论.

原文链接: https://www.cnblogs.com/YouJie/p/5331221.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/231148

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月13日 下午2:55
下一篇 2023年2月13日 下午2:55

相关推荐