Недефинирано поведение на цели числа със знак и Ръководство за безопасно кодиране на Apple

Ръководството за сигурно кодиране на Apple казва следното (стр. 27):

Също така, всички битове, които препълват дължината на целочислена променлива (независимо дали със знак или без знак), се премахват.

Въпреки това, по отношение на знаковото препълване на цели числа C стандарт (89) казва:

Пример за недефинирано поведение е поведението при целочислено препълване.

и

Ако възникне изключение по време на оценката на израз (т.е. ако резултатът не е математически дефиниран или не може да бъде представен), поведението е недефинирано.

Грешно ли е ръководството за кодиране? Има ли нещо тук, което не разбирам? Не съм убеден, че Apple Secure Coding Guide може да сбърка това.


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


Отговори (4)


Да вземем този пример:

1 << 32

Ако приемем 32-битов int, C ясно казва, че това е недефинирано поведение. Месечен цикъл.

Но всяка реализация може да дефинира това недефинирано поведение.

gcc например казва (въпреки че не е много изрично в дефинирането на поведението):

GCC не използва ширината, дадена в C99, само за да третира определени аспекти на знака '‹‹' като недефинирани, но това подлежи на промяна.

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

Не знам за clang, но подозирам, че що се отнася до gcc, оценката на израз като 1 << 32 не би донесла изненада (т.е. оценка на 0).

Но дори и да е дефинирана върху реализации, работещи в операционни системи на Apple, преносимата програма не трябва да използва изрази, които извикват недефинирано поведение в езика C.

РЕДАКТИРАНЕ: Мислех, че изречението на 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 стойност, тогава численото изчисление би дало резултат извън обхвата на стойността на временния израз и като такова би причинило Недефинирано поведение. Докато правилата диктуват какво трябва да се случи, ако изчисление, извършено върху по-дълъг тип, се съхрани в по-кратък, те не посочват какво трябва да се случи, ако изчисленията не се вписват в типа, с който се извършват. Доста неприятна празнина в стандарта (която IMHO трябва да бъде запушена) възниква с:

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 бита, стандартът понастоящем не налага изисквания по отношение на поведението на тази функция в случаите, когато аритметичният продукт би бил по-голям от 2147483647. Въпреки че всяка част от междинния резултат извън долните 16 бита ще бъде игнорирана, кодът ще се опита да оцени резултата, използвайки 32-битов тип цяло число със знак; стандартът не налага изисквания какво трябва да се случи, ако компилаторът разпознае, че продуктът ще прехвърли този тип.

person supercat    schedule 07.05.2015