简体   繁体   中英

memcpy() for a bare-metal environment

This is more of a curiosity than anything else. But I was wondering, how legitimate would this code be for implementing memcpy() for a bare-metal environment?

#define MY_MEMCPY(DST, SRC, SIZE) \
    { struct tmp { char mem[SIZE]; }; *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

We can then test it using

#include <stdio.h>

#define MY_MEMCPY(DST, SRC, SIZE) \
    { struct tmp { char mem[SIZE]; }; *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

int main () {

    char buffer[100] = "Hello world";

    printf("%s\n", buffer);

    MY_MEMCPY(buffer, "one", 4)
    printf("%s\n", buffer);

    MY_MEMCPY(buffer, "two", 4)
    printf("%s\n", buffer);

    MY_MEMCPY(buffer, "three", 6)
    printf("%s\n", buffer);

    return 0;

}

which prints

Hello world
one
two
three

From what I understand it would not violate the strict aliasing rule, since a pointer to a struct is always equal to a pointer to its first member, and in this case the first member is a char . See 6.7.2.1p15 :

A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa.

It would also not have alignment problems, since its _Alignof() is 1 .

Further reading:

EDIT #1

Only for literal strings , we can create another version of the macro that does not require any length to be passed as argument. Of course the destination still needs to have enough memory to hold the new string.

Here is the modified version:

/*
    **WARNING** This macro works only when `SRC` is **a literal string** or
    in all other cases where its size can be calculated using `sizeof()`
*/
#define MY_LITERAL_MEMCPY(DST, SRC) \
    { struct tmp { char mem[sizeof(SRC)]; }; *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

We can test it using

#include <stdio.h>

/*
    **WARNING** This macro works only when `SRC` is **a literal string** or
    in all other cases where its size can be calculated using `sizeof()`
*/
#define MY_LITERAL_MEMCPY(DST, SRC) \
    { struct tmp { char mem[sizeof(SRC)]; }; *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

int main () {

    char buffer[100] = "Hello world";

    printf("%s\n", buffer);

    MY_LITERAL_MEMCPY(buffer, "one")
    printf("%s\n", buffer);

    MY_LITERAL_MEMCPY(buffer, "two")
    printf("%s\n", buffer);

    MY_LITERAL_MEMCPY(buffer, "three")
    printf("%s\n", buffer);

    return 0;

}

EDIT #2

In case you are worried about any possible padding added by a hypothetical alien compiler, adding a _Static_assert() will make the macro very safe:

MY_MEMCPY() :

#define MY_MEMCPY(DST, SRC, SIZE) \
    { struct tmp { char mem[SIZE]; }; _Static_assert(sizeof(struct tmp) \
    == SIZE, "You have a very stupid compiler"); \
    *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

MY_LITERAL_MEMCPY() :

/*
    **WARNING** This macro works only when `SRC` is **a literal string** or
    in all other cases where its size can be calculated using `sizeof()`
*/
#define MY_LITERAL_MEMCPY(DST, SRC) \
    { struct tmp { char mem[sizeof(SRC)]; }; _Static_assert(sizeof(struct tmp) \
    == sizeof(SRC), "You have a very stupid compiler"); \
    *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

EDIT #3

Discussion about the legitimacy of the code

If it is legal to cast any memory location to a char * , then we can map each single byte of a non- char type to a different char * variable:

    some_non_char_type test;

    char * one = (char *) &test;
    char * two = (char *) &test + 1;
    char * three = (char *) &test + 2;

    ...

    char * last = (char *) &test + sizeof(test) - 1;

If the code above is legal, it is also legal to map collectively all the bytes above to a single char array, since we are mapping adjacent bytes:

    char (* all_of_them)[sizeof(some_non_char_type)] = (char (*)[sizeof(some_non_char_type)]) &test;

In this case we would access them as (*all_of_them)[0] , (*all_of_them)[1] , (*all_of_them)[2] , etc.

If it is legal to map a collection of adjacent bytes to a char array, then it is legal to cast such array as a single-member aggregate type, provided that the compiler does not add padding to the latter:

    struct tmp {
        char mem[sizeof(some_non_char_type)];
    };

    _Static_assert(sizeof(struct tmp) == sizeof(some_non_char_type),
        "You have a very stupid compiler");

    struct tmp * wrap = (struct tmp *) &test;

EDIT #4

This is a reply to Nate Eldredge's answer – as it seems that with optimizations enabled the compiler can make wrong assumptions. It is enough to tell explicitly the compiler about our aliasing by adding a simple *((char *) DST) = 0 before copying the data to DST . Here the new versions of the macros that will work also with optimizations enabled:

MY_MEMCPY() :

#define MY_MEMCPY(DST, SRC, SIZE) \
    { struct tmp { char mem[SIZE]; }; _Static_assert(sizeof(struct tmp) \
    == SIZE, "You have a very stupid compiler"); *((char *) DST) = 0; \
    *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

MY_LITERAL_MEMCPY(): :

/*
    **WARNING** This macro works only when `SRC` is **a literal string** or
    in all other cases where its size can be calculated using `sizeof()`
*/
#define MY_LITERAL_MEMCPY(DST, SRC) \
    { struct tmp { char mem[sizeof(SRC)]; }; _Static_assert(sizeof(struct tmp) \
    == sizeof(SRC), "You have a very stupid compiler"); *((char *) DST) = 0; \
    *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }

Original

From what I understand it would not violate the strict aliasing rule, since a pointer to a struct is always equal to a pointer to its first member,…

This is incorrect. The rule that a pointer to a structure can be converted to a pointer to its first member and vice-versa is solely a rule about pointer conversion. The strict aliasing rule is a rule about accessing an object, regardless of how the pointer to it was obtained.

Whatever type your DST or SRC is, it is presumably not a struct tmp . Then accessing its memory as if it were a struct tmp violates the aliasing rule in C 2018 6.5 7, which says that an object shall be accessed only via a compatible type, a character type, or certain other special cases.

One of those special cases may save you if DST or SRC is an array of char . One of the allowed types for access is “an aggregate or union type that includes one of the aforementioned types among its members…” Since struct tmp includes an array of char among its members, this would seem to satisfy the requirement. However, in order to be a compatible type, the two arrays—the one in struct tmp and the one that is SRC or DST —must be the same size.

A purpose of C aliasing rule is to allow a compiler to conclude that accesses to two different types of things access different memory. For example, if a function is passed a float * and an int * , and it copies elements from one into the other, the aliasing rule allows the compiler to conclude the arrays they point to do not overlap, and therefore it can optimize the loop by copying several elements at a time. For the same reason, when you use an assignment of struct tmp types, the optimizer in the compiler may conclude that this assignment does not affect nearby operations on DST that are performed in a different way, and the resulting optimization can break your program.

Another problem is that a C implementation is allowed to include padding at the end of an array, regardless of whether it is needed for alignment. So you cannot be sure, based on the C standard alone, that the size of struct tmp { char mem[SIZE]; } struct tmp { char mem[SIZE]; } is SIZE bytes.

“Edit #3”

“EDIT #3” argues that any object can be written using a char * . This is correct, although it incorrectly phrases it as that “it is legal to cast any memory location to a char * .“ As stated previously, the fact that various pointer conversions are allowed and are partially or fully defined is separate from whether or not accessing an object via particular lvalue types is defined. C 2018 6.3.2.3 7 says any pointer to an object type may be converted to any other pointer to an object type as long as the alignment is correct, but 6.5 7 says an object shall be accessed only by lvalue expressions with certain types. So clearly the fact that a pointer may be converted does not mean the pointed-to object may be accessed with the new type. (For convenience herein, “may” or “may not” refers to whether the behavior of the action is or is not defined by the C standard. Literally, a program “may” try essentially anything but, for this discussion, we are only interested in what the C standard defines.)

“Edit #3” further argues that “it is also legal to map collectively all the bytes above to a single char array, since we are mapping adjacent bytes.” At this point, “Edit #3” presents code defining a pointer to an array of the bytes. There is no basis for this inference in the C standard; no rule in the C standard says that the bytes of an object in memory, which may be accessed individually via a character lvalue, may be treated as an array of characters. Further, the rules for pointer conversion do not say that converting a pointer to char to a pointer to an array of char results in a pointer value that actually points to the memory where the array of char would be. 6.3.2.3 7 only says that converting the pointer back again yields a value equal to the original pointer.

“Edit #3” further argues that because we can “map” the bytes as an array of char , we can “cast” (again wrong, the issue is the lvalue used for access, not the validity of the pointer conversion) the pointer to the array to a pointer to a structure containing the array type. This is incorrect because the rule in 6.5 7 does not say that, if you can access something using type X, and type Y is compatible with X or is one of the other cases listed with respect to X, then you can access the thing with type Y. 6.5 7 is phrased in terms of what the type of the object is , not the other types with which you might legitimately access it.

In other words 6.5 7 is not transitive. If there is some circumstance in its rules in which an object O of type X can be accessed with type Y, and there is some circumstance in which an object of type Y can be accessed with type Z, that does not mean an object of type X can be accessed with type Z.

Furthermore, 6.5 7 is phrased as an absolute prohibition. It says an object shall have its stored value accessed only by certain lvalue expressions. If that rule is violated, the behavior is undefined, and that is conclusive.

Dynamic Allocation

In a comment moved to chat, the OP argued “If I can access a series of adjacent bytes as if they were of type char, I can access all of them collectively as if they were of type char []. If that was not the case, we could not do char * newmem = malloc(1000); newmem[whatever] = 'a';, since we would be illegitimately casting a char * to a char [].”

This argument omits certain conditions. The C standard states rules for the effective type of objects. For dynamically allocated objects (which have no declared type; they are created by allocating memory and assigning values to them through pointers), you can change its effective type by assigning a value to it using any lvalue other than a character type (6.5 6). This means the assignment to DST via an lvalue that is a structure containing an array of char would be defined by the C standard (if the value of the right operand were also defined). However, two problems remain:

  • Reading SRC is not defined, because it is not (in general) a structure containing an array of char , so reading it via an lvalue that is a structure containing an array of char is not defined, by 6.5 7 (except in cases where it actually is such a structure or one of the types specifically permitted by 6.5 7).
  • Once DST is given its new effective type by the assignment, reading it as any type other than those permitted by 6.5 7 has undefined behavior.

I'm not enough of a language lawyer to be able to opine on whether the code is correct as far as strict aliasing. However, it appears that GCC thinks it is not.

I came up with the following counterexample:

#include <string.h>
#include <stdio.h>

#define SIZE 50
#define STRUCT_COPY 

#ifdef STRUCT_COPY
// your proposed macro
#define MY_MEMCPY(DST, SRC, SIZE) \
    { struct tmp { char mem[SIZE]; }; _Static_assert(sizeof(struct tmp) \
    == SIZE, "You have a very stupid compiler"); \
    *((struct tmp *) ((void *) DST)) = *((struct tmp *) ((void *) SRC)); }
#else
#define MY_MEMCPY memcpy
#endif


void foo(int *x, void *a, void *b) {
    (*x)++;
    MY_MEMCPY(a, b, SIZE * sizeof(int));
    (*x)++;
}

int a[SIZE], b[SIZE];

int main(void) {
    a[0] = 10;
    b[0] = 20;
    foo(&a[0], a, b);
    printf("a[0] = %d", a[0]);
}

If MY_MEMCPY() was really equivalent to memcpy() then the program should output 21. However, with gcc -O2 on x86-64, it instead outputs 12. Try it on godbolt .

The compiler apparently assumed, contrary to your opinion, that struct tmp * in the macro could not alias int * , and so the (*x)++ could be reordered around the MY_MEMCPY() assignment. As it happens, it loads *x into a register before the copy, adds 2, and stores it back after the copy. So the valued that was copied into a[0] is overwritten by its old value plus 2, instead of being incremented.

Now maybe you think that GCC is in the wrong here, and I suppose you could take that up with the GCC developers by filing a bug report, though I suspect they will stand by their interpretation and decline to "fix" it. But at least from a pragmatic point of view, it would seem unwise to adopt your struct copy implementation if you know that a widely used compiler won't compile it the way you want.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM