0

我有 3 个文件在啊我有一个包装 std::string str 的#define ENABLE_STR,我仅在定义类 A 时启用此宏,但当我使用 A 时,它被遗漏了。

这是一种a.cpp认为有str成员但main.cpp不知道的情况。并且当程序运行时int istring 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;
}
  • 理想情况下,我会假设编译器/链接器会将其标记为错误,但事实并非如此。
  • 为什么 address sanitizer/valgrind 无法检测到这一点,因为这似乎是在写入不属于它的内存。
  • 除了不在标题中使用这样的宏之外,如何检测到这一点?
4

1 回答 1

2

理想情况下,我会假设编译器/链接器会将其标记为错误,但事实并非如此。

您正在为编译器提供不同翻译单元的同一类的不同类定义。这是未定义的行为,因此没有编译器必须诊断它。正如评论中所提到的,编译器开发人员除了编译器用户以这种方式弄乱他们的定义之外还有其他担忧。

在更技术层面上,每个翻译单元都会发出一个目标文件。目标文件由链接器链接在一起。但是目标文件对类一无所知,只知道函数。因此,它没有关于对象大小或成员偏移量的明确知识。

是否可以在目标文件中发出“编译器注释”来检测这一点?或者这可能是调试符号的一部分吗?是的,但它可能会引入显着的膨胀并增加编译时间。此外,您的库二进制文件不需要任何这些,因此在这种情况下它没有帮助。因此,在极少数用户搞砸的情况下,这将是一种不可靠的帮助,并且有很大的缺点。

为什么 address sanitizer/valgrind 无法检测到这一点,因为这似乎是在写入不属于它的内存。

我对 valgrind 的内部工作原理知之甚少,无法在这里给出一个好的答案,但大概假设是内部实际位置的str访问对 valgrind 来说似乎并没有立即产生怀疑,因为开始仍在分配的内存中. 如果你只有一个字符,小字符串优化也可能导致永远不会访问为.getiamainstrAgetmainAint

除了不在标题中使用这样的宏之外,如何检测到这一点?

以这种方式使用宏是一个可怕的想法——正是因为这些问题几乎无法检测到。您自己提到了一些工具,它们可能会迅速捕获“恶意”未定义行为(例如,在覆盖其控制结构后尝试管理内存的 std::vector),并且您可以配置您的操作系统和编译器以更严格地检测您的程序以当它做一些可疑的事情时得到通知(例如-fstack-protector-all在 gcc/Gs/RTCsMSVC 上,在较新的 Windows 上的这些安全功能等等)。

尽管如此,这仍然是我们正在谈论的未定义行为,因此没有保证找到问题的方法,主要是因为“当您尝试识别问题时一切都按预期工作”仍然在“一切都可能发生”的范围内。或者,换句话说,这些工具可能根本不存在任何症状,即使您的程序仍然存在细微的错误。

于 2018-06-07T13:54:23.257 回答