【问题标题】:Is there a way to set a variable as uninitialized in GCC/Clang?有没有办法在 GCC/Clang 中将变量设置为未初始化?
【发布时间】:2013-05-25 02:59:31
【问题描述】:

我很想知道是否可以将 C 中的变量显式污染为未初始化。

伪代码...

{
    int *array;
    array = some_alloc();
    b = array[0];
    some_free(array);
    TAINT_MACRO(array);

    /* the compiler should raise an uninitialized warning here */
    b = array[0];
}

这是污染变量的一种方法的一个示例,但是当 'a' 被分配给未初始化的 var 时,GCC 会发出警告,而不是第二次使用 'a'。

{
    int a = 10;
    printf("first %d\n", a);
    do {
        int b;
        a = b;
    } while(0);
    printf("second %d\n", a);
}

我能想出的唯一解决方案是用未初始化的变量显式隐藏变量,(添加了空格,因此没有未使用的警告)。

#define TAINT_MACRO_BEGIN(array) (void)(array); { void **array; (void)array;
#define TAINT_MACRO_END(array) } (void)(array);
{
    int *array;
    array = some_alloc();
    b = array[0];
    some_free(array);
    TAINT_MACRO_BEGIN(array);

    /* the compiler should raise an uninitialized warning here */
    b = array[0];
    TAINT_MACRO_END(array);
}

这种方法增加了太多开销,无法包含在现有代码中(增加了很多噪音并且维护起来很烦人),所以我想知道是否有其他方法可以告诉编译器变量未初始化。

我知道有静态检查器并且我确实使用了这些,但是我正在寻找可以在编译时发出警告并且没有误报的东西,我相信在这种情况下这是可能的并且可以避免某些类别的错误。

【问题讨论】:

  • 如果指针在一个翻译单元中被释放,然后在另一个翻译单元中使用怎么办?编译器无法捕捉到。
  • some_index未在您的第一个示例中声明。我希望所有编译器都会抱怨这一点,而不是初始化。
  • 不知道为什么您不喜欢a = b; 第二个示例中的警告 一个编译器也抱怨后续的错误使用 (printf("second %d\n", a);) 会很冗长。对于大多数调试来说,第一个警告就足够了。
  • 我明白了,您正在尝试实现一个穷人的静态分析工具。为什么不直接使用真正的静态分析工具?既然你愿意装饰你的代码,splint 可能很适合你。
  • @user315052:所有静态分析是否也提供允许污染变量的注释功能?如果事实上,我很高兴知道哪些工具具有该功能。

标签: c gcc clang


【解决方案1】:

我在 GCC 列表上发送了一个答案,但由于我自己先使用 SO...

在现代 C 和 C++ 中,我希望程序员使用有限的 可变范围来控制这种曝光。

例如,我认为您想要这样的东西(请注意 我正在使用的属性实际上并不存在,我只是想 解释你的要求)。

int x = 1; // initialized 
int y;     // uninitialized 

x = y;     // use of uninitialized value 'y' 

y = 2;     // no longer uninitialized 
x = y;     // fine 

y = ((__attr__ uninitialized))0; // tell gcc it's uninitialized again 

x = y;    // warn here please. 

如果是这样,我会在 C99(或更高版本)或 C++(漂亮 至少从 1993 年的 ARM 开始,它就已经“在使用点声明”了……):

int x = 1; // initialized 

{ 
    int y; // uninitialized 
    x = y; // warn here 
    y = 2; // ok, now it's initialized 
    x = y; // fine, no warning 
} 

{ 
    int y; // uninitialized again! 
    x = y; // warns here 
} 

额外的范围有点令人反感,但我已经习惯了 C++(来自大量使用 RAII 技术。)

由于主流语言对此有答案,我不知道 认为值得添加到编译器中。

看看你的例子,你关心的是一个数组。那应该 与额外的范围一样工作,不应该有额外的 运行时成本,因为整个堆栈帧是在函数上分配的 条目(至少 SFAIK)。

【讨论】:

  • 我有时会这样做,当本地的代码块可以被分割成块并且每个块都有自己的变量时,这有时是有意义的。但是,对于具有大量嵌套块的大型函数,我真的不希望通过添加块来引起更多缩进,因为可能会在不应该使用变量的情况下使用它。基本上,缩进现有代码的大块是非常具有破坏性的(与提交历史混淆),并且在大多数情况下,我认为这种权衡是不值得的。另一方面,污染变量的单行不会造成太多噪音,因此恕我直言,这是可以接受的。
  • 我理解您的反对意见,但我真诚地相信,从长远来看,如果您重组代码以使用更多范围——无论是通过向现有函数添加额外范围,还是拆分你的功能变成更小的功能。如果您遇到长度限制,并且没有横向缩进空间,那么这些都暗示您需要将该功能分解为更小的功能。最后,您实际上是在要求编译器将变量“视为新的”;恕我直言,实际上使它“新”会变得不那么混乱。祝你好运!
  • 这是一个好的做法答案。实际上,问题中的问题可以通过不在单个范围内重用变量来最好地避免,这是a)代码异味,b)如果变量可能在中途变得无效,则非常糟糕异味通过他们的生命周期。如果发生这种情况,那么您实际上拥有的是两个变量,那么为什么不明确说明呢?
【解决方案2】:

Based on an answer to a different question,您可以使用setjmplongjmp 使更改的局部变量具有不确定的值。

#define TAINT(x)                             \
        do {                                 \
            static jmp_buf jb;               \
            if (setjmp(jb) == 0) {           \
                memset(&x, '\0', sizeof(x)); \
                longjmp(jb, 1);              \
            }                                \
        } while (0)

如果x 是一个局部变量,它的值在应用TAINT 之后的代码行中将是不确定的。这是因为 C.11 §7.13.2 ¶3(强调我的):

所有可访问对象都有值,并且所有其他组件 抽象机有状态,截至longjmp 函数是 调用,除了自动存储对象的值 包含调用的函数的本地持续时间 对应的没有 volatile 限定的 setjmp 宏 类型并已在 setjmp 调用和 longjmp 之间更改 调用是不确定的

请注意,使用受污染的变量不需要诊断。但是,编译器编写者正在积极检测未定义的行为以增强优化,所以如果这永远无法诊断出来,我会感到惊讶。

【讨论】:

    【解决方案3】:

    我会反过来,将污染宏包装在分配和释放函数周围。这就是我的想法:

    #ifdef O_TAINT
    volatile int taint_me;
    #define TAINT(x, m) \
        if (taint_me) { goto taint_end_##x; } else {} x = m
    #define free(x) free(x); taint_end_##x: (void)0
    #else
    #define TAINT(x, m) x = m
    #endif
    

    因此,您的示例将如下所示:

    int *array;
    int b;
    
    TAINT(array, malloc(sizeof(int)));
    b = array[0];
    printf("%d\n", b);
    free(array);
    
    /* the compiler should raise an uninitialized warning here */
    b = array[0];
    printf("%d\n", b);
    

    这并不完美。每个受污染的变量只能调用一次free(),因为goto 标签与变量名称相关联。如果跳转跳过其他初始化,您可能会得到其他误报。如果分配发生在一个函数中,而内存在另一个函数中释放,则它不起作用。

    但是,它提供了您在示例中要求的行为。正常编译时,不会出现任何警告。如果使用-DO_TAINT 编译,则在第二次分配给b 时会出现警告。


    我确实制定了一个相当通用的解决方案,但它涉及使用开始/结束宏将整个函数括起来,并且依赖于 GCC 扩展 typeof 运算符。解决方案最终看起来像这样:

    void foo (int *array, char *buf)
    {
        TAINT_BEGIN2(array, buf);
        int b;
    
        puts(buf);
        b = array[0];
        printf("%d\n", b);
    
        free(array);
        free(buf);
    
        /* the compiler should raise an uninitialized warning here */
        puts(buf);
        b = array[0];
        printf("%d\n", b);
    
        TAINT_END;
    }
    

    这里,TAINT_BEGIN2 用于声明将获得污点处理的两个函数参数。不幸的是,宏有点乱,但很容易扩展:

    #ifdef O_TAINT
    volatile int taint_me;
    #define TAINT(x, m) \
        if (taint_me) { goto taint_end_##x; } else {} x = m
    #define TAINT1(x) \
        if (taint_me) { goto taint_end_##x; } else {} x = x##_taint
    #define TAINT_BEGIN(v1) \
        typeof(v1) v1##_taint = v1; do { \
        typeof(v1##_taint) v1; TAINT1(v1)
    #define TAINT_BEGIN2(v1, ...) \
        typeof(v1) v1##_taint = v1; TAINT_BEGIN(__VA_ARGS__); \
        typeof(v1##_taint) v1; TAINT1(v1)
    #define TAINT_BEGIN3(v1, ...) \
        typeof(v1) v1##_taint = v1; TAINT_BEGIN2(__VA_ARGS__); \
        typeof(v1##_taint) v1; TAINT1(v1)
    #define TAINT_END } while(0)
    #define free(x) free(x); taint_end_##x: (void)0
    #else
    #define TAINT_BEGIN(x) (void)0
    #define TAINT_BEGIN2(...) (void)0
    #define TAINT_BEGIN3(...) (void)0
    #define TAINT_END (void)0
    #define TAINT1(x) (void)0
    #define TAINT(x, m) x = m
    #endif
    

    【讨论】:

    • 关于您的解决方案,看起来它在初始化之前实际上使用了“taint_me”?还是您会在其他地方为它定义一个阴影?
    • @ideasman42: taint_me 是一个全局变量,所以初始化为0。但由于声明为volatile,编译时编译器不知道是否仍为0。跨度>
    • 关于使用开始/结束宏,我确信它可以工作,问题是这太破坏性了,我不能提交给我的项目代码库 - 会改变 100 秒(可能是 1000 秒)行和恕我直言,读起来不是很好。我认为允许在块内重新分配也很棘手(我猜可能使用 typeof() 和宏魔法)。如果可以在单个块中执行,则可以将其包装到一个包含函数调用的宏中(例如免费),然后可以使用该宏,而无需查看所有源文件并对其进行编辑。
    • @ideasman42:我可以重新定义free() 来做TAINT_END 所做的事情,如果这会使解决方案更可口。你应该意识到你的方法有点老套,所以解决方案也可能老套。
    • 代码中有很多地方 malloc() 和 free() 不在同一个函数中,我认为尝试将 malloc() 和 free() 包装成宏不值得开始/结束一段代码。由于编译器已经知道变量初始化状态,我希望有一些方法可以控制它,而不必对流控制进行更大的更改或限制变量范围。不管这个解决方案是否老套,如果它可以可靠地工作,我认为它会很有用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多