Friday 10 February 2017

Inflation Problems

Despite 64-bit operating systems being the default for over 10 years, some of the code I use is still compiled with "-m32" for 32-bit mode. The reasons for this are mostly lack of management will and developer time. As I got time between projects, I decided to update the code so that we can release in both 32-bit and 64-bit mode.

Upgrading the code to be ready for 64-bit mode proved to be a slow task that had many chances for errors. I hope that by showing these errors and some common fixes it helps others to also update their code.

Common Errors

int or unsigned int instead of size_t


On a 32-bit system this isn't really a problem as all 3 types use a 32-bit integer, so you won't get errors. However, it's not portable and on a 64-bit Linux system, size_t is a 64-bit (unsigned) integer. This can cause issues with comparisons and overflow. For example:

string s = "some string";

unsigned int pos = s.find("st");
if( pos == string::npos) {
    // code that can never be hit
}

The above causes issues because string::npos can never be equal to pos as the data type of an unsigned int is too small to match string::npos.


This issue can be caught with the compiler flag -Wtype-limits. Or preferably use -Werror=type=limits to cause the compilation to fail with the following error

error: comparison is always false due to limited range of data type [-Werror=type-limits]

As mentioned this can also cause overflow issues, for example:

unsigned int pos = string::npos;

This causes an overflow because string::npos is too big to fit in a 32-bit integer.

Again this can be caught by a compiler flag, in this case -Woverflow. And again I recommend to use -Werror=overflow to cause a compilation error.

Wrong printf arguments


The logger in our codebase uses printf style formatting for formatting log lines. As a result of this the most common warning on our 64-bit compile was related to this.

The most common cause was related to the above assumption that a size_t is a 32-bit integer. Below is an example of the warning showing this

warning: format '%u' expects argument of type 'unsigned int', but argument 2 has type 'size_t {aka long unsigned int}' [-Wformat=]
         TRACE(("Insert at position [%u]", pos));

The fix that I used for this warning to use the %zu format specifier for size_t. This was introduced in the C99 standard and should be available in gcc and clang. However, it may not be available in some older versions of the Visual Studio compiler.


TRACE(("Insert at position [%zu]", pos));

I have also seen the above error in relation to other types, for example time_t, uintptr_t, and long. If you are unsure of what the printf argument for a type is, then you can use helpful macros from the C "inttypes.h" header (<cinttypes> if using C++11 or later). This includes macros with the printf arguments for various system typedefs.

Note: Before C++11 you must define __STDC_FORMAT_MACROS before including this header. For example, to print a uintptr_t you can use the macro PRIuPTR


#define __STDC_FORMAT_MACROS 1
#include <inttypes.h>

bool MyList::insert(uintptr_t value)

{
....

    TRACE(("value [%" PRIuPTR "]", value));

Assuming the size of a type is always the same


Again this is somewhat related to the previous points. I saw a number of errors where it was assumed that a particular type was always the same length on different platforms.

The 2 most common were pointers and long.

In our code pointer length issues often manifest as the printf argument error, e.g. using %08x instead of %p but I also saw some cases where a pointer was cast to an int to pass it through a particular function. This would then cause it to then precision on a 64-bit system.


In the case of long it appears that in many cases it was assumed that long was always a 32-bit integer. I came across a number of errors caused by using bitwise operations which assumed that a long was 32-bits. For example:

long offset = getSomeValue();
if ( offset & (1 << 31) )

This causes errors because long is not guaranteed to be a 32-bit integer. If you need to guarantee a size then you should use the correct typedef for that sized integer from the C "stdint.h" header (<cstdint> for C++11). e.g.

#include <stdint.h>

int32_t i32 = get32bitInt();
int64_t i64 = get64bitint();
...

These can then be used in conjunction with the PRIxxx macros from inttypes.h if you need to log / format them

Even with stdint.h there were some ambiguous types that were being cast to / from different types. An example of this was time_t which is not actually defined in a standard. After some googling and testing, I discovered it aligns to the same size as a long (4 bytes on a 32-bit arch, 8 bytes on 64-bit). So when we needed  to pass a time_t value and can't use the time_t typedef I defaulted to using a long.


At the end of the article I show a very simple test program and it's output on RedHat Linux. This shows how the size of types can change depending on compilation mode.

Using the wrong type with malloc

This issue is not actually related to the 64-bit port but the symptoms of it only manifested when we ran the code in 64-bit mode.

There were a couple of blocks of code that were using malloc to get a block of code for an array and these were using the wrong type for the sizeof argument. For example, some code for a hash table included:


typedef struct HT
{
    int num_entries;
    int size;
    HTEntry **table;
} HT;

Then to initialize the table

HT *newtable = NULL;
newtable = (HT*)malloc(sizeof(HT));
newtable->size = size;

newtable->table = (HTEntry**)malloc(sizeof(int)*size);


This has been deployed and run error free for a number of years in our 32-bit software release. However, as the sizeof an int and the size of pointers differ on 64-bit systems, it  caused errors there.

The correct code is:

newtable->table = (HTEntry**)malloc(sizeof(HTEntry*)*size);

Unfortunately I was unable to catch this with any compiler warnings and it caused a crash when run. I had also run some static analyzers over the code which missed this.

Conclusions

The task of updating your code to make it 64-bit compatible is slow, however, can be made easier if you take care to listen to your tools. This includes enabling compiler warnings, making some warnings errors, and using static analysis tools. These will help catch many of the common errors that can occour.


As for the benefit of updating, it will be worth it because:
  •  It will help improve compatibility. As most OSes and software projects are now released in 64-bit mode by default, there is less chance of finding an incompatible package
  • Allow access to new CPU instructions. Compiling with 64bit mode allows access to new instructions and registers. Some initial tests have shown that certain sections of code can be up to 10% faster.
  • Improved code. Keeping the code compiling and working in both environments may lead to more careful programming.

References

http://www.drdobbs.com/cpp/porting-to-64-bit-platforms/226600156?pgno=1

http://www.viva64.com/en/a/0004/

http://www.drdobbs.com/parallel/multiplatform-porting-to-64-bits/184406427

Test program to check common sizes

In order to check sizes, I created a simple test program that will print out the sizes for some common types:

#include 
#include 
#include 
#include 

using namespace std;

int main()
{
    cout << "sizeof(int) : " << sizeof(int) << std::endl;
    cout << "sizeof(unsigned long) : " << sizeof(unsigned long) << std::endl;
    cout << "sizeof(long int) : " << sizeof(long int) << std::endl;
    cout << "sizeof(long long int) : " << sizeof(long long int) << std::endl;
    cout << "sizeof(int32_t) : " << sizeof(int32_t) << std::endl;
    cout << "sizeof(int64_t) : " << sizeof(int64_t) << std::endl;
    cout << "sizeof(double) : " << sizeof(double) << std::endl;
    cout << "sizeof(float) : " << sizeof(float) << std::endl;
    cout << "sizeof(size_t) : " << sizeof(size_t) << std::endl;
    cout << "sizeof(intptr_t) : " << sizeof(intptr_t) << std::endl;
    cout << "sizeof(uintptr_t) : " << sizeof(uintptr_t) << std::endl;
    cout << "sizeof(void*) : " << sizeof(void*) << std::endl;
    cout << "sizeof(char) : " << sizeof(char) << std::endl;
}

To compile and run, you can use:

$> .g++ sizes.cpp -m32 -o t32.sizes
$> ./t32.sizes 
sizeof(int) : 4
sizeof(unsigned long) : 4
sizeof(long int) : 4
sizeof(long long int) : 8
sizeof(int32_t) : 4
sizeof(int64_t) : 8
sizeof(double) : 8
sizeof(float) : 4
sizeof(size_t) : 4
sizeof(intptr_t) : 4
sizeof(uintptr_t) : 4
sizeof(void*) : 4
sizeof(char) : 1



$> .g++ sizes.cpp -o t64.sizes
$> ./t64.sizes 
sizeof(int) : 4
sizeof(unsigned long) :8
sizeof(long int) : 8
sizeof(long long int) : 8
sizeof(int32_t) : 4
sizeof(int64_t) : 8
sizeof(double) : 8
sizeof(float) : 4
sizeof(size_t) : 8
sizeof(intptr_t) : 8
sizeof(uintptr_t) : 8
sizeof(void*) : 8
sizeof(char) : 1


As you can see there are a number of types that have different sizes. These will be the same on all Linux systems, however they aren't guaranteed across different operating systems.

No comments:

Post a Comment