_UStrCatN
(функция конкатенации нескольких строк в RTL).Функция
_UStrCatN
написана на ассемблере, но если вникнуть в смысл проверок, то получится что-то такое:
DestLen := {... вычисляется длина результирующей строки ...}; if DestLen < 0 then _IntOver;где
_IntOver
- это функция RTL, которая и вызывает исключение Integer Overflow.Что происходит? Как длина строки может быть отрицательной? Это баг в EurekaLog?
Указанная проверка внутри
_UStrCatN
должна ограничить строки 2 Гб памяти: если результат сложения всех строк будет больше 2 Гб, то произойдёт переполнение, и длина станет отрицательной. Таким образом, Integer Overflow при сложении строк может возникать, если результат слишком большой.Но при чём тут тогда EurekaLog? И как проверка может срабатывать, если мы складываем небольшие строки? (клиент подтвердил это логом)
Такое "ложно-положительное" срабатывание возможно, если вы проводите операцию с уже удалённой строкой.
Посмотрите на такой код:
var Marker: String; function ReadLine: String; begin // ... Marker := { ... }; // ... end; begin // ... Data := Data + Marker + ReadLine; // ... end;Видите ли вы проблему в этом коде?
Чтобы понять проблему, нужно знать как выполняется строка "
Data := Data + Marker + ReadLine;
". На псевдо-коде это выглядит как-то так:Param0 := Pointer(Data); Param1 := Pointer(Marker); Param2 := Pointer(ReadLine); _UStrCatN(Data, [Param0, Param1, Param2]);Иными словами, оператор последовательно сохраняет указатели на аргументы, прежде чем вызвать функцию.
Вот вам и проблема: оператор сохраняет указатель на строку
Marker
, но строка Marker
меняется внутри функции ReadLine
. Это означает, что сохранённый указатель будет указывать на старую строку. Таким образом, на вход функции _UStrCatN
попадёт уже удалённая строка.Заметьте, что без EurekaLog в проекте этот баг не является "проблемой". Действительно, удалённая память просто помечается как свободная, но её содержимое не очищается. Это значит, что
_UStrCatN
успешно проведёт конкатенацию с уже удалённой строкой. И результат операции, скорее всего, будет корректным. Т.е. баг в коде есть, но его совершенно не видно, поскольку программа функционирует полностью правильно.Ситуация меняется в корне, если в проект добавляется EurekaLog (или любой другой инструмент для отладки проблем с памятью). По умолчанию в EurekaLog включены проверки памяти. Это означает, что удалённая память будет очищена. Как правило, это делается шаблоном вроде
DEADBEEF
. Заметьте, что Integer представление DEADBEEF
- отрицательно (равно -559038737). Т.е. прибавление к этому числу длин нескольких небольших строк также даст отрицательное число.Иными словами, если в проект добавлена EurekaLog, то операция с уже удалённой строкой больше не будет успешной. Ранее скрытый баг теперь виден.
У меня был похожий баг в коде вида
ОтветитьУдалитьa[GetIndexInA()] := value;
где a — массив-поле класса, и GetIndexInA иногда его реаллоцирует. Больше так не выделываюсь. :(
Если всё происходит так, как вы описали, то это явный баг компилятора. Я не видел в документации по конкатенации никаких предупреждений, что аргументы обязательно не должны иметь побочных эффектов.
ОтветитьУдалитьВ любом случае, правильно скомпилированная строка вида "Data := Data + Marker + ReadLine;" не должна вызывать подобных проблем, по той причине, что строки являются сущностями с подсчётом ссылок использования. Поэтому когда компилятор готовит строки перед вызовом _UStrCatN он должен у каждой этой строки увеличить счётчик ссылок, что гарантирует их неизменность, а после вызова _UStrCatN он должен отпустить эти строки уменьшив счётчик ссылок. Если же компилятор пытается обойти эту систему ссылок надеясь на "авось пройдёт", то вот и получаем такой неприятный баг...
Вопрос интересный. С одной стороны - ваши аргументы, с другой стороны - контракт: "Все параметры стабильны: вы не можете менять параметр, пока работает функция, в которую он передан". В данном случае "функция" - это вся строка целиком.
УдалитьПросто увеличение счётчика - это UStrLAsg / lock inc dword ptr [...]. Префикс lock означает блокировку шины и/или кэша процессора на время выполнения этой операции, это гарантирует, что текущий процессор/ядро будет иметь монопольный доступ к указанной памяти, и гарантирует порядок операций: результаты выполнения команды будут учитываться сразу (т.е. если в кэше было старое значение, оно не будет использовано). Короче говоря, это сильно не здорово для многопоточной производительности. И делать это N раз в одной стоке (на каждый аргумент), чтобы избежать сильно маловероятного сценария, который к тому же является скорее ошибкой программиста... В общем, можно понять, почему так не сделано.
Но если хотите - попробуйте, создайте тикет на QC.
Да нет, упоминается:
Удалить>For the register and pascal conventions, the evaluation order is not defined.
Что-то такое же и для операндов в выражениях должно быть.
И вообще так в «древних» языках исторически общепринято сложилось, как часть негласного правила «всё, что не определено — неопределённо».
В документации Free Pascal прямым текстом сказано, что const — это гарантия не от компилятора программисту, а от программиста компилятору, что переменная не изменится: https://www.freepascal.org/docs-html/ref/refsu66.html.
As a side effect, the following code will produce not the expected output:
Var
S : String = ’Something’;
Procedure DoIt(Const T : String);
begin
S := ’Something else’;
Writeln(T);
end;
begin
DoIt(S);
end.
Will write
Something else
This behavior is by design.
Это нужно как раз затем, чтобы пропустить подсчёт ссылок при передаче const-аргументов. В свою очередь, о выражениях, типа a + b или [a, b, c], можно думать как об эквивалентных функциях add(const a, const b) или make_array(const a, const b, const c).