2015-02-18 3 views
10

Я переживаю всплеск интереса к системе с изменяемым изменением типа 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, , не делая ни одного акта и даже включив все предупреждения! Или я чего-то не хватает?

+0

Если вы еще этого не сделали, попробуйте добавить -std = c99 -pedantic-errors в вашу строку компиляции gcc и посмотреть, не имеет значения. – jschultz410

+0

@ jschultz410: хорошая идея, но no-it не имеет никакого значения вообще = ( – Mints97

+0

Существует много случаев, когда компилятору было бы невозможно статически вывести значение c (например, - c вводится из stdin). Поэтому, часто бывает невозможно сделать какую-либо значимую статическую проверку типа таких параметров определения функции. Кажется, что если вы это сделаете, тогда компилятор говорит «ОК, я позволю вам передать все, что вам нужно, как d, поэтому длинный, так как его тип представляет собой двукратно индексированный массив ints. Удачи! » – jschultz410

ответ

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), когда во время выполнения переменная величина отличается от измерения постоянной времени компиляции. И не является ли неопределенное поведение тем, что вы ожидаете в любом случае? Иначе зачем поднимать вопрос?

Предположим, мы можем согласиться с тем, что поведение не определено при возникновении такого несоответствия. Я вижу, что компиляторы вообще не обязаны распознавать неопределенное или, возможно, неопределенное поведение, и не выдавать какой-либо диагностики вообще, если они их распознают. В этом случае я надеюсь, что компилятор сможет предупредить о возможном неопределенном поведении, но он должен успешно скомпилировать код, потому что он синтаксически корректен и удовлетворяет всем применимым ограничениям. Обратите внимание, что компилятор, способный предупреждать о таких целях, может не делать этого по умолчанию.

+0

Спасибо, это отличный ответ! Я думаю, что ты прав, мое чтение этого правила было, вероятно, неверным. Я, вероятно, слишком часто использовал, чтобы набирать несовместимость, поскольку UB вызывает номер один ... Это оставляет стандарт C в ясном, но не gcc =), в любом случае, разглагольствовать об этом, не выдавая предупреждений, вероятно, не очень хорошо ... – Mints97

+0

И если это было до меня, я бы прямо объявлял типы VM несовместимыми со всем, чтобы быть уверенным =) Имеет смысл, ИМО. – Mints97

+1

Но @Mints, типы VM должны быть совместимы, по крайней мере, с самими собой, иначе они будут непригодными для использования. Вы все еще можете иметь неопределенное поведение с типами VM, имеющими точно такой же декларатор, однако, когда во время выполнения переменные размеры отличаются. Учитывая эту проблему, что нужно сделать, если виртуальные машины автоматически несовместимы со всем остальным?Это еще один пример того, как C дает программисту мощные инструменты, с помощью которых он может нанести мощный урон. C не для веников. (Не подразумевая ничего там.) –

-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() делает динамически выяснить, как далеко заранее d на основе с, так как ваш код также демонстрирует.

Однако часто компилятор не может статически определять, неверно ли вы вызываете foo(). Похоже, что если вы это сделаете, компилятор говорит «ОК, я позволю вам передать все, что вам нужно, как d, если его тип представляет собой двукратно индексированный массив символов. Операции над указателем d будут определены к. Удачи! "

То есть, да, компилятор часто не может выполнять проверку статического типа по этим параметрам, поэтому стандарт почти наверняка не позволяет компиляторам улавливать все случаи, когда можно статически определять несовместимость типов.

+0

Да, поскольку 'sizeof (* d)' вычисляется с использованием 'c', так как это s hown в моем вопросе =) – Mints97

+0

«Однако для компилятора обычно невозможно определить, некорректно ли вы вызываете foo()». Это точно проблема. Почему мне даже позволено называть 'foo', если это полностью убивает статическую проверку? Кроме того, можно было бы использовать динамическую проверку типов. Реализация VLA должна была бы поддерживать свой размер как метаданные в какой-либо форме для динамических вызовов 'sizeof', поэтому почему бы и нет, скажем, segfault или go UB, если этот размер не соответствует размеру параметра, который был передан в виде? – Mints97

+2

@ Mints97: C не требует проверки динамического типа. Если размер не совпадает, поведение не определено; он * делает *, как вы говорите, «go UB». Неопределенное поведение не означает, что ваша программа выйдет из строя. Это означает, что поведение не определено. Это часто включает в себя программу, которая появляется «работать». –

Смежные вопросы