【问题标题】:How can I check that all my init functions have been called?如何检查我的所有 init 函数是否已被调用?
【发布时间】:2009-09-28 02:58:15
【问题描述】:

我正在编写一个用于嵌入式的大型 C 程序。该程序中的每个模块都有一个 init() 函数(如构造函数)来设置其静态变量。

问题是我必须记住从main() 调用所有这些初始化函数。如果我出于某种原因将它们注释掉,我还必须记住将它们放回去。

我有什么聪明的办法来确保所有这些函数都被调用吗?类似于在每个 init 函数中放置一个宏,当您稍后调用 check_inited() 函数时,如果没有调用所有函数,则会向 STDOUT 发送警告。

我可以增加一个计数器,但我必须在某处保持正确数量的 init 函数,这也容易出错。

想法?

以下是我决定的解决方案,这个帖子中有几个人的意见

我的目标是确保我的所有 init 函数都被实际调用。我想要做 这无需维护多个文件中的模块列表或计数。我不能打电话 正如 Nick D 建议的那样,它们会自动调用,因为它们需要按特定顺序调用。

为此,每个模块中包含的宏使用 gcc constructor 属性来 将 init 函数名称添加到全局列表中。

init 函数主体中包含的另一个宏更新全局列表以创建一个 请注意,该函数实际上已被调用。

最后,在所有初始化完成后,在main() 中调用一个检查函数。

注意事项:

  1. 我选择将字符串复制到数组中。这不是绝对必要的,因为 传递的函数名在正常使用中始终是静态字符串。如果记忆短暂 你可以只存储一个指向传入字符串的指针。

  2. 我的可重用实用函数库称为“nx_lib”。因此所有的“nxl”名称。

  3. 这不是世界上最高效的代码,但它只被称为引导时间,因此 对我来说没关系。

  4. 每个模块需要添加两行代码。如果其中一个被省略, 检查功能会让你知道。

  5. 您或许可以将构造函数设为静态,这样就无需为其指定一个在整个项目中唯一的名称。

  6. 此代码仅经过轻微测试,而且已经很晚了,因此请在信任它之前仔细检查。

感谢:

pierr 向我介绍了constructor 属性。

Nick D 演示了 ## 预处理器技巧并为我提供了框架。

tod Frye 了解一种可以与许多编译器一起使用的基于链接器的巧妙方法。

其他人提供帮助并分享有用的花絮。

nx_lib_public.h

这是我的库头文件的相关片段

#define NX_FUNC_RUN_CHECK_NAME_SIZE 20

typedef struct _nxl_function_element{
  char func[NX_FUNC_RUN_CHECK_NAME_SIZE];
  BOOL called;
} nxl_function_element;

void nxl_func_run_check_add(char *func_name);
BOOL nxl_func_run_check(void);
void nxl_func_run_check_hit(char *func_name);

#define NXL_FUNC_RUN_CHECK_ADD(function_name) \
  void cons_ ## function_name() __attribute__((constructor)); \
  void cons_ ## function_name() { nxl_func_run_check_add(#function_name); }

nxl_func_run_check.c

这是被调用以添加函数名称并稍后检查它们的库代码。

#define MAX_CHECKED_FUNCTIONS 100

static nxl_function_element  m_functions[MAX_CHECKED_FUNCTIONS]; 
static int                   m_func_cnt = 0; 


// call automatically before main runs to register a function name.
void nxl_func_run_check_add(char *func_name)
{
  // fail and complain if no more room.
  if (m_func_cnt >= MAX_CHECKED_FUNCTIONS) {
    print ("nxl_func_run_check_add failed, out of space\r\n");
    return; 
  }

  strncpy (m_functions[m_func_cnt].func, func_name, 
           NX_FUNC_RUN_CHECK_NAME_SIZE);

  m_functions[m_func_cnt].func[NX_FUNC_RUN_CHECK_NAME_SIZE-1] = 0;

  m_functions[m_func_cnt++].called = FALSE;
}

// call from inside the init function
void nxl_func_run_check_hit(char *func_name)
{
  int i;

  for (i=0; i< m_func_cnt; i++) {
    if (! strncmp(m_functions[i].func, func_name, 
                  NX_FUNC_RUN_CHECK_NAME_SIZE)) {
      m_functions[i].called = TRUE;   
      return;
    }
  }

  print("nxl_func_run_check_hit(): error, unregistered function was hit\r\n");
}

// checks that all registered functions were called
BOOL nxl_func_run_check(void) {
  int i;
  BOOL success=TRUE;

  for (i=0; i< m_func_cnt; i++) {
    if (m_functions[i].called == FALSE) {
      success = FALSE;
      xil_printf("nxl_func_run_check error: %s() not called\r\n", 
                 m_functions[i].func);
     } 
  }
  return success;
}

solo.c

这是一个需要初始化的模块示例

#include "nx_lib_public.h"

NXL_FUNC_RUN_CHECK_ADD(solo_init)
void solo_init(void)
{
  nxl_func_run_check_hit((char *) __func__);

  /* do module initialization here */
}

【问题讨论】:

  • .. 如果您忘记将宏放在特定的 init 函数中?然后你又回到了原点。
  • 嗯,是的,但至少这是我在编写函数时可以做的事情,记住这样做比同步另一个文件中的内容更容易。
  • 把错误写到stderr不是更好吗?
  • stdout 转到仅用于调试的串口,不使用 stderr。
  • PS Jonathan 我在你删除它之前看到了你的帖子,我认为这是一个非常有趣的技术,即使它不适合这种情况。

标签: c embedded


【解决方案1】:

如果 gcc 适合您的项目,您可以使用 gcc 的扩展名 __attribute__((constructor))

#include <stdio.h>
void func1() __attribute__((constructor));
void func2() __attribute__((constructor));

void func1()
{
    printf("%s\n",__func__);
}

void func2()
{
    printf("%s\n",__func__);
}

int main()
{
    printf("main\n");
    return 0;
}

//the output
func2
func1
main

【讨论】:

  • 是的,我正在使用 GCC。这一点,结合 Nick D 的想法,可能是最好的答案。我现在正在尝试。
【解决方案2】:

我不知道下面的内容有多难看,但我还是把它贴出来了 :-)

(基本思想是注册函数指针,就像atexit函数所做的那样。
当然atexit的实现不同)

在主模块中我们可以有这样的东西:

typedef int (*function_t)(void);

static function_t  vfunctions[100]; // we can store max 100 function pointers
static int         vcnt = 0; // count the registered function pointers

int add2init(function_t f)
{
  // todo: error checks
  vfunctions[vcnt++] = f;
  return 0;
}
...

int main(void) {
 ...
 // iterate vfunctions[] and call the functions
 ...
}

...以及在其他一些模块中:

typedef int (*function_t)(void);
extern int add2init(function_t f);
#define M_add2init(function_name)  static int int_ ## function_name = add2init(function_name)

int foo(void)
{
   printf("foo\n");
   return 0;
}
M_add2init(foo); // <--- register foo function

【讨论】:

  • 好的,我想我明白了。非常聪明。但是 C 可以在静态初始化程序中调用函数吗?我尝试了一个简单的测试并从 gcc 得到一个错误:初始化元素不是常量我希望我可以将代码放在注释中,以便向您展示我尝试过的内容。
  • @NXT,它在 C 中不起作用?啊,如果您找不到更好的解决方案,我会尝试想出一个解决方法。
  • @Nick D,我即将发布使用constructor 属性的解决方案。但是,如果您碰巧知道另一种方法,我很乐意听到它。
  • @NXT,我打算提出一个与您的 NXL_FUNC_RUN_CHECK_ADD 宏完全相同的解决方案。您的解决方案似乎还可以。我稍后会尝试它。顺便说一句,您可以(可以)将您的解决方案作为答案发布并接受 - 如果您愿意,您仍然可以这样做。
【解决方案3】:

为什么不编写一个后处理脚本来为您进行检查。然后将该脚本作为构建过程的一部分运行......或者更好的是,将其作为您的测试之一。你在写测试,对吧? :)

例如,如果您的每个模块都有一个头文件 modX.c。如果您的 init() 函数的签名是“void init()”...

让您的脚本 grep 遍历所有 .h 文件,并创建需要 init() 的模块名称列表。然后让脚本检查是否确实在 main() 中的每个模块上调用了 init()。

【讨论】:

    【解决方案4】:

    如果您的单个模块代表“类”实体并具有实例构造函数,则可以使用以下构造:

    static inline void init(void) { ... }
    static int initialized = 0;
    #define INIT if (__predict_false(!initialized)) { init(); initialized = 1; }
    struct Foo *
    foo_create(void)
    {
        INIT;
        ...
    }
    

    其中“__predict_false”是编译器的分支预测提示。创建第一个对象时,模块会自动初始化(一次)。

    【讨论】:

      【解决方案5】:

      Splint(可能还有其他 Lint 变体)可以针对已定义但未调用的函数发出警告。

      有趣的是,大多数编译器都会警告您未使用的变量,而不是未使用的函数。

      【讨论】:

      • 检测未使用的函数相当困难。函数指针会使检测函数是否真正被调用变得相当困难。
      • GCC 会警告您未使用的静态函数,这很方便。
      【解决方案6】:

      更长的运行时间不是问题

      您可以想象为每个模块实现一种“状态机”,其中函数的操作取决于模块所处的状态。该状态可以设置为 BEFORE_INIT 或 INITIALIZED。

      例如,假设我们有模块 A,其中包含函数 foo 和 bar。

      函数的实际逻辑(即它们实际做什么)将被声明如下:

      void foo_logic();
      void bar_logic();
      

      或任何签名。

      然后,模块的实际函数(即声明的实际函数 foo())将对模块的条件执行运行时检查,并决定做什么:

      void foo() {
             if (module_state == BEFORE_INIT) {
                 handle_not_initialized_error();
             }
             foo_logic();
      }
      

      对所有函数重复此逻辑。

      需要注意的几点:

      1. 这显然会导致性能方面的巨大损失,因此 可能不是一个好主意(我发布了 无论如何,因为你说运行时是 没问题)。
      2. 这不是真正的状态机,因为只有两个状态使用基本 if 进行检查,没有某种智能通用逻辑。
      3. 这种“设计模式”在您使用单独的线程/任务时非常有效,并且您调用的函数实际上是使用某种 IPC 调用的。
      4. 状态机可以很好地用 C++ 实现,可能值得一读。可以想象,同样的想法可以用 C 语言用函数指针数组编码,但几乎可以肯定不值得你花时间。

      【讨论】:

      • 对不起,我的意思是运行时间对于 init() 函数来说不是问题,它是其他任何地方的问题。不过还是感谢您发布它。
      【解决方案7】:

      您可以使用链接器部分按照这些思路进行操作。每当您定义一个 init 函数时,将指向它的指针放在链接器部分中,仅用于 init 函数指针。那么你至少可以找出已经编译了多少个init函数。

      如果调用 init 函数的顺序无关紧要,并且它们都具有相同的原型,则可以在 main 循环中调用它们。

      确切的细节让我记忆犹新,但它的工作原理是这样的:: 在模块文件中...

      //this is the syntax in GCC..(or would be if the underscores came through in this text editor)
      initFuncPtr thisInit __attribute((section(.myinits)))__= &moduleInit;
      
      void moduleInit(void)
      {
      // so init here
      }
      

      这会在 .myinits 部分中放置一个指向模块 init 函数的指针,但将代码留在 .code 部分中。所以 .myinits 部分不过是指针。您可以将其视为模块文件可以添加到的可变长度数组。

      然后您可以从主目录访问该部分的开始和结束地址。然后从那里出发。

      如果 init 函数都具有相同的原型,您可以遍历本节,将它们全部调用。

      这实际上是在 C 中创建您自己的静态构造函数系统。

      如果你正在做一个大型项目,而你的链接器至少没有这个功能齐全,你可能会遇到问题......

      【讨论】:

      • 非常好。你碰巧知道从 C 代码访问段开始和结束地址的语法吗?如果您不知道,我会自己查找。
      • IIRC,每个部分都至少有一个开始和结束,所以 extern unsigned int myinits_start 和 extern unsigned int myinits_end 应该得到开始和结束地址。您可能需要修改链接器脚本以添加该部分,并添加这些符号...
      【解决方案8】:

      我可以回答我的问题吗?

      我的想法是让每个函数都将其名称添加到全局函数列表中,就像 Nick D 的解决方案一样。

      然后我会遍历 -gstab 生成的符号表,并查找任何名为 init_* 且未被调用的函数。

      这是一个嵌入式应用程序,所以我在闪存中方便地使用了精灵图像。

      但是我不喜欢这个想法,因为这意味着我总是必须在二进制文件中包含调试信息。

      【讨论】:

      • 您可以回答您的问题。你甚至可以接受它是正确的,如果你等待两天,但你不会得到任何声誉。
      猜你喜欢
      • 1970-01-01
      • 2011-09-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-11-08
      • 2019-06-23
      • 1970-01-01
      • 2020-12-02
      相关资源
      最近更新 更多