ОБНОВЛЕНИЕ 3: Отправлена проблема с Clang, поскольку теперь я уверен, что это ранее не зарегистрированная ошибка компилятора. (В системе отслеживания ошибок LLVM есть много похожих, но разных проблем.) Спасибо всем, кто искренне пытался помочь.
ОБНОВЛЕНИЕ 2: Оказывается, эта ошибка НЕ требует использования опции -n
(--nmagic
). Такие же сбои происходят с двоичными файлами, только что созданными без использования библиотеки C (опция -nostdlib
Clang). Таким образом, создание двоичных файлов NMAGIC, очевидно, не имеет никакого отношения к проблеме.
ОБНОВЛЕНИЕ: собственный компоновщик LLVM, LLD, поддерживает опцию -n
(--nmagic
), поэтому я установил ее и попробовал. Происходит точно такой же сегфолт. Поскольку собственный компоновщик LLVM поддерживает создание двоичных файлов NMAGIC, это настоятельно предполагает, что он должен работать и при использовании их компилятора (Clang). Я отправлю отчет об ошибке.
Исходное сообщение:
Я столкнулся с проблемой C++, при которой создание экземпляров некоторых объектов в программах, скомпилированных с помощью Clang, приводит к сбою. Заранее благодарю всех, кто поможет пролить свет на проблему.
Отладка предполагает, что Clang генерирует инструкции SSE movaps
для инициализации некоторых массивов символов, и именно эти инструкции в некоторых случаях вызывают сбои сегментов.
Я тестировал несколько систем Linux с компоновщиком binutils как с Clang 16, так и с Clang 17, и получил одинаковые результаты. Я не уверен, возникает ли такая же проблема в других операционных системах x86-64 или с другими компоновщиками. Проблема не возникает при использовании версий компилятора GCC вместо Clang.
Сегфолты возникают для некоторых объектов при следующем минимальном наборе условий:
-n
(--nmagic
). Все, что необходимо, — это собрать двоичный файл без стандартной библиотеки C (опция -nstdlib
для Clang). Я также должен отметить, что использование опции -fno-builtin
не имеет никакого значения.]Вот минимальный код для воспроизведения. Скомпилируйте следующее в системе Linux x86-64 [РЕДАКТИРОВАНИЕ: удалена ненужная опция компоновщика]:
$ clang -O1 -nostdlib -fno-stack-protector -static clang_segv.s clang_segv.cc -o clang_segv
clang_segv.cc:
struct SegV
{
void set(const char *s) { char *b = buf; while ( *s ) { *b++ = *s++; } *b = '\0'; }
char buf[128] = "";
char *cursor = buf; // needed for segfault
};
int
main()
{
SegV v;
v.set("aa");
return 0;
}
clang_segv.s
.intel_syntax noprefix
.global _start
_start:
xor rbp,rbp # Zero stack base pointer
xor r9,r9
pop rdi # Pop argc off stack -> rdi for 1st arg to main()
mov rsi,rsp # Argv @top of stack -> rsi for 2nd arg to main()
call main # Call main()... return result ends up in rax
xor r9,r9
mov rdi,rax # Move main()'s return to 1st argument for exit()
mov rax,231 # exit_group() syscall
syscall # Tell kernel to exit program
Этот пример представляет собой минимальный воспроизводитель, который я мог придумать, и он не похож на исходный код, в котором я заметил проблему, за исключением того, что оба имеют объекты с массивами символов. Изменение кода может замаскировать или демаскировать проблему, которая обычно предполагает ошибку кодирования, но я не могу найти ее в этом простом примере.
Мой отладчик, кажется, думает, что проблемы заключаются в инструкциях, которые Clang генерирует для инициализации массива символов 'buf': Изображение отладчика, предлагающее проблему с инструкциями movaps.
Мой отладчик сообщает, что Clang генерирует следующий код для инициализации char buf[128]
:
0x400171 xorps %xmm0,%xmm0
0x400174 movaps %xmm0,-0x10(%rsp)
0x400179 movaps %xmm0,-0x20(%rsp)
0x40017e movaps %xmm0,-0x30(%rsp)
0x400183 movaps %xmm0,-0x40(%rsp)
0x400188 movaps %xmm0,-0x50(%rsp)
0x40018d movaps %xmm0,-0x60(%rsp)
0x400192 movaps %xmm0,-0x70(%rsp)
0x400197 movaps %xmm0,-0x80(%rsp)
0x40019c lea -0x80(%rsp),%rax
и что segfault генерируется первой инструкцией movapps.
Очевидно, я ожидаю, что инициализация массива не приведет к сбою.
Не имеет значения, используется ли инициализация внутри класса, как я делаю здесь, в этом примере, или используется инициализация по списку инициализаторов. Оба метода страдают от одной и той же проблемы.
Я считаю, что проблема может заключаться в несоответствии того, как код, сгенерированный clang, считает, что члены объекта выровнены (должны быть) выровнены, и того, как эти члены на самом деле выровнены. Возможно, я ошибаюсь, но если я добавлю alignas(32) в структуру или массив символов, проблема исчезнет. Я не знаю, зачем мне выравнивать по 32 байтам. К моему удивлению, выравнивание по 16 байтам не маскирует проблему.
Проблема также исчезнет, если я просто скажу Clang не генерировать какие-либо инструкции SSE с помощью -mno-sse
, но я бы предпочел не потерять эти оптимизации. В этом случае Clang использует инструкции movq
для инициализации массива вместо movaps
.
Проблема также исчезнет, если я откажусь от инициализации элемента массива символов и сделаю это вручную в конструкторе, но, конечно, это менее эффективно.
На данный момент для меня это выглядит как ошибка компилятора. Или я просто неправильно его использую?
Спасибо!
@user12002570 user12002570 Я еще не сообщал о системе отслеживания ошибок Clang. Я хотел сначала привлечь к этому внимание других в качестве проверки здравомыслия, прежде чем сообщать.
Я предполагаю, что компилятор просто не знает, что вы собираетесь изменить настройки компоновщика, чтобы нарушить выравнивание структуры, поэтому предполагает, что все выровнено, хотя это не так.
@CarlE.Thompson Официальная поддержка по вопросам Clang — лучший способ сделать это, поскольку там вы напрямую будете задавать вопросы экспертам clang.
@AlanBirtles - Я думаю, что другой способ взглянуть на это заключается в том, что компилятор не должен делать предположений о выравнивании, над которыми он не имеет контроля.
@user12002570 user12002570 Спасибо, что предложили мне немедленно сообщить об ошибке LLVM. Мне следовало последовать вашему совету, поскольку остальные комментарии здесь бесполезны. Если вы хотите отправить это предложение в качестве ответа, я приму его, поскольку вы были единственным здесь, кто действительно помог.
Пожалуйста, не добавляйте решение к вопросу. Опубликуйте это как ответ ниже.
Связан с опцией компоновщика -n (--nmagic) для отключения выравнивания страниц разделов ELF.
Вопрос: Доктор, мне больно, когда я это делаю.
А. Пробовали ли вы этого не делать?
Clang (вполне разумно) предполагает, что имеет дело со стандартным поведением компоновщика, которое учитывает требуемое выравнивание объектов.
Когда вы используете специальный флаг, чтобы сказать компоновщику не делать этого, вы сами по себе.
На данный момент для меня это выглядит как ошибка компилятора.
Это не. Если вы сообщите об этом разработчикам Clang, они, скорее всего, закроют его как ошибку пользователя «не делайте этого».
Проблема также исчезнет, если я просто скажу Clang не генерировать какие-либо инструкции SSE с -mno-sse, но я бы не хотел терять эти оптимизации.
-nmagic
. Это подводит нас к вопросу: с какой стати вам это делать? Смотрите также http://xyproblem.infoP.S. Эта проблема не ограничивается инициализацией — Clang может развернуть любой случайный memset
небольшого фиксированного размера серией MOVAPS
. Таким образом, даже если вы «исправите» известные случаи этого сегодня, несущественные изменения в источнике могут привести к их повторному появлению завтра.
Можете ли вы определить, что вы подразумеваете под «стандартным поведением компоновщика»? Какой стандарт гласит, что использование опции компоновщика, поддерживаемой самим компоновщиком, ELF, Linux, ассемблером, другими компиляторами и т. д., запрещено?
«Выравнивание разделов» — стандартное поведение. Этот ответ stackoverflow.com/a/61352891/50617 дает некоторые подсказки о том, что такое файлы NMAGIC — этот формат устарел 40 лет назад.
Простое повторение фразы «стандартное поведение» на самом деле ничего не значит без реального стандарта, придающего ей смысл. Как вы думаете, почему использование этой опции компоновщика не разрешено стандартом (а это разрешено)? И вы ошибаетесь, когда предполагаете, что двоичные файлы NMAGIC устарели и больше не используются в Linux. Они полностью поддерживаются Linux, инструментами, форматом ELF и всем остальным, имеют различное применение и необходимы на некоторых платформах. Даже собственный компоновщик LLVM, lld, полностью поддерживает эту опцию. (И при использовании собственного компоновщика LLVM ошибка сегментации все равно происходит.)
РЕШЕНО: в _start стек уже выровнен, поэтому, удаляя 8 байтов в моем коде запуска, я фактически смещал его перед вызовом main(). ABI SysV x86-64 требует, чтобы стек был выровнен перед любыми вызовами, поэтому для Clang вполне разумно предположить, что стек выровнен определенным образом при входе в main(), и соответствующим образом сгенерировать код. Другими словами: это не ошибка. (И, опять же, это не имеет ничего общего с использованием опции компоновщика --nmagic
, которую вполне можно использовать.)
Пробовали ли вы сообщить/спросить об этом в официальной службе поддержки ошибок clang. Обычно они отвечают быстро.