A collection of examples of 64 bit errors in real programs

990 views
876 views

Published on

A collection of 64-bit errors in real programs. Nice, fairly complete list.

Published in: Technology
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
990
On SlideShare
0
From Embeds
0
Number of Embeds
17
Actions
Shares
0
Downloads
15
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

A collection of examples of 64 bit errors in real programs

  1. 1. A Collection of Examples of 64-bit Errors in RealPrograms Abstract Introduction Example 1. Buffer overflow Example 2. Unnecessary type conversions Example 3. Incorrect #ifdefs Example 4. Confusion of int and int* Example 5. Using deprecated (obsolete) functions Example 6. Truncation of values at an implicit type conversion Example 7. Undefined functions in C Example 8. Remains of dinosaurs in large and old programs Example 9. Virtual functions Example 10. Magic constants as parameters Example 11. Magic constants denoting size Example 12. Stack overflow Example 13. A function with a variable number of arguments and buffer overflow Example 14. A function with a variable number of arguments and wrong format Example 15. Storing integer values in double Example 16. Address arithmetic. A + B != A - (-B) Example 17. Address arithmetic. Signed and unsigned types. Example 18. Address arithmetic. Overflows. Example 19. Changing an arrays type Example 20. Wrapping a pointer in a 32-bit type Example 21. Memsize-types in unions Example 22. An infinity loop Example 23. Bit operations and NOT operation Example 24. Bit operations, offsets Example 25. Bit operations and sign extension Example 26. Serialization and data exchange Example 27. Changes in type alignment Example 28. Type alignments and why you mustnt write sizeof(x) + sizeof(y) Example 29. Overloaded functions Example 30. Errors in 32-bit units working in WoW64 Summary ReferencesAbstractThis article is the most complete collection of examples of 64-bit errors in the C andC++ languages. The article is intended for Windows-application developers who useVisual C++, however, it will be useful for other programmers as well.Introduction
  2. 2. Our company OOO "Program Verification Systems" develops a special static analyzerViva64 that detects 64-bit errors in the code of C/C++ applications. During thisdevelopment process we constantly enlarge our collection of examples of 64-bitdefects, so we decided to gather the most interesting ones in this article. Here you willfind examples both taken directly from the code of real applications and composedsynthetically relying on real code since such errors are too "extended" throughout thenative code.The article only demonstrates various types of 64-bit errors and does not describemethods of detecting and preventing them. If you want to know how to diagnose and fixdefects in 64-bit programs, please see the following sources: Lessons on development of 64-bit C/C++ applications [1]; About size_t and ptrdiff_t [2]; 20 issues of porting C++ code on the 64-bit platform [3]; PVS-Studio Tutorial [4]; A 64-bit horse that can count [5].You may also try the demo version of the PVS-Studio tool that includes the Viva64static code analyzer which detects almost all the errors described in this article. Thedemo version of the tool can be downloaded here: http://www.viva64.com/pvs-studio/download/.Example 1. Buffer overflowstruct 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));In this program, two objects of the STRUCT_1 and STRUCT_2 types are defined whichmust be zeroed (all the fields must be initialized with nulls) before being used. Whileimplementing the initialization, the programmer decided to copy a similar line andreplaced "&Abcd" with "&Qwer" in it. But he forgot to replace "sizeof(Abcd)" with"sizeof(Qwer)". Due to mere luck, the sizes of the STRUCT_1 and STRUCT_2structures coincided on a 32-bit system and the code has been working correctly for a
  3. 3. long time.When porting the code on the 64-bit system, the size of the Abcd structure increasedand it resulted in a buffer overflow error (see Figure 1).Figure 1 - Schematic explanation of the buffer overflow exampleSuch an error is difficult to detect if the data which should be used much later getspoiled.Example 2. Unnecessary type conversionschar *buffer;char *curr_pos;int length;...while( (*(curr_pos++) != 0x0a) && ((UINT)curr_pos - (UINT)buffer < (UINT)length) );
  4. 4. This code is bad yet it is real. Its task is to search for the end of the line marked withthe 0x0A symbol. The code will not process lines longer than INT_MAX characterssince the length variable has the int type. But we are interested in another error, so letsassume that the program works with a small buffer and it is correct to use the int typehere.The problem is that the buffer and curr_pos pointers might lie outside the first 4 Gbytesof the address space in a 64-bit system. In this case, the explicit conversion of thepointers to the UINT type will throw away the significant bits and the algorithm will beviolated (see Figure 2).Figure 2 - Incorrect calculations when searching for the terminal symbolWhat is unpleasant about this error is that the code can work for a long time as long asbuffer memory is allocated within the first four Gbytes of the address space. To fix theerror, you should remove the type conversions which are absolutely unnecessary:while(curr_pos - buffer < length && *curr_pos != n) curr_pos++;Example 3. Incorrect #ifdefs
  5. 5. You may often see code fragments wrapped in #ifdef - -#else - #endif constructs inprograms with long history. When porting programs to the new architecture, theincorrectly written conditions might result in compilation of other code fragments thanthe developers intended before (see Figure 3). For example:#ifdef _WIN32 // Win32 code cout << "This is Win32" << endl;#else // Win16 code cout << "This is Win16" << endl;#endif//Alternative incorrect variant:#ifdef _WIN16 // Win16 code cout << "This is Win16" << endl;#else // Win32 code cout << "This is Win32" << endl;#endif
  6. 6. Figure 3 - Two variants - this is too littleIt is dangerous to rely on the #else variant in such cases. It is better to explicitly checkbehavior for each case (see Figure 4) and add a message about a compilation error intothe #else branch:#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
  7. 7. Figure 4 - All the possible compilation ways are checkedExample 4. Confusion of int and int*In obsolete programs, especially those written in C, you may often see code fragmentswhere a pointer is stored in the int type. However, sometimes it is done through lack ofattention rather than on purpose. Lets consider an example with confusion caused byusing the int type and a pointer to the int type:
  8. 8. 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 sample, the XX variable is used as a buffer to store the pointer. This code willwork correctly on those 32-bit systems where the size of the pointer coincides with theint types size. In a 64-bit system, this code is incorrect and the callGetValue((int **)&XX);will cause corruption of the 4 bytes of memory next to the XX variable (see Figure 5).
  9. 9. Figure 5 - Memory corruption near the XX variableThis code was being written either by a novice or in a hurry. The explicit typeconversions signal that the compiler was resisting the programmer till the last hinting tohim that the pointer and the int type are different entities. But crude force won.Correction of this error is elementary and lies in choosing an appropriate type for theXX variable. The explicit type conversion becomes no longer necessary:int *XX;GetValue(&XX);SetValue(XX);Example 5. Using deprecated (obsolete) functions
  10. 10. Some API-functions can be dangerous when developing 64-bit applications althoughthey were composed for compatibility purpose. The functions SetWindowLong andGetWindowLong are a typical example of these. You may often see the following codefragment in programs:SetWindowLong(window, 0, (LONG)this);...Win32Window* this_window = (Win32Window*)GetWindowLong(window, 0);You cannot reproach the programmer who once wrote this code for anything. During thedevelopment process, he created this code relying on his experience and MSDN five orten years ago and it is absolutely correct from the viewpoint of the 32-bit Windows. Theprototype of these functions looks as follows:LONG WINAPI SetWindowLong(HWND hWnd, int nIndex, LONG dwNewLong);LONG WINAPI GetWindowLong(HWND hWnd, int nIndex);The explicit conversion of the pointer to the LONG type is also justified since the sizesof the pointer and the LONG type coincide in Win32 systems. However, I think youunderstand that these type conversions might cause a crash or false behavior of theprogram after its being recompiled in the 64-bit version.What is unpleasant about this error is that it occurs irregularly or very rarely at all.Whether the error will reveal itself or not depends upon the area of memory where theobject is created referred to by the "this" pointer. If the object is created in the 4 leastsignificant Gbytes of the address space, the 64-bit program can work correctly. Theerror might occur unexpectedly in a long time when the objects will start to be createdoutside the first four Gbytes due to memory allocation.In a 64-bit system, you can use the SetWindowLong/GetWindowLong functions only ifthe program really saves some values of the LONG, int, bool types and the like. If youneed to work with pointers, you should use the following extended function versions:SetWindowLongPtr/GetWindowLongPtr. However, I should recommend you to use newfunctions anyway in order to avoid new errors in future.Examples with the SetWindowLong and GetWindowLong functions are classic and citedalmost in all the articles on 64-bit software development. But you should understandthat it is not only these functions that you must consider. Among other functions are:SetClassLong, GetClassLong, GetFileSize, EnumProcessModules,GlobalMemoryStatus (see Figure 6).
  11. 11. Figure 6 - A table with the names of some obsolete and contemporary functionsExample 6. Truncation of values at an implicit type conversionAn implicit conversion of the size_t type to the unsigned type and similar conversionsare easily diagnosed by the compilers warnings. But in large programs, such warningsmight be easily missed. Lets consider an example similar to real code where thewarning was ignored because it seemed to the programmer that nothing bad mighthappen when working with short strings.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 function searches for the text "ABC" in the array of strings and returns true if atleast one string contains the sequence "ABC". After recompilation of the 64-bit versionof the code, this function will always return true.The "string::npos" constant has value 0xFFFFFFFFFFFFFFFF of the size_t type in the64-bit system. When putting this value into the "n" variable of the unsigned type, it istruncated to 0xFFFFFFFF. As a result, the condition " n != string::npos" is always truesince 0xFFFFFFFFFFFFFFFF is not equal to 0xFFFFFFFF (see Figure 7).
  12. 12. Figure 7 - Schematic explanation of the value truncation errorThe correction of this error is elementary - you just should consider the compilerswarnings:for (auto it = arrStr.begin(); it != arrStr.end(); ++it){ auto n = it->find("ABC"); if (n != string::npos) return true;}return false;Example 7. Undefined functions in CDespite the passing years, programs or some of their parts written in C remain as largeas life. The code of these programs is much more subject to 64-bit errors because ofless strict rules of type checking in the C language.In C, you can use functions without preliminary declaration. Lets look at an interestingexample of a 64-bit error related to this feature. Lets first consider the correct versionof the code where allocation takes place and three arrays, one Gbyte each, are used:#include <stdlib.h>
  13. 13. 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 into the first item of each array andfree the occupied memory. The code is absolutely correct on a 64-bit system.Now lets remove or write a comment on the line "#include <stdlib.h>". The code will bestill compiled but the program will crash right after the launch. If the header file"stdlib.h" is not included in, the C compiler supposes that the malloc function will returnthe int type. The first two instances of memory allocation will be most likely successful.When the memory is being allocated for the third time, the malloc function will returnthe array address outside the first 2 Gbytes. Since the compiler supposes that thefunctions result has the int type, it will interpret the result incorrectly and save anincorrect value of the pointer in the Pointers array.Lets consider the assembler code generated by the Visual C++ compiler for the 64-bitDebug version. In the beginning, there is the correct code that will be generated whenthe definition of the malloc function is present (i.e. the "stdlib.h" file is included in):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],raxNow lets look at the incorrect code when the definition of the malloc function is absent:Pointers[i] = (char *)malloc(Gbyte);mov rcx,qword ptr [Gbyte]call malloc (1400011A6h)
  14. 14. cdqemov rcx,qword ptr [i]mov qword ptr Pointers[rcx*8],raxNote that there is the CDQE (Convert doubleword to quadword) instruction. Thecompiler supposes that the result is contained in the eax register and extends it to a 64-bit value in order to write it into the Pointers array. Correspondingly, the mostsignificant bits of the rax register will be lost. Even if the address of the allocatedmemory lies within the first four Gbytes, we will still get an incorrect result if the mostsignificant bit of the eax register equals 1. For instance, address 0x81000000 will turninto 0xFFFFFFFF81000000.Example 8. Remains of dinosaurs in large and old programsLarge old program systems that have been developing for tens of years are abound invarious atavisms and code fragments written with popular paradigms and styles ofdifferent years. In such systems, you can watch the evolution of programminglanguages when the oldest fragments are written in C and the freshest ones containcomplex templates of Alexandrescu style.Figure 8 - Dinosaur excavationsThere are atavisms referring to 64 bits as well. To be more exact, these are atavismsthat prevent contemporary 64-bit code from correct work. Consider an example:// beyond this, assume a programming error#define MAX_ALLOCATION 0xc0000000void *malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size){ void *ptr;
  15. 15. ... 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,%dn", getpid(), (unsigned)num_items, (unsigned)size); return NULL; } ptr = zone->calloc(zone, num_items, size); ... return ptr;}First, the functions code contains the check of accessible sizes of allocated memorywhich are strange for the 64-bit system. Second, the generated diagnostic message isincorrect because if we ask to allocate memory for 4 400 000 000 items, we will see astrange message saying that the program cannot allocate memory for (only) 105 032704 items. This happens because of the explicit type conversion to the unsigned type.Example 9. Virtual functionsOne of the nice examples of 64-bit errors is the use of wrong argument types indefinitions of virtual functions. Usually it is not ones mistake but just an "accident". It isnobodys fault but the error still remains. Consider the following case.For a very long time there has been the CWinApp class in the MFC library that has theWinHelp function:class CWinApp { ... virtual void WinHelp(DWORD dwData, UINT nCmd);};To show the programs own help in a user application you had to overrid this function:class CSampleApp : public CWinApp { ... virtual void WinHelp(DWORD dwData, UINT nCmd);};Everything was alright until 64-bit systems appeared. The MFC developers had tochange the interface of the WinHelp function (and some other functions as well) in thefollowing way:
  16. 16. class CWinApp { ... virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);};The DWORD_PTR and DWORD types coincided in the 32-bit mode but they do notcoincide in the 64-bit mode. Of course, the user applications developers must alsochange the type to DWORD_PTR but they have to learn about it somehow before doingthis. As a result, an error occurs in the 64-bit version since the WinHelp function cannotbe called in the user class (see Figure 9).Figure 9 - The error related to virtual functions
  17. 17. Example 10. Magic constants as parametersMagic numbers contained in bodies of programs provoke errors and using them is abad style. Such numbers are, for instance, numbers 1024 and 768 that strictly definescreen resolution. Within the scope of this article, we are interested in those magicnumbers that might cause issues in a 64-bit application. The most widely used magicnumbers dangerous for 64-bit programs are shown in the table in Figure 10.Figure 10 - Magic numbers dangerous for 64-bit programsConsider an example of working with the CreateFileMapping function taken from someCAD-system:HANDLE hFileMapping = CreateFileMapping( (HANDLE) 0xFFFFFFFF, NULL, PAGE_READWRITE, dwMaximumSizeHigh, dwMaximumSizeLow, name);Number 0xFFFFFFFF is used instead of the correct reserved constantINVALID_HANDLE_VALUE. It is incorrect from the viewpoint of a Win64-programwhere the INVALID_HANDLE_VALUE constant takes value 0xFFFFFFFFFFFFFFFF.Here is a correct way of calling the function:HANDLE hFileMapping = CreateFileMapping( INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, dwMaximumSizeHigh,
  18. 18. dwMaximumSizeLow, name);Note. Some people think that value 0xFFFFFFFF turns into 0xFFFFFFFFFFFFFFFFwhile extending to the pointer. It is not so. According to C/C++ rules, value0xFFFFFFFF has the "unsigned int" type since it cannot be represented with the "int"type. Correspondingly, value 0xFFFFFFFFu turns into 0x00000000FFFFFFFFu whenextending to the 64-bit type. But if you write (size_t)(-1), you will get the expected0xFFFFFFFFFFFFFFFF. Here "int" extends to "ptrdiff_t" first and then turns into"size_t".Example 11. Magic constants denoting sizeAnother frequent error is using magic constants to define an objects size. Consider anexample of buffer allocation and zeroing:size_t count = 500;size_t *values = new size_t[count];// Only a part of the buffer will be filledmemset(values, 0, count * 4);In this case, in the 64-bit system, the amount of memory being allocated is larger thanthe amount of memory which is filled with zero values then (see Figure 11) . The errorlies in the assumption that the size of the size_t type is always four bytes.
  19. 19. Figure 11 - Only a part of the array is filledThis is the correct code:size_t count = 500;size_t *values = new size_t[count];memset(values, 0, count * sizeof(values[0]));You may encounter similar errors when calculating sizes of memory being allocated ordata serialization.
  20. 20. Example 12. Stack overflowIn many cases, a 64-bit program consumes more memory and stack. Allocation ofmore physical memory is not dangerous since a 64-bit program can access muchlarger amounts of this type of memory than a 32-bit one. But increase of stack memoryconsumption might cause a stack overflow.The mechanism of using the stack differs in various operating systems and compilers.We will consider the specifics of using the stack in the code of Win64 applications builtwith the Visual C++ compiler.When developing calling conventions in Win64 systems, the developers decided tobring an end to different versions of function calls. In Win32, there were a lot of callingconventions: stdcall, cdecl, fastcall, thiscall and so on. In Win64, there is only one"native" calling convention. The compiler ignores modifiers like __cdecl.The calling convention on the x86-64 platform resembles the fastcall convention in x86.In the x64-convention, the first four integer arguments (left to right) are passed in 64-bitregisters used specially for this purpose:RCX: 1-st integer argumentRDX: 2-nd integer argumentR8: 3-rd integer argumentR9: 4-th integer argumentAll the rest integer arguments are passed through the stack. The "this" pointer isconsidered an integer argument so it is always put into the RCX register. If floating-point values are passed, the first four of them are passed in the XMM0-XMM3 registersand all the next are passed through the stack.Although arguments may be passed in registers, the compiler will still reserve space forthem in stack therefore reducing the value of the RSP register (stack pointer). Eachfunction must reserve at least 32 bytes (four 64-bit values corresponding to theregisters RCX, RDX, R8, R9) in the stack. This space in the stack lets you easily savethe contents of registers passed into the function in the stack. The function being calledis not required to drop input parameters passed through the registers into the stack butstack space reservation allows to do this if necessary. If more than four integerparameters are passed, the corresponding additional space is reserved in the stack.The described feature leads to a significant growth of stack consumption speed. Even ifthe function does not have parameters, 32 bytes will be "bit off" the stack all the sameand they will not be used anyhow then. The use of such a wasteful mechanism isdetermined by the purposes of unification and debugging simplification.
  21. 21. Consider one more thing. The stack pointer RSP must be aligned on a 16-byteboundary before the next call of the function. Thus, the total size of the stack beingused when calling a function without parameters in 64-bit code is 48 bytes: 8 (returnaddress) + 8 (alignment) + 32 (reserved space for arguments).Can everything be so bad? No. Do not forget that a larger number of registers availableto the 64-bit compiler allows it to build a more effective code and avoid reserving stackmemory for some local function variables. Thus, the 64-bit version of a function insome cases uses less stack memory than its 32-bit version. To learn more about thisquestion, see the article "The reasons why 64-bit programs require more stackmemory".It is impossible to predict if a 64-bit program will consume more or less stack memory.Since a Win64-program can use 2-3 times more stack memory, you should secureyourself and change the project option responsible for the size of stack being reserved.Choose the Stack Reserve Size (/STACK:reserve switch) parameter in the projectsettings and increase the size of stack being reserved three times. This size is 1 Mbyteby default.Example 13. A function with a variable number of arguments and bufferoverflowAlthough it is considered a bad style in C++ to use functions with a variable number ofarguments such as printf and scanf, they are still widely used. These functions provokea lot of problems while porting applications to other systems including 64-bit ones.Consider an example:int x;char buf[9];sprintf(buf, "%p", &x);The author of this code did not take into account that the pointers size might becomelarger than 32 bits in future. As a result, this code will cause a buffer overflow on the64-bit architecture (see Figure 12). This error might be referred to the type of errorscaused by magic numbers (number 9 in this case) but the buffer overflow can occurwithout magic numbers in a real application.
  22. 22. Figure 12 - A buffer overflow when working with the sprintf functionThere are several ways to correct this code. The most reasonable one is to factor thecode in order to get rid of dangerous functions. For example, you may replace printfwith cout and sprintf with boost::format or std::stringstream.Note. Linux-developers often criticize this recommendation arguing that gcc checks ifthe format string corresponds to actual parameters which are being passed, forinstance, into the printf function. Therefore it is safe to use the printf function. But theyforget that the format string can be passed from some other part of the program orloaded from resources. In other words, in a real program, the format string is seldompresent explicitly in the code and therefore the compiler cannot check it. But if thedeveloper uses Visual Studio 2005/2008/2010, he will not get a warning on the code like"void *p = 0; printf("%x", p);" even if he uses the /W4 and /Wall switches.Example 14. A function with a variable number of arguments and wrongformatYou may often see incorrect format strings in programs when working with the printffunction and other similar functions. Because of these you will get wrong output values.Although it will not cause a crash, it is certainly an error:const char *invalidFormat = "%u";size_t value = SIZE_MAX;// A wrong value will be printedprintf(invalidFormat, value);In other cases, an error in the format string will be crucial. Consider an example basedon an implementation of the UNDO/REDO subsystem in one program:// The pointers were saved as strings here
  23. 23. int *p1, *p2;....char str[128];sprintf(str, "%X %X", p1, p2);// In another function this string// was processed in the following way:void foo(char *str){ int *p1, *p2; sscanf(str, "%X %X", &p1, &p2); // The result is incorrect values of p1 and p2 pointers. ...}The "%X" format is not intended to work with pointers and therefore such code isincorrect from the viewpoint of 64-bit systems. In 32-bit systems, it is quite efficient yetlooks ugly.Example 15. Storing integer values in doubleWe did not encounter this error ourselves. Perhaps it is rare yet quite possible.The double type has the size 64 bits and it is compatible with the IEEE-754 standard on32-bit and 64-bit systems. Some programmers use the double type to store and handleinteger types:size_t a = size_t(-1);double b = a;--a;--b;size_t c = b; // x86: a == c // x64: a != cThe code of this example can be justified in case of a 32-bit system since the doubletype has 52 significant bits and can store a 32-bit integer values without loss. But whenyou try to store a 64-bit integer value into double, you might lose an exact value (seeFigure 13).
  24. 24. Figure 13 - The number of significant bits in the types size_t and doubleExample 16. Address arithmetic. A + B != A - (-B)Address arithmetic is a means of calculating an address of some object with the help ofarithmetic operations over pointers and also using pointers in comparison operations.Address arithmetic is also called pointer arithmetic.It is address arithmetic that many 64-bit errors refer to. Errors often occur inexpressions where pointers and 32-bit variables are used together.Consider the first error of this type:char *A = "123456789";unsigned B = 1;char *X = A + B;char *Y = A - (-B);if (X != Y) cout << "Error" << endl;The reason why A + B == A - (-B) in a Win32 program is explained in Figure 14.Figure 14 - Win32: A + B == A - (-B)
  25. 25. The reason why A + B != A - (-B) in a Win64 program is explained in Figure 15.Figure 15 - Win64: A + B != A - (-B)You can eliminate the error if you use an appropriate memsize-type. In this case, theptrdfiff_t type is used:char *A = "123456789";ptrdiff_t B = 1;char *X = A + B;char *Y = A - (-B);Example 17. Address arithmetic. Signed and unsigned types.Consider one more kind of the error related to signed and unsigned types. In this case,the error will immediately cause a program crash instead of a wrong comparisonoperation.LONG p1[100];ULONG x = 5;LONG y = -1;LONG *p2 = p1 + 50;p2 = p2 + x * y;*p2 = 1; // Access violationThe "x * y" expression has value 0xFFFFFFFB and its type is unsigned. This code isefficient in the 32-bit version since addition of the pointer to 0xFFFFFFFB is equivalentto its decrement by 5. In the 64-bit version, the pointer will point far outside the p1arrays boundaries after being added to 0xFFFFFFFB (see Figure 16).
  26. 26. Figure 16 - Out of the arrays boundariesTo correct this issue, you should use memsize-types and be careful when working withsigned and unsigned types:LONG p1[100];LONG_PTR x = 5;LONG_PTR y = -1;LONG *p2 = p1 + 50;p2 = p2 + x * y;*p2 = 1; // OKExample 18. Address arithmetic. Overflows.class Region { float *array; int Width, Height, Depth;
  27. 27. float Region::GetCell(int x, int y, int z) const; ...};float Region::GetCell(int x, int y, int z) const { return array[x + y * Width + z * Width * Height];}This code is taken from a real application of mathematic modeling where the size ofphysical memory is a very crucial resource, so the possibility to use more than 4Gbytes of memory on the 64-bit architecture significantly increases the computationalpower. In programs of this class, one-dimensional arrays are often used in order tosave memory, and they are handled like third-dimensional arrays. To do this, thereexist functions similar to GetCell that provide access to necessary items.This code works correctly with pointers if the result of the " x + y * Width + z * Width *Height" expression does not exceed INT_MAX (2147483647). Otherwise an overflowwill occur leading to an unexpected program behavior.This code could always work correctly on the 32-bit platform. Within the scope of the32-bit architecture, the program cannot get the necessary memory amount to create anarray of such a size. But this limitation is absent on the 64-bit architecture and thearrays size might easily exceed INT_MAX items.Programmers often make a mistake trying to fix the code this way:float Region::GetCell(int x, int y, int z) const { return array[static_cast<ptrdiff_t>(x) + y * Width + z * Width * Height];}They know that the expression to calculate the index will have the ptrdiff_t typeaccording to C++ rules and try to avoid the overflow therefore. But the overflow mightoccur inside the "y * Width" or "z * Width * Height" subexpressions since it is still theint type that is used to calculate them.If you want to fix the code without changing the types of the variables participating inthe expression, you may explicitly convert each subexpression to the ptrdiff_t type:float Region::GetCell(int x, int y, int z) const { return array[ptrdiff_t(x) + ptrdiff_t(y) * Width + ptrdiff_t(z) * Width * Height];}Another, better, solution is to change the variables types:
  28. 28. typedef ptrdiff_t TCoord;class Region { float *array; TCoord Width, Height, Depth; float Region::GetCell(TCoord x, TCoord y, TCoord z) const; ...};float Region::GetCell(TCoord x, TCoord y, TCoord z) const { return array[x + y * Width + z * Width * Height];}Example 19. Changing an arrays typeSometimes programmers change the type of an array while processing it for thepurpose of convenience. The following code contains dangerous and safe typeconversions:int array[4] = { 1, 2, 3, 4 };enum ENumbers { ZERO, ONE, TWO, THREE, FOUR };//safe cast (for MSVC)ENumbers *enumPtr = (ENumbers *)(array);cout << enumPtr[1] << " ";//unsafe castsize_t *sizetPtr = (size_t *)(array);cout << sizetPtr[1] << endl;//Output on 32-bit system: 2 2//Output on 64-bit system: 2 17179869187As you may see, the output results differ in the 32-bit and 64-bit versions. On the 32-bitsystem, the access to the arrays items is correct because the sizes of the size_t andint types coincide and we get the output "2 2".On the 64-bit system, we got "2 17179869187" in the output since it is this very value17179869187 which is located in the first item of the sizePtr array (see Figure 17).Sometimes this behavior is intended but most often it is an error.
  29. 29. Figure 17 - Representation of array items in memoryNote. The size of the enum type by default coincides with the size of the int type in theVisual C++ compiler, i.e. the enum type is a 32-bit type. You can use enum of adifferent size only with the help of an extension which is considered non-standard inVisual C++. That is why the given example is correct in Visual C++ but from theviewpoint of other compilers conversion of an int-item pointer to an enum-item pointer isalso incorrect.Example 20. Wrapping a pointer in a 32-bit typeSometimes pointers are stored in integer types. Usually the int type is used for thispurpose. This is perhaps one of the most frequent 64-bit errors.
  30. 30. char *ptr = ...;int n = (int) ptr;...ptr = (char *) n;In a 64-bit program, this is incorrect since the int type remains 32-bit and cannot storea 64-bit pointer. The programmer often cannot notice it at once. Due to mere luck, thepointer might always refer to objects located within the first 4 Gbytes of the addressspace during the testing. In this case, the 64-bit program will work efficiently and crashonly in a large period of time (see Figure 18).Figure 18 - Putting a pointer into a variable of int typeIf you still need to store a pointer in a variable of an integer type, you should use suchtypes as intptr_t, uintptr_t, ptrdiff_t and size_t.Example 21. Memsize-types in unions
  31. 31. When you need to work with a pointer as an integer, it is sometimes convenient to usea union as shown in the example and work with the numeric representation of the typewithout explicit conversions:union PtrNumUnion { char *m_p; unsigned m_n;} u;u.m_p = str;u.m_n += delta;This code is correct on 32-bit systems and incorrect on 64-bit ones. Changing the m_nmember on a 64-bit system, we work only with a part of the m_p pointer (see Figure19).Figure 19 - Representation of a union in memory on a 32-bit system and 64-bitsystems.You should use a type that would correspond to the pointers size:union PtrNumUnion { char *m_p; uintptr_t m_n; //type fixed} u;Example 22. An infinity loopMixed use of 32-bit and 64-bit types can unexpectedly cause infinity loops. Consider asynthetic sample illustrating a whole class of such defects:size_t Count = BigValue;for (unsigned Index = 0; Index != Count; Index++)
  32. 32. { ... }This loop will never stop if the Count value > UINT_MAX. Assume that this codeworked with the number of iterations less than UINT_MAX on 32-bit systems. But the64-bit version of this program can process more data and it may require moreiterations. Since the values of the Index variable lie within the range [0..UINT_MAX], thecondition "Index != Count" will never be fulfilled and it will cause an infinity loop (seeFigure 20).Figure 20 - The mechanism of an infinity loop
  33. 33. Example 23. Bit operations and NOT operationBit operations require special care from the programmer when developing crossplatformapplications where data types may have different sizes. Since migration of a program tothe 64-bit platform also makes capacity of some types change, it is highly probable thaterrors will occur in those code fragments that work with separate bits. Most often ithappens when 32-bit and 64-bit data types are handled together. Consider an erroroccurring in the code because of an incorrect use of the NOT operation:UINT_PTR a = ~UINT_PTR(0);ULONG b = 0x10;UINT_PTR c = a & ~(b - 1);c = c | 0xFu;if (a != c) cout << "Error" << endl;The error consists in that the mask defined by the "~(b - 1)" expression has the ULONGtype. It causes zeroing of the most significant bits of the "a" variable although it is onlythe four least significant bits that should have been zeroed (see Figure 21).
  34. 34. Figure 21 - The error occurring because of zeroing of the most significant bitsThe correct version of the code looks as follows:UINT_PTR c = a & ~(UINT_PTR(b) - 1);This example is extremely simple but it is very good to demonstrate the class of errorsthat might occur when you actively work with bit operations.Example 24. Bit operations, offsetsptrdiff_t SetBitN(ptrdiff_t value, unsigned bitNum) { ptrdiff_t mask = 1 << bitNum; return value | mask;}This code works well on the 32-bit architecture and allows to set a bit with the numbersfrom 0 to 31 into one. After porting the program to the 64-bit platform, you need to set
  35. 35. bits with the numbers from 0 to 63. But this code cannot set the most significant bitswith the numbers 32-63. Note that the numeric literal "1" has the int type and anoverflow will occur after an offset at 32 positions as shown in Figure 22. We will get 0(Figure 22-B) or 1 (Figure 22-C) - it depends upon the compilers implementation.Figure 22 - a) correct setting of the 31st bit in the 32-bit code (the bits are countedbeginning with 0); b,c) - The error of setting the 32nd bit on the 64-bit system (the twovariants of behavior that depend upon the compiler)To correct the code, you should make the "1" constants type the same as the type ofthe mask variable:ptrdiff_t mask = static_cast<ptrdiff_t>(1) << bitNum;Note also that the incorrect code will lead to one more interesting error. When settingthe 31-st bit on the 64-bit system, the result of the function is 0xffffffff80000000 (seeFigure 23). The result of the 1 << 31 expression is the negative number -2147483648.This number is represented in a 64-bit integer variable as 0xffffffff80000000.
  36. 36. Figure 23 - The error of setting the 31-st bit on the 64-bit systemExample 25. Bit operations and sign extensionThe error shown below is rare yet, unfortunately, quite difficult to understand. So letsdiscuss it in detail.struct BitFieldStruct { unsigned short a:15; unsigned short b:13;};BitFieldStruct obj;obj.a = 0x4000;size_t x = obj.a << 17; //Sign Extensionprintf("x 0x%Ixn", x);//Output on 32-bit system: 0x80000000//Output on 64-bit system: 0xffffffff80000000In the 32-bit environment, the sequence of expression calculation looks as shown inFigure 24.
  37. 37. Figure 24 - Calculation of the expression in the 32-bit codeNote that sign extension of the unsigned short type to int takes place during thecalculation of the "obj.a << 17" expression. The following code makes it clearer:#include <stdio.h>template <typename T> void PrintType(T){ printf("type is %s %d-bitn", (T)-1 < 0 ? "signed" : "unsigned", sizeof(T)*8);}struct BitFieldStruct { unsigned short a:15; unsigned short b:13;};int main(void){ BitFieldStruct bf; PrintType( bf.a ); PrintType( bf.a << 2); return 0;}Result:type is unsigned 16-bittype is signed 32-bit
  38. 38. Now lets see the consequence of a sign extension in 64-bit code. The sequence ofexpression calculation is shown in Figure 25.Figure 25 - Calculation of the expression in 64-bit codeThe member of the obj.a structure is cast from the bit field of the unsigned short typeinto int. The "obj.a << 17" expression has the int type but it is cast to ptrdiff_t and thento size_t before being assigned to the addr variable. As a result, we will get value0xffffffff80000000 instead of 0x0000000080000000 we have expected.Be careful when working with bit fields. To avoid the described situation in our example,you just need to convert obj.a to the size_t type....size_t x = static_cast<size_t>(obj.a) << 17; // OKprintf("x 0x%Ixn", x);//Output on 32-bit system: 0x80000000//Output on 64-bit system: 0x80000000Example 26. Serialization and data exchangeSuccession to the existing communications protocols is an important element inmigration of a software solution to a new platform. You must provide the possibility ofreading existing project formats, data exchange between 32-bit and 64-bit processes
  39. 39. and so on.In general, the errors of this kind consist in serialization of memsize-types and dataexchange operations that use them.:size_t PixelsCount;fread(&PixelsCount, sizeof(PixelsCount), 1, inFile);You cannot use types that change their size depending upon the developmentenvironment in binary data exchange interfaces. In C++, most types do not have strictsizes and therefore they all cannot be used for these purposes. That is why thedevelopers of development tools and programmers themselves create data types thathave strict sizes such as __int8, __int16, INT32, word64, etc.Even on correcting all the issues referring to type sizes, you might encounter theproblem of incompatibility of binary formats. The reason lies in a different datarepresentation. Most often it is determined by a different byte order.Byte order is a method of writing bytes of multi-byte numbers (see Figure 26). The little-endian order means that writing begins with the least significant byte and ends with themost significant byte. This writing order is accepted in the memory of personalcomputers with x86 and x86-64-processores. The big-endian order means that writingbegins with the most significant byte and ends with the least significant byte. This orderis a standard for TCP/IP protocols. That is why the big-endian byte order is often calledthe network byte order. This byte order is used in processors Motorola 68000 andSPARC.By the way, some processors can work in both orders. For instance, IA-64 is such aprocessor.Figure 26 - Byte order in a 64-bit type in little-endian and big-endian systems
  40. 40. While developing a binary data interface or format, you should remember about the byteorder. If the 64-bit system you are porting your 32-bit application to has a different byteorder, you will just have to take this into account for your code. To convert between thebig-endian and little-endian byte orders, you may use the functions htonl(), htons(),bswap_64, etc.Example 27. Changes in type alignmentBesides changes of sizes of some data types, errors might also due to changes ofrules of their alignment in a 64-bit system (see Figure 27).
  41. 41. Figure 27 - Sizes of types and their alignment boundaries (the figures are exact forWin32/Win64 but may vary in the "Unix-world", so they are given only for demonstrationpurpose)Consider a description of the issue found in some forum:I have encountered an issue in Linux today. There is a data structure consisting ofseveral fields: a 64-bit double, 8 unsigned char and one 32-bit int. All in all there are 20bytes (8 + 8*1 + 4). On 32-bit systems, sizeof equals 20 and everything is ok. But on
  42. 42. the 64-bit Linux, sizeof returns 24. That is, there is a 64-bit boundary alignment.Then this person discusses the problem of data compatibility and asks for advice howto pack the data in the structure. We are not interested in this at the moment. What isrelevant, this is another type of errors that might occur when you port applications to64-bit systems.It is quite clear and familiar that changes of the sizes of fields in a structure cause thesize of the structure itself to change. But here we have a different case. The sizes ofthe fields remain the same but the structures size still changes due to other alignmentrules (see Figure 28). This behavior might lead to various errors, for instance, errors offormat incompatibility of saved data.Figure 28 - A scheme of structures and type alignment rulesExample 28. Type alignments and why you mustnt write sizeof(x) +sizeof(y)Sometimes programmers use structures with an array of a variable size at the end.
  43. 43. Such a structure and the mechanism of memory allocation for it might look as follows:struct MyPointersArray { DWORD m_n; PVOID m_arr[1];} object;...malloc( sizeof(DWORD) + 5 * sizeof(PVOID) );...This code is correct in the 32-bit version but fails in the 64-bit version.When allocating memory needed to store an object like MyPointersArray that contains 5pointers, you should consider that the beginning of the m_arr array will be aligned on an8-byte boundary. Data arrangement in memory on different systems (Win32/Win64) isshown in Figure 29.Figure 29 - Data arrangement in memory in 32-bit and 64-bit systemsThe correct calculation of the size looks in the following way:struct MyPointersArray { DWORD m_n; PVOID m_arr[1];} object;...
  44. 44. malloc( FIELD_OFFSET(struct MyPointersArray, m_arr) + 5 * sizeof(PVOID) );...In this code, we determine the offset of the last structures member and add this offsetto its size. The offset of a structures or class member may be obtained with the help ofthe offsetof or FIELD_OFFSET macros. You should always use these macros to obtainthe offset in a structure without relying on your assumptions about sizes of types andrules of their alignment.Example 29. Overloaded functionsWhen you recompile a program, some other overloaded function might start to beselected (see Figure 30).Figure 30 - Choosing an overloaded function in a 32-bit system and 64-bit systemHere is an example of the problem:class MyStack {...public: void Push(__int32 &); void Push(__int64 &); void Pop(__int32 &); void Pop(__int64 &);} stack;ptrdiff_t value_1;
  45. 45. stack.Push(value_1);...int value_2;stack.Pop(value_2);The inaccurate programmer put and then chose from the stack values of different types(ptrdiff_t and int). Their sizes coincided on the 32-bit system and everything wasalright. When the size of the ptrdiff_t type changed in the 64-bit program, the number ofbytes put in the stack became larger than the number of bytes that would be fetchedfrom it then.Example 30. Errors in 32-bit units working in WoW64The last example covers errors in 32-bit programs that occur when they are executed inthe 64-bit environment. 64-bit software systems will include 32-bit units for a long timeand therefore we must provide for their correct work in the 64-bit environment. TheWoW64 subsystem fulfills this task very well by isolating a 32-bit application, so thatalmost all 32-bit applications work correctly. However, sometimes errors happen andthey refer most often to the redirection mechanism when working with files andWindows register.For instance, when dealing with a system that consists of 32-bit and 64-bit units whichinteract with each other, you should consider that they use different registerrepresentations. Thus, the following line stopped working in a 32-bit unit in oneprogram:lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWAREODBCODBC.INIODBC Data Sources", 0, KEY_QUERY_VALUE, &hKey);To make this program friends with other 64-bit parts, you should insert theKEY_WOW64_64KEY switch:lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWAREODBCODBC.INIODBC Data Sources", 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &hKey);SummaryThe method of static code analysis shows the best result in searching for the errorsdescribed in the article. As an example of such a tool that performs this kind ofanalysis, we can name the Viva64 tool included into the PVS-Studio package we aredeveloping.The methods of static search of defects allow to detect defects relying on the sourceprogram code. The program behavior is estimated at all the execution pathssimultaneously. Because of this, static analysis lets you find defects that occur only at
  46. 46. non-standard execution paths with rare input data. This feature supplements othertesting methods and increases security of applications. Static analysis systems mightbe used in source code audit, for the purpose of systematic elimination of defects inexisting programs; they can integrate into the development process and automaticallydetect defects in the code being created.References Andrey Karpov, Evgeniy Ryzhkov. Lessons on development of 64-bit C/C++ applications. http://www.viva64.com/articles/x64-lessons/ Andrey Karpov. About size_t and ptrdiff_t. http://www.viva64.com/art-1-2- 710804781.html Andrey Karpov, Evgeniy Ryzhkov. 20 issues of porting C++ code on the 64-bit platform. http://www.viva64.com/art-1-2-599168895.html Evgeniy Ryzhkov. PVS-Studio Tutorial. http://www.viva64.com/art-4-2- 747004748.html Andrey Karpov. A 64-bit horse that can count. http://www.viva64.com/art-1-2- 377673569.html

×