@@ -41,10 +41,10 @@ ranges from ignoring the situation completely with unpredictable results, to
41
41
behavior during translation or program execution in a documented manner
42
42
characteristic of the environment (with or without the issuance of a
43
43
diagnostic message), to terminating a translation or execution (with the
44
- issuance of a diagnostic message).
44
+ issuance of a diagnostic message)."
45
45
46
46
For some, those lines might be a bit hard to follow, but the
47
- [ C FAQ] ( https://c-faq.com/ansi/undef.html ) nicely:
47
+ [ C FAQ] ( https://c-faq.com/ansi/undef.html ) sums it up nicely:
48
48
49
49
> Anything at all can happen; the Standard imposes no requirements.
50
50
> The program may fail to compile, or it may execute incorrectly (either
@@ -77,7 +77,7 @@ an example stating, "EXAMPLE An example of unspecified behavior is the order in
77
77
which the arguments to a function are evaluated."
78
78
79
79
Summed up, the compiler team is free to do whatever they want in certain
80
- scenarios when they face undefined behavior, and they never need to report it
80
+ scenarios when they face unspecified behavior, and they never need to report it
81
81
or document it in any way.
82
82
83
83
There are two more defined behaviors in the spec worth touching on briefly:
@@ -178,7 +178,8 @@ int main(void) {
178
178
This allows you to work with the color data at both the bit level and as
179
179
floating-point values without having to copy or cast the data. Cool, right?
180
180
As described earlier, this has caveats. Let's take a look at what the C99 and
181
- C11 ISO specifications say about this.
181
+ [C11 ISO](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf)
182
+ specifications say about this.
182
183
183
184
### The C99 and C11 Standard Definition
184
185
@@ -219,7 +220,7 @@ undefined behavior when accessed.
219
220
220
221
This change is important because it guarantees that type punning itself is not
221
222
undefined behavior according to the actual specification. However, there is a
222
- subtle edge case not mentioned, and that is that reading an invalid object
223
+ subtle edge case not mentioned, one being that reading an invalid object
223
224
representation resulting from the punning could still cause undefined behavior.
224
225
225
226
To handle this, devs relied on `{0}` to zero the entire union. This was
@@ -231,7 +232,9 @@ valid memory state.
231
232
### The C++11 Standard Definition
232
233
233
234
For completion's sake, let's take a look at what C++ states regarding unions.
234
- We'll take a look at the C++14 ISO draft spec, specifically:
235
+ We'll take a look at the
236
+ [C++14 ISO draft spec](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf),
237
+ specifically:
235
238
236
239
> In a union, at most one of the non-static data members can be active at any
237
240
> time, that is, the value of at most one of the non-static data members can be
@@ -280,12 +283,13 @@ set of rules applies to the other. They are different specs, after all.
280
283
281
284
## Compiler Wars
282
285
283
- The two main compilers we tend to use these days in C are GNU GCC and Clang.
284
- The latest news of GNU GCC 15.1 changing the behavior of zero'ing a union had
285
- me check into what Clang is doing, and the most interesting this is that they
286
- almost swapped their own unspecified behaviors.
286
+ The two main compilers we tend to use these days in C are GNU GCC and
287
+ [Clang](https://clang.llvm.org/). The latest news of GNU GCC 15.1 changing the
288
+ behavior of zero'ing a union had me check into what Clang is doing, and the
289
+ most interesting this is that they almost swapped their own unspecified
290
+ behaviors.
287
291
288
- ### GCC (old) - Initialize Entire Union
292
+ ### Creating an Example
289
293
290
294
We'll start with the first base-case of GNU GCC and its historic behavior of
291
295
zero'ing a union. We'll use this example code:
@@ -319,10 +323,14 @@ int main(void) {
319
323
}
320
324
```
321
325
326
+ Now let's run this into [ godbolt] ( https://godbolt.org/ ) and see what happens!
327
+
328
+ ### GCC (old) - Initialize Entire Union
329
+
322
330
The following is the resulting x86-64 assembly from GCC 11.1. We will focus
323
331
on the ` init_f ` assembly that is generated:
324
332
325
- ``` asm
333
+ ``` bash
326
334
init_f:
327
335
push rbp
328
336
mov rbp, rsp
@@ -340,7 +348,7 @@ is the expected behavior devs have come to know.
340
348
341
349
In memory, it would look something like this:
342
350
343
- ``` ascii
351
+ ``` bash
344
352
Memory:
345
353
[00][00][00][00][00][00][00][00]
346
354
^^^^^^^^ first 4 bytes = float f
@@ -356,7 +364,7 @@ Result:
356
364
357
365
Now, let's take a look at how GCC 15.1 handles it with the new change:
358
366
359
- ``` asm
367
+ ``` bash
360
368
init_f:
361
369
push rbp
362
370
mov rbp, rsp
@@ -371,7 +379,7 @@ init_f:
371
379
372
380
Here, we see the following:
373
381
374
- ``` asm
382
+ ``` bash
375
383
pxor xmm0, xmm0
376
384
movss DWORD PTR [rax], xmm0
377
385
```
@@ -381,7 +389,7 @@ the full 8 bytes like we were back with GCC 11.1.
381
389
382
390
In memory, it would look something like this:
383
391
384
- ``` ascii
392
+ ``` bash
385
393
Memory:
386
394
[00][00][00][00][?? ][?? ][?? ][?? ]
387
395
^^^^^^^^ first 4 bytes = float f
@@ -399,7 +407,7 @@ is doing...
399
407
400
408
Same code, same model. We'll take a look at Clang 6.0.0 first:
401
409
402
- ``` asm
410
+ ``` bash
403
411
init_f:
404
412
push rbp
405
413
mov rbp, rsp
@@ -416,7 +424,7 @@ init_d:
416
424
417
425
So this is interesting. The real key points are here:
418
426
419
- ``` asm
427
+ ``` bash
420
428
xorps xmm0, xmm0 # zero xmm0
421
429
movss [rbp-16], xmm0 # write 4 bytes (float)
422
430
mov rax, [rbp-16]
@@ -433,7 +441,7 @@ see what the very latest version of Clang does.
433
441
434
442
This is from Clang 20.1.0:
435
443
436
- ``` asm
444
+ ``` bash
437
445
init_f:
438
446
push rbp
439
447
mov rbp, rsp
@@ -458,7 +466,7 @@ init_f:
458
466
459
467
This is doing a lot more under the hood than before. The key point is here:
460
468
461
- ``` asm
469
+ ``` bash
462
470
xorps xmm0, xmm0 # zero xmm0
463
471
movss [rbp-16], xmm0 # write 4 bytes (float)
464
472
@@ -484,10 +492,10 @@ For anyone who wants to verify this, I created some godbolt links.
484
492
485
493
## Where GCC Breaks Historical Behavior
486
494
487
- So here is where we get to the crux of the issue. For over a decade, C devs
488
- have expected ` {0} ` to fully clear unions. Type punning is allowed in the newer
489
- C standards, and compilers have gone to great lengths to try to maintain memory
490
- safety by adhering to expected behavior, even if it is undefined or
495
+ So here is where we get to the main point of the issue. For over a decade, C
496
+ devs have expected ` {0} ` to fully clear unions. Type punning is allowed in the
497
+ newer C standards, and compilers have gone to great lengths to try to maintain
498
+ memory safety by adhering to expected behavior, even if it is undefined or
491
499
unspecified.
492
500
493
501
The latest change in GCC's handling of this breaks what I consider to be
@@ -506,10 +514,14 @@ code that "worked fine" and are porting it to purpose-built embedded
506
514
Yocto-based distros will be the ones to start finding all these fun bugs.
507
515
508
516
I completely understand why the GNU GCC team is allowed to do this given how
509
- the spec reads. There's also no such thing as truly historical behavior in the
510
- spec. However, there are certain things programmers rely on to be valid, even
511
- if they aren't well-defined. This is one of those cases where I think the
512
- compiler team might want to re-evaluate their decision.
517
+ the spec reads. Many GCC devs claim that
518
+ [ type punning via unions is undefined] ( https://gcc.gnu.org/bugzilla/show_bug.cgi?id=118141#c13 ) .
519
+ There's also no such thing as truly historical behavior in the spec. However,
520
+ there are certain things programmers rely on to be valid, even if they aren't
521
+ well-defined. This is one of those cases where I think the compiler team might
522
+ want to re-evaluate their decision, or at the very least present a solid
523
+ argument as to why they are breaking away from a historical behavior that many
524
+ of us have come to rely on, even if it was considered UB.
513
525
514
526
## Further Reading
515
527
0 commit comments