Patterns of 64-bit errors in
games
Andrey Karpov
karpov@viva64.com
Speaker
• Karpov Andrey Nikolaevich
• Habr profile: habr.com/users/Andrey2008/
• Microsoft MVP, Intel Black Belt Software Developer
• One of the founders of the PVS-Studio project, which
started right from the search of 64-bit errors
• https://www.viva64.com
Technically speaking, there are no
«64-bit errors»
In practice
• 64-bit errors are the ones which reveal themselves after building a
64-bit application
• By the way, many of them do it only when the 4 Gigabyte limit is
exceeded
Explanation on the example of malloc
• Who will notice an error?
void Foo()
{
const size_t Gigabyte = 1073741824;
void *A[10];
for (size_t i = 0; i < 10; ++i)
{
A[i] = malloc(Gigabyte);
printf("%pn", A[i]);
}
for (size_t i = 0; i < 10; ++i)
free(A[i]);
}
In C-code developers forget to include
headers
• Your program might not contain calls of malloc, but they might be in
third-party libraries
• It is revealed only when consuming large amount of memory
#include <stdio.h>
#include <string.h>
//#include <stdlib.h>
The case isn’t only about malloc
• Fennec Media
• memset
• memcpy
• Ffdshow (media decoder)
• memset
• memcpy
• libZPlay
• strlen
Let’s get back to implicit
truncation of 64-bit values
long type in Win64
const std::vector<Vec2> &vertexList =
body->getCalculatedVertexList();
unsigned long length = vertexList.size();
PVS-Studio: V103 Implicit type conversion from memsize to 32-bit type.
CCArmature.cpp 614
Cocos2d-x
int type is a bad idea in any case
PVS-Studio: V104 Implicit conversion of 'i' to memsize type in an arithmetic
expression: i < points_.size() sweep_context.cc 76
std::vector<Point*> points_;
void SweepContext::InitTriangulation()
{
// Calculate bounds.
for (unsigned int i = 0; i < points_.size(); i++) {
Point& p = *points_[i];
....
Cocos2d-x
By the way, such errors are more insidious
than it looks
• When overflowing int, undefined behavior occurs
• UB might be the case when everything works correctly :)
• More details: "Undefined behavior is closer than you think"
https://www.viva64.com/ru/b/0374/
size_t N = foo();
for (int i = 0; i < N; ++i)
Usage of correct memsize-types
"memsize" types
• ptrdiff_t - signed type (distance between pointers)
• size_t – maximum size of theoretically possible object of any type,
including arrays
• rsize_t – size of a single object
• intptr_t - signed type, can store a pointer
• uintptr_t – unsigned type, can store a pointer
Usage of memsize-types
• Number of elements in an array
• Buffer size
• Counters
• Pointer storing (although, it’s better not to do it)
• Pointer arithmetic
• And so on
Using of memsize-types: bad thing
PVS-Studio: V109 Implicit type conversion of return value 'a * b * c' to memsize
type. test.cpp 22
Potential overflow
size_t Foo(int a, int b, int c)
{
return a * b * c;
}
Using of memsize-types: OK
size_t Foo(int a, int b, int c)
{
return static_cast<size_t>(a)
* static_cast<size_t>(b)
* static_cast<size_t>(c);
}
size_t Foo(int a, int b, int c)
{
return static_cast<size_t>(a) * b * c;
}
JUST DON’T DO IT!
std::vector<unsigned char> buf(
static_cast<size_t>(exr_image->width * h * pixel_data_size));
PVS-Studio: V1028 CWE-190 Possible overflow. Consider casting operands, not the
result. tinyexr.h 11682
Possible
overflow again
TinyEXR
This way errors only hide more
if (components == 1) {
images[0].resize(static_cast<size_t>(width * height));
memcpy(images[0].data(), data, sizeof(float) * size_t(width * height));
} else {
images[0].resize(static_cast<size_t>(width * height));
images[1].resize(static_cast<size_t>(width * height));
images[2].resize(static_cast<size_t>(width * height));
images[3].resize(static_cast<size_t>(width * height));
for (size_t i = 0; i < static_cast<size_t>(width * height); i++) {
TinyEXR
More about pointer arithmetic
int A = -2;
unsigned B = 1;
int array[5] = { 1, 2, 3, 4, 5 };
int *ptr = array + 3;
ptr = ptr + (A + B);
printf("%in", *ptr); //Access violation
//on 64-bit platform
Developers forget to expand 32-bit
types
int64_t X = 1 << N
• This code doesn’t work in 32-bit programs as well
• Setting most significant bits is usually not needed there
class FMallocBinned : public FMalloc
{
uint64 PoolMask;
....
FMallocBinned(uint32 InPageSize, uint64 AddressLimit)
{
....
PoolMask = (( 1 << ( HashKeyShift - PoolBitShift ) ) - 1);
....
}
};
Unreal Engine 4
PVS-Studio: V629 Consider inspecting the '1 << (HashKeyShift - PoolBitShift)'
expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-
bit type. mallocbinned.h 800
class FMallocBinned : public FMalloc
{
uint64 PoolMask;
....
FMallocBinned(uint32 InPageSize, uint64 AddressLimit)
{
....
PoolMask = (( 1ULL << ( HashKeyShift - PoolBitShift ) ) - 1);
....
}
};
Unreal Engine 4
Godot Engine
void vp9_adjust_mask(....) {
....
const uint64_t columns = cm->mi_cols - mi_col;
const uint64_t mask_y =
(((1 << columns) - 1)) * 0x0101010101010101ULL;
....
}
PVS-Studio: V629 CWE-190 Consider inspecting the '1 << columns'
expression. Bit shifting of the 32-bit value with a subsequent expansion to
the 64-bit type. vp9_loopfilter.c 851
size_t N = ~0U;
• In 32-bit code everything is OK
• 64-bit code, expectation: 0xFFFFFFFFFFFFFFFF
• 64-bit code, reality: 0x00000000FFFFFFFF
size_t nRenderMeshSize = ~0U;
PVS-Studio: V101 Implicit assignment type conversion to memsize type.
StatObjLoad.cpp 1369
CryEngine
size_t N = ~(1024u - 1);
• In 32-bit code everything is OK
• 64-bit code, expectation: 0xFFFFFFFFFFFFFC00
• 64-bit code, reality: 0x00000000FFFFFC00
static const UINT_PTR SmallBlockAlignMask = ~(SmallBlockLength - 1);
FreeBlockHeader* InsertFreeBlock(FreeBlockHeader* after,
UINT_PTR start, UINT_PTR end)
{
bool isFreeBlockDone = (start & SmallBlockAlignMask) ==
(end & SmallBlockAlignMask) ||
(start > end - (SmallBlockLength / 2));
....
}
CryEngine
Increased consumption of the
stack and dynamic memory
Stack
• I suggest taking it easy and just increase it by 2 times :)
Dynamic memory
• Is it worth doing some fighting?
• Are 64-bit integer types needed everywhere?
• Are the pointers needed everywhere?
• Structures’ size
Redundant growth of structures’ size
struct Candidate {
uint32_t face;
ChartBuildData *chart;
float metric;
};
PVS-Studio: V802 On 64-bit platform, structure size can be reduced from 24 to 16
bytes by rearranging the fields according to their sizes in decreasing order. xatlas.cpp
5237
24 bytes
Redundant growth of structures’ size
struct Candidate {
ChartBuildData *chart;
uint32_t face;
float metric;
};
16 bytes
Other error patterns
Magic constants
• 1024 x 768
• 1600 x 1200
Magic constants
• 1024 x 768
• 1600 x 1200
• 4
• 32
• 0x7FFFFFFF
• 0x80000000
• 0xFFFFFFFF
Dangerous tricks with type casting
ResourceBase::Header *header = (*iter).value;
char fourCC[ 5 ];
*( ( U32* ) fourCC ) = header->getSignature();
fourCC[ 4 ] = 0;
Everything will be fine thanks to header, but still very unstable.
PVS-Studio: V1032 The pointer 'fourCC' is cast to a more strictly aligned pointer
type. resourceManager.cpp 127
Torque 3D
Data serialization
• No specific errors and tips
• Review the code
Arrays of various types
Virtual functions
Virtual functions
How to write a 64-bit program of
high-quality?
Specify the code standard
• Use correct types:
ptrdiff_t, size_t, intptr_t, uintptr_t, INT_PTR, DWORD_PTR и т.д.
• Say «NO!» to magic numbers. Use this:
UINT_MAX, ULONG_MAX, std::numeric_limits<size_t>::max() и т.д.
• Explain how to correctly use the explicit type casting in the
expressions and what’s the difference between 1 and
static_cast<size_t>(1)
• unsigned long != size_t
Tooling: unit-tests
Tooling: dynamic analyzers
Tooling: static analyzers
Conclusion
• Even if you already have released a 64-bit application, this doesn’t
mean that it works correctly
• A 64-bit error can lie low for ages
• How to solve the situation:
• learning
• Code standard
• Static code analysis
Useful links
• Lessons on development of 64-bit C/C++ applications (single file)
https://www.viva64.com/ru/l/full/
• A Collection of Examples of 64-bit Errors in Real Programs
https://www.viva64.com/ru/a/0065/
• Undefined behavior is closer than you think
https://www.viva64.com/ru/b/0374/
Time for your questions!
Andrey Karpov
karpov@viva64.com
www.viva64.com

Patterns of 64-bit errors in games

  • 1.
    Patterns of 64-biterrors in games Andrey Karpov karpov@viva64.com
  • 3.
    Speaker • Karpov AndreyNikolaevich • Habr profile: habr.com/users/Andrey2008/ • Microsoft MVP, Intel Black Belt Software Developer • One of the founders of the PVS-Studio project, which started right from the search of 64-bit errors • https://www.viva64.com
  • 4.
    Technically speaking, thereare no «64-bit errors»
  • 5.
    In practice • 64-biterrors are the ones which reveal themselves after building a 64-bit application • By the way, many of them do it only when the 4 Gigabyte limit is exceeded
  • 6.
    Explanation on theexample of malloc • Who will notice an error?
  • 7.
    void Foo() { const size_tGigabyte = 1073741824; void *A[10]; for (size_t i = 0; i < 10; ++i) { A[i] = malloc(Gigabyte); printf("%pn", A[i]); } for (size_t i = 0; i < 10; ++i) free(A[i]); }
  • 9.
    In C-code developersforget to include headers • Your program might not contain calls of malloc, but they might be in third-party libraries • It is revealed only when consuming large amount of memory #include <stdio.h> #include <string.h> //#include <stdlib.h>
  • 10.
    The case isn’tonly about malloc • Fennec Media • memset • memcpy • Ffdshow (media decoder) • memset • memcpy • libZPlay • strlen
  • 11.
    Let’s get backto implicit truncation of 64-bit values
  • 12.
    long type inWin64 const std::vector<Vec2> &vertexList = body->getCalculatedVertexList(); unsigned long length = vertexList.size(); PVS-Studio: V103 Implicit type conversion from memsize to 32-bit type. CCArmature.cpp 614 Cocos2d-x
  • 13.
    int type isa bad idea in any case PVS-Studio: V104 Implicit conversion of 'i' to memsize type in an arithmetic expression: i < points_.size() sweep_context.cc 76 std::vector<Point*> points_; void SweepContext::InitTriangulation() { // Calculate bounds. for (unsigned int i = 0; i < points_.size(); i++) { Point& p = *points_[i]; .... Cocos2d-x
  • 14.
    By the way,such errors are more insidious than it looks • When overflowing int, undefined behavior occurs • UB might be the case when everything works correctly :) • More details: "Undefined behavior is closer than you think" https://www.viva64.com/ru/b/0374/ size_t N = foo(); for (int i = 0; i < N; ++i)
  • 15.
    Usage of correctmemsize-types
  • 16.
    "memsize" types • ptrdiff_t- signed type (distance between pointers) • size_t – maximum size of theoretically possible object of any type, including arrays • rsize_t – size of a single object • intptr_t - signed type, can store a pointer • uintptr_t – unsigned type, can store a pointer
  • 17.
    Usage of memsize-types •Number of elements in an array • Buffer size • Counters • Pointer storing (although, it’s better not to do it) • Pointer arithmetic • And so on
  • 18.
    Using of memsize-types:bad thing PVS-Studio: V109 Implicit type conversion of return value 'a * b * c' to memsize type. test.cpp 22 Potential overflow size_t Foo(int a, int b, int c) { return a * b * c; }
  • 19.
    Using of memsize-types:OK size_t Foo(int a, int b, int c) { return static_cast<size_t>(a) * static_cast<size_t>(b) * static_cast<size_t>(c); } size_t Foo(int a, int b, int c) { return static_cast<size_t>(a) * b * c; }
  • 20.
    JUST DON’T DOIT! std::vector<unsigned char> buf( static_cast<size_t>(exr_image->width * h * pixel_data_size)); PVS-Studio: V1028 CWE-190 Possible overflow. Consider casting operands, not the result. tinyexr.h 11682 Possible overflow again TinyEXR
  • 21.
    This way errorsonly hide more if (components == 1) { images[0].resize(static_cast<size_t>(width * height)); memcpy(images[0].data(), data, sizeof(float) * size_t(width * height)); } else { images[0].resize(static_cast<size_t>(width * height)); images[1].resize(static_cast<size_t>(width * height)); images[2].resize(static_cast<size_t>(width * height)); images[3].resize(static_cast<size_t>(width * height)); for (size_t i = 0; i < static_cast<size_t>(width * height); i++) { TinyEXR
  • 22.
    More about pointerarithmetic int A = -2; unsigned B = 1; int array[5] = { 1, 2, 3, 4, 5 }; int *ptr = array + 3; ptr = ptr + (A + B); printf("%in", *ptr); //Access violation //on 64-bit platform
  • 23.
    Developers forget toexpand 32-bit types
  • 24.
    int64_t X =1 << N • This code doesn’t work in 32-bit programs as well • Setting most significant bits is usually not needed there
  • 25.
    class FMallocBinned :public FMalloc { uint64 PoolMask; .... FMallocBinned(uint32 InPageSize, uint64 AddressLimit) { .... PoolMask = (( 1 << ( HashKeyShift - PoolBitShift ) ) - 1); .... } }; Unreal Engine 4 PVS-Studio: V629 Consider inspecting the '1 << (HashKeyShift - PoolBitShift)' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64- bit type. mallocbinned.h 800
  • 26.
    class FMallocBinned :public FMalloc { uint64 PoolMask; .... FMallocBinned(uint32 InPageSize, uint64 AddressLimit) { .... PoolMask = (( 1ULL << ( HashKeyShift - PoolBitShift ) ) - 1); .... } }; Unreal Engine 4
  • 27.
    Godot Engine void vp9_adjust_mask(....){ .... const uint64_t columns = cm->mi_cols - mi_col; const uint64_t mask_y = (((1 << columns) - 1)) * 0x0101010101010101ULL; .... } PVS-Studio: V629 CWE-190 Consider inspecting the '1 << columns' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. vp9_loopfilter.c 851
  • 28.
    size_t N =~0U; • In 32-bit code everything is OK • 64-bit code, expectation: 0xFFFFFFFFFFFFFFFF • 64-bit code, reality: 0x00000000FFFFFFFF
  • 29.
    size_t nRenderMeshSize =~0U; PVS-Studio: V101 Implicit assignment type conversion to memsize type. StatObjLoad.cpp 1369 CryEngine
  • 30.
    size_t N =~(1024u - 1); • In 32-bit code everything is OK • 64-bit code, expectation: 0xFFFFFFFFFFFFFC00 • 64-bit code, reality: 0x00000000FFFFFC00
  • 31.
    static const UINT_PTRSmallBlockAlignMask = ~(SmallBlockLength - 1); FreeBlockHeader* InsertFreeBlock(FreeBlockHeader* after, UINT_PTR start, UINT_PTR end) { bool isFreeBlockDone = (start & SmallBlockAlignMask) == (end & SmallBlockAlignMask) || (start > end - (SmallBlockLength / 2)); .... } CryEngine
  • 32.
    Increased consumption ofthe stack and dynamic memory
  • 33.
    Stack • I suggesttaking it easy and just increase it by 2 times :)
  • 34.
    Dynamic memory • Isit worth doing some fighting? • Are 64-bit integer types needed everywhere? • Are the pointers needed everywhere? • Structures’ size
  • 35.
    Redundant growth ofstructures’ size struct Candidate { uint32_t face; ChartBuildData *chart; float metric; }; PVS-Studio: V802 On 64-bit platform, structure size can be reduced from 24 to 16 bytes by rearranging the fields according to their sizes in decreasing order. xatlas.cpp 5237 24 bytes
  • 36.
    Redundant growth ofstructures’ size struct Candidate { ChartBuildData *chart; uint32_t face; float metric; }; 16 bytes
  • 37.
  • 38.
    Magic constants • 1024x 768 • 1600 x 1200
  • 39.
    Magic constants • 1024x 768 • 1600 x 1200 • 4 • 32 • 0x7FFFFFFF • 0x80000000 • 0xFFFFFFFF
  • 40.
    Dangerous tricks withtype casting ResourceBase::Header *header = (*iter).value; char fourCC[ 5 ]; *( ( U32* ) fourCC ) = header->getSignature(); fourCC[ 4 ] = 0; Everything will be fine thanks to header, but still very unstable. PVS-Studio: V1032 The pointer 'fourCC' is cast to a more strictly aligned pointer type. resourceManager.cpp 127 Torque 3D
  • 41.
    Data serialization • Nospecific errors and tips • Review the code
  • 42.
  • 43.
  • 44.
  • 45.
    How to writea 64-bit program of high-quality?
  • 46.
    Specify the codestandard • Use correct types: ptrdiff_t, size_t, intptr_t, uintptr_t, INT_PTR, DWORD_PTR и т.д. • Say «NO!» to magic numbers. Use this: UINT_MAX, ULONG_MAX, std::numeric_limits<size_t>::max() и т.д. • Explain how to correctly use the explicit type casting in the expressions and what’s the difference between 1 and static_cast<size_t>(1) • unsigned long != size_t
  • 47.
  • 48.
  • 49.
  • 50.
    Conclusion • Even ifyou already have released a 64-bit application, this doesn’t mean that it works correctly • A 64-bit error can lie low for ages • How to solve the situation: • learning • Code standard • Static code analysis
  • 51.
    Useful links • Lessonson development of 64-bit C/C++ applications (single file) https://www.viva64.com/ru/l/full/ • A Collection of Examples of 64-bit Errors in Real Programs https://www.viva64.com/ru/a/0065/ • Undefined behavior is closer than you think https://www.viva64.com/ru/b/0374/
  • 52.
    Time for yourquestions! Andrey Karpov karpov@viva64.com www.viva64.com