struct Point<'a> {
x: i32,
caption: &'a str,
y: i32,
}
static mut global_var: Point = Point {
x: 123,
y: 344,
caption: "123",
};
相应的LLVM IR为:
%Point = type
{
[0 x i64],
{ [0 x i8]*, i64 },
[0 x i32],
i32,
[0 x i32],
i32,
[0 x i32]
}
@_ZN5hello10global_var17h76c725a117a5fdc6E = internal global
<{ i8*, [16 x i8] }>
<{
i8* getelementptr inbounds
(
<{ [3 x i8] }>,
<{ [3 x i8] }>* @6,
i32 0,
i32 0,
i32 0
),
[16 x i8] c"\03\00\00\00\00\00\00\00{\00\00\00X\01\00\00"
}>,
align 8,
!dbg !330
我试图找到两个有趣的问题的答案:
%Point
的类型定义中没有空数组?这里似乎不是Pascal样式的数组。global_var
用一种特定于体系结构的间接方式初始化(整数的内容直接用一个小字节序的缓冲区填充,因为LLVM IR代码在结构上是独立的?如果可能,我们可以以更具可读性的样式获取带有这些初始化的LLVM IR代码吗?
更新以回应一些评论:
[如果我们记下字符串的十六进制表示,我们将发现i64 3,i32 123和i23 344恰好存储在小端架构中。
cargo rustc -- --emit=llvm-bc
生成,然后使用llvm-dis
进行分解。免责声明:这个答案是有根据的猜测。
TL; DR:我想显式的填充数组和显式的初始化是为了避免留下任何未初始化的字节,以及由此导致的未定义行为。
意识到LLVM从C继承了许多低层语义很重要。毕竟,它的第一个也是最重要的前端是Clang,并且这已经构成了它的大部分。
[当Clang将struct
降低到LLVM IR时,它肯定会离开LLVM找出填充link。
因此:
struct A
{
int a;
struct { char const* ptr; size_t len; } str;
char c;
};
A const GLOBAL{ 1, { "hello", 5 }, 'c' };
降低为:
%struct.A = type { i32, %struct.anon, i8 }
%struct.anon = type { i8*, i64 }
@_ZL6GLOBAL = internal constant %struct.A
{
i32 1,
%struct.anon
{
i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str, i32 0, i32 0),
i64 5
},
i8 99
}, align 8
@.str = private unnamed_addr constant [6 x i8] c"hello\00", align 1
这意味着填充字节保持未初始化状态,并且以典型的C方式读取未初始化的字节是未定义的行为。
[这意味着用未初始化的填充字节进行位复制是未定义的行为,虽然memcpy
调用(降为内在函数)似乎没有受到影响,但我不知道C标准中提供了[ C0]通过...
只要出现未定义的行为,铁锈就站稳立场:
[留下未初始化的填充字节,并让用户执行位复制,在很大程度上似乎是不必要的未定义行为来源:
似乎没有太多(如果有的话)性能优势:Rust可以自由地重新排列结构成员并压缩结构,因此填充字节通常很少(仅尾随几个)。
因此,我的猜测是rustc显式指定填充数组2并显式初始化它们,以避免遗留任何未初始化的填充字节。
1 还有。例如,由于LLVM考虑如果值不适合,将memcpy
转换为float
是UB,或者是LLVM考虑无副作用的无限循环是UB-两者都从C继承。正在进行中。
2 这不提供0大小的数组的原理,这些数组对我来说似乎完全多余。