3

In low level languages it is possible to mov a dword (32 bit) to the first array element this will overflow to write to the second, third and fourth element, or mov a word (16 bit) to the first and it will overflow to the second element.

How to achieve the same effect in c? as when trying for example:

char txt[] = {0, 0};
txt[0] = 0x4142;

it gives a warning [-Woverflow]

and the value of txt[1] doesn't change and txt[0] is set to 0x42.

How to get the same behavior as in assembly:

mov word [txt], 0x4142

the previous assembly instruction will set the first element [txt+0] to 0x42 and the second element [txt+1] to 0x41.

EDIT

What about this suggestion?

define the array as a single variable.

uint16_t txt;
txt = 0x4142;

and accessing the elements with ((uint8_t*) &txt)[0] for the first element and ((uint8_t*) &txt)[1] for the second element.

user37421
  • 407
  • 1
  • 5
  • 12
  • 4
    `*(uint16_t*)txt = 0x4142;` – Jester May 17 '18 at 11:36
  • 4
    In C you can use a `union` for type-punning like that. In both C and C++ you can use `memcpy`. I'm not sure if casting like shown in the comment by @Jester doesn't break strict aliasing, and the rules for such casting may be different in C and C++ (so please pick *one* programming language). – Some programmer dude May 17 '18 at 11:39
  • @Jester Will it work in terms of endian and data align? – Jose May 17 '18 at 11:41
  • 3
    @Someprogrammerdude `(uint16_t*)txt`, at a minimum, is an issue due to alignment concerns. – chux - Reinstate Monica May 17 '18 at 11:42
  • 1
    @Jester `*(uint16_t*)txt = 0x4142;` is a strict-aliasing violation and UB. – Andrew Henle May 17 '18 at 11:42
  • 3
    @Someprogrammerdude I believe it would be a strict aliasing violation. The address didn't start out as that of a `uint16_t`. – Andrew Henle May 17 '18 at 11:42
  • Yeah it's bad. But given the type already, and the equivalent x86 instruction, that's basically what it is. Sure you can achieve the same thing with a `union` but that would need changing of the type. `memcpy` would also work but that also doesn't match the assembly which does not copy memory. – Jester May 17 '18 at 11:44
  • 1
    While the Jester's code is UB generally, on x86 platform ("nasm" tag) the endianness and alignment are not an issue for `uint16_t` type (could be issue for 64+ bit types, then `memcpy` from well defined constant is safer, but overall the OP is requesting non-C like feature, so he must expect some UB/breakage of strict C rules. – Ped7g May 17 '18 at 11:45
  • 2
    The -Woverflow represents the fact that a `char` cannot represent the value `0x4142` (at least on your target system). The value `0x4142` will be converted to `char` BEFORE doing the assignment. If `char` is `unsigned` the conversion will use modulo arithmetic to produce a value in the range that a `char` can represent. The assignment `txt[0] = 0x4142;` therefore does not affect `txt[1]`. If `char` is `signed`, the result of the conversion is undefined behaviour. In short, there is no defined way that an assignment `txt[0] = some_integral_value` changes `txt[1]`. – Peter May 17 '18 at 11:55
  • @peter Perhaps not UB when "If char is signed", but _implementation-defined_. Details: C11 §6.3.1.3 3. Still the overall comment is good. – chux - Reinstate Monica May 17 '18 at 12:21

4 Answers4

8

If you are totally sure this will not cause a segmentation fault, which you must be, you can use memcpy()

uint16_t n = 0x4142;
memcpy((void *)txt, (void *)&n, sizeof(uint16_t));

By using void pointers, this is the most versatile solution, generalizable to all the cases beyond this example.

Attersson
  • 4,755
  • 1
  • 15
  • 29
2

txt[0] = 0x4142; is an assignment to a char object, so the right hand side is implicitly cast to (char) after being evaluated.

The NASM equivalent is mov byte [rsp-4], 'BA'. Assembling that with NASM gives you the same warning as your C compiler:

foo.asm:1: warning: byte data exceeds bounds [-w+number-overflow]

Also, modern C is not a high-level assembler. C has types, NASM doesn't (operand-size is on a per-instruction basis only). Don't expect C to work like NASM.

C is defined in terms of an "abstract machine", and the compiler's job is to make asm for the target CPU which produces the same observable results as if the C was running directly on the C abstract machine. Unless you use volatile, actually storing to memory doesn't count as an observable side-effect. This is why C compilers can keep variables in registers.

And more importantly, things that are undefined behaviour according to the ISO C standard may still be undefined when compiling for x86. For example, x86 asm has well-defined behaviour for signed overflow: it wraps around. But in C, it's undefined behaviour, so compilers can exploit this to make more efficient code for for (int i=0 ; i<=len ;i++) arr[i] *= 2; without worrying that i<=len might always be true, giving an infinite loop. See What Every C Programmer Should Know About Undefined Behavior.

Type-punning by pointer-casting other than to char* or unsigned char* (or __m128i* and other Intel SSE/AVX intrinsic types, because they're also defined as may_alias types) violates the strict-aliasing rule. txt is a char array, but I think it's still a strict-aliasing violation to write it through a uint16_t* and then read it back via txt[0] and txt[1].

Some compilers may define the behaviour of *(uint16_t*)txt = 0x4142, or happen to produce the code you expect in some cases, but you shouldn't count on it always working and being safe other code also reads and writes txt[].

Compilers (i.e. C implementations, to use the terminology of the ISO standard) are allowed to define behaviour that the C standard leaves undefined. But in a quest for higher performance, they choose to leave a lot of stuff undefined. This is why compiling C for x86 is not similar to writing in asm directly.

Many people consider modern C compilers to be actively hostile to the programmer, looking for excuses to "miscompile" your code. See the 2nd half of this answer on gcc, strict-aliasing, and horror stories, and also the comments. (The example in that answer is safe with a proper memcpy; the problem was a custom implementation of memcpy that copied using long*.)


Here's a real-life example of a misaligned pointer leading to a fault on x86 (because gcc's auto-vectorization strategy assumed that some whole number of elements would reach a 16-byte alignment boundary. i.e. it depended on the uint16_t* being aligned.)


Obviously if you want your C to be portable (including to non-x86), you must use well-defined ways to type-pun. In ISO C99 and later, writing one union member and reading another is well-defined. (And in GNU C++, and GNU C89).

In ISO C++, the only well-defined way to type-pun is with memcpy or other char* accesses, to copy object representations.

Modern compilers know how to optimize away memcpy for small compile-time constant sizes.

#include <string.h>
#include <stdint.h>
void set2bytes_safe(char *p) {
    uint16_t src = 0x4142;
    memcpy(p, &src, sizeof(src));
}

void set2bytes_alias(char *p) {
    *(uint16_t*)p = 0x4142;
}

Both functions compile to the same code with gcc, clang, and ICC for x86-64 System V ABI:

# clang++6.0 -O3 -march=sandybridge
set2bytes_safe(char*):
    mov     word ptr [rdi], 16706
    ret

Sandybridge-family doesn't have LCP decode stalls for 16-bit mov immediate, only for 16-bit immediates with ALU instructions. This is an improvement over Nehalem (See Agner Fog's microarch guide), but apparently gcc8.1 -march=sandybridge doesn't know about it because it still likes to:

    # gcc and ICC
    mov     eax, 16706
    mov     WORD PTR [rdi], ax
    ret

define the array as a single variable.

... and accessing the elements with ((uint8_t*) &txt)[0]

Yes, that's fine, assuming that uint8_t is unsigned char, because char* is allowed to alias anything.

This is the case on almost any implementation that supports uint8_t at all, but it's theoretically possible to build one where it's not, and char is a 16 or 32-bit type, and uint8_t is implemented with a more expensive read/modify/write of the containing word.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
1

One option is to Trust Your Compiler(tm) and just write proper code.

With this test code:

#include <iostream>

int main() {
    char txt[] = {0, 0};
    txt[0] = 0x41;
    txt[1] = 0x42;

    std::cout << txt;
}

Clang 6.0 produces:

int main() {
00E91020  push        ebp  
00E91021  mov         ebp,esp  
00E91023  push        eax  
00E91024  lea         eax,[ebp-2]  
char txt[] = {0, 0};
00E91027  mov         word ptr [ebp-2],4241h    <-- Combined write, without any tricks!
txt[0] = 0x41;
txt[1] = 0x42;

std::cout << txt;
00E9102D  push        eax  
00E9102E  push        offset cout (0E99540h)  
00E91033  call        std::operator<<<std::char_traits<char> > (0E91050h)  
00E91038  add         esp,8  
}
00E9103B  xor         eax,eax  
00E9103D  add         esp,4  
00E91040  pop         ebp  
00E91041  ret  
Bo Persson
  • 90,663
  • 31
  • 146
  • 203
  • +1, but note that gcc6 and earlier don't do store coalescing (https://godbolt.org/g/HZqFLK). ICC18 also compiles it to two separate byte stores. gcc7/8, clang, and MSVC all get it right, though. But note that this is only the same as `memcpy` from `0x4142` on a little-endian target like x86. But it's more likely that you know what order you want the bytes in memory, so this is probably an advantage for this code and a disadvantage for `memcpy`, though! – Peter Cordes May 17 '18 at 21:41
0

You're looking to do a deep copy which you'll need to use a loop to accomplish (or a function that does the loop for you internally: memcpy).

Simply assigning 0x4142 to a char will have to be truncated to fit in the char. This should throw a warning as the outcome will be implementation specific, but typically the least significant bits are retained.


In any case, if you know the numbers you want to assign you could just construct using them: const char txt[] = { '\x41', '\x42' };


I'd suggest doing this with an initializer-list, obviously it's on you to make sure the initializer list is at least as long as size(txt). For example:

copy_n(begin({ '\x41', '\x42' }), size(txt), begin(txt));

Live Example

Jonathan Mee
  • 37,899
  • 23
  • 129
  • 288
  • `char txt[] = { '\x41', '\x42' };` also requires knowledge about the endian. Given x86 (implied by NASM tag) being little endian, OP may want the opposite order. – chux - Reinstate Monica May 17 '18 at 11:56
  • @chux No? Characters have no endianness. If you're saying that the OP may have intended `const char txt[] = { '\x412, '\x41' }`, then yes I assume he would be able to write that? – Jonathan Mee May 17 '18 at 11:59
  • 2
    My comment was based on " first element [txt+0] to 0x42 and the second element [txt+1] to 0x41", and `char txt[] = { '\x41', '\x42' };` unnecessarily shows the opposite. – chux - Reinstate Monica May 17 '18 at 12:01
  • @chux OK yeah I got what you're saying. I view this question as a little strategically questionable. Conversion from a larger integer type to a `char[]` seems like the difficult way to do this. I've updated my answer to include a suggestion to use an `initializer_list` which will not suffer from endian ambiguities, and will be platform independent. – Jonathan Mee May 17 '18 at 12:14