I dedicate this article to the user f0b0s , which constantly monitors our activity, accompanying it with a subtle humor that keeps us in good shape.Readers of our articles on the development of 64-bit applications often reproach us for the lack of validity of the problems described. Namely, that we do not give examples of errors in real applications.
I decided to collect examples of various types of errors that we found ourselves in real programs that were read on the Internet or reported to us by PVS-Studio users. So, I bring to your attention the article, which is a collection of 30 examples of 64-bit errors in C and C ++.
')
Continuation of the article >>Introduction
Our company OOO “Program Verification Systems” develops a specialized static analyzer Viva64 that detects
64-bit errors in the code of C / C ++ applications. In the course of this work, our collection of examples of 64-bit defects is constantly updated, and we decided to collect the most interesting errors in our article in this article. The article provides examples both taken directly from the code of real applications, and compiled synthetically based on the real code, since they are too “stretched” in it.
The article only demonstrates various types of 64-bit errors and does not describe methods for their detection and prevention. You can get acquainted with the methods of diagnosing and fixing defects in 64-bit programs by referring to the following resources:
- Course on developing 64-bit C / C ++ applications [1];
- What is size_t and ptrdiff_t [2];
- 20 issues of porting C ++ code on a 64-bit platform [3];
- Tutorial on PVS-Studio [4];
- 64-bit horse that can count [5].
You can also get acquainted with the demo version of the
PVS-Studio tool, which includes the static code analyzer Viva64, which reveals almost all the errors described in the article. The demo version is available for download at:
http://www.viva64.com/en/pvs-studio/download/ .
Example 1. Buffer overflow
struct STRUCT_1
{
int * a;
};
struct STRUCT_2
{
int x;
};
...
STRUCT_1 Abcd;
STRUCT_2 Qwer;
memset (& Abcd, 0, sizeof (Abcd));
memset (& Qwer, 0, sizeof (Abcd));
The program declared two objects of the type STRUCT_1 and STRUCT_2, which must be cleared before use (initialize all fields with zeros). Implementing initialization, the programmer decided to copy a similar line and replaced it with "& Abcd" with "& Qwer". But he forgot to replace “sizeof (Abcd)” with “sizeof (Qwer).” By luck, the size of STRUCT_1 and STRUCT_2 structures coincided in a 32-bit system and the code worked correctly for a long time.
When transferring code to a 64-bit system, the size of the Abcd structure increased and, as a consequence, a buffer overflow error occurred (see Figure 1).

Figure 1 - Schematic explanation of an example buffer overflow
Such an error can be difficult to identify, if this corrupts data used much later.
Example 2. Unnecessary type conversions
char * buffer;
char * curr_pos;
int length;
...
while ((* (curr_pos ++)! = 0x0a) &&
((UINT) curr_pos - (UINT) buffer <(UINT) length));
The code is bad, but it is a real code. Its task is to find the end of the line indicated by the symbol 0x0A. The code will not work with strings longer than INT_MAX characters, since the variable length is of type int. However, we are interested in another error, so we will assume that the program works with a small buffer and the use of the int type is correct.
The problem is that in a 64-bit system, the buffer and curr_pos pointers may lie outside the first 4 gigabytes of address space. In this case, explicitly casting pointers to the UINT type will discard significant bits, and the algorithm will fail (see Figure 2).

Figure 2 - Incorrect calculations when searching for a terminal symbol
The error is unpleasant because the code can work correctly for a long time while the memory for the buffer is allocated in the lower four gigabytes of the address space. The correction of the error is to remove completely unnecessary explicit type conversions:
while (curr_pos - buffer <length && * curr_pos! = '\ r')
curr_pos ++;
Example 3. Incorrect #ifdef
Often in programs with a long history, you can find code sections wrapped in the #ifdef - - # else - #endif construction. When migrating programs to a new architecture, incorrectly written conditions can lead to the compilation of the wrong code fragments, as planned by developers in the past (see Figure 3). Example:
#ifdef _WIN32 // Win32 code
cout << "This is Win32" << endl;
#else // Win16 code
cout << "This is Win16" << endl;
#endif
// Alternative incorrect option:
#ifdef _WIN16 // Win16 code
cout << "This is Win16" << endl;
#else // Win32 code
cout << "This is Win32" << endl;
#endif

Figure 3 - Two options are too few
Relying on the #else option in such situations is dangerous. It is better to explicitly consider the behavior for each case (see Figure 4), and in the #else branch put a compilation error message:
#if defined _M_X64 // Win64 code (Intel 64)
cout << "This is Win64" << endl;
#elif defined _WIN32 // Win32 code
cout << "This is Win32" << endl;
#elif defined _WIN16 // Win16 code
cout << "This is Win16" << endl;
#else
static_assert (false, "Unknown platform");
#endif

Figure 4 - All possible compilation paths are checked.
Example 4. Confusion with int and int *
In old programs, especially in C, code fragments are not rare, where the pointer is stored in the int type. However, sometimes this is not done intentionally, but rather out of carelessness. Consider an example containing confusion caused by the use of the int type and a pointer to the int type:
int GlobalInt = 1;
void GetValue (int ** x)
{
* x = & GlobalInt;
}
void SetValue (int * x)
{
GlobalInt = * x;
}
...
int XX;
GetValue ((int **) & XX);
SetValue ((int *) XX);
In this example, the variable XX is used as a buffer for storing the pointer. This code will work correctly in those 32-bit systems where the pointer size coincides with the size of the int type. In a 64-bit system, this code is incorrect and the call
GetValue ((int **) & XX);
will result in corruption of 4 bytes of memory next to the variable XX (see Figure 5).

Figure 5 - Memory corruption near variable XX
The code above was written either by a novice or in a hurry. Moreover, explicit type conversions show that the compiler resisted to the last, hinting to the developer that a pointer and an int are different entities. However, brute force won.
Error correction is elementary and consists in choosing the right type for variable XX. At the same time, an explicit type conversion is no longer necessary:
int * XX;
GetValue (& XX);
SetValue (XX);
Example 5. Using obsolete features
A number of API functions, although left for compatibility, is a danger when developing 64-bit applications. A classic example is the use of functions such as SetWindowLong and GetWindowLong. In programs you can find code similar to the following:
SetWindowLong (window, 0, (LONG) this);
...
Win32Window * this_window = (Win32Window *) GetWindowLong (window, 0);
The programmer who once wrote this code has nothing to reproach. In the course of development, 5-10 years ago, the programmer, drawing on his experience and MSDN, composed the code completely correct from the point of view of the 32-bit Windows system. The prototype of these functions is as follows:
LONG WINAPI SetWindowLong (HWND hWnd, int nIndex, LONG dwNewLong);
LONG WINAPI GetWindowLong (HWND hWnd, int nIndex);
The fact that the pointer is explicitly cast to the LONG type is also justified, since the size of the pointer and the LONG type match on
Win32 systems. But I think it is clear that when a program is recompiled in the 64-bit version, these type conversions can cause a crash or incorrect application operation.
The trouble of an error lies in its irregular or even extremely rare manifestation. An error will occur whether or not it depends on the area of ​​memory in which the object is created, pointed to by the “this” pointer. If an object is created in the lower 4 gigabytes of the address space, then a 64-bit program can function correctly. The error can unexpectedly manifest itself through a large period of time, when, due to memory allocation, objects will begin to be created beyond the first four gigabytes.
In a 64-bit system, the SetWindowLong / GetWindowLong functions can be used only if the program actually saves certain values ​​such as LONG, int, bool, and the like. If you need to work with pointers, then you should use advanced options:
SetWindowLongPtr / GetWindowLongPtr. Although, perhaps, it should be recommended to use new functions in any case in order not to provoke future errors in the future.
Examples with the SetWindowLong and GetWindowLong functions are classic and are given in almost all articles devoted to the development of 64-bit applications. However, it should be noted that business is not limited to these functions. Pay attention to: SetClassLong, GetClassLong, GetFileSize, EnumProcessModules, GlobalMemoryStatus (see Figure 6).

Figure 6 - Table with the names of some obsolete and modern features
Example 6. Clipping of values ​​with implicit type casting
Implicit conversions of type
size_t to unsigned types and similar ones are well diagnosed by compiler warnings. However, in large programs, such warnings can easily get lost. Consider an example similar to real code where the warning was ignored, since it seemed that nothing bad could happen with short lines.
bool Find (const ArrayOfStrings & arrStr)
{
ArrayOfStrings :: const_iterator it;
for (it = arrStr.begin (); it! = arrStr.end (); ++ it)
{
unsigned n = it-> find ("ABC"); // Truncation
if (n! = string :: npos)
return true;
}
return false;
};
The above function searches for the text “ABC” in the array of strings and returns true if at least one line contains the sequence “ABC”. When compiling the 64-bit version of the code, this function will always return true.
The constant "string :: npos" in a 64-bit system is set to 0xFFFFFFFFFFFFFFFF of type size_t. When placing this value in the variable “n” of the unsigned type, it is trimmed to 0xFFFFFFFF. As a result, the condition "n! = String :: npos" is always true, since 0xFFFFFFFFFFFFFFFF is not equal to 0xFFFFFFFF (see Figure 7).

Figure 7 - A schematic explanation of the circumcision error
The fix is ​​elementary, just listen to the compiler warnings:
for (auto it = arrStr.begin (); it! = arrStr.end (); ++ it)
{
auto n = it-> find ("ABC");
if (n! = string :: npos)
return true;
}
return false;
Example 7. Undeclared functions in C
Despite the years, programs or parts of programs written in the C language remain alive. The code of these programs is much more prone to 64-bit errors due to less strict type control rules in the C language.
In C, functions can be used without prior declaration. Let's analyze the related interesting example of a 64-bit error. To begin, consider the correct version of the code in which the allocation and use of three arrays of the size of each gigabyte takes place:
#include <stdlib.h>
void test ()
{
const size_t Gbyte = 1024 * 1024 * 1024;
size_t i;
char * pointers [3];
// Allocate
for (i = 0; i! = 3; ++ i)
Pointers [i] = (char *) malloc (Gbyte);
// Use
for (i = 0; i! = 3; ++ i)
Pointers [i] [0] = 1;
// Free
for (i = 0; i! = 3; ++ i)
free (pointers [i]);
}
This code will correctly allocate memory, write one in the first element of each array, and free the occupied memory. The code works perfectly on a 64-bit system.
Now remove or comment out the line "#include <stdlib.h>". The code will still be collected, but when the program starts it will crash. If the header file “stdlib.h” is not connected, the C compiler assumes that the malloc function will return the type int. The first two allocations are likely to be successful. On the third call, the malloc function returns the address of the array outside of the first 2 gigabytes. Since the compiler believes that the result of the function is of type int, it interprets the result incorrectly and stores in the Pointers array the incorrect pointer value.
Consider the assembler code generated by the Visual C ++ compiler for the 64-bit Debug version. The first is the correct code that will be generated when the declaration of the malloc function is present (the file “stdlib.h” is included):
Pointers [i] = (char *) malloc (Gbyte);
mov rcx, qword ptr [Gbyte]
call qword ptr [__imp_malloc (14000A518h)]
mov rcx, qword ptr [i]
mov qword ptr Pointers [rcx * 8], rax
Now consider the variant of incorrect code when the malloc function declaration is missing:
Pointers [i] = (char *) malloc (Gbyte);
mov rcx, qword ptr [Gbyte]
call malloc (1400011A6h)
cdqe
mov rcx, qword ptr [i]
mov qword ptr Pointers [rcx * 8], rax
Note the presence of a CDQE (Convert doubleword to quadword) instruction. The compiler considered that the result is contained in the eax register and expanded it to a 64-bit value in order to write to the Pointers array. Accordingly, the high bits of the rax register will be lost. Even if the address of the allocated memory lies within the first four gigabytes, in the case when the high bit of the eax register is 1, we still get an incorrect result. For example, the address 0x81000000 will turn into 0xFFFFFFFF81000000.
Example 8. Dinosaur remains in large and old programs
Large old software systems that have been developing for decades, are replete with a variety of atavisms and simply sections of code written using popular paradigms and styles of various years. In such systems, you can observe the evolution of the development of programming languages, when the oldest parts are written in the style of the C language, and in the most recent ones you can find complex patterns in the style of Alexandrescu.

Figure 8 - Dinosaur Excavations
There are atavisms associated with 64 bits. Rather, atavisms that impede the work of modern 64-bit code. Consider an example:
// beyond this, assume a programming error
#define MAX_ALLOCATION 0xc0000000
void * malloc_zone_calloc (malloc_zone_t * zone,
size_t num_items, size_t size)
{
void * ptr;
...
if (((unsigned) num_items> = MAX_ALLOCATION) ||
((unsigned) size> = MAX_ALLOCATION) ||
((long long) size * num_items> =
(long long) MAX_ALLOCATION))
{
fprintf (stderr,
"*** malloc_zone_calloc [% d]: arguments too large:% d,% d \ n",
getpid (), (unsigned) num_items, (unsigned) size);
return NULL;
}
ptr = zone-> calloc (zone, num_items, size);
...
return ptr;
}
First, the function code contains a check for the allowable size of the allocated memory, which is strange for a 64-bit system. And secondly, the diagnostic message issued will be incorrect, because if we ask to allocate memory for 4,400,000,000 elements, due to the explicit type conversion to unsigned, we will get a strange message about the impossibility of allocating memory for only 105,032,704 elements.
Example 9. Virtual Functions
One of the beautiful examples of 64-bit errors is the use of incorrect argument types in virtual function declarations. And usually it is not someone's carelessness, but simply an “accident” where there are no guilty ones, but there is an error. Consider the following situation.
From time immemorial, the MFC library has a class CWinApp, in which there is a WinHelp function:
class CWinApp {
...
virtual void WinHelp (DWORD dwData, UINT nCmd);
};
To show your own help in a user application, you had to override this function:
class CSampleApp: public CWinApp {
...
virtual void WinHelp (DWORD dwData, UINT nCmd);
};
And everything was fine until 64-bit systems appeared. MFC developers had to change the interface of the WinHelp function (and some other functions) as follows:
class CWinApp {
...
virtual void WinHelp (DWORD_PTR dwData, UINT nCmd);
};
In the 32-bit mode, the DWORD_PTR and DWORD types coincided, but in the 64-bit mode they are not. Naturally, the developers of a user application must also change the type to DWORD_PTR, but in order to do this, you need to know about it at the beginning. As a result, an error occurs in the 64-bit program, since the WinHelp function in the user class is not called (see Figure 9).

Figure 9 - Error related to virtual functions
Example 10. Magic numbers as parameters
The magic numbers contained in the body of the programs are bad style and provoke errors. As an example of magic numbers, 1024 and 768 can be given, which strictly indicate the size of the screen resolution. In the framework of this article, we are interested in those magic numbers that can lead to problems in a 64-bit application. The most common numbers dangerous for 64-bit programs are shown in the table in Figure 10.

Figure 10 - Magic numbers dangerous for 64-bit programs
Let us demonstrate an example of working with the CreateFileMapping function, encountered in one of the CAD systems:
HANDLE hFileMapping = CreateFileMapping (
(HANDLE) 0xFFFFFFFF,
Null
PAGE_READWRITE,
dwMaximumSizeHigh,
dwMaximumSizeLow,
name);
Instead of the correct reserved constant INVALID_HANDLE_VALUE, the number 0xFFFFFFFF is used. This is incorrect in the
Win64 program, where the constant INVALID_HANDLE_VALUE is 0xFFFFFFFFFFFFFFFF. The correct way to call a function is:
HANDLE hFileMapping = CreateFileMapping (
INVALID_HANDLE_VALUE,
Null
PAGE_READWRITE,
dwMaximumSizeHigh,
dwMaximumSizeLow,
name);
Note. Some believe that the value 0xFFFFFFFF when expanding to a pointer turns into 0xFFFFFFFFFFFFFFFF. This is not true. According to the rules of the C / C ++ language, the value 0xFFFFFFFF has the type “unsigned int”, since it cannot be represented by the type “int”. Accordingly, expanding to the 64-bit type, the value 0xFFFFFFFFu turns into 0x00000000FFFFFFFFu. But if we write (size_t) (- 1) like this, we will get the expected 0xFFFFFFFFFFFFFFFF. Here “int” first expands to “ptrdiff_t” and then turns into “size_t”.
Example 11. Magic constants denoting size
Another common mistake is to use magic numbers to set the size of an object. Consider an example of buffer allocation and zeroing:
size_t count = 500;
size_t * values ​​= new size_t [count];
// Only part of the buffer will be filled
memset (values, 0, count * 4);
In this case, in a 64-bit system, more memory is allocated than it is then filled with zero values ​​(see Figure 11). The error is in the assumption that the size of the size_t type is always four bytes.

Figure 11 - Filling only part of the array
The correct option is:
size_t count = 500;
size_t * values ​​= new size_t [count];
memset (values, 0, count * sizeof (values ​​[0]));
Similar errors can be found when calculating the size of allocated memory or data serialization.
Example 12. Stack Overflow
In many cases, a 64-bit program consumes more memory and stack. Allocating more memory in a heap is not dangerous, since this type of 64-bit memory is available many times more than 32-bit one. But an increase in the used stack memory can lead to its unexpected overflow (stack overflow).
The mechanism for using the stack is different in different operating systems and compilers. We will consider the peculiarity of using the stack in the code of Win64 applications built by the Visual C ++ compiler.
When developing
calling conventions (
calling conventions) in Win64 systems, they decided to put an end to the existence of various function call options. In Win32, there were a number of calling conventions: stdcall, cdecl, fastcall, thiscall, and so on. In Win64, there is only one native calling convention. Modifiers like __cdecl are ignored by the compiler.
The x86-64 calling convention is similar to the fastcall agreement in x86. In the x64 agreement, the first four integer arguments (from left to right) are transmitted in 64-bit registers chosen specifically for this purpose:
RCX: 1st integer argument
RDX: 2nd integer argument
R8: 3rd integer argument
R9: 4th integer argument
The remaining integer arguments are passed through the stack. The “this” pointer is considered an integer argument, so it is always placed in the RCX register. If floating-point values ​​are transmitted, the first four of them are transmitted in the XMM0-XMM3 registers, and the subsequent ones via the stack.
Although the arguments can be passed in registers, the compiler still reserves space for them in the stack, reducing the value of the RSP register (stack pointer). At a minimum, each function must reserve 32 bytes in the stack (four 64-bit values ​​corresponding to the registers RCX, RDX, R8, R9). This space in the stack makes it easy to save the contents of the registers passed to the function in the stack. The called function is not required to drop the input parameters passed through the registers to the stack, but reserving a place in the stack, if necessary, allows this. If more than four integer parameters are passed, the corresponding additional space is reserved in the stack.
The described feature leads to a significant increase in the rate of absorption of the stack. Even if the function has no parameters, 32 bytes will still be “bit off” from the stack, which are then not used at all. The meaning of using such an uneconomical mechanism is related to the unification and simplification of debugging.
Let's pay attention to one more moment. The RSP stack pointer must be aligned to 16 bytes before another function call. Thus, the total size of the used stack when calling a function
without parameters in the 64-bit code
is 48 bytes: 8 (return address) + 8 (alignment) + 32 (reserve for arguments).
is it so bad? Not. We should not forget that a larger number of registers available in the 64-bit compiler allow us to build a more efficient code and not to reserve memory in the stack for some local function variables. Thus, in some cases, the 64-bit version of the function uses a smaller stack than the 32-bit version. This issue and various examples are discussed in more detail in the article "
Reasons why 64-bit programs require more stack memory ."
It is impossible to predict whether a 64-bit program will consume more stack or less. Due to the fact that the Win64-program can use 2-3 times more stack memory, it is necessary to make secure and change the project setting, which is responsible for the size of the reserved stack. Select the Stack Reserve Size parameter in the project settings (/ STACK: reserve key) and increase the size of the reserved stack three times. The default size is 1 megabyte.
Example 13. Variable Argument Function and Buffer Overflow
Although using functions with a variable number of arguments, such as printf, scanf is considered a bad style in C ++, they are still widely used. These functions create many problems when transferring applications to other systems, including 64-bit systems. Consider an example:
int x;
char buf [9];
sprintf (buf, "% p", & x);
The code author did not take into account that the size of the pointer in the future may be more than 32 bits. As a result, on the 64-bit architecture, this code will cause a buffer overflow (see Figure 12). This error may well be attributed to the use of the magic number '9', but in a real application, a buffer overflow can occur without magic numbers.

Figure 12 - Buffer overflow when working with the sprintf function
Options for correcting this code are different. It is more rational to refactor code in order to get rid of the use of dangerous functions. For example, you can replace printf with cout, and sprintf with boost :: format or std :: stringstream.
Note. This recommendation is often criticized by developers under Linux, arguing that gcc checks that the format string matches the actual parameters passed, for example, to the printf function. And, therefore, using printf is safe. However, they forget that the format string can be transferred from another part of the program, loaded from resources. In other words, in a real program, the formatting string is rarely present explicitly in the code, and, accordingly, the compiler cannot check it. If the developer uses Visual Studio 2005/2008/2010, then he will not be able to receive a warning on the code of the form void * p = 0; printf ("% x", p); even using the / W4 and / Wall keys.Example 14. Function with a variable number of arguments and incorrect format
Often in programs you can find incorrect formatting lines when working with the printf function and other similar functions. Because of this, incorrect values ​​will be displayed, which, although it will not lead to a program crash, is, of course, an error:
const char * invalidFormat = "% u";
size_t value = SIZE_MAX;
// The wrong value will be printed.
printf (invalidFormat, value);
In other cases, an error in the format string will be critical. Consider an example based on the implementation of the UNDO / REDO subsystem in one of the programs:
// Here, the pointers were saved as a string
int * p1, * p2;
....
char str [128];
sprintf (str, "% X% X", p1, p2);
// In another function, this string
// processed as follows:
void foo (char * str)
{
int * p1, * p2;
sscanf (str, "% X% X", & p1, & p2);
// The result is an incorrect value of the p1 and p2 pointers.
...
}
The "% X" format is not intended for working with pointers, and as a result, such a code is incorrect from the point of view of 64-bit systems. In 32-bit systems, it is quite efficient, although not beautiful.
Example 15. Storing integer values ​​in double
We did not have to meet such a mistake ourselves. This error is probably rare, but very real.
The double type is 64-bit in size and is compatible with the IEEE-754 standard on 32-bit and 64-bit systems. Some programmers use the double type to store and work with integer types:
size_t a = size_t (-1);
double b = a;
--a;
--b;
size_t c = b; // x86: a == c
// x64: a! = c
This example can still be tried to justify on a 32-bit system, since the double type has 52 significant bits and is capable of storing a 32-bit integer value without loss. But if you try to save a 64-bit integer to double, the exact value may be lost (see Figure 13).

Figure 13 - Number of significant bits in size_t and double types
The second part of the article.