首页 文章

可以通过标记变量volatile来修复不安全类型的惩罚吗?

提问于
浏览
4

在zwol对Is it legal to implement inheritance in C by casting pointers between one struct that is a subset of another rather than first member?的回答中,他给出了一个例子,说明为什么类似结构之间的简单类型转换是不安全的,并且在注释中有一个样本环境,它在其中出现意外行为:在-O2上使用gcc编译以下内容会导致它打印"x=1.000000 some=2.000000"

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

struct base
{
    double some;
    char space_for_subclasses[];
};
struct derived
{
    double some;
    int value;
};

double test(struct base *a, struct derived *b)
{
    a->some = 1.0;
    b->some = 2.0;
    return a->some;
}

int main(void)
{
    size_t bufsz = sizeof(struct base);
    if (bufsz < sizeof(struct derived)) bufsz = sizeof(struct derived);
    void *block = malloc(bufsz);

    double x = test(block, block);
    printf("x=%f some=%f\n", x, *(double *)block);
    return 0;
}

我正在愚弄代码以更好地理解它的行为,因为我需要做类似的事情,并注意到将 a 标记为 volatile 足以阻止它打印不同的值 . 这符合我对出错的期望 - gcc假设 a->some 不受写入 b->some 的影响 . 但是,如果 ab 标记为restrict,我会认为gcc只能假设这个 .

我误解了这里发生的事情和/或限制限定符的含义吗?如果不是,gcc是否可以自由地做出这个假设,因为 ab 是不同类型的?最后,将 ab 标记为 volatile 使该代码符合标准,或者至少防止未定义的行为允许gcc进行上述假设吗?

2 回答

  • 1

    如果使用 volatile 限定左值独占访问存储区域,则编译器必须走得太远,不要处理每次写入,而是将写入的值转换为存储位,并将每次读取作为读取来自内存的位模式并将其转换为值 . 标准实际上并没有强制要求这样做,理论上编译器给出了:

    long long volatile foo;
    ...
    int test(void)
    {
      return *((short volatile*)(&foo));
    }
    

    可以假设任何可以调用 test 的代码分支永远不会被执行,但我还不知道任何编译器的行为方式如此极端 .

    另一方面,给定如下函数:

    void zero_aligned_pair_of_shorts(uint16_t *p)
    {
      *((uint32_t void volatile*)&p) = 0;
    }
    

    gccclang 这样的编译器无法可靠地识别出它可能对使用 uint16_t 类型的非限定左值访问的对象的存储值产生一些影响 . 像icc这样的一些编译器将 volatile 访问作为指示器来同步任何已经采用地址的寄存器缓存对象,因为这样做对于编译器来说是一种廉价而简单的方法来维护标准章程和理由文档中描述的C语言原则 . as "Don't prevent the programmer from doing what needs to be done",无需特殊语法 . 然而,像gcc和clang这样的其他编译器要求程序员使用gcc / clang特定的内在函数,或者使用命令行选项来全局阻止大多数形式的寄存器缓存 .

  • 0

    这个特殊问题和zwol's answer的问题在于它们混淆了类型惩罚和严格别名 . 由于用于初始化结构的类型,Zwol的答案对于该特定用例是正确的;但不是一般情况下,也不是 . struct sockaddr POSIX类型可能会读到暗示的答案 .

    对于具有公共初始成员的结构类型之间的类型惩罚,您需要做的就是声明(不使用!)这些结构的并集,并且您可以通过任何结构类型的指针安全地访问公共成员 . 这是自ANSI C 3.3.2.3以来明确允许的行为,包括C11 6.5.2.3p6(链接到n1570草案) .

    如果一个实现包含对用户空间应用程序可见的所有 struct sockaddr_ 结构的并集,那么zwol's answer OP链接会产生误导,在我看来,如果读取它意味着 struct sockaddr 结构支持需要编译器非标准的东西 . (如果定义 _GNU_SOURCE ,glibc将这样的联合定义为包含所有此类类型的匿名联合的 struct __SOCKADDR_ARG . 但是,glibc设计为使用GCC编译,因此它可能有其他问题 . )

    Strict aliasing要求函数的参数不引用相同的存储(内存) . 举个例子,如果你有

    int   i = 0;
    char *iptr = (char *)(&i);
    
    int modify(int *iptr, char *cptr)
    {
        *cptr = 1;
        return *iptr;
    }
    

    然后调用 modify(&i, iptr) 是严格的别名违规 . iptr 定义中的类型惩罚是偶然的,实际上是允许的(因为您可以使用 char 类型来检查任何类型的存储表示形式; C11 6.2.6.1p4) .

    以下是类型惩罚的正确示例,避免严格的别名问题:

    struct item {
        struct item *next;
        int          type;
    };
    
    struct item_int {
        struct item *next;
        int          type; /* == ITEMTYPE_INT */
        int          value;
    };
    
    struct item_double {
        struct item *next;
        int          type; /* == ITEMTYPE_DOUBLE */
        double       value;
    };
    
    struct item_string {
        struct item *next;
        int          type;    /* == ITEMTYPE_STRING */
        size_t       length;  /* Excluding the '\0' */
        char         value[]; /* Always has a terminating '\0' */
    };
    
    enum {
        ITEMTYPE_UNKNOWN = 0,
        ITEMTYPE_INT,
        ITEMTYPE_DOUBLE,
        ITEMTYPE_STRING,
    };
    

    现在,如果在同一作用域中可以看到以下联合,我们可以在指向上述结构类型的指针之间输入-shin,并完全安全地访问 nexttype 成员:

    union item_types {
        struct item         any;
        struct item_int     i;
        struct item_double  d;
        struct item_string  s;
    };
    

    对于其他(非常见)成员,我们必须使用与初始化结构相同的结构类型 . 这就是 type 字段存在的原因 .

    作为这种完全安全使用的示例,请考虑以下函数,该函数在项列表中打印值:

    void print_items(const struct item *list, FILE *out)
    {
        const char *separator = NULL;
    
        fputs("{", out);        
    
        while (list) {
    
            if (separator)
                fputs(separator, out);
            else
                separator = ",";
    
            if (list->type == ITEMTYPE_INT)
                fprintf(out, " %d", ((const struct item_int *)list)->value);
            else
            if (list->type == ITEMTYPE_DOUBLE)
                fprintf(out, " %f", ((const struct item_double *)list)->value);
            else
            if (list->type == ITEMTYPE_STRING)
                fprintf(out, " \"%s\"", ((const struct item_string *)list)->value);
            else
                fprintf(out, " (invalid)");
    
            list = list->next;
        }
    
        fputs(" }\n", out);
    }
    

    请注意,我使用相同的名称 value 作为值字段,只是因为我没有想到更好的一个;他们不需要是一样的 .

    类型惩罚发生在 fprintf() 语句中,并且当且仅当1)使用结构匹配初始化结构时才有效 type 字段,以及2) union item_types 在当前范围内可见 .

    我尝试过的当前C编译器都没有遇到上述代码的任何问题,即使在极端优化级别上打破了标准行为的某些方面 . (我没有检查过MSVC,但是那个实际上是一个C编译器,它也可以编译大多数C代码 . 但是,如果它与上面的代码有任何问题,我会感到惊讶 . )

相关问题