可以通过将变量标记为volatile来修复不安全的类型校正吗?

时间:2018-12-10 03:50:11

标签: c undefined-behavior volatile type-punning restrict-qualifier

在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?的回答中,他举例说明了为什么在类似结构之间进行简单类型转换并不安全,并且在注释中有一个示例环境,其行为异常:在gcc上编译以下内容-O2使其打印“ 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的影响。但是,我本以为gcc仅在ab被标记为restrict的情况下才可以假设。

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

2 个答案:

答案 0 :(得分:3)

如果仅使用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精神的廉价而简便的方法。宪章和基本原理文件,因为“不要阻止程序员做需要做的事情”,而无需特殊的语法。但是,其他编译器(例如gcc和clang)要求程序员要么使用gcc / clang特定的内在函数,要么使用命令行选项来全局阻止大多数形式的寄存器缓存。

答案 1 :(得分:0)

此特定问题和zwol's answer的问题在于它们将类型调整和严格别名混为一谈。 Zwol的答案对于该特定用例是正确的,因为它用于初始化结构。但在一般情况下,也不是wrt。像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进行编译,因此可能会有其他问题。)< / p>

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)。 / p>

这里是类型校正的正确示例,避免了严格的别名问题:

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,
};

现在,如果在相同的范围内可以看到以下联合,则可以在指向上述结构类型的指针之间进行类型双打,并完全安全地访问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代码。但是,如果上面的代码有任何问题,我会感到惊讶。)