Что может изменить указатель фрейма?

У меня сейчас очень странная ошибка, возникающая в довольно массивном приложении C++ на работе (огромное с точки зрения использования ЦП и ОЗУ, а также длины кода - более 100 000 строк). Он работает на двухъядерном компьютере Sun Solaris 10. Программа подписывается на каналы котировок акций и отображает их на «страницах», настроенных пользователем (страница - это оконная конструкция, настроенная пользователем - программа позволяет пользователю настраивать такие страницы). Эта программа работала без проблем, пока одна из базовых библиотек не стала многопоточной. Части программы, затронутые этим, были соответственно изменены. К моей проблеме.

Примерно один раз из каждых трех выполнений программа будет давать сбой при запуске. Это не обязательно жесткое правило - иногда он вылетает три раза подряд, а затем срабатывает пять раз подряд. Интересен segfault (читай: болезненный). Это может проявляться по-разному, но чаще всего происходит то, что функция A вызывает функцию B, и при входе в функцию B указатель кадра внезапно устанавливается на 0x000002. Функция А:

   result_type emit(typename type_trait<T_arg1>::take _A_a1) const
     { return emitter_type::emit(impl_, _A_a1); }

Это простая реализация сигнала. impl_ и _A_a1 четко определены в их кадре на момент сбоя. При фактическом выполнении этой инструкции мы попадаем на счетчик программы 0x000002.

Это не всегда происходит с этой функцией. На самом деле это происходит во многих местах, но это один из самых простых случаев, который не оставляет места для ошибок. Иногда происходит то, что переменная, выделенная стеком, внезапно оказывается в ненужной памяти (всегда на 0x000002) без какой-либо причины. В других случаях тот же код будет работать нормально. Итак, мой вопрос: что может так сильно испортить стек? Что на самом деле может изменить значение указателя фрейма? Я точно никогда не слышал о таком. Единственное, что я могу придумать, - это запись за пределы массива, но я построил его с помощью протектора стека, который должен обнаруживать любые случаи, когда это происходит. Я здесь тоже в рамках своего стека. Я также не вижу, как другой поток может перезаписать переменную в стеке первого потока, поскольку каждый поток имеет свой собственный стек (это все pthreads). Я пробовал создать это на Linux-машине, и, хотя у меня там не было ошибок, примерно в одном из трех случаев это зависало у меня.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
0
1 968
14

Ответы 14

Это похоже на проблему переполнения стека - что-то записывается за пределы массива и попирает кадр стека (и, вероятно, адрес возврата) в стеке. По этой теме существует обширная литература. В «Руководстве программиста оболочки» (2-е издание) есть примеры SPARC, которые могут вам помочь.

Имеет ли смысл присвоить переменной значение 2, а вместо этого присвоить ее адрес 2?

Остальные детали я не понимаю, но «2» - это повторяющаяся тема в вашем описании проблемы. ;)

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

Итак, если возможно, используйте vector вместо этого, потому что любая реализация decend STL будет давать хорошие сообщения компилятора, если вы попробуете это в режиме отладки (тогда как массивы C наказывают вас segfaults).

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

Я не уверен, что вы называете «указателем кадра», как вы говорите:

On actual execution of that instruction, we end up at program counter 0x000002

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

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

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

  • Попадание в оперативную память. Любая запись через неверный указатель может привести к попаданию мусора в стек. Я не знаком с Solaris, но в большинстве реализаций потоков потоки находятся в одном адресном пространстве процесса, поэтому любой поток может получить доступ к стеку любого другого потока. Один из способов, которым поток может получить указатель в стек другого потока, - это передача адреса локальной переменной API, который в конечном итоге имеет дело с указателем в другом потоке. если вы не синхронизируете вещи должным образом, это приведет к тому, что указатель будет обращаться к недопустимым данным. Учитывая, что вы имеете дело с «простой реализацией сигнала», похоже, что один поток отправляет сигнал другому. Может быть, один из параметров в этом сигнале имеет указатель на локальный?

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

Извините, если это не дает намек на то, как его найти.

Повреждение стека, определенно 99,9%.

Вам следует внимательно искать следующие запахи:

  • Использование массивов 'C'
  • Использование функций в стиле strcpy 'C'
  • memcpy
  • malloc и бесплатно
  • потокобезопасность всего, что использует указатели
  • Неинициализированные переменные POD.
  • Указатель арифметики
  • Функции, пытающиеся вернуть локальные переменные по ссылке

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

Можно ли запустить штуку через Valgrind? Возможно, Sun предлагает аналогичный инструмент. Intel VTune (на самом деле я думал о Thread Checker) также имеет несколько очень хороших инструментов для отладки потоков и тому подобное.

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

Я пробовал на нем Valgrind, но, к сожалению, он не обнаруживает ошибок стека:

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

Я склонен согласиться с тем, что это проблема переполнения стека. Сложнее всего его отследить. Как я уже сказал, эта штука содержит более 100000 строк кода (включая собственные библиотеки, разработанные собственными силами - некоторые из них появились еще в 1992 году), поэтому, если у кого-то есть какие-нибудь хорошие уловки для отлова подобных вещей, я был бы благодарный. Повсюду работают над массивами, и приложение использует OI для своего графического интерфейса (если вы не слышали об OI, будьте благодарны), поэтому просто поиск логической ошибки - гигантская задача, а у меня мало времени.

Также согласился, что 0x000002 является подозрительным. Это примерно единственная постоянная между сбоями. Еще более странным является тот факт, что это произошло только с многопоточным переключателем. Я думаю, что меньший стек в результате многопоточности - вот что заставляет это возникать сейчас, но с моей стороны это чистое предположение.

Никто об этом не спрашивал, но я построил с помощью gcc-4.2. Кроме того, я могу гарантировать безопасность ABI, так что это тоже не проблема. Что касается «мусора в конце стека» при попадании в ОЗУ, тот факт, что он универсально равен 2 (хотя и в разных местах кода), заставляет меня сомневаться, что мусор имеет тенденцию быть случайным.

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

Во-первых, я бы проверил, что все библиотеки и связанные объекты были полностью перестроены, а все параметры компилятора согласованы - у меня раньше была аналогичная проблема (Solaris 2.5), которая была вызвана объектным файлом, который не был перестроен. .

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

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

Есть некоторая путаница между переполнение стека и повреждение стека.

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

void foo()
{
  foo();  // endless recursion - whoops!
}

void foo2()
{
  char myBuffer[A_VERY_BIG_NUMBER];  // The stack can't hold that much.
}

class bigObj
{
  char myBuffer[A_VERY_BIG_NUMBER];  
}

void foo2( bigObj big1)  // pass by value of a big object - whoops!
{
}

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

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

Самое простое: -

void foo()
{ 
  char message[10];

  message[10] = '!';  // whoops! beyond end of array
}

Also agreed that the 0x000002 is suspect. It is about the only constant between crashes. Even weirder is the fact that this only cropped up with the multi-threaded switch. I think that the smaller stack as a result of the multiple-threads is what's making this crop up now, but that's pure supposition on my part.

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

Возможно, вы сможете воспроизвести это, установив приложение на один процессор. Я не знаю, как вы это делаете со Sparc.

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

  • В pthreads вы должны выделить стек и передать его потоку. Вы выделили достаточно? Нет автоматического увеличения стека, как в однопоточном процессе.
  • Если вы уверены, что не повредите стек, выполнив предыдущую проверку выделенных данных стека для указателей румян (в основном неинициализированных указателей).
  • Один из потоков может перезаписать некоторые данные, от которых зависят другие (проверьте синхронизацию данных).
  • Отладка здесь обычно не очень помогает. Я бы попытался создать много вывода журнала (трассировки для входа и выхода каждого вызова функции / метода), а затем проанализировать журнал.
  • Может помочь тот факт, что в Linux ошибка проявляется по-разному. Какое отображение потоков вы используете в Solaris? Убедитесь, что вы сопоставили каждый поток с его собственным LWP, чтобы упростить отладку.

Другие вопросы по теме