可变修改类型兼容性及其安全隐患

时间:2015-02-18 18:27:08

标签: c gcc c99 variable-length-array

我对C99的可变修改型系统感兴趣。这个问题的灵感来自this one

检查这个问题的代码,我发现了一些有趣的东西。请考虑以下代码:

int myFunc(int, int, int, int[][100]);

int myFunc(int a, int b, int c, int d[][200]) {
    /* Some code here... */
}

这显然不会(并且没有)编译。但是,这段代码:

int myFunc(int, int, int, int[][100]);

int myFunc(int a, int b, int c, int d[][c]) {
    /* Some code here... */
}

在没有警告的情况下编译(在gcc上)。

这似乎意味着可变修改的数组类型与任何非可变修改的数组类型兼容!

但并非全部。您期望一种可变修改类型至少会使用哪个变量来设置其大小。但它似乎没有这样做!

int myFunc(int, int b, int, int[][b]);

int myFunc(int a, int b, int c, int d[][c]) {
    return 0;
}

同时编译没有任何错误。

所以,我的问题是:这是正确的标准化行为吗?

另外,如果一个可变修改的数组类型真的与任何具有相同维度的数组兼容,那么这不是一个令人讨厌的安全问题吗?例如,请考虑以下代码:

int myFunc(int a, int b, int c, int d[][c]) {
    printf("%d\n", sizeof(*d) / sizeof((*d)[0]));
    return 0;
}

int main(){
    int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    myFunc(0, 0, 100, &arr);

    return 0;
}

编译并输出100,没有错误或警告,什么都没有。正如我所看到的那样,即使您通过sizeof严格检查数组的大小,没有进行单一演员甚至包含所有警告,这意味着简单的越界数组写入打开!或者我错过了什么?

2 个答案:

答案 0 :(得分:4)

C99,第6.7.5.2节似乎是给出相关规则的地方。特别是,

第6行:

  

对于两个要兼容的数组类型,两者都应具有兼容的元素类型,如果两个大小说明符都存在,并且是整数常量表达式,则两个大小说明符应具有相同的常量值。如果在要求它们兼容的上下文中使用这两种数组类型,则如果两个大小说明符计算为不相等的值,则它是未定义的行为。

之前已删除的答案也引用了第6行。对该答案的评论认为第二句受第一句末尾的条件影响,但这似乎不太可能。该部分的示例3可以澄清(摘录):

int c[n][n][6][m];
int (*r)[n][n][n+1];
r=c;   // compatible, but defined behavior only if
       // n == 6 and m == n+1

这似乎与问题中的示例相当:两种数组类型,一种具有恒定维度,另一种具有相应的可变维度,并且需要兼容。行为未定义(在示例3中的每个注释和6.7.5.2/6的一个合理读数),在运行时变量维度与编译时常量维度不同。并且不是未定义的行为,无论如何你会期望什么?为什么要提出这个问题?

假设我们可以同意在发生这种不匹配时行为未定义,我观察到编译器通常不需要识别未定义或可能未定义的行为,也不会发出任何类型的诊断,如果他们确实认识到这种行为。在这种情况下,我希望编译器能够警告可能未定义的行为,但它必须成功编译代码,因为它在语法上是正确的并且满足所有适用的约束。请注意,能够警告此类用途的编译器可能不会默认

答案 1 :(得分:-1)

#include <stdio.h>

void foo(int c, char d[][c])
{
  fprintf(stdout, "c = %d; d = %p; d + 1 = %p\n", c, d, d + 1);
}

int main()
{
  char x[2][4];
  char y[3][16];
  char (*z)[4] = y;  /* Warning: incompatible types */

  foo(4, x);
  foo(16, y);
  foo(16, x);        /* We are lying about x. What can / should the compiler / code do? */
  foo(4, y);         /* We are lying about y. What can / should the compiler / code do? */

  return 0;
}

输出:

c = 4; d = 0x7fff5b295b70; d + 1 = 0x7fff5b295b74
c = 16; d = 0x7fff5b295b40; d + 1 = 0x7fff5b295b50
c = 16; d = 0x7fff5b295b70; d + 1 = 0x7fff5b295b80
c = 4; d = 0x7fff5b295b40; d + 1 = 0x7fff5b295b44

因此,foo()会动态地计算出基于c推进d的程度,正如您的代码所示。

但是,编译器通常无法静态确定是否/何时错误地调用foo()。似乎如果你这样做,那么编译器会说&#34;好的,我允许你传递你想要的任何东西作为d,只要它的类型是一个双重索引的字符数组。指针d的操作将由c确定。祝你好运!&#34;

也就是说,编译器通常不能对这些参数进行静态类型检查,因此标准几乎肯定不会要求编译器捕获所有可能静态确定类型不兼容的情况。