Повреждение памяти из-за несоответствия #define в заголовке

У меня есть 3 файла. В a.h у меня есть #define ENABLE_STR, который обертывает std::string str, я включаю этот макрос только при определении класса A, но когда я использую A, он пропускается.

Это ситуация, когда a.cpp думает, что есть str участник, но main.cpp не знает об этом. И когда программа запускается, int i перезаписывается файлом string str. Ни AddressSanitizer, ни Valgind не обнаруживают это как недопустимый доступ к памяти.

// a.h
#pragma once 
#include <string>
class A
{
   public:
      A();
      std::string& get();
#ifdef ENABLE_STR
      std::string str;
#endif
      int i;
};

// a.cpp
#define ENABLE_STR
#include <iostream>
#include "a.h"

A::A():i(0){ }

std::string& A::get()
{
   std::cin >> str;
   return str;
}

//main.cpp
#include <iostream>
#include "a.h"

int main()
{
   A a;
   std::cout << a.get()  << "\n\n i:" << a.i << std::endl;
}
  • В идеале я бы предположил, что компилятор/компоновщик пометит это как ошибку, но это не так.
  • Почему адрес sanitizer/valgrind не может обнаружить это, так как это похоже на запись в память, которая ему не принадлежит.
  • Кроме того, что нельзя использовать такие макросы в заголовках, как это обнаружить?

person tejas    schedule 07.06.2018    source источник
comment
Кроме того, что нельзя использовать такие макросы в заголовках, как это обнаружить? - Проблема не в этом - использование #define в файле cpp убивает вас   -  person UKMonkey    schedule 07.06.2018
comment
Компиляция программы на C++ достаточно сложна без проверки того, что программисты не злоупотребляют препроцессором. Может быть более одного определения типа класса... Если такой объект с именем D определен более чем в одной единице перевода, то каждое определение D должно состоять из одной и той же последовательности токенов; и... Если определения D не удовлетворяют этим требованиям, то поведение не определено.. Этот абзац посвящен использованию включения для совместного использования определений классов. И последний бит возлагает на вас бремя ответственности, чтобы не злоупотреблять им.   -  person StoryTeller - Unslander Monica    schedule 07.06.2018
comment
А при запуске программы int I перезаписывается на string str немного непонятно. Каков ваш фактический и ожидаемый результат?   -  person Sean Monroe    schedule 07.06.2018
comment
Я чувствую, что это дубликат: stackoverflow.com/questions/4192170/   -  person UKMonkey    schedule 07.06.2018
comment
@StoryTeller Хорошо, что в стандарте упоминается, что это UB, но я ожидаю, что компилятор по крайней мере выставит здесь предупреждение.   -  person tejas    schedule 07.06.2018
comment
@UKMonkey Это упрощение проблемы. На самом деле это были совершенно разные библиотеки. Также вопрос не отвечает, как можно сузить этот вопрос.   -  person tejas    schedule 07.06.2018
comment
@acraig5075 исправил название заголовка. также я должен где-то #define макрос, этот пример просто упрощение.   -  person tejas    schedule 07.06.2018
comment
@tejas, вы пропустили часть, выделенную жирным шрифтом, в ответе, который используется в этой программе. Это более общее, чем просто библиотека   -  person UKMonkey    schedule 07.06.2018
comment
@tejas - С чего бы этого ожидать? Стандарт определяет это таким образом, чтобы, среди прочего, облегчить работу автора компилятора. Очевидное решение — не использовать подобные хаки. Кроме этого, вопрос, почему неупомянутый компилятор не хотел взорваться во время диагностики этого, в лучшем случае неясен.   -  person StoryTeller - Unslander Monica    schedule 07.06.2018
comment
Препроцессор C — это дьявол. Такое использование препроцессора C более чем плохо. Не делай этого.   -  person Eljay    schedule 07.06.2018
comment
@tejas I would expect the Compiler to at-least put up a warning here. Как так? Он не знает, что вы определили свой класс по-разному в двух разных единицах компиляции. Какова цель определения class A двумя разными способами? Используется ли он в двух разных программах? (Это было бы нормально).   -  person Paul Sanders    schedule 07.06.2018
comment
@PaulSanders Class A, например, предоставляется внешней библиотекой, которая, когда я ее использую, может не осознавать, что она скомпилирована с помощью #define.   -  person tejas    schedule 08.06.2018
comment
@tejas Это была бы одна странная библиотека. Я действительно не думаю, что люди кодируют таким образом.   -  person Paul Sanders    schedule 08.06.2018


Ответы (1)


В идеале я бы предположил, что компилятор/компоновщик пометит это как ошибку, но это не так.

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

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

Можно ли выдавать «комментарии компилятора» в объектных файлах, чтобы обнаружить это? Или это может быть частью символов отладки? Да, но это, вероятно, привело бы к значительному раздуванию и увеличению времени компиляции. Кроме того, не требуется, чтобы в бинарном файле вашей библиотеки было что-либо из этого, поэтому в этом случае это не поможет. Так что это будет ненадежная помощь в редком случае, когда пользователь запутается, со значительными недостатками.

Почему адрес sanitizer/valgrind не может обнаружить это, так как это похоже на запись в память, которая ему не принадлежит.

Я недостаточно знаю внутреннюю работу valgrind, чтобы дать здесь хороший ответ, но, по-видимому, str доступ, который get предполагает, что i на самом деле находится внутри a в main, не кажется сразу подозрительным для valgrind, потому что начало str все еще в пределах памяти, выделенной для A. Если вы get используете только один символ, оптимизация небольших строк также может привести к тому, что main никогда не будет обращаться к A за пределами тех первых нескольких байтов, зарезервированных для int.

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

Это ужасная идея использовать макросы таким образом - именно потому, что эти проблемы почти невозможно обнаружить. Вы сами упомянули некоторые инструменты, которые могут быстро обнаружить «вредоносное» неопределенное поведение (например, std::vector, пытающийся управлять памятью после того, как его структура управления была перезаписана), и вы можете настроить свою операционную систему и компилятор так, чтобы ваша программа более строго соответствовала получать уведомления (например, -fstack-protector-all в gcc, /Gs и /RTCs в MSVC, эти функции безопасности в более новых версиях Windows и т. д.), когда он делает что-то сомнительное.

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

person Max Langhof    schedule 07.06.2018
comment
Мое мнение о компиляторе заключалось в том, что в нем есть вся необходимая информация, и у людей обычно есть отладочные и выпускные сборки, его можно было бы включить, вероятно, только в отладку. иногда медлительность приемлема, когда мы отлаживаем сбой. Я возился с флагами gcc и обнаружил, что, хотя он не может обнаружить именно эту проблему, симптомы можно обнаружить с помощью флага защиты стека -fstack-protector-all. Не могли бы вы обновить свой ответ с помощью этого флага компилятора, и я приму его. - person tejas; 08.06.2018
comment
Я подробно остановился на диагностике этого по симптомам. - person Max Langhof; 08.06.2018