65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 的黑暗角落和陷阱

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2019年8月14日

GPL3

20分钟阅读

viewsIcon

29460

downloadIcon

428

C++:爱与情趣

引言

C++ 是一种非常强大且通用的工具,但你必须为此付出代价。

正如 Bjarne Stroustrup 曾说

"C 语言让你很容易自找麻烦;C++ 让你更难,但一旦你自找麻烦,它会炸掉你整条腿"。

本文将教你如何以你能想象到的最有趣、最不可预测、最刺激的方式,彻底炸掉你的所有腿(手臂、头部和其他部位)!

背景

在本文中,我们希望展示理解编写稳定、安全和可靠代码的重要性,以及如何轻松地无意中注入漏洞。我们希望这对你来说既有趣又有用。

开始编码!

这里是一个抽象 C++ 代码的短片段。如你所见,这是来自 Windows DLL 的代码(这一点非常重要!)。假设有人希望在某个(当然是安全的!)解决方案中使用这段代码。

花点时间看看。谁知道你能在这里找到什么?这段代码中可能出了什么问题?

// Singleton
class Finalizer
{
    struct Data
    {
        int i = 0;
        char* c = nullptr;
        
        union U
        {
            long double d;
            
            int i[sizeof(d) / sizeof(int)];
            
            char c [sizeof(i)];
        } u = {};
        
        time_t time;
    };
    
    struct DataNew;
    DataNew* data2 = nullptr;
    
    typedef DataNew* (*SpawnDataNewFunc)();
    SpawnDataNewFunc spawnDataNewFunc = nullptr;
    
    typedef Data* (*Func)();
    Func func = nullptr;
    
    Finalizer()
    {
        func = GetProcAddress(OTHER_LIB, "func")
        
        auto data = func();
        
        auto str = data->c;
        
        memset(str, 0, sizeof(str));
        
        data->u.d = 123456.789;
        
        const int i0 = data->u.i[sizeof(long double) - 1U];
        
        spawnDataNewFunc = GetProcAddress(OTHER_LIB, "SpawnDataNewFunc")
        data2 = spawnDataNewFunc();
    }
    
    ~Finalizer()
    {
        auto data = func();
        
        delete[] data2;
    }
};

Finalizer FINALIZER;

HMODULE OTHER_LIB;
std::vector<int>* INTEGERS;

DWORD WINAPI Init(LPVOID lpParam)
{
    OleInitialize(nullptr);
    
    ExitThread(0U);
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    static std::vector<std::thread::id> THREADS;
    
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            CoInitializeEx(nullptr, COINIT_MULTITHREADED);
            
            srand(time(nullptr));
            
            OTHER_LIB = LoadLibrary("B.dll");
            
            if (OTHER_LIB = nullptr)
                return FALSE;
            
            CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);
        break;
        
        case DLL_PROCESS_DETACH:
            CoUninitialize();
            
            OleUninitialize();
            {
                free(INTEGERS);
                
                const BOOL result = FreeLibrary(OTHER_LIB);
                
                if (!result)
                    throw new std::runtime_error("Required module was not loaded");
                
                return result;
            }
        break;
        
        case DLL_THREAD_ATTACH:
            THREADS.push_back(std::this_thread::get_id());
        break;
        
        case DLL_THREAD_DETACH:
            THREADS.pop_back();
        break;
    }
    return TRUE;
}

__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()
{
    for (int i : integers)
        i *= c;
    
    INTEGERS = new std::vector<int>(integers);
}

int Random()
{
    return rand() + rand();
}

__declspec(dllexport) long long int __cdecl _GetInt(int a)
{
    return 100 / a <= 0 ? a : a + 1 + Random();
}

你觉得这段代码非常简单、显而易见、绝对安全且没有麻烦吗?或者你在这里发现了一些问题?或者你甚至发现了一打或两打?

实际上,这段代码中存在超过 43(是的,四十三!)个不同严重程度的潜在威胁。

关注点

  1. sizeof(d)(其中 dlong double)不一定是 sizeof(int) 的倍数
    int i[sizeof(d) / sizeof(int)];

    这种情况下未在此处进行检查或处理。例如,在某些平台上,long double 的大小可能是10(对于 MS VS 编译器并非如此,但对于 RAD studio,前身为 C++ Builder 则是)。

    int 的大小也可能因平台而异(好吧,上面的代码是针对 Windows 的,所以具体到当前情况,问题有些牵强,但对于可移植代码,问题确实存在)。

    https://www.viva64.com/en/t/0012

    如果我们要在这里进行类型双关,所有这些都将成为问题。顺便说一句,根据 C++ 语言标准,类型双关会导致未定义行为(然而,这仍然是常见做法,因为现代编译器通常**会**为它定义正确的预期行为例如 GCC)。

    顺便说一句,在现代 C 语言中,类型双关是完全允许的(你确实明白 CC++ 是不同的语言,你不应该期望如果你懂 C++ 就懂 C,反之亦然,对吗?)

    解决方案:在编译时使用static_assert来控制所有这些假设。如果类型大小出现问题,这会警告你

    static_assert(0U == (sizeof(d) % sizeof(int)),
                  "Size of the bigger type is not multiple of sizes of the smaller type");
  2. time_t 是一个宏,在 Visual Studio 中,它可以指代 32 位(旧)或 64 位(新)整数类型
    time_t time;

    如果两个不同的二进制模块(例如,一个可执行文件和它加载的 DLL)使用该类型的不同物理表示进行编译,则访问它可能会导致越界读/写或类型切片(损坏内存或导致读取垃圾数据)。

    解决方案:确保使用相同的严格大小类型在所有通信模块之间共享数据

    int64_t time;
  3. B.dll(应该由 OTHER_LIB 句柄引用)在此刻**尚未**加载,因此我们尝试从中获取地址将失败。
  4. 静态初始化顺序问题OTHER_LIB 对象在使用,而它**尚未**初始化并包含垃圾数据)。
    func = GetProcAddress(OTHER_LIB, "func");

    FINALIZER 是一个静态对象,它在调用 DllMain 之前构造。所以在它的构造函数中,我们试图使用稍后加载的库。问题变得更糟,因为 FINALIZER 静态对象使用的 OTHER_LIB 静态对象在翻译单元中定义得更晚,这意味着它将更晚初始化(置零)。这意味着它将简单地包含一些伪随机垃圾数据。幸运的是 WinAPI 应该正确处理,因为很可能没有模块加载了这样的句柄值,即使存在——它也可能缺少“func”函数(但如果最终确实存在,那可就麻烦了……)

    解决方案:一般建议是完全避免使用全局对象,特别是复杂的对象,尤其是如果它们相互依赖,特别是在 DLL 中。但是,如果你出于某种原因仍然需要它们,请非常小心它们的初始化顺序。为了控制该顺序,请将**所有**全局对象实例(定义)按正确顺序放置在**一个**翻译单元中,以确保它们正确初始化。

  5. 先前返回的结果在使用前未进行检查
    auto data = func();

    func 是一个指向函数的指针。它应该指向 B.dll 中的函数。但是,因为我们在上一步中完全失败了所有事情,它将是nullptr。所以尝试解引用它将导致一些有趣且引人入胜的事情,例如访问冲突或一般保护错误等。

    解决方案:处理外部代码(在本例中为 WinAPI)时,**始终**检查所提供函数的返回结果。对于可靠和故障安全系统,即使这些函数存在严格的契约,此规则仍然有用。

  6. 如果使用不同的对齐/填充设置进行编译,则会产生垃圾数据
    auto str = data->c;

    如果 `Data` `struct`(用于在通信模块之间共享信息)在二进制模块中具有不同的物理表示,我们将遇到前面提到的访问冲突、一般保护错误段错误堆损坏等问题。或者我们将读取垃圾数据。具体结果取决于使用该内存的实际场景。所有这些都可能发生,因为 `struct` 本身缺乏明确的对齐/填充设置,因此如果这些通信模块在编译时具有不同的全局设置,我们就会遇到麻烦。

    解决方案:确保所有共享数据结构都具有严格、明确定义和清晰的物理表示(固定大小类型、对齐定义等)和/或通信二进制文件使用相同的对齐/填充设置进行编译。

    另请参阅

  7. 使用指针的大小而不是它指向的数组的大小
    memset(str, 0, sizeof(str));

    这通常是打字错误。但在处理静态多态或使用 auto 关键字时(尤其是在过度使用时),事情可能会变得复杂。我真的希望现代编译器已经足够智能,能够在编译阶段利用其内部静态代码分析能力来检测此类问题。

    解决方案

  8. 访问另一个字段时出现 UB,而不是已设置的字段
  9. 如果 long double 的大小在二进制模块之间不同,可能会发生越界访问
    const int i0 = data->u.i[sizeof(long double) - 1U];

    嗯,这在前面已经提到了,所以这里我们只是又发现了一个之前讨论过的问题。

    解决方案:**不要**访问另一个字段,除非你非常确定你的编译器能正确处理。确保共享对象的类型大小在所有通信模块中都相同。

    另请参阅

  10. 即使 B.dll 已正确加载,并且“func”函数已正确导出和定位,B.dll 仍会在此时卸载(因为 DllMain/DLL_PROCESS_DETACH 回调部分中调用了 FreeLibrary),因此我们将在此处崩溃
    auto data = func();

    可能,使用已销毁的多态对象调用成员函数,或从已卸载的动态库中调用函数,将导致纯虚函数调用

    解决方案:在应用程序中实现正确的终结例程,确保所有动态库完成工作并以正确的顺序卸载。避免在 DLL 中使用带有复杂逻辑的静态对象。避免在 DLL 最终退出其入口点(并开始销毁静态对象)后执行操作。

    了解 DLL 生命周期:

    ... 其他模块调用 LoadLibrary ...

    1. 库静态对象的构造(应仅包含非常简单的逻辑,自动调用
    2. DllMain -> DLL_PROCESS_ATTACH 回调事件(应仅包含非常简单的逻辑,自动调用

      [!!] 从现在开始,应用程序的其他线程可以开始调用

      DllMain -> DLL_THREAD_ATTACH/DLL_THREAD_DETACH 并行执行(自动调用,参见第 30 页注释)

      这些部分_可能_包含一些复杂的逻辑(例如每个线程的随机种子),但仍需注意

    3. 调用自定义初始化例程(由 DLL 开发者导出)。

      包含所有繁重的初始化工作,应由加载您的库的人手动调用

      [您的库现在和以后可以创建自己的线程]

      [..] 库执行其主要工作

    4. 调用自定义初始化例程(由 DLL 开发者导出)。

      包含所有繁重的终结工作,应由加载您的库的人手动调用。

      [在此之后,避免在您的库中执行任何操作,所有先前启动的库线程应在从该函数返回**之前**完成。]

      ... 其他模块调用 FreeLibrary ...

    5. DllMain -> DLL_PROCESS_DETACH应只包含非常简单的逻辑,自动调用
    6. 库静态对象的销毁(应只包含非常简单的逻辑,自动调用
  11. 删除不透明指针编译器需要知道完整的类型才能调用析构函数,因此通过不透明指针删除对象可能导致内存泄漏和其他问题)
  12. (假设 `DataNew` 的析构函数是 `virtual`)即使类已正确导出和导入,并且我们获得了关于它的完整信息,此时调用其析构函数仍然是一个问题——它很可能导致纯`virtual`函数调用(因为 `DataNew` 类型是从已卸载的 `B.dll` 导入的)。即使不是(`virtual`),我们这里也遇到了问题
  13. 如果 `DataNew` 类是一个抽象多态类型,并且它的基类有一个没有实现的纯`virtual`析构函数,那么无论如何都会发生纯`virtual`函数调用
  14. 如果使用 new 分配,使用 delete[] 删除,则为 UB
    delete[] data2;

    一般来说,在释放和删除从外部模块接收的对象时,应始终保持谨慎

    此外,删除对象后将指针置空是一种良好的做法。

    解决方案是确保

    • 当对象被删除时,其完整类型(由我们删除的指针指向)是已知的
    • 所有析构函数都有主体
    • 导出任何代码的库不会过早卸载
    • 始终使用正确形式的 newdelete
    • 指向已删除对象(或多个对象)的指针被置空

    此外请注意

    另请参阅

  15. ExitThreadC 代码中退出线程的首选方法。在 C++ 代码中,线程在任何析构函数被调用或任何其他自动清理被执行之前退出,因此你应该从你的线程函数中返回
    ExitThread(0U);

    解决方案:切勿在 C++ 代码中手动使用此函数,而是通过从线程函数正常退出(通过返回语句)来退出。

  16. 调用需要 Kernel32.dll 以外的 DLL 的函数可能会导致难以诊断的问题。调用 UserShellCOM 函数可能会导致访问冲突错误,因为某些函数会加载其他系统组件
    CoInitializeEx(nullptr, COINIT_MULTITHREADED);

    解决方案 - 在 DllMain 入口点

    • 避免任何复杂的(去)初始化
    • 避免调用其他库中的函数(或者至少要非常小心)
  17. 多线程环境中随机种子初始化不正确
  18. 由于 time 具有 1 秒的分辨率,程序中在该时间段内调用 time 的任何线程都将拥有相同的种子,这可能导致冲突(例如,生成相同的伪随机临时文件名、相同的端口号等)。可能的解决方案之一是使用一些其他伪随机值(例如任何堆栈或更好的堆对象的地址、更精确的时间等)来混淆(异或)种子的位。
    srand(time(nullptr));

    解决方案MS VS 要求种子应按线程初始化。此外,使用 Unix 时间作为种子不能提供足够的随机性请优先使用更高级的种子生成方式

    另请参阅

  19. 可能导致死锁或崩溃(或在 DLL 加载顺序中创建依赖循环)
    OTHER_LIB = LoadLibrary("B.dll");

    解决方案**不要**在 DllMain 入口点使用 LoadLibrary。任何复杂的(去)初始化都应在特定的导出函数中完成,例如“Init”和“Deint”。你的模块根据导入和导出模块之间建立的契约提供这些函数。双方都必须严格执行契约。

  20. 错别字(条件总是 false),程序逻辑不正确和可能的资源泄漏(因为 OTHER_LIB 如果加载成功则永不卸载)
    if (OTHER_LIB = nullptr)
        return FALSE;

    拷贝赋值运算符返回左值引用,即 if 将检查 OTHER_LIB 的值(这将是 nullptr),并且 nullptr 将被解释为 false

    解决方案:始终使用反向形式以避免此类错别字

    if/while (<constant> == <variable/expression>)
  21. 最好使用 _beginthread(特别是如果链接到静态 C 运行时库),否则在调用 ExitThreadDisableThreadLibraryCalls 时可能会出现内存泄漏。
  22. DLL 通知是序列化的,入口点函数(DllMain)**不**应尝试创建或与其他线程或进程通信(可能发生死锁
    CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);
  23. 在终止期间调用 COM 函数可能会导致访问冲突错误,因为相应的组件可能已被卸载或未初始化
    CoUninitialize();
  24. 无法控制进程内服务器的加载或卸载顺序,因此**不要**从 DllMain 函数中调用 OleInitializeOleUninitialize
    OleUninitialize();

    另请参阅

    COM 客户端和服务器

    进程内、进程外和远程服务器

  25. 对使用 new 分配的内存块调用 free
  26. 如果进程正在终止(lpvReserved 参数为非NULL),进程中除当前线程以外的所有线程要么已经退出,要么已被调用 ExitProcess 函数显式终止,这可能会使一些进程资源(例如)处于不一致状态,因此 DLL 清理资源是不安全的。相反,DLL 应该允许操作系统回收内存
    free(INTEGERS);

    解决方案:确保不将旧的 C 动态内存处理方式与现代 C++ 方式混合使用。在 DllMain 入口点管理资源时要非常小心。

  27. 可能导致 DLL 在系统执行其终止代码后被使用
    const BOOL result = FreeLibrary(OTHER_LIB);

    解决方案:**不要**在 DllMain 入口点调用 FreeLibrary

  28. 将使当前(可能是主)线程崩溃
    throw new std::runtime_error("Required module was not loaded");

    解决方案:尽量**不要**在 DllMain 入口点抛出异常。如果 DLL 因任何原因无法正确加载,它应该返回 FALSE。在 DLL_PROCESS_DETACH 期间抛出异常不仅是一种糟糕的设计方法(而且几乎毫无意义),还可能导致在反初始化阶段出现问题。

    无论如何,在 DLL 之外抛出异常时务必非常小心。某些情况下,任何复杂的对象(例如标准库的类)可能具有不同的物理表示(甚至逻辑),例如,如果两个二进制模块使用不同(不兼容)版本的运行时库进行编译。

    优先在模块之间交换简单数据类型(具有固定大小和确定表示)

    另外,请记住,退出或终止主线程将自动终止所有其他线程(这些线程将没有机会正确完成,因此它们可能会损坏内存,使互斥量、堆和其他对象处于不可预测、不一致的状态,而且在静态对象开始自身销毁时,这些线程已经死亡,因此不要尝试在此处等待线程)。

    另请参阅

  29. 可能抛出异常(例如std::bad_alloc),此处未捕获
    THREADS.push_back(std::this_thread::get_id());

    由于 DLL_THREAD_ATTACH 部分是从某些未知的外部代码调用的,因此不要期望此处有正确的行为。

    解决方案:将可能抛出异常且无法预期被正确处理的指令(特别是当它们超出 DLL 范围时)用 try/catch 块包围起来。

    另请参阅

  30. 如果在此 DLL 加载**之前**存在线程,则为 UB
    THREADS.pop_back();

    现有线程(包括实际加载 DLL 的线程)不调用新加载 DLL 的入口点函数(因此在 DLL_THREAD_ATTACH 事件期间它们未在 THREADS 向量中注册),而它们在完成时仍以 DLL_THREAD_DETACH 调用它。

    这意味着认为对 DLL_THREAD_ATTACHDLL_THREAD_DETACH 的调用次数总是相等的考虑是**错误**的,这使得任何依赖于它的逻辑都变得危险。

  31. 编译器相关的 int 大小,最好使用 C++11 固定大小整数
  32. 在模块之间传递复杂的对象(如果它们使用不同的运行时(发布/调试、不同版本等)编译,可能会导致崩溃)
  33. 通过其虚拟地址(在模块之间共享)访问对象 c 可能会导致问题,如果指针在这些模块中的处理方式不同(例如,如果模块链接时使用了不同的 [/LARGEADDRESSAWARE] 选项)
    __declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()

    另请参阅

    还有...

    最后...

    等等,我忘了什么吗?我肯定忘了! :)

    因为指针实际上比人们通常认为的要复杂得多。我非常确定你可以在评论中添加一些重要的东西(也许是一些关于对象指针和函数指针区别的东西,可能并非所有指针值中的位都可用于形成地址等等)。

    for (int i : integers)
    
        i *= c;

    错误:容器中的原始项不会改变,需要使用引用(最好使用两种类型的引用:12:)

  34. 异常可能会在函数内部抛出
    INTEGERS = new std::vector<int>(integers);

    然而,该函数的 throw 规范是空的

    __declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()

    当违反动态异常规范时,C++ 运行时会调用std::unexpected:从一个其异常规范禁止抛出此类型异常的函数中抛出异常。

    解决方案:使用 try/catch(特别是在分配资源时,尤其是在 DLL 中)或使用nothrow 形式。无论如何,不要期望无限资源

    另请参阅

    问题 1:形成这样一个“更随机”的值是不正确的。正如中心极限定理所述,独立随机变量的和趋向于正态分布(即使原始变量本身不是正态分布的)。

    问题 2:可能的整数溢出(对于有符号整数是 UB

    return rand() + rand();
    

    处理随机化、加密等事物时,请警惕使用一些自制的“解决方案”。如果你缺乏特定的数学教育和知识、对这些概念的丰富经验,那么你很可能会聪明反被聪明误,使事情变得更糟。

  35. 导出的函数名将被修饰(mangled),为防止这种情况,请使用extern "C"
  36. 以 '_' 开头的名称在 C++ 中是隐式禁止的,因为这种命名风格是为 STL 保留的
    __declspec(dllexport) long long int __cdecl _GetInt(int a)

    多种问题(及其可能的解决方案

  37. rand **不是**线程安全的,需要改用rand_r/rand_s
  38. rand 已过时,考虑使用现代 C++11 <random>
  39. rand 的种子可能尚未为该线程初始化(MS VS 需要按线程初始化)
  40. 不具备密码安全性请使用特定的操作系统 API 或一些可移植解决方案(Libsodium/randombytes_bufOpenSSL/RAND_bytes 等)
  41. 可能发生除以零:可能导致当前线程终止
  42. 运算符优先级(使用括号)和/或序列点
  43. 可能发生整数溢出
    return 100 / a <= 0 ? a : a + 1 + Random();

    另请参阅

    还有...

这还没完!我们还有更多有趣的(有问题的)代码等着你;)

想象一下你的内存中有一些重要内容(例如用户密码)。当然,你不想长时间将其保留在内存中(增加了有人从这里读取它的可能性)。

实现这一目标的简单方法会是这样

bool login(char* const userNameBuf, const size_t userNameBufSize,
           char* const pwdBuf, const size_t pwdBufSize) throw()
{
    if (nullptr == userNameBuf || '\0' == *userNameBuf || nullptr == pwdBuf)
        return false;
    
    // Here is some actual implementation, which does not checks params
    //  nor does it care of the 'userNameBuf' or 'pwdBuf' lifetime,
    //   while both of them obviously contains private information 
    const bool result = doLoginInternall(userNameBuf, pwdBuf);
    
    // We want to minimize the time this private information is stored within the memory
    memset(userNameBuf, 0, userNameBufSize);
    memset(pwdBuf, 0, pwdBufSize);
}

嗯,那当然**不会**奏效。那么,该怎么办呢?

错误的“解决方案” #1:如果 memset 不起作用,我们手动来做!

void clearMemory(char* const memBuf, const size_t memBufSize) throw()
{
    if (!memBuf || memBufSize < 1U)
        return;
    
    for (size_t idx = 0U; idx < memBufSize; ++idx)
        memBuf[idx] = '\0';
}

而且现代编译器完全没有理由不优化它

顺便说一句,如果启用,memset 函数将是编译器固有函数。这在当前上下文中不会改变任何东西,只是一个有趣的事实。

另请参阅

错误的“解决方案” #2:尝试通过使用volatile关键字来“改进”之前的“解决方案

void clearMemory(volatile char* const volatile memBuf, const volatile size_t memBufSize) throw()
{
    if (!memBuf || memBufSize < 1U)
        return;
    
    for (volatile size_t idx = 0U; idx < memBufSize; ++idx)
        memBuf[idx] = '\0';
    
    *(volatile char*)memBuf = *(volatile char*)memBuf;
    // There is also possibility for someone to remove this "useless" code in the future
}

这会奏效吗?嗯,可能会。大概吧。例如,这种方法在 MS VSRtlSecureZeroMemory 中使用(你可以在 Windows SDK 源代码中查看其实际实现)。然而,这严重依赖于编译器

另请参阅

错误的“解决方案” #3:尝试使用错误的 OS API(例如 RtlZeroMemory)甚至 STL(例如 std::fillstd::for_each)来代替 CRT 或自制代码

RtlZeroMemory(memBuf, memBufSize);

而且还有更多可能错误的解决方案!

最后,如何真正解决这个问题?

  1. 使用特定的 OS API 函数,例如 WindowsRtlSecureZeroMemory
  2. C11 函数memset_s也适用于此目的
引用

与 memset 不同,对 memset_s 函数的任何调用都应严格按照抽象机的规则进行评估。

此外,我们可以通过将变量值输出(到文件、控制台或其他流)来阻止编译器优化代码,但这种方法显然不太有用。

待续...

当然,这并不是你在使用 C/C++ 编写应用程序时可能遇到的所有问题的完整列表。

还有一些很棒的东西,比如活锁竞态条件(例如,由不正确的无阻塞算法实现、ABA 问题、同时不正确地更改多个原子变量、线程不安全的引用计数器、不正确的双重检查锁定模式实现等引起)、对象切片算术精度损失(例如,由于舍入或数值不稳定算法,例如,不对许多双精度浮点数进行排序就进行求和)、线程和 GDI 对象volatile vs atomic、不正确使用整数文字(603 vs 0603)、检查时间到使用时间、生命周期超出其引用捕获对象的 lambda、不正确的 printf 系列函数格式化程序、不同字节序的两个设备之间不正确地共享数据(例如,通过网络)、位域细节、混淆 C++ 异常和 SEH、执行不正确的堆栈分配、禁用ASLRAPI 中可能存在的后门、混淆sizeof_countof、**不**使用正确的内存锁定(还要注意笔记本电脑和某些台式电脑上的挂起模式会将系统 RAM 的副本保存到磁盘,一些架构惊喜,无论内存锁定如何)、堆栈损坏等等等等。

想添加更多?在评论中分享你自己的有趣资料!

想了解更多吗?

我们还想向您展示一些其他有用的外部链接。您可以参考这些资料来进一步扩展您的知识。感谢互联网上那些精彩的作者为我们带来了如此多令人兴奋的文章!

P. S.

当这篇文章实际完成并准备发表时,在网上搜索额外信息以添加到这里,发现了这条惊人的评论here

历史

2019年8月13日 - 增加了更多有用链接

© . All rights reserved.