7 steps to transfer a program to a 64-bit system

You should start mastering 64-bit systems with the question “Do we need to rebuild our project for a 64-bit system?” This question must be answered, but not in a hurry, after thinking. On the one hand, you can fall behind your competitors by not offering 64-bit solutions in time. On the other hand, you can waste your time on a 64-bit application that will not provide any competitive advantages.
We list the main factors that will help you make a choice.

2.1. Application Lifecycle Length

You should not create a 64-bit version of an application with a short life cycle. Thanks to the WOW64 subsystem, old 32-bit applications work quite well on 64-bit Windows systems and therefore it makes no sense to make a 64-bit program that will no longer be supported in 2 years. Moreover, practice has shown that the transition to 64-bit versions of Windows has been delayed and perhaps the majority of your users in the short term will use only the 32-bit version of your software solution.
If you plan for long-term development and long-term support of a software product, then you should start working on the 64-bit version of your solution. This can be done slowly, but keep in mind that the longer you don't have a full 64-bit version, the more difficulties it may be to support such an application installed on 64-bit versions of Windows.

2.2. Application resource intensity

Recompiling the program for a 64-bit system will allow it to use huge amounts of RAM, and will also speed up its operation by 5-15%. A 5-10% speedup will occur due to the use of architectural capabilities of a 64-bit processor, for example, a larger number of registers. Another 1%-5% increase in speed is due to the absence of the WOW64 layer, which translates API calls between 32-bit applications and the 64-bit operating system.
If your program does not work with large amounts of data (more than 2GB) and its speed is not critical, then switching to a 64-bit system in the near future is not so relevant.
By the way, even simple 32-bit applications can benefit from running them in a 64-bit environment. You probably know that a program compiled with the /LARGEADDRESSAWARE:YES key can allocate up to 3 gigabytes of memory if the 32-bit Windows operating system is launched with the /3gb key. The same 32-bit program running on a 64-bit system can allocate almost 4 GB of memory (in practice, about 3.5 GB).

2.3. Library development

If you develop libraries, components, or other elements that third-party developers use to create their software, then you must be proactive in creating a 64-bit version of your product. Otherwise, your customers interested in releasing 64-bit versions will be forced to look for alternative solutions. For example, some software and hardware protection developers responded with a long delay to the emergence of 64-bit programs, which forced a number of clients to look for other tools to protect their programs.
An additional benefit of releasing a 64-bit version of the library is that you can sell it as a separate product. Thus, your customers who want to create both 32-bit and 64-bit applications will be forced to purchase 2 different licenses. For example, this policy is used by Spatial Corporation when selling the Spatial ACIS library.

2.4. Dependency of your product on third-party libraries

Before you plan to create 64-bit versions of your product, find out whether there are 64-bit versions of the libraries and components that it uses. Also find out what the pricing policy is for the 64-bit version of the library. All this can be found out by visiting the website of the library developer. If support is not available, then look in advance for alternative solutions that support 64-bit systems.

2.5. Availability of 16-bit applications

If your solutions still contain 16-bit modules, then it's time to get rid of them. 16-bit applications are not supported on 64-bit versions of Windows.
Here we should clarify one point related to the use of 16-bit installers. They are still used to install some 32-bit applications. A special mechanism has been created that on the fly replaces a number of the most popular 16-bit installers with newer versions. This may cause the misconception that 16-bit programs still run in a 64-bit environment. Remember, this is not true.

2.6. Availability of assembler code

Don't forget that using a large amount of assembly code can significantly increase the cost of creating a 64-bit version of the application.
After weighing all the listed facts, all the pros and cons, decide whether you should port your project to 64-bit systems. And if that's the case, then let's move on.

3. Step three. Tools

Just because you've decided to develop a 64-bit version of your product and are willing to spend time on it doesn't guarantee success. The fact is that you must have all the necessary tools and there may be unpleasant incidents here.
The simplest, but also the most insurmountable, problem may be the lack of a 64-bit compiler. The article is written in 2009, but there is still no 64-bit C++ Builder compiler from Codegear. Its release is expected only by the end of this year. It is impossible to get around a similar problem, unless, of course, you rewrite the entire project, for example, using Visual Studio. But if everything is clear with the lack of a 64-bit compiler, then other similar problems may turn out to be more hidden and emerge already at the stage of work on transferring the project to a new architecture. Therefore, I would like to advise you to conduct research in advance to determine whether all the necessary components exist that will be required to implement the 64-bit version of your product. Unpleasant surprises may await you.
Of course, it is impossible to list everything that might be needed for a project here, but I will still offer a list that will help you get your bearings and perhaps remember other points that are necessary to implement your 64-bit project:

3.1. Availability of a 64-bit compiler

It's difficult to say anything more about the importance of having a 64-bit compiler. It just has to be.
If you plan to develop 64-bit applications using the latest version (at the time of writing) of Visual Studio 2008, then the following table N2 will help determine which edition of Visual Studio you need.


Table N2. Features of different editions of Visual Studio 2008

3.2. Availability of 64-bit computers running 64-bit operating systems

You can, of course, use virtual machines to run 64-bit applications on 32-bit hardware, but this is extremely inconvenient and will not provide the required level of testing. It is advisable that machines have at least 4-8 gigabytes of RAM installed.

3.3. Availability of 64-bit versions of all used libraries

If libraries are provided in source code, then a 64-bit project configuration must be present. Upgrading a library yourself to build it for a 64-bit system can be a thankless and difficult task, and the result can be unreliable and error-prone. You may also violate license agreements by doing so. If you are using libraries as binary modules, then you should also find out if 64-bit modules exist. You won't be able to use 32-bit DLLs inside a 64-bit application. You can create a special harness via COM, but this will be a separate large, complex task. Also note that purchasing the 64-bit version of the library may cost additional money.

3.4. Lack of built-in assembler code

Visual C++ does not support 64-bit inline assembler. You must use either an external 64-bit assembler (for example, MASM) or have a C/C++ implementation of the same functionality.

3.5. Modernization of testing methodology

Significant revision of testing methodology, modernization of unit tests, use of new tools. This will be discussed in more detail below, but do not forget to take this into account when estimating the time spent migrating an application to a new system.

3.6. New data for testing

If you are developing resource-intensive applications that consume a large amount of RAM, then you need to take care of replenishing the database of test input data. When load testing 64-bit applications, it is advisable to go beyond 4 gigabytes of memory consumption. Many errors can only appear under such conditions.

3.7. Availability of 64-bit protection systems

The protection system used must support 64-bit systems to the full extent you require. For example, Aladdin quickly released 64-bit drivers to support Hasp hardware keys. But for a very long time there was no system for automatic protection of 64-bit binary files (Hasp Envelop program). Thus, the protection mechanism had to be implemented independently within the program code, which was an additional complex task that required qualifications and time. Don’t forget about similar issues related to security, the update system, and so on.

3.8. Installer

You need a new installer that can fully install 64-bit applications. I would like to immediately warn you about one traditional mistake. This is the creation of 64-bit installers for installing 32/64-bit software products. When preparing a 64-bit version of an application, developers often want to make it completely 64-bit. And they create a 64-bit installer, forgetting that users of a 32-bit operating system simply will not run such an installation package. Please note that it is not the 32-bit application included in the distribution along with the 64-bit that will not start, but the installer itself. After all, if the distribution is a 64-bit application, then, of course, it will not run on a 32-bit operating system. The most annoying thing about this is that the user will not be able to guess what is happening. He will simply see an installation package that cannot be launched.

4. Step four. Setting up a project in Visual Studio 2005/2008

Creating a 64-bit project configuration in Visual Studio 2005/2008 looks quite simple. Difficulties will await you at the stage of assembling a new configuration and searching for errors in it. To create a 64-bit configuration, just complete the following 4 steps:
Launch the configuration manager, as shown in Figure N1:


Figure 1. Launching the Configuration Manager

In the configuration manager, select support for the new platform (Figure N2):


Figure 2. Creating a new configuration

We select the 64-bit platform (x64), and as a basis we select the settings from the 32-bit version (Figure N3). The Visual Studio environment will adjust the settings that affect the build mode itself.


Figure 3. Select x64 as the platform and take the Win32 configuration as a basis

Adding a new configuration is complete and we can select the 64-bit configuration option and start compiling the 64-bit application. The choice of 64-bit configuration for assembly is shown in Figure N4.


Figure 4. 32-bit and 64-bit configuration now available

If you are lucky, there will be no need to additionally configure a 64-bit project. But this greatly depends on the project, its complexity and the number of libraries used. The only thing that needs to be changed right away is the stack size. If your project uses a default stack size, that is, 1 megabyte, then it makes sense to set it to 2 megabytes for the 64-bit version. This is not necessary, but it is better to be on the safe side in advance. If you use a stack size different from the default size, then it makes sense to make it 2 times larger for the 64-bit version. To do this, in the project settings, find and change the Stack Reserve Size and Stack Commit Size parameters.

5. Step five. Compiling the application

Here it would be good to talk about typical problems that arise during the compilation stage of a 64-bit configuration. Consider what problems arise with third-party libraries, tell that the compiler in the code associated with WInAPI functions will no longer allow placing a pointer in the LONG type and you will need to modernize your code and use the LONG_PTG type. And many many others. Unfortunately, there is so much of this and the errors are so varied that it is not possible to present it in one article or even, perhaps, a book. You will have to look through all the errors that the compiler will produce and new warnings that were not there before and in each individual case figure out how to modernize the code.
A collection of links to resources dedicated to the development of 64-bit applications can make life partly easier: http://www.viva64.com/links/64-bit-development/. The collection is constantly being updated and the author will be grateful to readers if they send him links to resources that, in their opinion, deserve attention.
We will focus here only on the types that may be of interest to developers when migrating applications. These types are presented in Table N3. Most compilation errors will be associated with the use of these types.
Type Dimensiontype on platform x32 / x64 Note
int 32 / 32 Basic type. On 64-bit systems it remains 32-bit.
long 32 / 32 Basic type. On 64-bit Windows systems it remains 32-bit. Please note that on 64-bit Linux systems this type has been extended to 64-bit. Don't forget about this if you are developing code that should work and compile for Windows and Linux systems.
size_t 32 / 64 Basic unsigned type. The size of the type is chosen so that it can accommodate the maximum size of a theoretically possible array. A pointer can be safely placed into the type size_t (the exception is pointers to class functions, but this is a special case).
ptrdiff_t 32 / 64 Similar to the size_t type, but signed. The result of an expression where one pointer is subtracted from another (ptr1-ptr2) will be of type ptrdiff_t.
Pointer 32 / 64 The size of the pointer directly depends on the bit capacity of the platform. Be careful when casting pointers to other types.
__int64 64 / 64 Signed 64-bit type.
DWORD 32 / 32 32-bit unsigned type. Declared in WinDef.h as: typedef unsigned long DWORD;
DWORDLONG 64 / 64 64-bit unsigned type. Declared in WinNT.h as: typedef ULONGLONG DWORDLONG;
DWORD_PTR 32 / 64 An unsigned type that can hold a pointer. Declared in BaseTsd.h as: typedef ULONG_PTR DWORD_PTR;
DWORD32 32 / 32 32-bit unsigned type. Declared in BaseTsd.h as: typedef unsigned int DWORD32;
DWORD64 64 / 64 64-bit unsigned type. Declared in BaseTsd.h as: typedef unsigned __int64 DWORD64;
HALF_PTR 16 / 32 Half a pointer. Declared in Basetd.h as:#ifdef _WIN64 typedef int HALF_PTR;#else typedef short HALF_PTR;#endif
INT_PTR 32 / 64 A signed type into which a pointer can be placed. Declared in BaseTsd.h as:#if defined(_WIN64) typedef __int64 INT_PTR; #else typedef int INT_PTR;#endif
LONG 32 / 32 A signed type that remains 32-bit. Therefore, in many cases LONG_PTR should now be used. Declared in WinNT.h as: typedef long LONG;
LONG_PTR 32 / 64 A signed type into which a pointer can be placed. Declared in BaseTsd.h as:#if defined(_WIN64) typedef __int64 LONG_PTR; #else typedef long LONG_PTR;#endif
LPARAM 32 / 64 Parameter for sending messages. Declared in WinNT.h as: typedef LONG_PTR LPARAM;
SIZE_T 32 / 64 Analogous to the size_t type. Declared in BaseTsd.h as: typedef ULONG_PTR SIZE_T;
SSIZE_T 32 / 64 Analogous to the ptrdiff_t type. Declared in BaseTsd.h as: typedef LONG_PTR SSIZE_T;
ULONG_PTR 32 / 64 An unsigned type that can hold a pointer. Declared in BaseTsd.h as:#if defined(_WIN64) typedef unsigned __int64 ULONG_PTR;#else typedef unsigned long ULONG_PTR;#endif
WORD 16 / 16 Unsigned 16-bit type. Declared in WinDef.h as: typedef unsigned short WORD;
WPARAM 32 / 64 Parameter for sending messages. Declared in WinDef.h as: typedef UINT_PTR WPARAM;

Table N3. Types of interest when porting 32-bit programs to 64-bit Windows systems.

6. Diagnosis of hidden errors

If you think that after correcting all compilation errors the long-awaited 64-bit application will be obtained, then you will have to be disappointed. The hardest part is yet to come. At the compilation stage, you will correct the most obvious errors that the compiler could detect, which are mainly related to the impossibility of implicit type casting. But this is the tip of the iceberg. Most of the errors are hidden. From the point of view of the abstract C++ language, these errors look safe or are disguised by explicit type casts. There are several times more such errors than the number of errors identified at the compilation stage.
You shouldn't pin your hopes on the /Wp64 key. This key is often touted as a miracle tool for finding 64-bit errors. In fact, the /Wp64 switch only makes it possible, when compiling 32-bit code, to receive some warnings that certain sections of the code will be incorrect in 64-bit mode. When compiling 64-bit code, these warnings will be issued by the compiler in any case. And therefore, when compiling a 64-bit application, the /Wp64 key is ignored. And even more so, this key will not help in finding hidden errors.
Let's look at a few examples of hidden errors.

6.1. Explicit type casting

The simplest, but not the easiest to detect, class of errors is associated with explicit type casting, in which significant bits are cut off.
A common example is casting pointers to 32-bit types when passing them to functions such as SendMessage:
MyObj* pObj = ... ::SendMessage(hwnd, msg, (WORD)x, (DWORD)pObj);

Here an explicit type cast is used to convert a pointer to a numeric type. For a 32-bit architecture, the above example is correct, since the last parameter of the SendMessage function is of type LPARAM, which is the same as DWORD on a 32-bit architecture. For 64-bit architecture, the use of DWORD is erroneous and should be replaced with LPARAM. The LPARAM type has a size of 32 or 64 bits, depending on the architecture.
This is a simple case, but often the type cast looks more sophisticated and cannot be detected using compiler warnings or searching through the program text. Explicit type casts suppress compiler diagnostics because they are intended to tell the compiler that the type cast is correct and the programmer has accepted responsibility for the safety of the code. An explicit search won't help either. Types may not have standard names (specified by the programmer via typedef), and there are also quite a few ways to implement explicit type casting. To reliably diagnose such errors, you need to use only special tools, such as Viva64 or PC-Lint analyzers.

6.2. Implicit type casting

The next example is associated with an implicit type cast, which also results in loss of significant bits. The fread function code reads from a file, but is incorrect when trying to read more than 2 gigabytes of data on a 64-bit system.
size_t __fread(void * __restrict buf, size_t size, size_t count, FILE * __restrict fp); size_t fread(void * __restrict buf, size_t size, size_t count, FILE * __restrict fp) ( int ret; FLOCKFILE(fp); ret = __fread(buf, size, count, fp); FUNLOCKFILE(fp); return (ret) ; )

The __fread function returns a size_t type, but the int type is used to store the number of bytes read. As a result, with large amounts of data being read, the function may return a different number of bytes than will actually be read.
You can say that this is ignorant code for beginners, that the compiler will report such a type cast, and that in general such code is easy to find and correct. This is theoretical. But in real life, with large projects, everything can be different. This example is taken from the FreeBSD source code. The error was corrected only in December 2008! This is despite the fact that the first (experimental) 64-bit version of FreeBSD was released back in June 2003.
Here is the source code before the fix:
http://www.freebsd.org/cgi/cvsweb.cgi/src/lib/libc/stdio/fread.c?rev=1.14
And here is the corrected version (December 2008):
http://www.freebsd.org/cgi/cvsweb.cgi/src/lib/libc/stdio/fread.c?rev=1.15

6.3. Working with bits, shifts

It's easy to make mistakes in code that works with individual bits. The next type of error is associated with shift operations. Let's look at an example:
ptrdiff_t SetBitN(ptrdiff_t value, unsigned bitNum) ( ptrdiff_t mask = 1<< bitNum; return value | mask; }

The above code works on a 32-bit architecture and allows you to set bits numbered 0 to 31 to one. After transferring the program to a 64-bit platform, there will be a need to set bits from 0 to 63. But this code will never set bits numbered 32-63. Please note that “1” is of type int and when shifted by 32 positions an overflow will occur, as shown in Figure 5. Whether the result is 0 (Figure 5-B) or 1 (Figure 5-C) depends on the compiler implementation.


Figure 5. A - Correct installation of the 31st bit in a 32-bit code; B,C - Error installing the 32nd bit on a 64-bit system (two behavior options)

To fix the code, you need to make a constant “1” of the same type as the mask variable:

ptrdiff_t mask = ptrdiff_t(1)<< bitNum;

Note also that uncorrected code will lead to another interesting bug. When setting 31 bits on a 64-bit system, the result of the function will be the value 0xffffffff80000000 (see Figure 6). The result of expression 1<< 31 является отрицательное число -2147483648. Это число представляется в 64-битной целой переменной как 0xffffffff80000000.


Figure 6. Error installing the 31st bit on a 64-bit system

6.4. Magic numbers

Magic constants, that is, numbers used to set the size of a particular type, can cause a lot of trouble. The correct solution is to use sizeof() operators for such purposes, but in a large program an old piece of code may well be lost, where they were firmly convinced that the pointer size was 4 bytes, and the size_t type is always 32 bits. Typically these errors look like this:
size_t ArraySize = N * 4; size_t *Array = (size_t *)malloc(ArraySize);

The main numbers that should be taken with caution when switching to a 64-bit platform are given in Table N4.


Table N4. Basic magic values ​​that are dangerous when migrating applications from a 32-bit to a 64-bit platform

6.5. Errors in using 32-bit variables as indices

In programs that process large amounts of data, errors associated with indexing large arrays may occur or eternal loops may occur. The following example contains 2 errors at once:
const size_t size = ...; char *array = ...; char *end = array + size; for (unsigned i = 0; i != size; ++i) ( const int one = 1; end[-i - one] = 0; )

The first error is that if the size of the processed data exceeds 4 gigabytes (0xFFFFFFFF), then an eternal loop may occur, since the variable "i" is of type "unsigned" and will never reach the value 0xFFFFFFFF. I specifically write that the occurrence is possible, but it will not necessarily happen. It depends on what code the compiler builds. For example, in debug mode the eternal loop will be present, but in the release code the looping will disappear, so the compiler will decide to optimize the code using a 64-bit register for the counter and the loop will be correct. All this adds to the confusion, and code that worked yesterday may suddenly stop working the next day.
The second error is associated with passing through an array from end to beginning, for which negative index values ​​are used. The above code works in 32-bit mode, but when it is run on a 64-bit machine, on the very first iteration of the loop, the array will be accessed beyond the boundaries and the program will crash. Let's consider the reason for this behavior.

According to the C++ language rule on a 32-bit system, the expression “-i - one” will be evaluated as follows (at the first step i = 0):

  1. The "-i" expression is of type unsigned and has the value 0x00000000u.
  2. The variable "one" will be expanded from type "int" to type unsigned and will be equal to 0x00000001u. Note: The type int is expanded (according to the C++ language standard) to type "unsigned" if it participates in an operation where the second argument is of type unsigned.
  3. A subtraction operation occurs, which involves two values ​​of type unsigned, and the result of the operation is 0x00000000u - 0x00000001u = 0xFFFFFFFFu. Note that the result is of unsigned type.
  4. On a 32-bit system, accessing an array at index 0xFFFFFFFFu is equivalent to using index -1. That is, end is analogous to end[-1]. As a result, the array element is processed correctly.
In a 64-bit system in the last paragraph the picture will be different. The unsigned type will be expanded to signed ptrdiff_t and the array index will be equal to 0x00000000FFFFFFFFi64. As a result, the array will be out of bounds.
To fix the code, you need to use types such as ptrdiff_t and size_t.

6.6. Errors associated with changing the types of functions used

There are mistakes for which, in general, no one is to blame, but this does not stop them from being mistakes. Imagine that a long time ago in a distant galaxy (in Visual Studio 6.0) a project was developed that contained the CSampleApp class, which is a descendant of CWinApp. The base class has a virtual function called WinHelp. The heir overrides this function and performs the necessary actions. This is visually shown in Figure 7.


Figure 7. Working correct code created in Visual Studio 6.0

Then the project is transferred to Visual Studio 2005, where the prototype of the WinHelp function has changed, but no one notices this, since in 32-bit mode the DWORD and DWORD_PTR types are the same and the program continues to work correctly (Figure 8).


Figure 8. Incorrect, but workable 32-bit code

The bug is waiting to manifest itself on a 64-bit system, where the size of the DWORD and DWORD_PTR types is different (Figure 9). It turns out that in 64-bit mode the classes contain two DIFFERENT WinHelp functions, which is naturally incorrect. Please note that such traps can be hidden not only in MFC, where some functions have changed the types of their arguments, but also in the code of your applications and third-party libraries.


Figure 9. The error manifests itself in 64-bit code

6.7. Diagnosis of hidden errors

Examples of such 64-bit errors can be given and given. Those who are interested in such errors and want to learn more about them will be interested in the article “20 pitfalls of porting C++ code to a 64-bit platform.”
As you can see, the stage of finding hidden errors is a non-trivial task, especially since many of them will appear irregularly or only on large input volumes of data. Static code analyzers are well suited for diagnosing such errors, since they can check the entire application code, regardless of the input data and the frequency of execution of its sections in real conditions. It makes sense to use static analysis both at the stage of porting an application to 64-bit platforms in order to find most errors at the very initial stage, and in the further development of 64-bit solutions. Static analysis will warn and teach the programmer to better understand the features of errors associated with the 64-bit architecture and write more efficient code. The author of the article is the developer of one of these specialized code analyzers, called Viva64. You can get acquainted with the tool in more detail and download a demo version from the website of the Software Verification Systems LLC company.
To be fair, it should be said that code analyzers such as Gimpel PC-Lint and Parasoft C++Test have sets of rules for diagnosing 64-bit errors. But, firstly, these are general-purpose analyzers and the rules for diagnosing 64-bit errors are poorly represented in them. Secondly, they are more focused on the LP64 data model used in the Linux operating system family, which reduces their usefulness for Windows programs that use the LLP64 data model.

7. Step seven. Modernizing the testing process

The step of finding errors in the program code described in the previous section is a necessary, but not sufficient step. No method, including static code analysis, provides a complete guarantee of detecting all errors, and the best result can only be achieved by combining different techniques.
If your 64-bit program processes more data than the 32-bit version, then you need to expand the tests to include processing of data larger than 4 gigabytes. This is the boundary beyond which many 64-bit errors begin to manifest themselves. Such tests can take an order of magnitude longer and you need to be prepared for this in advance. Typically tests are written in such a way as to process a small number of elements in each test and thereby be able to pass all internal unit tests, for example? in a few minutes, and automated tests (for example, using AutomatedQA TestComplete) in a few hours. A sorting function on a 32-bit system, if it sorts 100 elements, is almost guaranteed to behave correctly on 100,000 elements. But the same function on a 64-bit system can fail when trying to process 5 billion elements. The execution speed of a unit test can decrease by millions of times. Don't forget to factor in the cost of adapting tests when mastering 64-bit systems. One solution is to divide unit tests into fast ones (working with a small amount of memory) and slow ones, processing gigabytes and running, for example, at night. Automated testing of resource-intensive 64-bit programs can be built on the basis of distributed computing.

Tags:

  • 64-bit programming
  • 64-bit
  • Viva64
  • Intel 64
  • AMD64
  • c++
  • 64-bit
Add tags