Ошибка в генерации кода Clang для инициализаторов членов в объектах на x86-64, когда выравнивание страницы отключено?

ОБНОВЛЕНИЕ 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.

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

  • генерация кода x86-64
  • Оптимизация включена (-O1 или выше)
  • [РЕДАКТИРОВАТЬ - Это было неправильно. Для сегментированных ошибок не требуется опция компоновщика -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.

Проблема также исчезнет, ​​если я откажусь от инициализации элемента массива символов и сделаю это вручную в конструкторе, но, конечно, это менее эффективно.

На данный момент для меня это выглядит как ошибка компилятора. Или я просто неправильно его использую?

Спасибо!

Пробовали ли вы сообщить/спросить об этом в официальной службе поддержки ошибок clang. Обычно они отвечают быстро.

user12002570 27.03.2024 04:21

@user12002570 user12002570 Я еще не сообщал о системе отслеживания ошибок Clang. Я хотел сначала привлечь к этому внимание других в качестве проверки здравомыслия, прежде чем сообщать.

Carl E. Thompson 27.03.2024 08:15

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

Alan Birtles 27.03.2024 08:25

@CarlE.Thompson Официальная поддержка по вопросам Clang — лучший способ сделать это, поскольку там вы напрямую будете задавать вопросы экспертам clang.

user12002570 27.03.2024 08:48

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

Carl E. Thompson 27.03.2024 09:17

@user12002570 user12002570 Спасибо, что предложили мне немедленно сообщить об ошибке LLVM. Мне следовало последовать вашему совету, поскольку остальные комментарии здесь бесполезны. Если вы хотите отправить это предложение в качестве ответа, я приму его, поскольку вы были единственным здесь, кто действительно помог.

Carl E. Thompson 28.03.2024 01:51

Пожалуйста, не добавляйте решение к вопросу. Опубликуйте это как ответ ниже.

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

Ответы 2

Связан с опцией компоновщика -n (--nmagic) для отключения выравнивания страниц разделов ELF.

Вопрос: Доктор, мне больно, когда я это делаю.
А. Пробовали ли вы этого не делать?

Clang (вполне разумно) предполагает, что имеет дело со стандартным поведением компоновщика, которое учитывает требуемое выравнивание объектов.

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

На данный момент для меня это выглядит как ошибка компилятора.

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

Проблема также исчезнет, ​​если я просто скажу Clang не генерировать какие-либо инструкции SSE с -mno-sse, но я бы не хотел терять эти оптимизации.

  1. Нет никакой гарантии, что другие предположения, сделанные Кланом, не приведут к сбою в другом месте.
  2. Как видите, оптимизации несовместимы с -nmagic. Это подводит нас к вопросу: с какой стати вам это делать? Смотрите также http://xyproblem.info

P.S. Эта проблема не ограничивается инициализацией — Clang может развернуть любой случайный memset небольшого фиксированного размера серией MOVAPS. Таким образом, даже если вы «исправите» известные случаи этого сегодня, несущественные изменения в источнике могут привести к их повторному появлению завтра.

Можете ли вы определить, что вы подразумеваете под «стандартным поведением компоновщика»? Какой стандарт гласит, что использование опции компоновщика, поддерживаемой самим компоновщиком, ELF, Linux, ассемблером, другими компиляторами и т. д., запрещено?

Carl E. Thompson 27.03.2024 21:06

«Выравнивание разделов» — стандартное поведение. Этот ответ stackoverflow.com/a/61352891/50617 дает некоторые подсказки о том, что такое файлы NMAGIC — этот формат устарел 40 лет назад.

Employed Russian 27.03.2024 21:14

Простое повторение фразы «стандартное поведение» на самом деле ничего не значит без реального стандарта, придающего ей смысл. Как вы думаете, почему использование этой опции компоновщика не разрешено стандартом (а это разрешено)? И вы ошибаетесь, когда предполагаете, что двоичные файлы NMAGIC устарели и больше не используются в Linux. Они полностью поддерживаются Linux, инструментами, форматом ELF и всем остальным, имеют различное применение и необходимы на некоторых платформах. Даже собственный компоновщик LLVM, lld, полностью поддерживает эту опцию. (И при использовании собственного компоновщика LLVM ошибка сегментации все равно происходит.)

Carl E. Thompson 27.03.2024 22:45
Ответ принят как подходящий

РЕШЕНО: в _start стек уже выровнен, поэтому, удаляя 8 байтов в моем коде запуска, я фактически смещал его перед вызовом main(). ABI SysV x86-64 требует, чтобы стек был выровнен перед любыми вызовами, поэтому для Clang вполне разумно предположить, что стек выровнен определенным образом при входе в main(), и соответствующим образом сгенерировать код. Другими словами: это не ошибка. (И, опять же, это не имеет ничего общего с использованием опции компоновщика --nmagic, которую вполне можно использовать.)

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