C ++:狡猾和爱,还是会出错?





“ C使轻松射击自己的脚变得容易。用C ++很难做到这一点,但是要花整整一条时间。”-C ++ Creator的BjörnStroustrup。


在本文中,我们将向您展示如何编写稳定,安全和可靠的代码,以及完全无意间将其完全破坏的容易程度。为此,我们试图收集最有用和最有趣的材料。







在SimbirSoft,我们与Secure Code Warrior项目紧密合作,以培训其他开发人员创建安全解决方案。特别是对于Habr,我们翻译了作者在CodeProject.com门户上写的一篇文章



所以要代码!



这是一小段抽象的C ++代码。编写此代码是为了演示可能在非常真实的项目中发现的各种问题和漏洞。如您所见,这是Windows DLL中的代码(这很重要)。假设有人将在某些(当然是安全的)解决方案中使用此代码。



仔细看一下代码。您认为这可能会出错吗?



代码
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)(其中d是长整数)不一定是sizeof(int)的倍数



int i[sizeof(d) / sizeof(int)];


这里没有测试或处理这种情况。例如,在某些平台上,长双精度字可能是10个字节(对于MS VS编译器而言并非如此,但对于RAD Studio(以前称为C ++ Builder)而言则为true )。



根据平台的不同,int的大小也可以不同(上面的代码是Windows的,因此,相对于这种特殊情况,该问题在某种程度上是人为造成的,但对于可移植的代码,此问题非常相关)。



如果我们要使用所谓的pun,所有这些都会成为问题。顺便说一句,它导致未定义的行为根据C ++语言标准。但是,通常使用type pun ,因为现代编译器通常会为给定情况定义正确的预期行为(例如,GCC会这样做)。







来源:Medium.com



顺便说一句,不像C ++,在现代C打字双关语是完全合法的(你知道,C ++和C是不同的语言,你应该希望了解C,如果你知道C ++和另一种方法,对吗?)



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



static_assert(0U == (sizeof(d) % sizeof(int)), “Houston, we have a problem”);


2)time_t是宏,在Visual Studio中它可以引用32位(旧)或64位(新)整数类型



time_t time;


如果两个二进制文件使用不同的此类物理表示形式进行编译,则从不同的可执行模块(例如,可执行文件及其加载的DLL)访问此类型的变量可能导致在对象范围之外进行读/写操作。反过来,这将导致内存损坏或垃圾读取。







解决方案:确保在所有模块之间使用严格定义大小的相同类型进行数据交换:



int64_t time;


3)当我们访问上述变量时,尚未加载B.dll(句柄由OTHER_LIB变量存储,因此我们无法获取该库的功能地址 4)静态对象(SIOF的初始化顺序问题:(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;


如果数据结构(用于在通信模块之间交换信息)在不同的物理表示形式中具有这些相同的模块,则将导致所有前面提到的访问冲突错误存储器保护故障分段堆损坏等。否则我们就读垃圾。确切结果将取决于此内存的实际使用情况。所有这些都可能发生,因为结构本身没有显式的对齐/填充设置。因此,如果在编译时这些全局设置对于交互模块而言是不同的,那么我们将遇到问题。







决定:确保所有共享数据结构都具有强大的,明确定义的和明显的物理表示形式(使用固定大小类型,明确指定的对齐方式等)和/或具有相同全局对齐方式设置的可互操作二进制文件/ 填充。



也可以看看
Alignment (C++ Declarations)

Data structure alignment

Struct padding in C++



7)使用指向数组的指针的大小而不是数组本身的大小



memset(str, 0, sizeof(str));


这通常是拼写错误的结果。但是,当处理静态多态性或不加思索地使用auto关键字尤其是当它明显被过度使用时),也可能会出现此问题但是,人们希望,现代的编译器足够聪明,可以使用内部静态分析器的功能在编译时检测到此类问题



决定:



  • 永远不要混淆sizeof(<完整对象类型>)和sizeof(<对象指针类型>);
  • 不要忽略编译器警告;

  • 您还可以通过组合typeid,constexpr和static_assert使用一点C ++样板魔术,以确保类型在编译时是正确的(类型特征在这里也很有用,尤其是std :: is_pointer)。


8)尝试读取不同于先前用于设置值的联合字段时的未定义行为



9)如果二进制模块之间的long double大小不同,则可以尝试从有效存储区中读出



const int i0 = data->u.i[sizeof(long double) - 1U];


前面已经提到了这一点,因此在这里我们仅提到了前面提到的问题。



解决方案:除非确定编译器正确处理了该字段,否则不要引用您之前设置的字段。确保所有交互模块之间共享对象类型的大小相同。



也可以看看
Type-punning and strict-aliasing

What is the Strict Aliasing Rule and Why do we care?



10)即使B.dll已正确加载并且“ func”函数已正确导出和导入,但此时B.dll仍从内存中卸载(因为FreeLibrary系统函数先前已在DllMain回调函数的DLL_PROCESS_DETACH部分中调用)



auto data = func();


在先前破坏的多态类型的对象上调用虚拟函数,以及在已经卸载的动态库中调用函数,可能会导致纯虚拟调用错误



解决方案:在应用程序中实施正确的完成过程,以确保所有动态库均以正确的顺序完成/卸载。避免在DL L中使用具有复杂逻辑的静态对象。避免在调用DllMain / DLL_PROCESS_DETACH之后(在库进入其生命周期的最后阶段-销毁其静态对象的阶段)在库中执行任何操作。



您需要了解DLL的生命周期是什么:



) LoadLibrary



  • ( , )
  • DllMain -> DLL_PROCESS_ATTACH ( , )
  • [] DllMain -> DLL_THREAD_ATTACH / DLL_THREAD_DETACH ( , . 30).
  • , , (, ),
  • ( / , , )
  • , ()
  • ( / , , )
  • - : ,


) FreeLibrary



  • DllMain -> DLL_PROCESS_DETACH ( , )
  • ( , )






11)删除不透明指针编译器需要知道完整的类型才能调用析构函数,因此使用不透明指针删除对象可能导致内存泄漏和其他问题)



12)如果DataNew析构函数是虚拟的,即使类已正确导出和导入且完整关于它的信息,无论如何在此阶段调用其析构函数都是一个问题-这可能会导致纯虚拟函数调用(因为DataNew类型是从已经卸载的B.dll文件导入的)。即使析构函数不是虚拟的,也可能出现此问题。



13)如果DataNew类是抽象多态类型,并且其基类具有不带主体的纯虚拟析构函数,无论如何都将发生纯虚拟函数调用。



14)如果使用new分配内存并使用delete []删除内存,则行为不确定



delete[] data2;


通常,释放从外部模块收到的对象时应始终小心将指向被破坏对象的指针清零



也是一个好习惯决定:







  • 删除对象时,必须知道其完整类型
  • 所有破坏者必须有一个身体
  • 从中导出代码的库不应过早卸载
  • 始终使用不同的新形式并正确删除它们,不要混淆它们
  • 指向远程对象的指针必须清零。






还要注意以下几点:

-呼吁指针无效删除会导致不确定的行为

纯属虚函数不应该从构造方法中调用

-调用构造一个虚拟函数不会是

-尽量避免手动内存管理-使用的容器移动语义,智能指针



也可以看看
Heap corruption: What could the cause be?



15)ExitThread是退出C语言中线程的首选方法。在C ++中,当调用此函数时,线程将在调用本地对象的析构函数之前终止(以及任何其他自动清除),因此在C ++中终止线程应简单地通过从线程函数返回来完成



ExitThread(0U);


解决方案:切勿在C ++代码中手动使用此功能。



16)在DllMain的正文中,调用需要Kernel32.dll以外的系统DLL的任何标准函数都可能导致各种难以诊断的问题



CoInitializeEx(nullptr, COINIT_MULTITHREADED);


DllMain中的解决方案:



  • 避免任何复杂的(de)初始化
  • 避免从其他库调用函数(或至少要非常小心)






17)在多线程环境中伪随机数生成器的错误初始化



18)由于time函数返回的时间的分辨率为1秒,因此在此时间段内调用此函数的程序中的任何线程都将在输出端收到相同的值。使用此数字初始化PRNG可能导致冲突(例如,为临时文件生成相同的伪随机名称,相同的端口号等)。一种可能的解决方案是将结果与某些伪随机值混合(异或,例如堆中任何堆栈或对象的地址,更准确的时间等。



srand(time(nullptr));


解决方案: MS VS要求每个线程都进行PRNG初始化另外,使用Unix时间作为初始化程序无法提供足够的熵,因此更高级的初始化值生成是首选



也可以看看
Is there an alternative to using time to seed a random number generation?

C++ seeding surprises

Getting random numbers in a thread-safe way [C#]


19)可能会死锁或崩溃(或以DLL加载顺序创建依赖关系循环



OTHER_LIB = LoadLibrary("B.dll");


解决方案: 不要在DllMain入口点使用LoadLibrary。任何复杂的(de)初始化都必须在某些DLL开发人员导出的函数中执行,例如“ Init”“ Deint”。该库向用户提供了这些功能,用户必须在正确的时间正确调用它们。双方都必须严格遵守该合同。







20)错字(条件始终为假),错误的程序逻辑和可能的资源泄漏(因为成功下载后永远不会卸载OTHER_LIB)



if (OTHER_LIB = nullptr)
    return FALSE;


赋值运算符通过复制返回左类型的链接,即 if将检查OTHER_LIB值(将为nullptr),并将nullptr解释为false。



解决方案:始终使用相反的形式来避免出现如下拼写错误:



if/while (<constant> == <variable/expression>)


21)建议使用_beginthread系统函数在应用程序中创建新线程(特别是如果应用程序与C运行时库的静态版本链接),否则在调用ExitThread,DisableThreadLibraryCalls时可能会发生内存泄漏



22)对DllMain的所有外部调用都已序列化,因此在主体中此函数不应尝试创建线程/进程或与其进行交互,否则可能会发生死锁



CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);


23)在DLL终止期间调用COM函数可能会导致错误的内存访问,因为相应的组件可能已经被卸载



CoUninitialize();


24)无法控制进程内COM / OLE服务的加载和卸载顺序,因此请勿从DllMain函数调用OleInitialize或OleUninitialize



OleUninitialize();


也可以看看
COM Clients and Servers

In-process, Out-of-process, and Remote Servers



25)释放为分配有新内存的内存块释放的空间



26)如果应用程序进程正在终止其工作(如lpvReserved参数的非零值所示),则该进程中的所有线程(当前线程除外)已经终止或在以下情况下被强制停止:调用ExitProcess函数,该函数可能会使某些进程资源(例如堆)处于不一致状态。结果,清理资源不是DLL安全的。而是,DLL应该允许操作系统回收内存。



free(INTEGERS);


解决方案:确保手动分配内存的旧C样式未与“新” C ++样式混合。DllMain函数中管理资源时要格外小心



27)即使在系统执行完退出代码后也可能导致DLL被使用



const BOOL result = FreeLibrary(OTHER_LIB);


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



28)当前(可能是主)线程将崩溃



throw new std::runtime_error("    ");


解决方案:避免在DllMain函数中引发异常。如果由于某种原因无法正确加载DLL,则该函数应仅返回FALSE。您也不应从DLL_PROCESS_DETACH部分引发异常。



在DLL之外引发异常时,请务必小心。如果使用复杂(不兼容)的运行时库版本对任何复杂对象(例如,标准库的)进行编译,则它们在不同的可执行模块中可以具有不同的物理表示形式(甚至是工作逻辑) 尝试仅在模块之间交换简单数据类型







(具有固定大小和定义明确的二进制表示形式)。



请记住,终止主线程会自动终止所有其他线程(这些线程不会正确终止,因此会损坏内存,使同步原语和其他对象处于不可预测和错误的状态。此外,这些线程在静态对象将开始自己的解构,因此请勿尝试等待任何线程在静态对象的析构函数中完成)。



也可以看看
Top 20 C++ multithreading mistakes and how to avoid them



29)您可以引发一个异常(例如std :: bad_alloc),该异常不在此处捕获



THREADS.push_back(std::this_thread::get_id());


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



解决方案:使用try / catch命令附加可能引发最有可能无法正确处理的异常的语句(特别是如果它们从DLL退出)。



也可以看看
How can I handle a destructor that fails?



30)UB(如果加载此DLL之前已提供流



THREADS.pop_back();


加载DLL时已经存在的线程(包括直接加载DLL的线程)不会调用已加载的DLL的入口点函数(这就是为什么它们在DLL_THREAD_ATTACH事件期间未向THREADS矢量注册的原因),而仍会通过DLL_THREAD_DETACH事件调用它完成后。

这意味着对DllMain函数的DLL_THREAD_ATTACH和DLL_THREAD_DETACH节的调用次数将不同。



31)最好使用固定大小的整数类型



32)如果使用不同的链接以及编译设置和标志(不同版本的运行时库等)进行编译,则在模块之间传递复杂的对象可能会崩溃。



33)如果在这些模块中对指针的处理方式不同(例如,如果模块与不同的LARGEADDRESSAWARE参数关联,则通过其虚拟地址(由模块共享)访问对象c可能会导致问题。



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


也可以看看
Is it possible to use more than 2 Gbytes of memory in a 32-bit program launched in the 64-bit Windows?

Application with LARGEADDRESSAWARE flag set getting less virtual memory

Drawbacks of using /LARGEADDRESSAWARE for 32 bit Windows executables?

how to check if exe is set as LARGEADDRESSAWARE [C#]

/LARGEADDRESSAWARE [Ru]

ASLR (Address Space Layout Randomization) [Ru]



和...
Virtual memory

Physical Address Extension

Tagged pointer

std::ptrdiff_t

What is uintptr_t data type

Pointer arithmetic

Pointer aliasing

What is the strict aliasing rule?

reinterpret_cast conversion

restrict type qualifier



上面的列表很难完成,因此您可以在注释中添加一些重要的内容。



实际上,使用指针比人们通常认为的要复杂得多。毫无疑问,经验丰富的开发人员将能够记住其他现有的细微差别和微妙之处(例如,关于对象的指针与函数的指针之间的区别的一些知识,因此,也许并非所有指针的位都可以使用,等等。 )。







34)的异常可以被抛出 函数内部



INTEGERS = new std::vector<int>(integers);


此函数的throw()规范为空:



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


当违反异常规范时,C ++运行时将调用std :: unexpant:从函数抛出异常,该函数的异常规范不允许这种类型的异常。



解决方案:使用try / catch(尤其是在分配资源时,尤其是在DLL中)或使用nothrow形式的新运算符。无论如何,永远不要从天真的假设开始,即所有分配各种资源的尝试都将始终成功结束



也可以看看
RAII

We do not use C++ exceptions

Memory Limits for Windows and Windows Server Releases









问题1:这样的“更随机”值的形成是不正确的。根据中心极限定理,独立随机变量的总和倾向于正态分布,而不是均匀分布(即使原始值本身是均匀分布的)。



问题2:可能出现整数类型溢出(对于有符号整数类型,这是未定义的行为



return rand() + rand();


在使用伪随机数生成器,加密等方法时,请始终注意不要使用自制的“解决方案”。除非您在这些高度特定的领域具有专门的教育和经验,否则,您很有可能会超越自己,使情况变得更糟。



35)导出函数的名称将进行修饰(更改)以防止使用外部“ 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)有特殊的伪随机数生成器,最好将其用于抗黑客攻击的解决方案(它们很适合便携式解决方案,如Libsodium / randombytes_bufOpenSSL / RAND_bytes等)



。41)可能被零除:可能导致当前线程崩溃



42)在同一行使用具有不同优先级的运算符,这在计算顺序中引入了混乱—使用括号和/或顺序点指定明显的计算顺序



43)潜在的整数溢出



return 100 / a <= 0 ? a : a + 1 + Random();




也可以看看
Do not use std::rand() for generating pseudorandom numbers





和...
ExitThread function

ExitProcess function

TerminateThread function

TerminateProcess function





不仅如此!



想象一下,您的内存中有一些重要内容(例如,用户密码)。当然,您不希望将其保留在内存中的时间超过实际需要的时间,因此会增加有人从那里读取它的可能性



解决这个问题的幼稚方法看起来像这样:



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 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的功能,如果仍然使用它,将最有可能被内置)。



也可以看看
The as-if rule

Are there situations where this rule does not apply?

Copy elision

Atomics and optimization



错误的“解决方案”#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
}


这样行吗?也许。例如,在RtlSecureZeroMemory中使用了这种方法(通过查看Windows SDK源代码中此功能的实际实现,您可以自己看到它)。



但是,该技术无法在所有编译器上正常工作



也可以看看
volatile member functions



错误的“解决方案”#3:使用不合适的OS API函数(例如RtlZeroMemory)或STL(例如std :: fill,std :: for_each)



RtlZeroMemory(memBuf, memBufSize);


尝试更多的例子来解决这个问题在这里



那怎么回事?



  • 使用正确的OS API函数,例如Windows的RtlSecureZeroMemory
  • 使用C11 memset_s函数


另外,我们可以通过将变量的值打印(到文件,控制台或其他流中)来防止编译器优化代码,但这显然不是很有用。



也可以看看
Safe clearing of private Data



加起来



当然,这并不是用C / C ++编写应用程序时可能遇到的所有可能出现的问题,细微差别和微妙之处的完整列表



还有一些很棒的事情,例如:





以及更多。







有什么要补充的吗?在评论中分享您的有趣经验!



PS想了解更多?
Software security errors

Common weakness enumeration

Common types of software vulnerabilities



Vulnerability database

Vulnerability notes database

National vulnerability database



Coding standards

Application security verification standard

Guidelines for the use of the C++ language in critical systems



Secure programming HOWTO

32 OpenMP Traps For C++ Developers

A Collection of Examples of 64-bit Errors in Real Programs




All Articles