【问题标题】:ELF INIT section code to prepopulate objects used at runtime用于预填充运行时使用的对象的 ELF INIT 部分代码
【发布时间】:2018-07-25 16:18:12
【问题描述】:

我对 c++ 还很陌生,并且真的有兴趣了解更多信息。已经读了不少。最近发现了 init/fini elf 部分。 我开始想知道是否以及如何使用 init 部分来预填充将在运行时使用的对象。比如说你想要 将性能测量添加到您的代码中,记录时间、文件名、行号,也许还有一些 ID(ex 的单调递增 int)或名称。 例如,您可以放置​​:

PROBE(0,"EventProcessing",__FILE__,__LINE__)
...... //process event
PROBE(1,"EventProcessing",__FILE__,__LINE__)
......//different processing on same event
PROBE(2,"EventProcessing",__FILE__,__LINE__)

PROBE 可能是一些宏,它填充包含此数据的结构(可能在数组/列表等上,使用 id 作为索引器)。 是否有可能在 init 部分中有代码可以为每个 PROBE 预填充所有这些数据(当然时间除外),因此只需要在运行时检索/复制时间?

据我所知__attribute__((constructor))不能应用于成员函数?

我最初的想法是创建某种 每个节点指向每个探针的链表和 init 部分中的代码可以迭代它以填充 id、文件、行等,但是 这个想法假设我可以使用一个可以在“init”部分运行的成员函数,但这似乎是不可能的。任何提示表示赞赏!

【问题讨论】:

    标签: c++ gcc assembly elf dynamic-linking


    【解决方案1】:

    据我了解,这里实际上不需要 ELF 构造函数。相反,您可以使用extended asm statements 为您的探测器发出描述符(使用数据,而不是代码)。这还涉及切换到用于探测描述符的专用 ELF 部分,例如 __probes。

    链接器将连接所有探针并形成一个数组,并生成特殊符号__start___probes 和__stop___probes,您可以在程序中使用它们来访问这些探针。请参阅Input Section Example 中的最后一段。

    Systemtap 为它的用户空间探测实现了一些非常相似的东西:

    Linux 内核中也使用了类似的结构来实现其自我修补机制。

    【讨论】:

      【解决方案2】:

      有一种非常简单的方法可以让代码在模块加载时运行:使用全局变量的构造函数:

      struct RunMeSomeCode
      {
        RunMeSomeCode()
        {
          // your code goes here
        }
      } do_it;
      

      .init/.fini 部分基本上是为了在某些平台上实现全局构造函数/析构函数作为ABI 的一部分。其他平台可能使用不同的机制,例如_start 和_init 函数或.init_array/.deinit_array 和.preinit_array。所有这些方法之间存在许多细微的差异,而使用哪一种方法来解决真正只能通过目标平台的文档来回答的问题。并非所有平台都使用 ELF 开头...

      要理解的要点是,ELF 二进制文件中的 .init/.fini 部分发生在 C++ 作为一种语言的级别之下。 C++ 编译器可以使用这些东西在某个目标平台上实现某些行为。在不同的平台上,C++ 编译器可能必须使用不同的机制来实现相同的行为。许多编译器会以__attributes__ 或#pragmas 等语言扩展的形式为您提供工具,以控制此类特定于平台的细节。但这些通常只有意义,并且只能在该特定平台上与该特定编译器一起使用。

      【讨论】:

      • 我理解.. 我要问的是如何在 main 之前将每个 PROBE 填充到文件中,并增加 ID、FILE、LINE,等等?如何访问代码中每个声明的 PROBE?
      • 这样的PROBE究竟应该是什么?全局对象?问题在于,在编译时跨多个单独的翻译单元工作并没有真正简单的机制。 ID是否必须增加数字?获取唯一 ID 的一种简单方法是简单地使用全局对象或函数的地址。
      • 哦,你的意思是PROBEs 在同一个文件中!?为此,您可以简单地使用 __COUNTER__ 宏。另请注意,同一翻译单元中的全局变量保证按照定义的顺序进行初始化……
      • 我实际上在今天早些时候发现了该计数器宏,但这引入了我想要避免的相同问题之一。我不想使用 PROBE(COUNTER,...)。我希望探针已经有一个 id,并且只传递了一个可选的名称。
      • 您可以将PROBE() 本身设为一个在内部使用__COUNTER__ 宏的宏。除此之外,您对身份证的具体要求是什么?正如我所说,如果您只需要一个唯一标识符,只需使用全局对象或函数的地址即可。
      【解决方案3】:

      你不需要一个成员函数(它得到一个 this 指针作为参数传递);相反,您可以简单地创建引用全局数组的类似构造函数的函数,例如

      #define PROBE(id, stuff, more_stuff) \
        __attribute__((constructor)) void \
        probeinit##id(){ probes[id] = {id, stuff, 0/*to be written later*/, more_stuff}; }
      

      诀窍是让这个宏在另一个函数的中间工作。 GNU C / C++ 允许嵌套函数,但如果你可以让它们成为构造函数,则 IDK。

      您不想声明static int dummy#id = something,因为这样会增加您分析的函数的开销。 (gcc 必须发出线程安全的一次性锁定机制。)


      您真正想要的是某种对源代码的单独传递,以识别所有 PROBE 宏并收集它们的 args 以声明

      struct probe global_probes[] = {
          {0, "EventName", 0 /*placeholder*/, filename, linenum},
          {1, "EventName", 0 /*placeholder*/, filename, linenum},
          ...
       };
      

      我不相信您可以使用 CPP 宏来实现这一点;我认为 #define PROBE 不可能在每次扩展时重新定义另一个宏来添加更多内容。

      但是您可以使用 awk/perl/python/您最喜欢的脚本语言程序轻松做到这一点,该程序会扫描您的程序并构造一个 .c 来声明一个具有静态存储的数组。


      或者更好(对于单线程程序):将运行时时间戳保存在一个数组中,将名称和内容保存在一个单独的数组中。因此探针的缓存占用空间更小。对于多线程程序,不同线程存储到同一个缓存行称为假共享,会产生缓存行乒乓。

      所以你会有#define PROBE(id, evname, blah blah) do { probe_times[id] = now(); }while(0)

      并将后面的 args 的处理留给您单独的预处理。

      【讨论】:

      • 将运行时数据和静态数据保存在单独的数组中是个好主意,我正计划使用全局数组。不太确定我将如何使用该宏 probeinit##id() ?我将如何获得身份证?您的意思是在代码中随意调用它,还是在主运行之前已经在探测对象中以某种方式定义了它?
      • @SubliminalBroccoli: ##id 是宏参数的 CPP 连接。就像我说的那样,第一个想法可能不太可行,因为它会扩展到您实际放置 PROBE() 的任何位置。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-04
      • 2018-04-24
      • 2017-12-09
      • 2018-04-24
      • 2017-12-09
      • 2022-12-30
      • 1970-01-01
      相关资源
      最近更新 更多