严格别名规则的那些事

2024-05-12
What is the Strict Aliasing Rule and Why do we care?

Original from github gist

(或者叫类型别名、未定义行为和对齐.. 唔呃)

首先,什么是严格别名(Strict Aliasing)?我们首先描述什么是别名(Aliasing),然后我们可以了解严格别名是什么意思。

在C和C++中,别名(Aliasing)与我们可以通过什么样的表达式类型来访问存储的值有关。在C和C++中,标准规定了哪些表达式类型可以别名哪些类型。编译器和优化器可以假定我们严格遵守别名规则,因此有了严格别名规则这个术语。如果我们试图使用不允许的类型来访问一个值,它被归类为未定义行为(UB)。一旦我们有未定义的行为,直接全部完蛋,程序的结果将不再可靠。

不幸的是,对于严格别名违规,我们通常会得到我们期望的结果,留下可能性,即未来版本的编译器的新优化可能会破坏我们认为是有效的代码。这是不可取的,理解严格别名规则并避免违反它们是一个值得追求的目标。

为了更深入地了解我们为什么关心这个问题,我们将讨论违反严格别名规则时出现的问题,类型别名,因为常用的类型别名技术经常违反严格别名规则,以及如何正确地进行类型别名,以及C++20可能提供的一些帮助,使类型别名更简单、更不容易出错。我们将通过讨论一些逮捕严格别名违规的方法来结束这个讨论。

初步示例

我们先看一些例子,然后我们可以讨论标准具体说了啥,再看一些进一步的例子,然后看看如何避免严格的别名,以及捕获我们漏掉的违规行为。这是一个比较常规的例子(在线例子):

int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

我们有一个*int*指向一个int*占用的内存,这是一个有效的别名。优化器必须假设通过ip的赋值可能更新x占用的值。

下一个例子显示了导致未定义行为的别名(在线例子):

int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
return *i;
}
int main() {
int x = 0;
std::cout << x << "\n"; // Expect 0
x = foo(reinterpret_cast<float*>(&x), &x);
std::cout << x << "\n"; // Expect 0?
}

在函数foo中,我们接收一个int*和一个float*,在这个例子中,我们调用foo并将两个参数都设置为指向同一内存位置,这个例子中包含一个int。注意,reinterpret_cast告诉编译器将表达式视为其模板参数指定的类型。在这种情况下,我们告诉它将表达式 &x 视为float*类型。我们可能会天真地期望第二个cout的结果是0,但是在使用-O2启用优化的情况下,gcc和clang都会产生以下结果:

0
1

这可能不是预期的结果,但完全有效,因为我们已经调用了未定义的行为。一个float不能有效地别名一个int对象。因此,优化器可以假设存储在解引用i时的constant 1将是返回值,因为通过f的存储不能有效地影响一个int对象。将代码插入编译器资源管理器显示这正是发生的事情(在线例子):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret

优化器使用基于类型的别名分析(TBAA)6假定1将被返回,并直接将常量值移动到携带返回值的寄存器eax中。TBAA使用语言规则关于哪些类型可以别名来优化加载和存储。在这种情况下,TBAA知道一个float不能别名一个int,并优化掉了对i的加载。

现在,看规则书

标准到底允许我们做什么,不允许我们做什么?标准语言并不直接,所以对于每一项,我会尽量提供代码示例来说明其含义。

C11标准是怎么说的?

C11标准26.5表达式第7段中说:

"

一个对象只能通过以下类型的lvalue表达式5访问其存储值:88) — 一个与对象的有效类型兼容的类型,

int x = 1;
int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int
"

— 一个与对象的有效类型兼容的类型的限定版本,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int
"

— 一个与对象的有效类型对应的有符号或无符号类型,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to
// the effective type of the object

参见脚注12关于gcc/clang扩展,允许将*unsigned int*赋值给int**,即使它们不是兼容的类型。

"

— 一个与对象的有效类型的限定版本对应的有符号或无符号类型,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type
// that corresponds with to a qualified verison of the effective type of the object
"

— 包含上述类型之一的聚合或联合类型作为其成员(包括递归地作为子聚合或包含的联合的成员),或

struct foo {
int x;
};
void foobar( struct foo *fp, int *ip ); // struct foo is an aggregate that includes int among its members so it can
// can alias with *ip
foo f;
foobar( &f, &f.x );
"

— 字符类型。

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p ); // *p gives us an lvalue expression of type char which is a character type.
// The results are not portable due to endianness issues.

C++17草案标准是怎么说的

C++17草案标准3在*[basic.lval]第11段*中说:

"

如果一个程序试图通过一个除以下类型之外的glvalue访问一个对象的存储值,该行为是未定义的:63) (11.1) — 对象的动态类型,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0}; // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n"; // *ip gives us a glvalue expression of type int which matches the dynamic type
// of the allocated object
"

(11.2) — > cv-qualified版本的动态类型。

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n"; // *cip给出了一个类型为const int的glvalue表达式,这是x的动态类型的cv-qualified版本
"

(11.3) — 与对象动态类型相似(如定义在7.5中)的类型,

int *a[3];
const int *const *p = a;
const int *q = p[1]; // ok, 通过类型相似的lvalue 'const int*'读取 'int*'
"

(11.4) — 与对象动态类型对应的有符号或无符号类型,

// si和ui都是对应于彼此动态类型的有符号或无符号类型
// 我们可以从这个godbolt(https://godbolt.org/g/KowGXB)看到优化器假定了别名。
signed int foo( signed int &si, unsigned int &ui ) {
si = 1;
ui = 2;
return si;
}
"

(11.5) — 与对象动态类型的cv-qualified版本对应的有符号或无符号类型,

signed int foo( const signed int &si1, int &si2); // 这个例子很难展示出假设了别名
"

(11.6) — 在其元素或非静态数据成员中(包括递归地,子聚合或包含的联合的元素或非静态数据成员)包含上述类型之一的聚合或联合类型,

struct foo {
int x;
};
// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
fp.x = 1;
ip = 2;
return fp.x;
}
foo f;
foobar( f, f.x );
"

(11.7) — 对象动态类型的(可能是cv-qualified的)基类类型,

struct foo { int x ; };
struct bar : public foo {};
int foobar( foo &f, bar &b ) {
f.x = 1;
b.x = 2;
return f.x;
}
"

(11.8) — char,unsigned char,或std::byte类型。

int foo( std::byte &b, uint32_t &ui ) {
b = static_cast<std::byte>('a');
ui = 0xFFFFFFFF;
return std::to_integer<int>( b ); // b给出了一个std::byte类型的glvalue表达式,它可以别名一个uint32_t类型的对象
}

值得注意的是,上述列表中没有包括signed char,这与C语言中的字符类型有显著的不同。

微妙的差异

虽然我们可以看到C和C++对别名有类似的描述,但我们应该意识到一些差异。C++没有C的有效类型兼容类型的概念,C没有C++的动态类型相似类型的概念。虽然两者都有lvaluervalue表达式5,C++还有glvalueprvaluexvalue9表达式。这些差异大多超出了本文的范围,但一个有趣的例子是如何用malloc'd内存创建一个对象。在C中,我们可以通过lvaluememcpy11写入内存来设置有效类型10

// 下面的代码在C中有效,但在C++中无效
void *p = malloc(sizeof(float));
float f = 1.0f;
memcpy( p, &f, sizeof(float)); // 在C中*p的有效类型是float
// 或者
float *fp = p;
*fp = 1.0f; // 在C中*p的有效类型是float

在C++中,这两种方法都不足够,需要使用placement new

float *fp = new (p) float{1.0f} ; // *p的动态类型现在是float

int8_t和uint8_t是char类型吗?

理论上,int8_tuint8_t都不必是char类型,但实际上它们是以这种方式实现的。这很重要,因为如果它们真的是char类型,那么它们也会像char类型那样别名。如果你对此不了解,可能会导致出乎意料的性能影响。我们可以看到,glibc将int8_tuint8_t分别定义为signed charunsigned char

这将很难改变,因为对于*C++*来说,这将是一个ABI中断。这将改变名称混淆,并破坏任何在其接口中使用这两种类型的API。

什么是类型别名(Type Punning)

我们已经讨论到这个问题,我们可能会想,为什么我们会想要别名呢?答案通常是为了类型别名,通常使用的方法会违反严格的别名规则。

有时我们想要绕过类型系统,将一个对象解释为另一种类型。这就叫做类型别名,它是重新解释一段内存为另一种类型的过程。类型别名对于想要访问对象的底层表示以查看、传输或操作的任务是有用的。我们通常在编译器、序列化、网络代码等地方看到类型别名的使用。

传统上,这是通过获取对象的地址,将其转换为我们想要重新解释的类型的指针,然后访问该值,或者换句话说,通过别名来实现的。例如:

int x = 1 ;
// 在C中
float *fp = (float*)&x ; // 不是一个有效的别名
// 在C++中
float *fp = reinterpret_cast<float*>(&x) ; // 不是一个有效的别名
printf(%f\n”, *fp ) ;

正如我们之前看到的,这不是一个有效的别名,所以我们正在调用未定义的行为。但传统上,编译器并没有利用严格的别名规则,这种类型的代码通常只是有效的,开发者不幸地习惯了这种方式。一个常见的类型别名的替代方法是通过联合,这在C中是有效的,但在C++中是未定义行为13 (看实例):

union u1
{
int n;
float f;
} ;
union u1 u;
u.f = 1.0f;
printf( "%d\n", u.n ); // 在C++中是UB,n不是活跃的成员

这在C++中是无效的,有些人认为联合的目的仅仅是为了实现变体类型,认为使用联合进行类型别名是滥用。

我们如何正确地使用类型别名?

在C和C++中,标准赞成的类型别名方法是memcpy。这可能看起来有点重手,但优化器应该能识别出memcpy用于类型别名,并优化掉它,生成一个寄存器到寄存器的移动。例如,如果我们知道int64_tdouble的大小是一样的:

static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17不需要信息

我们可以使用memcpy

void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...

在足够的优化级别下,任何像样的现代编译器都会生成与前面提到的reinterpret_cast方法或联合方法相同的代码进行类型别名。检查生成的代码,我们看到它只使用寄存器移动(在线编译器示例)。

类型别名数组

但是,如果我们想要将一个unsigned char数组类型别名为一系列unsigned int,然后对每个unsigned int值进行操作呢?我们可以使用memcpyunsigned char array类型别名为unsigned int类型的临时变量。优化器仍然能够看穿memcpy,优化掉临时变量和副本,并直接操作底层数据,在线编译器示例

// 简单的操作,只是返回值
int foo( unsigned int x ) { return x ; }
// 假设len是sizeof(unsigned int)的倍数
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
unsigned int ui = 0;
std::memcpy( &ui, &p[index], sizeof(unsigned int) );
result += foo( ui ) ;
}
return result;
}

在这个例子中,我们取一个*char** p,假设它指向多个sizeof(unsigned int)数据的块,我们将每个数据块类型别名为*unsigned int*,计算每个类型别名数据的foo(),并将其加入result,然后返回最终值。

循环体的汇编显示优化器将循环体简化为直接访问底层的unsigned char array作为unsigned int,直接加入eax

add eax, dword ptr [rdi + rcx]

使用reinterpret_cast进行类型别名的相同代码(违反严格别名规则):

// 假设len是sizeof(unsigned int)的倍数
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
unsigned int ui = *reinterpret_cast<unsigned int*>(&p[index]);
result += foo( ui );
}
return result;
}

C++20和bit_cast

在C++20中,我们可能会得到bit_cast14,它提供了一种简单而安全的方式来进行类型别名,同时还可以在constexpr上下文中使用。

下面是一个例子,展示了如何使用bit_castunsigned int类型别名为float,(在线查看):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //假设sizeof(float) == sizeof(unsigned int)

ToFrom类型的大小不一样的情况下,它要求我们使用一个中间的结构体15。我们将使用一个包含**sizeof( unsigned int )**字符数组的结构体(假设4字节的unsigned int)作为From类型,unsigned int作为To类型:

struct uint_chars {
unsigned char arr[sizeof( unsigned int )] = {} ; // 假设sizeof( unsigned int ) == 4
};
// 假设len是4的倍数
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
uint_chars f;
std::memcpy( f.arr, &p[index], sizeof(unsigned int));
unsigned int result = bit_cast<unsigned int>(f);
result += foo( result );
}
return result ;
}

需要这种中间类型是不幸的,但这是bit_cast当前的限制。

什么是公共初始序列

公共初始序列在草案标准的【class.mem.general】p23部分定义。

草案标准给出了以下示例来演示这个概念:

struct A { int a; char b; };
struct B { const int b1; volatile char b2; };
struct C { int c; unsigned : 0; char b; };
struct D { int d; char b : 4; };
struct E { unsigned int e; char b; };

A和B的公共初始序列包括两个类的所有成员。 A和C以及A和D的公共初始序列包括每个情况下的第一个成员。 A和E的公共初始序列是空的。

它说我们可以读取非活动成员的非静态数据成员,如果它是结构的公共初始序列的一部分【class.mem.general】p26

struct T1 { int a, b; };
struct T2 { int c; double d; };
union U { T1 t1; T2 t2; };
int f() {
U u = { { 1, 2 } }; // 活动成员是t1
return u.t2.c; // OK,就像是提名了u.t1.a
}

所以像下面这样的东西是可以的:

union U {
U(int x) : a{.x=x}{}
struct { int x; } a;
struct { int x; } b;
};
int f() {
U u(10);
u.b.x = 20; // 改变活动成员,开始b的生命周期
u.a.x = 20; // 再次改变活动成员,开始a的生命周期
return u.b.x; // ok,公共初始序列
}
int main() {
int a = f();
}

注意这依赖于【class.union.general】p6.3

它说如果赋值是开始适当类型的生命周期,有限制,比如我们使用内置的或平凡的赋值运算符。

这意味着下面的例子会引发未定义行为:

union U {
U(int x) : a{.x=x}{}
struct {
int x;
auto &operator=(int r) {
x = r ;
return *this;
}
} a;
struct {
int x;
auto &operator=(int r) {
x = r ;
return *this;
}
} b;
};
int f() {
U u(10);
u.b = 20; // 没有改变活动成员
// 赋值不是平凡的
// 并且UB,因为存储到生命周期外的对象
u.a = 20; // 没有改变活动成员
// 赋值不是平凡的
// 并且UB,因为存储到生命周期外的对象
return u.b.x; // 仍然是公共初始序列
// 但我们已经调用了UB,所以不ok
}

还有其他需要注意的棘手情况:

union A {
struct { int x, y; } a;
struct { int x, y; } b;
};
int f() {
A a = {.a = {}};
a.b.x = 1; // 改变活动成员,开始b的生命周期
// 没有初始化y
return a.b.y; // UB
}

公共初始序列规则可能是为了允许有区别的联合,而不需要在联合外部有区别器,因此可能在区别器和联合本身之间有填充,例如:

union { struct { char kind; ... } a; struct { char kind; ... } b; ... };

所以公共初始序列规则会允许我们读取kind区别器,无论哪个成员是活动的。

公共初始序列规则不能在常量表达式上下文中使用,参见【expr.const】p5.10,它说:

"

一个表达式E是一个核心常量表达式,除非...

...

  • 一个lvalue-to-rvalue转换被应用到一个glvalue,它引用了联合的非活动成员或其子对象;

对齐

我们已经在之前的例子中看到,违反严格别名规则可能会导致存储被优化掉。违反严格别名规则也可能导致违反对齐要求。C和C++标准都指出,对象有对齐要求,这些要求限制了对象可以分配(在内存中)和因此访问的位置17。C11的 "6.2.8 对象的对齐" 部分说:

"

完整的对象类型有对齐要求,这些要求限制了可以分配该类型对象的地址。对齐是一个由实现定义的整数值,表示可以分配给定对象的连续地址之间的字节数。一个对象类型对该类型的每一个对象都强加了对齐要求:可以使用_Alignas关键字请求更严格的对齐。

C++17草案标准在*[basic.align]第1段*中说:

"

对象类型有对齐要求(6.7.1,6.7.2),这些要求限制了可以分配该类型对象的地址。对齐是一个由实现定义的整数值,表示可以分配给定对象的连续地址之间的字节数。一个对象类型对该类型的每一个对象都强加了对齐要求;可以使用对齐说明符(10.6.2)请求更严格的对齐。

C99和C11明确指出,导致未对齐指针的转换是未定义行为,"6.3.2.3 指针"部分说:

"

一个指向对象或不完整类型的指针可以转换为指向不同对象或不完整类型的指针。如果结果指针对于指向的类型没有正确对齐57),那么行为是未定义的。...

尽管C++没有那么明确,但我认为*[basic.align]第1段*中的这句话已经足够了:

"

...一个对象类型对该类型的每一个对象都强加了对齐要求;...

一个例子

所以让我们假设:

  • alignof(char)alignof(int) 分别是1和4
  • sizeof(int)是4

那么将大小为4的char数组作为int类型别名,违反了严格别名规则,但也可能违反对齐要求,如果数组的对齐是1或2字节。

char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; // 可能在1或2字节边界上分配
int x = *reinterpret_cast<int*>(arr); // 未定义行为,我们有一个未对齐的指针

这可能会导致性能降低,或在某些情况下出现总线错误18。而使用alignas强制数组与int的对齐相同,可以防止违反对齐要求:

alignas(alignof(int)) char arr[4] = { 0x0F, 0x0, 0x0, 0x00 };
int x = *reinterpret_cast<int*>(arr);

原子性

另一个由于未对齐访问导致的意外惩罚是,它在某些架构上破坏了原子性。在x86上,如果它们未对齐,原子存储可能不会对其他线程显得原子7

捕获严格别名违规

我们没有很多好的工具来捕获C++中的严格别名,我们有的工具可以捕获一些严格别名违规的情况,以及一些未对齐的加载和存储的情况。

使用标志**-fstrict-aliasing** 和 -Wstrict-aliasing19的gcc可以捕获一些情况,尽管有假阳性/假阴性。例如,下面的情况21将在gcc中生成警告(在线查看):

int a = 1;
short j;
float f = 1.f; // 最初没有初始化,但tis-kernel捕获到
// 下面访问了一个不确定的值
printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

尽管它不会捕获这个额外的情况(在线查看):

int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

尽管clang允许这些标志,但它显然并未实际实现这些警告20

我们还有一个可用的工具是ASan22,它可以捕获未对齐的加载和存储。虽然这些并不直接违反严格别名规则,但它们是严格别名规则违规的常见结果。例如,下面的情况23在使用**-fsanitize=address**的clang编译时会生成运行时错误:

int *x = new int[2]; // 8字节:[0,7]。
int *u = (int*)((char*)x + 6); // 无论x的对齐是多少,这都不会是一个对齐的地址
*u = 1; // 访问范围[6-9]
printf( "%d\n", *u ); // 访问范围[6-9]

我将推荐的最后一个工具是特定于C++的,不严格地说不是一个工具,而是一个编码实践,不允许C风格的强制转换。使用**-Wold-style-cast**的gcc和clang都会为C风格的强制转换产生诊断。这将强制所有未定义的类型别名使用reinterpret_cast,一般来说,reinterpret_cast应该是进行更仔细代码审查的标志。在你的代码库中搜索reinterpret_cast进行审计也更容易。

对于C,我们已经覆盖了所有的工具,我们还有tis-interpreter24,一个静态分析器,可以对C语言的大部分子集进行详尽的程序分析。给定一个C版本的上面的例子,其中使用**-fstrict-aliasing**的情况会遗漏一个情况(在线查看

int a = 1;
short j;
float f = 1.0 ;
printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));
int *p;
p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter能够捕获所有三个,下面的例子调用tis-kernal作为tis-interpreter(输出已经为简洁而编辑):

./bin/tis-kernel -sa example1.c
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
rules by accessing a cell with effective type int.
...
example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
accessing a cell with effective type float.
Callstack: main
...
example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
accessing a cell with effective type int.

最后,正在开发中的TySan26。这个消毒器在一个影子内存段中添加了类型检查信息,并检查访问是否违反了别名规则。这个工具可能应该能够捕获所有的别名违规,但可能有大的运行时开销。

结论

我们已经了解了C和C++中的别名规则,编译器期望我们严格遵守这些规则是什么意思,以及不这样做的后果。我们学习了一些可以帮助我们捕获别名滥用的工具。我们已经看到类型别名的一个常见用途是类型别名,并了解了如何正确地进行类型别名。

优化器在基于类型的别名分析上慢慢变得更好,已经破坏了一些依赖于严格别名违规的代码。我们可以期待优化只会变得更好,并且会破坏更多我们习惯于工作的代码。

我们有符合标准的类型别名方法,在发布和有时在调试构建中,这些方法应该是无成本的抽象。我们有一些工具可以捕获严格别名违规,但对于C++,它们只能捕获一小部分情况,对于C,使用tis-interpreter,我们应该能够捕获大多数违规。

感谢那些对这篇文章提供反馈的人:JF Bastien,Christopher Di Bella,Pascal Cuoq,Matt P. Dziubinski,Patrice Roy,Richard Smith和Ólafur Waage

当然,最后,所有的错误都是(原)作者的。

Footnotes

1 Undefined behavior described on cppreference http://en.cppreference.com/w/cpp/language/ub [↩]

2 Draft C11 standard is freely available http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf

3 Draft C++17 standard is freely available https://github.com/cplusplus/draft/raw/master/papers/n4659.pdf

4 Latest C++ draft standard can be found here: http://eel.is/c++draft/ [↩]

5 Understanding lvalues and rvalues in C and C++ https://eli.thegreenplace.net/2011/12/15/understanding-lvalues-and-rvalues-in-c-and-c

6 Type-Based Alias Analysis https://www.drdobbs.com/cpp/type-based-alias-analysis/184404273

7 Demonstrates torn loads for misaligned atomics https://gist.github.com/michaeljclark/31fc67fe41d233a83e9ec8e3702398e8 and tweet referencing this example https://twitter.com/corkmork/status/944421528829009925

8 Comment in gcc bug report explaining why changing int8*t and uint8_t to not be char types would be an ABIak for C++ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66110#c13 and twitter thread discussing the issue https://twitter.com/shafikyaghmour/status/822179548825468928 [↩]

9 "New” Value Terminology which explains how glvalue, xvalue and prvalue came about http://www.stroustrup.com/terminology.pdf

10 Effective types and aliasing https://gustedt.wordpress.com/2016/08/17/effective-types-and-aliasing/

11 “constructing” a trivially-copyable object with memcpy https://stackoverflow.com/q/30114397/1708801

12 Why does gcc and clang allow assigning an unsigned int * to int _ since they are not compatible types, although they may alias https://twitter.com/shafikyaghmour/status/957702383810658304 and https://gcc.gnu.org/ml/gcc/2003-10/msg00184.html [↩]

13 Unions and memcpy and type punning https://stackoverflow.com/q/25664848/1708801

15 How to use bit_cast to type pun a unsigned char array https://gist.github.com/shafik/a956a17d00024b32b35634eeba1eb49e

16 bit_cast implementation of pop() https://godbolt.org/g/bXBie7 [↩]

17 Unaligned access https://en.wikipedia.org/wiki/Bus_error#Unaligned_access

18 A bug story: data alignment on x86 http://pzemtsov.github.io/2016/11/06/bug-story-alignment-on-x86.html

19 gcc documentation for -Wstrict-aliasing https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wstrict-aliasing

20 Comments indicating clang does not implement -Wstrict-aliasing https://github.com/llvm-mirror/clang/blob/master/test/Misc/warning-flags-tree.c

21 Stack Overflow questions examples came from https://stackoverflow.com/q/25117826/1708801

22 ASan documentation https://clang.llvm.org/docs/AddressSanitizer.html

23 The unaligned access example take from the Address Sanitizer Algorithm wiki https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm#unaligned-accesses

24 TrustInSoft tis-interpreter https://trust-in-soft.com/tis-interpreter/ , strict aliasing checks can be run by building tis-kernel https://github.com/TrustInSoft/tis-kernel

25 Detecting Strict Aliasing Violations in the Wild https://trust-in-soft.com/wp-content/uploads/2017/01/vmcai.pdf a paper that covers dos and don't w.r.t to aliasing in C [↩]

26 TySan patches, clang: https://reviews.llvm.org/D32199 runtime: https://reviews.llvm.org/D32197 llvm: https://reviews.llvm.org/D32198

©2018-2024 Secirian | CC BY-SA 4.0