Си Должен Умереть

Язык C — один из самых влиятельных языков программирования в истории.

Он стал незаменимым инструментом разработки операционных систем, сместив с этого пьедестала языки ассемблера.

Изучение C является обязательным для любого уважающего себя программиста.

Этот язык любят за кажущуюся простоту и ненавидят за безжалостность к ошибкам.

Благодаря ему у нас есть ядро Linux и тысячи уязвимостей в нем для загрузки.

Попробуем разобраться, что же представляет собой этот противоречивый язык Си — благословение или проклятие? История языка Си берет свое начало в недрах американской компании Bell Labs и тесно связана с судьбой операционной системы UNIX. Его создатели Кен Томпсон и Деннис Ритчи разработали свой проект для компьютеров PDP-11, и первые два года их основным инструментом был язык ассемблера.

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

С его помощью было полностью переписано ядро операционной системы и большинство утилит. Язык C позволил создавать эффективные низкоуровневые программы на PDP-11, практически не используя язык ассемблера.

Со временем встал вопрос о портировании UNIX на новые аппаратные платформы.

Использование языка C значительно упростило эту задачу.

Ведь если бы при разработке использовался только язык ассемблера, то операционную систему пришлось бы переписывать под каждую архитектуру компьютера.

С другой стороны, исходный код UNIX все еще содержал много кода, написанного специально для компьютера PDP-11. Да и сам язык Си не всегда точно отражал особенности и детали конкретной аппаратной платформы.

Последнее еще больше усложнило процесс портирования и лишило язык одного из его главных преимуществ — прозрачной и понятной генерации машинного кода.

Чем больше компьютерных архитектур C захватывало популярность, тем менее очевидной становилась его связь с низким уровнем.

В процессе миграции UNIX на новые аппаратные платформы обнаружилась еще одна проблема.

Перенесенные программы на C работали медленнее, чем ожидалось.

Чем больше архитектура целевого компьютера отличалась от PDP-11, тем менее эффективным был полученный код. Чтобы компенсировать этот недостаток, разработчики компиляторов все чаще стали использовать неявные оптимизации.

И хотя это решение улучшало производительность самих программ, Си всё дальше уходил от низкого уровня.

Теперь нужно было не только понять, как именно определялись языковые конструкции для каждой из компьютерных архитектур, но и как они оптимизировались.

Разумеется, любой компилятор самостоятельно решал, как транслировать исходный код для каждой аппаратной платформы.

В результате стало практически невозможно написать низкоуровневую программу на языке C, независимую от используемого компилятора.

Необходимо было понять, как эффективно реализовать высокоуровневые конструкции языка Си, сохранив при этом его низкоуровневые свойства.

Попыткой решить эту проблему стала публикация первого языкового стандарта в 1989 году.

Его обычно называют «ANSI C» или «C89», и именно к нему мы будем обращаться дальше.

Создатели стандарта решили окончательно разорвать связь C с архитектурой PDP-11 и сделать язык полностью высокоуровневым.

Была введена так называемая «абстрактная машина» — воображаемый исполнитель кода на языке Си (раздел 2.1.2.3, «Выполнение программы»):

Семантические описания в этом стандарте описывают поведение абстрактной машины, в которой вопросы оптимизации не имеют значения.

Это означает, что оптимизации компилятора не повлияют на работу программы, пока ее исходный код соответствует стандарту.

Абстрактная машина должна была решать две задачи одновременно.

Во-первых, следование стандарту позволило создавать легко переносимые программы на языке Си.

Во-вторых, абстрактная машина может дать компиляторам свободу в оптимизации.

Но возникает вполне резонный вопрос — чем же тогда язык Си отличается от любого другого компилируемого языка высокого уровня? Ответ кроется в тексте стандарта.

Чтобы все же дать программистам теоретическую возможность писать низкоуровневые, а значит, непереносимые процедуры, было введено еще одно понятие — неопределенное поведение (раздел 1.6, «ОПРЕДЕЛЕНИЯ ТЕРМИНОВ»):

Неопределенное поведение — поведение, предполагающее использование непереносимых или ошибочных программных конструкций, ошибочных данных или объектов с неопределенными значениями, к которым стандарт не предъявляет никаких требований.

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

сообщение).

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

Они также могут быть восприняты как неприемлемые в тексте программы.

Давайте подробнее рассмотрим неопределенное поведение на конкретном примере.

Возьмем следующий фрагмент кода на языке C:

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

int x = 1; x = x << sizeof(int) * 8;

Попробуем угадать, какой результат мы получим.

Допустим, мы скомпилировали этот код для процессоров ARM. Команда сдвига битов в этой аппаратной платформе определена таким образом, что результирующее значение переменной «x» должно быть «0».

С другой стороны, мы можем перевести нашу программу в машинный код x86. И уже там битовый сдвиг реализован таким образом, что значение «х» не изменится и останется равным «1».

Мы могли бы заключить, что вывод данного фрагмента кода зависит от того, для какой аппаратной платформы мы его скомпилировали.

Но на самом деле это не так.

В действительности этот фрагмент кода может быть обработан компилятором любым возможным или невозможным способом.

Причина в следующем: согласно тексту стандарта языка Си сдвиг битов на величину, большую или равную размеру выражения в битах, является неопределённым поведением.

Получается, нет никакой гарантии, что этот кусок кода вообще будет работать.

Фактически, даже в рамках одной и той же архитектуры один и тот же компилятор может генерировать совершенно разные исполняемые файлы.

Приведем примеры компиляции и запуска программы с выводом значения переменной «х».

В обоих случаях мы используем компилятор gcc версии 10.2.1 для целевой архитектуры x86-64.

$ cat test.c #include <stdio.h> int main() { int x = 1; x = x << sizeof(int) * 8; printf("%d\n", x); return 0; } $ gcc test.c -o test $ .

/test 1 $ gcc -O test.c -o test $ .

/test 0

Флаг «-O» позволяет компилятору gcc использовать оптимизацию исходного кода.

Какие именно механизмы оптимизации можно применять, а также какие флаги за них отвечают, зависит от конкретного компилятора.

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

Поэтому единственный способ писать переносимые программы на языке C — полностью избегать неопределенного поведения во время разработки.

Давайте рассмотрим немного более сложный пример.

Другой тип неопределенного поведения — разыменование нулевого указателя.

Тривиальной версией этого будет следующий фрагмент кода:

* (char *) 0;

Конечно, никто в здравом уме не станет писать подобное в своей программе.

Однако нет необходимости явно разыменовывать нулевой указатель, чтобы вызвать неопределенное поведение.

В серии статей «Что должен знать каждый программист на языке C о неопределенном поведении» на сайте blog.llvm.org Вот фрагмент кода, который доказывает это:

void contains_null_check(int *p) { int dead = *p; if(p == 0) return; *p = 4; }

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

Один из них удаляет ненужный, «мертвый» код, а второй — бесполезные проверки на нулевой указатель.

Если к приведенному выше фрагменту кода применить первый механизм оптимизации, он преобразует функцию следующим образом:

void contains_null_check(int *p) { if(p == 0) return; *p = 4; }

Тогда второй механизм не обнаружит ненужных проверок на нулевой указатель и исходный код функции примет окончательный вид. Однако на самом деле порядок оптимизации может быть другим.

Например, компилятор имеет право сначала исключить ненужные проверки на нулевой указатель, а затем функция преобразуется следующим образом:

void contains_null_check(int *p) { int dead = *p; if(0) return; *p = 4; }

Поскольку мы разыменовываем указатель перед его проверкой, компилятор спокойно решает, что сам указатель никогда не будет нулевым.

Это заменяет сравнение «p == 0» выражением, которое всегда возвращает false. Затем компилятор запускает первый механизм оптимизации и удаляет мертвый код:

void contains_null_check(int *p) { *p = 4; }

Важно подчеркнуть, что обе эти оптимизации верны.

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

Такой код может создать уязвимость для программ, работающих без защиты памяти, то есть ядра операционной системы или прошивки микроконтроллера.

Конечно, в этом примере есть ошибка, но основная проблема не в ней, а в том, как с ней справится компилятор.

Допустим, вы случайно ввели в свою программу неопределенное поведение.

В лучшем случае вы сразу обнаружите ошибку и исправите ее.

Если это не так успешно, вы не сделаете это сразу.

Однако гораздо более вероятно, что компилятор не воспользуется вашей оплошностью в своих целях.

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

Проще говоря, неопределенное поведение — это бомба замедленного действия.

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

Оптимизация компилятора также может повлиять на функции стандартной библиотеки C, включая, например, memset. Он широко известен обилием ошибок, которые допускают программисты при его вызове.

Заголовок функции выглядит следующим образом:

void *memset(void *ptr, int value, size_t num);

memset записывает «num» байтов со значением «value» по адресу «ptr».

Несмотря на то, что параметр «value» является целым числом, фактически используется только его младший байт. Функция активно используется для сброса больших объемов данных, но сам компилятор часто любит вставлять свой вызов туда, где нужно и не очень.

Итак, курьезный случай обсуждался 15 апреля 2018 года на форуме.

osdev.org .

Пользователь под ником ScropTheOSAdventurer создал тема , в котором он рассказал о процессе разработки собственной образовательной операционной системы.

К своему несчастью, он позволил компилятору оптимизировать исходный код проекта, в результате чего последний перестал работать.

В процессе отладки программист обнаружил ошибку в следующем фрагменте кода:

void *memset(void *ptr, int value, size_t num) { unsigned char *ptr_byte = (unsigned char *) ptr; for(size_t i = 0; i < num; ptr_byte[i] = (unsigned char) value, i++); return ptr; }

Для своей операционной системы разработчик решил использовать собственную реализацию функции memset. Но он не учел, что в процессе трансляции компилятор gcc найдет в этом коде весьма заманчивую возможность для оптимизации.

Фактически функция в итоге была преобразована к следующему виду:

void *memset(void *ptr, int value, size_t num) { return memset(ptr, value, num); }

Вполне вероятно, что разработчики компилятора gcc были непревзойденными мастерами софистики.

В любом случае, возможности оптимизации компилятора явно превосходят все пределы, доступные человеческому разуму.

Приведем еще один пример с функцией memset. Компилятор способен не только создавать свои вызовы с нуля, но и выбрасывать их из исходного кода по своему желанию.

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

Обычно такое поведение избыточно, но представьте себе следующую ситуацию.

Ваша программа работает с базой данных пользователей, в которой хранятся их имена и пароли, и вы описали функцию примерно так:

int check_password(const char *pwd) { char real_pwd[32]; get_password(real_pwd); return !strcmp(pwd, real_pwd); }

Проблема только одна — после вызова check_password в стеке окажется строка с настоящим паролем пользователя.

Если в вашей программе есть хотя бы одна уязвимость, позволяющая читать данные из памяти, то есть реальная вероятность того, что пароль будет украден из стека.

Примером такой уязвимости стал печально известный баг " Сердцекровие ".

Чтобы снизить возможные риски, проще всего очистить фрагмент стека, содержащий пароль:

int check_password(const char *pwd) { int result; char real_pwd[32]; get_password(real_pwd); result = !strcmp(pwd, real_pwd); memset(real_pwd, 0, sizeof(real_pwd)); return result; }

Казалось бы, решение найдено, но не все так просто.

Опытный в вопросах оптимизации компилятор может решить, что вызов memset здесь излишен, и спокойно удалит его из тела функции.

Ведь для работы самой программы это действие абсолютно бесполезно.

Что еще хуже, компилятор может сгенерировать код, в котором пароль окажется в одном из регистров процессора.

В этом случае его может быть еще проще получить, используя уязвимость в программе.

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

Подробнее об этой проблеме можно прочитать на сайте связь .

Одним из самых коварных типов неопределенного поведения является строгое псевдонимирование.

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

В тексте стандарта дано такое описание строгого псевдонима (раздел 3.3, «ВЫРАЖЕНИЯ»):

Значение объекта должно быть доступно только через выражение lvalue одного из следующих типов: - объявленный тип объекта, - квалифицированная версия объявленного типа объекта, - знаковый или беззнаковый тип, соответствующий объявленному типу объекта, - подписанный или беззнаковый тип, соответствующий квалифицированной версии объявленного типа объекта, - тип массива, структуры или объединения, включающий в число своих членов один из вышеперечисленных типов (включая рекурсивно член внутренней структуры, массива или объединения), - тип персонажа.

Самый простой способ проиллюстрировать строгое псевдонимирование — это конкретный пример:

int x = 42; float *p = &x; *p = 13;

Чтобы вызвать неопределенное поведение, достаточно обратиться к переменной типа, несовместимого с объявленным.

Это ограничение можно обойти, используя тип символа (char), на который не распространяются строгие правила псевдонимов:

int x = 42; char *p = &x; *p = 13;

Но разбиение переменной на символы может оказаться трудоемкой задачей.

Вам придется учитывать размер данных, а также используемый порядок байтов.

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

union u { int a; short b }; union u x; x.a = 42; x.b = 13;

Однако этот метод не лишен недостатков — объединение должно содержать члены всех возможных типов, которые будут использоваться программой.

Все это серьезно усложняет использование «типового каламбура» или так называемого типизированного каламбура — намеренного нарушения системы типов.

Этот метод необходим для более гибкого управления низкоуровневой памятью машины.

Чтобы проиллюстрировать полезность набора слов, давайте рассмотрим небольшой пример.

Допустим, вы читаете содержимое файла изображения в память программы.

И теперь вам нужно написать функцию, возвращающую цвет пикселя по указанным координатам.

Для простоты будем считать, что размер типа int такой же, как размер пикселя, как и порядок байтов в обоих случаях:

int get_pixel(const char *buf, int width, int x, int y) { buf += get_header_size(buf); return ((const int *) buf)[y * width + x]; }

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

Вместо типа int мы могли бы выбрать любой другой тип известного размера.

Но все это не имеет значения, потому что функция get_pixel абсолютно некорректна с точки зрения стандарта, поскольку нарушает строгие правила алиасинга.

Чтобы использовать каламбур, вам придется переписать весь код, связанный с используемым буфером, включая тот, который отвечает за чтение файла.

Существует огромное количество примеров программ, не удовлетворяющих строгим правилам псевдонимов.

К ним относится знаменитая быстрая функция обратного квадратного корня из Quake 3:

float FastInvSqrt(float x) { float xhalf = 0.5f * x; int i = *(int *) &x; i = 0x5f3759df - (i >> 1); /* What the fuck? */ x = *(float *) &i; x = x * (1.5f - (xhalf * x * x)); return x; }

Этот код позволил вычислить обратный квадратный корень из числа с плавающей запятой в несколько раз быстрее, чем при использовании арифметического сопроцессора.

Однако проверку стандартом этот шедевр черной магии не выдерживает — похоже, создатель культовых игр Джон Кармак вообще не понимает язык Си.

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

Строгие правила псевдонимов применяются к доступу к любой памяти, включая динамическую память.

Так комитет по стандартизации отметил, что следующий фрагмент кода ( источник ):

void f(int *x, double *y) { *x = 0; *y = 3.14; *x = *x + 2; }

можно преобразовать следующим образом:

void f(int *x, double *y) { *x = 0; *y = 3.14; *x = 2; }

Согласно строгим правилам псевдонимов, указатель y не может содержать адрес той же ячейки памяти, что и указатель x. Именно этот факт позволяет заменить выражение «*x = *x + 2» на «*x = 2».

Активное использование таких оптимизаций компиляторами сломало огромное количество старого кода.

Так, в письме от 12 июля 1998 года один из разработчиков компилятора gcc Джефф Лоу, отвечая на вопросы о строгом псевдониме и связанных с ним ошибках, пишет ( источник ):

> Существует много кода, нарушающего строгие правила псевдонимов.

Одним из таких примеров является «переносимая» универсальная функция контрольной суммы IP, найденная в исходных кодах сетевых программ BSD. ИМХО, такого кода становится всё меньше и меньше — современные компиляторы уже некоторое время используют строгий алиасинг при анализе, в результате чего люди вынуждены исправлять свой код. Конечно, это не относится к Linux и некоторым другим бесплатным проектам, поскольку они используют только gcc. > Если мы начнем говорить, что этот код сломан, то нам лучше иметь какой-то план на случай, если люди начнут спрашивать, почему их код, который работал много лет, теперь не работает. Направьте их на стандарт языка C :-) :-)

Строгие правила псевдонимов для компилятора gcc можно включить с помощью флага «-fstrict-aliasing» и отключить с помощью флага «-fno-strict-aliasing».

Последнее рекомендуется, если вы не уверены, нарушаете ли вы текст стандарта – скорее всего, да.

Говоря об упомянутом в письме ядре Linux, его автор Линус Торвальдс также дал свою оценку строгому алиасингу в частности и работе комитета в целом.

Так, раскритиковав желание одного из разработчиков операционной системы в очередной раз перестраховаться от нарушения стандарта, Линус написал следующее письмо ( источник ):

Честно говоря, мне все это кажется сомнительным.

И я не говорю о самих изменениях – с этим можно жить.

Но обоснование этих самых изменений – абсолютная и полная ерунда, и очень опасная.

Дело в том, что использование объединений для реализации каламбуров является распространенным и СТАНДАРТНЫМ способом сделать это.

На самом деле это задокументировано для gcc и используется, если вы, будучи не слишком умными (исходное: «еб*ный дебил»), использовали «-fstrict aliasing» и теперь вам нужно избавиться от всего вреда, который наносит этот мусорный стандарт. Энди, в чем была причина всего этого идиотизма? И не говорите мне, что текст стандарта «недостаточно ясен».

Текст стандарта явно херня (о строгих правилах псевдонимов см.

выше) и в таких случаях его следует игнорировать.

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

То же самое следует делать и в ситуациях, когда нет полной ясности.

Вот почему мы используем «-fwrapv», «-fno-strict-aliasing» и другие флаги.

Я уже говорил и повторю еще раз: когда текст стандарта противоречит действительности, это обычный кусок туалетной бумаги.

Это не имеет абсолютно никакого значения.

На самом деле, я бы вместо этого взял рулон настоящей туалетной бумаги - по крайней мере, я не испачкаю всю задницу чернилами.

Судя по всему, Линус Торвальдс плохо изучил язык Си — настоящему программисту на Си такое никогда бы не пришло в голову.

Однако стандарт не ограничивается только строгим псевдонимом.

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

void f(int *p, int *q) { free(p); if(p == q) /* Undefined behaviour! */ do_something(); }

Использование значения указателя после освобождения его памяти запрещено текстом стандарта (раздел 4.10.3, «Функции управления памятью»):

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

Программисту важно понимать, что указатели в C не являются низкоуровневыми.

Стандарт пытался полностью искоренить любую связь между языком и реальным миром.

Даже сравнение указателей, ссылающихся на разные объекты, объявлено неопределённым поведением (раздел 3.3.8, «Операторы отношения»):

При сравнении двух указателей результат зависит от относительного расположения в адресном пространстве объектов, на которые они указывают. Если указанные объекты не являются членами одной и той же структуры, массива или объединения, результат не определен.

Вот небольшой фрагмент кода, демонстрирующий некорректное с точки зрения стандарта сравнение:

int *p = malloc(64 * sizeof(int)); int *q = malloc(64 * sizeof(int)); if(p < q) /* Undefined behaviour! */ do_something();

Однако наиболее интересным примером здесь является исходный код следующей программы:

#include <stdio.h> int main() { int x; int y; int *p = &x + 1; int *q = &y; printf("%p %p %d\n", (void *) p, (void *) q, p == q); return 0; }

Если вы переведете приведенный выше текст с помощью компилятора gcc, передав ему флаг «-O», полученный исполняемый файл при запуске выдаст что-то вроде следующей строки:

0x1badc0de 0x1badc0de 0

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

Пожалуй, разработчики уже отмеченного нами компилятора gcc не менее искусны в интерпретации текстов стандарта, чем в вопросах софистики.

Приобщиться к сложной герменевтике можно при обсуждении этого вопроса на официальном сайте.

Веб-сайт Организация ГНУ.

Большая часть примеров, связанных с работой указателей, взята с сайта kristerw.blogspot.com. Там вы можете найти дополнительную информацию о стандартных текстах языка C, а также о загадочных оптимизациях компилятора.

Может показаться, что если оптимизации отключены, все описанные выше проблемы обойдут вас стороной.

Просто не передавайте компилятору флаг «-O», и вы получите ожидаемый результат. Но на самом деле это не так.

В январе 2007 года на сайте gcc.gnu.org пользователь под ником felix-gcc опубликовано исходный код следующей программы:

#include <assert.h> int foo(int a) { assert(a + 100 > a); printf("%d %d\n",a + 100, a); return a; } int main() { foo(100); foo(0x7fffffff); }

Функция foo проверяет сумму предоставленного числа со знаком и константы «100» на предмет переполнения.

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

В случае переполнения такое число меняет знак, в результате чего проверка «a + 100 > a» возвращает false. В теле основной функции felix-gcc дважды вызывает foo. Во-первых, он передает на вход число, которое не приведет к переполнению.

Затем, предполагая, что размер типа данных int составляет четыре байта, felix-gcc вызывает foo с наибольшим положительным числом этого типа.

Логично предположить, что в этом случае сравнение вернет false, и Assert прервет программу.

Однако это результат, полученный felix-gcc после запуска исполняемого файла:

200 100 -2147483549 2147483647

Фактически gcc решил убрать проверку переполнения, и это несмотря на то, что компилятору не передавались никакие флаги.

И что ещё интереснее, более ранние версии gcc при тех же условиях проверку не снимали, в результате чего результирующая программа вела себя по-другому.

На обоснованную просьбу felix-gcc исправить неожиданную ошибку компилятора ответил пользователь под ником Andrew Pinski. Как разработчик gcc, Эндрю Пински отметил, что такое поведение не является ошибочным.

Более того, он сам оказался автором изменения кода компилятора, приведшего к такому странному результату.

Ниже приводится фрагмент диалога между felix-gcc и Эндрю Пински.

Комментарии не нужны:

Эндрю Пински Переполнение знакового числа — это неопределенное поведение в стандартном тексте C, используйте беззнаковый тип или флаг «-fwrapv».

Феликс-GCC Ты, должно быть, шутишь? Различные проблемы безопасности вызваны переполнением чисел, и вы просто говорите мне, что в gcc 4.1 я больше не могу проверять подписанные типы? Вы явно чего-то не понимаете.

ДОЛЖЕН быть способ обойти эту проблему.

Существующее программное обеспечение использует числа со знаком, и я не могу просто изменить тип на беззнаковый — мне все равно нужно проверять переполнение! Не похоже, что я мог бы использовать какой-либо обходной путь для этого.

Что вы хотите, чтобы я сделал - привел тип к беззнаковому, сдвинул на единицу вправо, затем добавил или что?! ПОЖАЛУЙСТА, ОТМЕНИТЕ ЭТО ИЗМЕНЕНИЕ.

Это создаст СЕРЬЕЗНЫЕ ПРОБЛЕМЫ С БЕЗОПАСНОСТЬЮ ВО ВСЕХ ВИДАХ ПРОГРАММ.

Меня не волнует, что говорят ваши стандартизаторы о нарушении gcc. ВСЕ ЭТО ПРИВЕДЕТ К ВЗЛОМАНИЮ ЛЮДЕЙ.

Я обнаружил эту проблему, потому что одна из проверок безопасности, предотвращающая взлом, не удалась.

?ЭТО НЕ ШУТКА.

ПОЧИНИ ЭТО! СЕЙЧАС! Эндрю Пински Я не шучу, стандарт языка C прямо гласит, что переполнение знакового числа является неопределенным поведением.

Феликс-GCC Итак, слушай, Эндрю, ты действительно думаешь, что эта проблема исчезнет, если ты будешь продолжать исправлять ошибки достаточно быстро? Тест, который я написал, охватывал все возможные ситуации.

Нет необходимости даже указывать, какой тип используется — указатель, беззнаковое или знаковое число.

Ну, вы тоже сломали указатели, но ваши изменения исправлены.

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

Давайте посмотрим правде в глаза – вы совсем облажались (исходное: «облажался по-королевски»), и теперь пытаетесь как можно быстрее замазать все ошибки, чтобы никто не заметил, какой большой ущерб вы нанесли.

Вы, сэр, непрофессиональны и позорите команду разработчиков GCC. Эта ошибка останется открытой до тех пор, пока вы не измените все обратно или не сделаете упомянутый вами флаг по умолчанию.

Пока вы ломаете программы, авторы которых по глупости включили оптимизацию, меня это не волнует. Но я не позволю вам сделать мою среду менее безопасной только потому, что ваш недостаток профессионализма мешает вам заняться оптимизацией после того, как было доказано, что она приносит больше вреда, чем пользы.

Сколько еще доказательств вам нужно предоставить? Боже мой, autoconf считает, что ваши «оптимизации» должны быть отключены везде.

Вы вообще замечаете взрывы вокруг себя? Эндрю Пински http://c-faq.com/misc/sd26.html Это все, о чем я буду с этого момента говорить.

Этот код ясно говорит вам, как распознать переполнение до того, как оно произойдет. Опять же, ваш код сломан и не соответствует стандартам.

Феликс-GCC МОЙ КОД НЕ НАРУШЕН.

Попытки обесценить проблему или оскорбить меня ничего не решат. Эндрю Пински Вы написали ошибку, поэтому я предположил, что ваш код сломан.

Феликс-GCC Так скажи мне, какую часть моего аргумента ты не понимаешь? Я мог бы использовать более простые слова, чтобы на этот раз вы меня поняли.

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

И от вас зависит МНОГО программ.

Когда вы нарушили точность вычислений с плавающей запятой, вы сделали ее доступной с помощью флага (-ffast-math).

Когда вы добавили строгое псевдонимирование, вы также сделали эту функцию доступной через флаг (-fstrict-aliasing).

Если я правильно помню, вы тогда тоже цитировали текст стандарта, пока вас не остановили люди с более адекватным пониманием мира.

И я собираюсь оставить эту ошибку открытой, пока то же самое не повторится.

Эндрю Пински

Теги: #информационная безопасность #программирование #C++ #ci #компиляторы #Системное программирование #неопределённое поведение
Вместе с данным постом часто просматривают: