Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly access mapped memory without undefined behavior in C++

Tags:

I've been trying to figure out how to access a mapped buffer from C++17 without invoking undefined behavior. For this example, I'll use a buffer returned by Vulkan's vkMapMemory.

So, according to N4659 (the final C++17 working draft), section [intro.object] (emphasis added):

The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition (6.1), by a new-expression (8.3.4), when implicitly changing the active member of a union (12.3), or when a temporary object is created (7.4, 15.2).

These are, apparently, the only valid ways to create a C++ object. So let's say we get a void* pointer to a mapped region of host-visible (and coherent) device memory (assuming, of course, that all the required arguments have valid values and the call succeeds, and the returned block of memory is of sufficient size and properly aligned):

void* ptr{}; vkMapMemory(device, memory, offset, size, flags, &ptr); assert(ptr != nullptr); 

Now, I wish to access this memory as a float array. The obvious thing to do would be to static_cast the pointer and go on my merry way as follows:

volatile float* float_array = static_cast<volatile float*>(ptr); 

(The volatile is included since this is mapped as coherent memory, and thus may be written by the GPU at any point). However, a float array doesn't technically exist in that memory location, at least not in the sense of the quoted excerpt, and thus accessing the memory through such a pointer would be undefined behavior. Therefore, according to my understanding, I'm left with two options:

1. memcpy the data

It should always be possible to use a local buffer, cast it to std::byte* and memcpy the representation over to the mapped region. The GPU will interpret it as instructed in the shaders (in this case, as an array of 32-bit float) and thus problem solved. However, this requires extra memory and extra copies, so I would prefer to avoid this.

2. placement-new the array

It appears that section [new.delete.placement] doesn't impose any restrictions on how the placement address is obtained (it need not be a safely-derived pointer regardless of the implementation's pointer safety). It should, therefore, be possible to create a valid float array via placement-new as follows:

volatile float* float_array = new (ptr) volatile float[sizeInFloats]; 

The pointer float_array should now be safe to access (within the bounds of the array, or one-past).


So, my questions are the following:

  1. Is the simple static_cast indeed undefined behavior?
  2. Is this placement-new usage well-defined?
  3. Is this technique applicable to similar situations, such as accessing memory-mapped hardware?

As a side note, I've never had an issue by simply casting the returned pointer, I'm just trying to figure out what the proper way to do this would be, according to the letter of the standard.

like image 746
Socrates Zouras Avatar asked Nov 16 '18 15:11

Socrates Zouras


People also ask

What is non-memory mapped region in C programming?

Non-memory mapped region includes internal general purpose and special function registers of CPU. These registers do not have addresses. We can access them using register names in assembly language. In C programming, we can access these registers using inline assembly language features of c programming.

What are the advantages of undefined behavior in C++?

Advantages of Undefined Behavior. C and C++ have undefined behaviors because it allows compilers to avoid lots of checks. Suppose a set of code with greater performing array need not keep a look at the bounds, which avoid the needs for complex optimization pass to check such conditions outside loops.

What is memory mapping?

Memory Mapping in Embedded Processors and Microcontrollers Microcontrollers or microprocessors have two types of memory regions such as memory mapped region and non-memory mapped region.

What is the addressable memory space of a 32-bit microcontroller?

For instance, if we take the example of ARM Cortex M4 32-bit microcontroller, its addressable memory space is 2^32 which is equal to 4 gigabytes of memory. Each byte of this memory space has a unique memory address and the Cortex M4 microcontroller can access each memory location either to read and write data to each memory location.


2 Answers

The C++ spec has no concept of mapped memory, so everything to do with it is undefined behavior as far as the C++ spec is concerned. So you need to look to the particular implementation (compiler and operating system) that you are using to see what is defined and what you can do safely.

On most systems, mapping will return memory that came from somewhere else, and may (or may not) have been initialized in a way that is compatible with some specific type. In general, if the memory was originally written as float values of the correct, supported form, then you can safely cast the pointer to a float * and access it that way. But you do need to know how the memory being mapped was originally written.

like image 43
Chris Dodd Avatar answered Oct 22 '22 20:10

Chris Dodd


Short answer

As per the Standard, everything involving hardware-mapped memory is undefined behavior since that concept does not exist for the abstract machine. You should refer to your implementation manual.


Long answer

Even though hardware-mapped memory is undefined behavior by the Standard, we can imagine any sane implementation providing some obeys common rules. Some constructs are then more undefined behavior than others (whatever that means).

Is the simple static_cast indeed undefined behavior?

volatile float* float_array = static_cast<volatile float*>(ptr); 

Yes, this is undefined behavior and have been discussed many times on StackOverflow.

Is this placement-new usage well-defined?

volatile float* float_array = new (ptr) volatile float[N]; 

No, even though this looks well defined, this is implementation dependent. As it happens, operator ::new[] is allowed to reserve some overhead1, 2, and you cannot know how much unless you check your toolchain documentation. As a consequence, ::new (dst) T[N] requires an unknown amount of memory greater or equal to N*sizeof T and any dst you allocate might be too small, involving buffer overflow.

How to proceed, then?

A solution would be to manually build a sequence of floats:

auto p = static_cast<volatile float*>(ptr); for (std::size_t n = 0 ; n < N; ++n) {     ::new (p+n) volatile float; } 

Or equivalently, relying on the Standard Library:

#include <memory> auto p = static_cast<volatile float*>(ptr); std::uninitialized_default_construct(p, p+N); 

This constructs contiguously N uninitialized volatile float objects at the memory pointed to by ptr. This means you must initialize those before reading them; reading an uninitialized object is undefined behavior.

Is this technique applicable to similar situations, such as accessing memory-mapped hardware?

No, again this is really implementation-defined. We can only assume your implementation took reasonable choices, but you should check what its documentation says.

like image 137
YSC Avatar answered Oct 22 '22 20:10

YSC