Неопределенное поведение целых чисел со знаком и руководство Apple по безопасному кодированию

В Руководстве по безопасному кодированию Apple говорится следующее (стр. 27):

Кроме того, любые биты, превышающие длину целочисленной переменной (со знаком или без знака), отбрасываются.

Однако в отношении переполнения целых чисел со знаком стандарт C (89) говорит:

Примером неопределенного поведения является поведение при целочисленном переполнении.

и

Если во время вычисления выражения возникает исключение (то есть, если результат не определен математически или не представим), поведение не определено.

Руководство по кодированию неверно? Есть ли здесь что-то, чего я не понимаю? Я сам не уверен, что Apple Secure Coding Guide может ошибиться.


person Arjun Sreedharan    schedule 18.04.2014    source источник
comment
Кому вы собираетесь доверять, стандарту C или какому-то стажеру в компании, которая продает блестящие коробки?   -  person Pascal Cuoq    schedule 19.04.2014
comment
@PascalCuoq Мне просто нужно твердое второе мнение, чтобы быть уверенным, что что-то, что Apple публикует в своем Secure руководстве по кодированию, неверно. Возможно, я ошибочно интерпретирую стандарт C.   -  person Arjun Sreedharan    schedule 19.04.2014
comment
Считаются ли 34 тыс. репутации Паскаля, 63 тыс. ouah или мои 89 тыс. сильным вторым мнением?   -  person R.. GitHub STOP HELPING ICE    schedule 19.04.2014


Ответы (4)


Возьмем этот пример:

1 << 32

Если мы предполагаем 32-битный int, C ясно говорит, что это поведение undefined. Период.

Но любая реализация может определить это неопределенное поведение.

gcc, например, говорит (хотя и не очень подробно определяет поведение):

GCC не использует широту, указанную в C99, только для обработки определенных аспектов подписанного '‹‹' как неопределенного, но это может быть изменено.

http://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html

Насчет clang не знаю, но подозреваю, что что касается gcc, то вычисление такого выражения, как 1 << 32, не вызовет удивления (то есть вычисление 0).

Но даже если он определен в реализациях, работающих в операционных системах Apple, переносимая программа не должна использовать выражения, вызывающие неопределенное поведение на языке C.

EDIT: я думал, что предложение Apple касается только побитового оператора <<. Похоже, это более общее, и в этом случае для языка C они совершенно неверны.

person ouah    schedule 18.04.2014
comment
+1 Особенно нравится забота о переносимости. Это действительно суть этого вопроса. - person chux - Reinstate Monica; 19.04.2014
comment
1 << 31 также имеет неопределенное поведение на 32-битных платформах, что гораздо менее интуитивно понятно. - person chqrlie; 08.05.2019

Вот второе мнение от статического анализатора, описанного как обнаружение неопределенного поведения:

int x;

int main(){
  x = 0x7fffffff + 1;
}

Анализатор запускается так:

$ frama-c -val -machdep x86_32 t.c

И производит:

[kernel] preprocessing with "gcc -C -E -I.  t.c"
[value] Analyzing a complete application starting at main
...
t.c:4:[kernel] warning: signed overflow. assert 0x7fffffff+1 ≤ 2147483647;
...
[value] Values at end of function main:
  NON TERMINATING FUNCTION

Это означает, что программа t.c содержит неопределенное поведение и что ее выполнение никогда не завершается без возникновения неопределенного поведения.

person Pascal Cuoq    schedule 18.04.2014

Два утверждения не являются взаимно несовместимыми.

  • Стандарт не определяет, какое поведение должна обеспечивать каждая реализация (поэтому разные реализации могут делать разные вещи и при этом соответствовать стандарту).
  • Apple разрешено определять поведение своей реализации.

Вам, как программисту, рекомендуется рассматривать поведение как неопределенное, поскольку ваш код может потребоваться перенести на другие платформы, где поведение отличается, и, возможно, потому, что Apple теоретически может передумать в будущем и по-прежнему соответствовать стандарт.

person Jonathan Leffler    schedule 18.04.2014
comment
Я почти уверен, что Apple также рассматривает это как неопределенное, а их документы просто дерьмо... - person R.. GitHub STOP HELPING ICE; 19.04.2014
comment
Теоретически это верно, и эти два утверждения не являются взаимоисключающими, но на практике и gcc, и clang сильно оптимизируют, предполагая, что целочисленные переполнения являются неопределенным поведением. - person ouah; 19.04.2014
comment
@R..: то, что документы Apple оставляют желать лучшего, это, конечно, еще одна возможность. Я думаю, что все еще уместно указать, что стандарт и реализация имеют разные цели, и утверждения не являются автоматически несовместимыми. - person Jonathan Leffler; 19.04.2014
comment
Я согласен с тем, что стоит указать на общий принцип, согласно которому реализация может предоставлять определения для чего-то, что стандарт оставляет неопределенным, но я не думаю, что у Apple есть реализация, которая фактически определяет поведение переполнения. Как сказал ouah, clang оптимизируется на основе неопределенного подписанного переполнения. Поэтому я думаю, что в данном случае документ просто неправильный. - person R.. GitHub STOP HELPING ICE; 19.04.2014

Рассмотрим код

void test(int mode)
{
  int32_t a = 0x12345678;
  int32_t b = mode ? a*0x10000 : a*0x10000LL;
  return b;
}

Если этот метод вызывается с нулевым значением mode, код вычислит длинное длинное значение 0x0000123456780000 и сохранит его в a. Поведение этого полностью определяется стандартом C: если бит 31 результата чист, он отрезает все, кроме нижних 32 бит, и сохраняет результирующее (положительное) целое число в a. Если бы бит 31 был установлен и результат сохранялся бы в 32-битном int, а не в переменной типа int32_t, реализация имела бы некоторую свободу действий, но реализациям разрешено определять int32_t только в том случае, если они будут выполнять такие сужающие преобразования в соответствии с правила математики с дополнением до двух.

Если бы этот метод был вызван с ненулевым значением mode, тогда числовое вычисление дало бы результат вне диапазона значения временного выражения, и как таковое вызвало бы Неопределенное поведение. Хотя правила предписывают, что должно произойти, если вычисление, выполненное для более длинного типа, сохраняется в более коротком, они не указывают, что должно произойти, если вычисления не соответствуют типу, с которым они выполняются. Довольно неприятный пробел в стандарте (который, ИМХО, должен быть закрыт) возникает с:

uint16_t multiply(uint16_t x, uint16_t y)
{
  return x*y;
}

Для всех комбинаций значений x и y, где Стандарт говорит что-либо о том, что должна делать эта функция, Стандарт требует, чтобы она вычисляла и возвращала мод продукта 65536. Если бы Стандарт предписывал это для всех комбинаций значений x и y 0- 65535 этот метод должен возвращать арифметическое значение (x*y) mod 65536, это было бы обязательным поведением, которому уже соответствуют 99,99% компиляторов, соответствующих стандартам. К сожалению, на машинах, где int составляет 32 бита, Стандарт в настоящее время не налагает требований в отношении поведения этой функции в случаях, когда арифметическое произведение будет больше 2 147 483 647. Даже если любая часть промежуточного результата за пределами младших 16 бит будет игнорироваться, код попытается оценить результат, используя 32-битный целочисленный тип со знаком; Стандарт не налагает требований на то, что должно произойти, если компилятор распознает, что продукт переполнит этот тип.

person supercat    schedule 07.05.2015